From 2a48780d16b098498248820dc45b6693fe09d0f5 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 7 May 2026 17:18:53 +0200 Subject: [PATCH 01/10] Apply latest templates and dev environment updates (#169) * Move essdiffraction to dev deps and simplify test * Add dev and user features to pixi.toml * Apply latest templates * Isolate tagged docs deployments from dev runs * Remove resolved dev documentation * Remove resolved ADP implementation plan --- .copier-answers.yml | 2 +- .github/workflows/docs.yml | 14 +- .github/workflows/issues-labels.yml | 152 +- .github/workflows/pr-labels.yml | 7 + .github/workflows/release-pr.yml | 2 +- docs/dev/adp_implementation.md | 321 ---- docs/dev/architecture.md | 84 +- docs/dev/cryspy-dwf-bug.md | 126 -- docs/dev/plan_powder-chart-y-range.md | 138 -- docs/docs/installation-and-setup/index.md | 10 +- pixi.lock | 1450 +++++++++++++---- pixi.toml | 94 +- pyproject.toml | 2 +- .../dream/test_package_import.py | 21 +- 14 files changed, 1400 insertions(+), 1023 deletions(-) delete mode 100644 docs/dev/adp_implementation.md delete mode 100644 docs/dev/cryspy-dwf-bug.md delete mode 100644 docs/dev/plan_powder-chart-y-range.md diff --git a/.copier-answers.yml b/.copier-answers.yml index 929409549..a807c84da 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.1-12-gbb9bb30 _src_path: gh:easyscience/templates app_docs_url: https://easyscience.github.io/diffraction-app app_doi: 10.5281/zenodo.18163581 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 117416a3f..def30827c 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: diff --git a/.github/workflows/issues-labels.yml b/.github/workflows/issues-labels.yml index 31a69ad20..56ab5b19a 100644 --- a/.github/workflows/issues-labels.yml +++ b/.github/workflows/issues-labels.yml @@ -1,6 +1,6 @@ -# 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 real `[scope]` label and one +# real `[priority]` label. If either is missing, the workflow adds a reminder +# label with a warning emoji. name: Issue labels check @@ -13,8 +13,6 @@ permissions: jobs: check-labels: - if: github.actor != 'easyscience[bot]' - runs-on: ubuntu-latest concurrency: @@ -25,27 +23,127 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Setup easyscience[bot] - id: bot - uses: ./.github/actions/setup-easyscience-bot + - name: Sync missing-label reminders + uses: ./.github/actions/github-script with: - app-id: ${{ vars.EASYSCIENCE_APP_ID }} - private-key: ${{ secrets.EASYSCIENCE_APP_KEY }} + script: | + const fs = require('fs'); - - name: Check for required [scope] label - uses: trstringer/require-label-prefix@v1 - with: - secret: ${{ steps.bot.outputs.token }} - prefix: '[scope]' - labelSeparator: ' ' - addLabel: true - defaultLabel: '[scope] ⚠️ label needed' - - - name: Check for required [priority] label - uses: trstringer/require-label-prefix@v1 - with: - secret: ${{ steps.bot.outputs.token }} - prefix: '[priority]' - labelSeparator: ' ' - addLabel: true - defaultLabel: '[priority] ⚠️ label needed' + const issueNumber = context.issue.number; + const action = context.payload.action; + const changedLabel = context.payload.label?.name ?? null; + const labels = context.payload.issue.labels.map(({ name }) => name); + const requirements = [ + { + prefix: '[scope] ', + reminder: '[scope] ⚠️ label needed', + }, + { + prefix: '[priority] ', + reminder: '[priority] ⚠️ label needed', + }, + ]; + + const labelsToAdd = []; + const labelsToRemove = []; + const evaluations = []; + + console.log(`::group::Issue label check for #${issueNumber}`); + console.log(`Event action: ${action}`); + if (changedLabel) { + console.log(`Event label: ${changedLabel}`); + } + console.log( + `Current labels: ${labels.length > 0 ? labels.join(', ') : '(none)'}`, + ); + + for (const { prefix, reminder } of requirements) { + const matchingRealLabels = labels.filter( + (name) => name.startsWith(prefix) && name !== reminder, + ); + const hasRealLabel = matchingRealLabels.length > 0; + const hasReminderLabel = labels.includes(reminder); + + evaluations.push({ + prefix, + reminder, + matchingRealLabels, + hasReminderLabel, + }); + + if (hasRealLabel && hasReminderLabel) { + labelsToRemove.push(reminder); + } else if (!hasRealLabel && !hasReminderLabel) { + labelsToAdd.push(reminder); + } + } + + for (const evaluation of evaluations) { + if (evaluation.matchingRealLabels.length > 0) { + console.log( + `Found required ${evaluation.prefix}label(s): ${evaluation.matchingRealLabels.join(', ')}`, + ); + } else { + console.log(`Missing required ${evaluation.prefix}label.`); + } + + if (evaluation.hasReminderLabel) { + console.log(`Reminder label already present: ${evaluation.reminder}`); + } + } + + if (labelsToAdd.length > 0) { + console.log(`Adding reminder labels: ${labelsToAdd.join(', ')}`); + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: labelsToAdd, + }); + } + + for (const name of labelsToRemove) { + console.log(`Removing reminder label: ${name}`); + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name, + }); + } + + if (labelsToAdd.length === 0 && labelsToRemove.length === 0) { + console.log('No label changes required.'); + } + + console.log('::endgroup::'); + + if (process.env.GITHUB_STEP_SUMMARY) { + const summaryLines = [ + '### Issue Label Check', + `- Issue: #${issueNumber}`, + `- Event: ${action}`, + `- Trigger label: ${changedLabel ?? '(none)'}`, + `- Current labels: ${labels.length > 0 ? labels.join(', ') : '(none)'}`, + '', + '#### Requirement status', + ...evaluations.map((evaluation) => { + const status = + evaluation.matchingRealLabels.length > 0 + ? `found ${evaluation.matchingRealLabels.join(', ')}` + : 'missing'; + const reminder = evaluation.hasReminderLabel + ? `reminder present: ${evaluation.reminder}` + : `reminder absent: ${evaluation.reminder}`; + return `- ${evaluation.prefix}: ${status}; ${reminder}`; + }), + '', + `- Labels to add: ${labelsToAdd.length > 0 ? labelsToAdd.join(', ') : '(none)'}`, + `- Labels to remove: ${labelsToRemove.length > 0 ? labelsToRemove.join(', ') : '(none)'}`, + ]; + + fs.appendFileSync( + process.env.GITHUB_STEP_SUMMARY, + `${summaryLines.join('\n')}\n`, + ); + } diff --git a/.github/workflows/pr-labels.yml b/.github/workflows/pr-labels.yml index 642cd3184..25710633b 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -9,6 +9,9 @@ # (e.g. by manual labeling, another workflow, or GitHub App). # # These are separate GitHub events, so two workflow runs can be started. +# The `concurrency` configuration below ensures that only the latest run +# for the same PR remains active, canceling any previous in-progress +# run. name: PR labels check @@ -16,6 +19,10 @@ on: pull_request_target: types: [opened, labeled, unlabeled, synchronize] +concurrency: + group: pr-labels-${{ github.event.pull_request.number }} + cancel-in-progress: true + permissions: pull-requests: read 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/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/architecture.md b/docs/dev/architecture.md index 6cb8c67e2..390aa77bd 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -338,7 +338,8 @@ per-key flags. 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`. +that switching type is a one-line operation on `adp_type`: parameter +objects stay stable while their values and CIF output names change. **Two sibling collections.** Following CIF conventions (`_atom_site` and `_atom_site_aniso` are separate loops), isotropic and anisotropic data @@ -346,27 +347,96 @@ 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 +```shell +Structure +├── cell (CategoryItem) +├── space_group (CategoryItem) +├── atom_sites (CategoryCollection of AtomSite) +└── atom_site_aniso (CategoryCollection of AtomSiteAniso) +``` + +**Type-neutral names.** `atom_site.adp_iso` is the 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. +| Parameter | Location | CIF names | +| ---------- | --------------- | -------------------------------------------------------- | +| `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` | + +`adp_type` is a `StringDescriptor` validated against `AdpTypeEnum` +(`Biso`, `Uiso`, `Bani`, `Uani`). + **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. +For example, switching from `Biso` to `Uiso` makes +`_atom_site.U_iso_or_equiv` the first CIF name on `adp_iso`; switching +from `Bani` to `Uani` does the same for all six `_atom_site_aniso.U_*` +tensor tags. No dynamic CIF handler is needed. + **Auto-conversion.** Setting `adp_type` triggers value conversion: B ↔ U via `B = 8π²U`; iso → ani seeds the diagonal; ani → iso averages the diagonal. +| Transition | Conversion rule | +| ---------- | --------------------------------------------------------------------- | +| B ↔ U | `B = 8π²U` | +| Iso → Ani | `adp_11 = adp_22 = adp_33 = adp_iso`; off-diagonal terms become `0.0` | +| Ani → Iso | `adp_iso = (adp_11 + adp_22 + adp_33) / 3` | + **Collection sync.** `Structure._update_categories()` reconciles the two collections: adds missing aniso entries, removes stale ones, and rekeys -on label rename. +on label rename. Sync follows the datablock dirty-flag pattern: category +or parameter changes mark the structure as needing an update, and the +next serialisation, plot, or fit call performs the reconciliation. + +| Event | Sync action | +| ---------------------------------- | --------------------------------------------------------- | +| Atom added to `atom_sites` | Create matching `AtomSiteAniso` entry with `0.0` defaults | +| Atom removed from `atom_sites` | Remove matching `AtomSiteAniso` entry | +| Atom label renamed in `atom_sites` | Rekey the matching `AtomSiteAniso` entry | + +**User-facing access.** ADP parameters follow the same two-level access +pattern as other category parameters: + +```python +structure.atom_sites['Si'].adp_type = 'Biso' +structure.atom_sites['Si'].adp_iso = 0.47 +structure.atom_site_aniso['Si'].adp_11 = 0.05 +``` + +Creating an atom uses `adp_iso`; the matching `atom_site_aniso` entry is +created by `_update_categories()`: + +```python +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, +) +``` -See [`adp_implementation.md`](adp_implementation.md) for the full -implementation plan. +**Design rule.** ADP parameter names are type-neutral because +type-specific names (`b_iso`, `u_iso`, etc.) would require replacing +parameter objects when the ADP type changes, which would break +constraints, free flags, and existing references. The always-present +`atom_site_aniso` collection avoids conditional branches in +serialisation, calculators, parameter tables, constraint wiring, and UI. --- 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/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/docs/installation-and-setup/index.md b/docs/docs/installation-and-setup/index.md index fdea87bfc..41036f1d9 100644 --- a/docs/docs/installation-and-setup/index.md +++ b/docs/docs/installation-and-setup/index.md @@ -244,9 +244,10 @@ 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: @@ -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 diff --git a/pixi.lock b/pixi.lock index 36a2ecb00..4ff7fdb61 100644 --- a/pixi.lock +++ b/pixi.lock @@ -2,6 +2,7 @@ version: 6 environments: default: channels: + - url: https://conda.anaconda.org/nodefaults/ - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple @@ -19,7 +20,7 @@ environments: - 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.4.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 @@ -65,7 +66,7 @@ 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 @@ -82,7 +83,7 @@ environments: - 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/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 @@ -94,14 +95,14 @@ environments: - 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/libsqlite-3.53.1-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-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/noarch/mistune-3.2.1-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/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda @@ -114,7 +115,7 @@ environments: - 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 @@ -154,14 +155,14 @@ environments: - 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/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 @@ -220,7 +221,7 @@ environments: - 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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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 @@ -241,7 +242,7 @@ environments: - 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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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 @@ -266,9 +267,9 @@ environments: - 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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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 @@ -277,7 +278,7 @@ environments: - 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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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 @@ -293,8 +294,8 @@ environments: - 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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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 @@ -303,7 +304,7 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -338,7 +339,7 @@ environments: - 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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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 @@ -357,7 +358,7 @@ environments: - 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.4.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 @@ -403,7 +404,7 @@ 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 @@ -419,7 +420,7 @@ environments: - 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/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_18.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_18.conda @@ -429,13 +430,13 @@ environments: - 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/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.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/noarch/mistune-3.2.1-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/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda @@ -448,7 +449,7 @@ environments: - 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 @@ -490,14 +491,14 @@ environments: - 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/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 @@ -556,7 +557,7 @@ environments: - 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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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 @@ -576,7 +577,7 @@ environments: - 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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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 @@ -601,9 +602,9 @@ environments: - 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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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 @@ -612,7 +613,7 @@ environments: - 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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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 @@ -628,8 +629,8 @@ environments: - 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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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 @@ -638,7 +639,7 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -673,7 +674,7 @@ environments: - 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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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 @@ -690,7 +691,7 @@ environments: - 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.4.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 @@ -735,7 +736,7 @@ 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 @@ -744,14 +745,14 @@ environments: - 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/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.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/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 @@ -759,7 +760,7 @@ environments: - 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/noarch/mistune-3.2.1-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/nbclient-0.10.4-pyhd8ed1ab_0.conda @@ -773,7 +774,7 @@ environments: - 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 @@ -807,13 +808,13 @@ environments: - 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/win-64/tbb-2023.0.0-ha3553a1_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/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 @@ -824,7 +825,7 @@ environments: - 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/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 @@ -884,7 +885,7 @@ environments: - 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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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 @@ -905,7 +906,7 @@ environments: - 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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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 @@ -930,9 +931,9 @@ environments: - 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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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 @@ -941,7 +942,7 @@ environments: - 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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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 @@ -957,8 +958,8 @@ environments: - 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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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 @@ -967,7 +968,7 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -1002,7 +1003,7 @@ environments: - 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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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 @@ -1011,6 +1012,7 @@ environments: - pypi: ./ py-312-env: channels: + - url: https://conda.anaconda.org/nodefaults/ - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple @@ -1028,7 +1030,7 @@ environments: - 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/linux-64/backports.zstd-1.4.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 @@ -1074,7 +1076,7 @@ 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 @@ -1091,7 +1093,7 @@ environments: - 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/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 @@ -1103,7 +1105,7 @@ environments: - 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/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/libsqlite-3.53.1-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda @@ -1111,7 +1113,7 @@ environments: - 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/noarch/mistune-3.2.1-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 @@ -1124,7 +1126,7 @@ environments: - 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 @@ -1164,14 +1166,14 @@ environments: - 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/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/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 @@ -1230,7 +1232,7 @@ environments: - 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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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 @@ -1251,7 +1253,7 @@ environments: - 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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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 @@ -1276,9 +1278,9 @@ environments: - 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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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 @@ -1287,7 +1289,7 @@ environments: - 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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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 @@ -1303,8 +1305,8 @@ environments: - 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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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 @@ -1313,7 +1315,7 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -1348,7 +1350,7 @@ environments: - 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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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 @@ -1367,7 +1369,7 @@ environments: - 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/osx-arm64/backports.zstd-1.4.0-py312h87c4bb7_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 @@ -1413,7 +1415,7 @@ 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 @@ -1429,7 +1431,7 @@ environments: - 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/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_18.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_18.conda @@ -1438,13 +1440,13 @@ environments: - 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/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.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/noarch/mistune-3.2.1-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/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda @@ -1457,7 +1459,7 @@ environments: - 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 @@ -1499,14 +1501,14 @@ environments: - 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/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 @@ -1565,7 +1567,7 @@ environments: - 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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl @@ -1585,7 +1587,7 @@ environments: - 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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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 @@ -1610,9 +1612,9 @@ environments: - 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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl @@ -1621,7 +1623,7 @@ environments: - 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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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 @@ -1637,8 +1639,8 @@ environments: - 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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.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/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 @@ -1647,7 +1649,7 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -1682,7 +1684,7 @@ environments: - 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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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 @@ -1699,7 +1701,7 @@ environments: - 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/win-64/backports.zstd-1.4.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 @@ -1744,7 +1746,7 @@ 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 @@ -1753,13 +1755,13 @@ environments: - 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/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.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/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 @@ -1767,7 +1769,7 @@ environments: - 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/noarch/mistune-3.2.1-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/nbclient-0.10.4-pyhd8ed1ab_0.conda @@ -1781,7 +1783,7 @@ environments: - 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 @@ -1815,13 +1817,13 @@ environments: - 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/win-64/tbb-2023.0.0-ha3553a1_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/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 @@ -1832,7 +1834,7 @@ environments: - 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/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 @@ -1892,7 +1894,7 @@ environments: - 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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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 @@ -1913,7 +1915,7 @@ environments: - 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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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 @@ -1938,9 +1940,9 @@ environments: - 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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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 @@ -1949,7 +1951,7 @@ environments: - 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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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 @@ -1965,8 +1967,8 @@ environments: - 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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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 @@ -1975,7 +1977,7 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -2010,7 +2012,7 @@ environments: - 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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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 @@ -2019,6 +2021,7 @@ environments: - pypi: ./ py-314-env: channels: + - url: https://conda.anaconda.org/nodefaults/ - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple @@ -2036,7 +2039,7 @@ environments: - 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.4.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 @@ -2082,7 +2085,7 @@ 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 @@ -2099,7 +2102,7 @@ environments: - 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/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 @@ -2111,14 +2114,14 @@ environments: - 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/libsqlite-3.53.1-h0c1763c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-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/noarch/mistune-3.2.1-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/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda @@ -2131,7 +2134,7 @@ environments: - 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 @@ -2171,14 +2174,14 @@ environments: - 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/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 @@ -2237,7 +2240,7 @@ environments: - 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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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 @@ -2258,7 +2261,7 @@ environments: - 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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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 @@ -2283,9 +2286,9 @@ environments: - 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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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 @@ -2294,7 +2297,7 @@ environments: - 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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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 @@ -2310,8 +2313,8 @@ environments: - 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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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 @@ -2320,7 +2323,7 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -2355,7 +2358,7 @@ environments: - 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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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 @@ -2374,7 +2377,7 @@ environments: - 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.4.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 @@ -2420,7 +2423,7 @@ 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 @@ -2436,7 +2439,7 @@ environments: - 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/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_18.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_18.conda @@ -2446,13 +2449,13 @@ environments: - 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/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.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/noarch/mistune-3.2.1-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/nbclient-0.10.4-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.1-pyhcf101f3_0.conda @@ -2465,7 +2468,7 @@ environments: - 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 @@ -2507,14 +2510,14 @@ environments: - 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/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 @@ -2573,7 +2576,7 @@ environments: - 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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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 @@ -2593,7 +2596,7 @@ environments: - 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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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 @@ -2618,9 +2621,9 @@ environments: - 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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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 @@ -2629,7 +2632,7 @@ environments: - 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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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 @@ -2645,8 +2648,8 @@ environments: - 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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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 @@ -2655,7 +2658,7 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -2690,7 +2693,7 @@ environments: - 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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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 @@ -2707,7 +2710,7 @@ environments: - 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.4.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 @@ -2752,7 +2755,7 @@ 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 @@ -2761,14 +2764,14 @@ environments: - 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/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.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/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 @@ -2776,7 +2779,7 @@ environments: - 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/noarch/mistune-3.2.1-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/nbclient-0.10.4-pyhd8ed1ab_0.conda @@ -2790,7 +2793,7 @@ environments: - 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 @@ -2824,13 +2827,13 @@ environments: - 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/win-64/tbb-2023.0.0-ha3553a1_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/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 @@ -2841,7 +2844,7 @@ environments: - 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/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 @@ -2901,7 +2904,7 @@ environments: - 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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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 @@ -2922,7 +2925,7 @@ environments: - 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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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 @@ -2947,9 +2950,9 @@ environments: - 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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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 @@ -2958,7 +2961,7 @@ environments: - 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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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 @@ -2974,8 +2977,8 @@ environments: - 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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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 @@ -2984,7 +2987,7 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -3019,13 +3022,752 @@ environments: - 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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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: ./ + user: + 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-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.4.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/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/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/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/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-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.1-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/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/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.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/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 + - 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-py314h5bd0f2a_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.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/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/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/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/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-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/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/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/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.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/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/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/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-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/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.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/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/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/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/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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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/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/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/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.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/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/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-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/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/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/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/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/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-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/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.4.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/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/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/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/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/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/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py314h6c2aa35_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/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.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/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 + - 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/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.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.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/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/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/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-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/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/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/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.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/06/41/4e70dea1d0311016c0b0b1c53a24a266f9f8a34c6bc1af0f17cfca20aa1d/gemmi-0.7.5-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-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/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/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.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/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.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/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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/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/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/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.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/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/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-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/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/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/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/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/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-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.4.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/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/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/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/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mistune-3.2.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py314h5a2d7ad_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/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.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/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/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.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.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 + - 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/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/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-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/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/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/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.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/ff/1c/a28b27effb13a381fe077ea3e3e78f6debd6315f2b3edff67bbb93d0ef51/gemmi-0.7.5-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/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.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/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/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.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/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.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/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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/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/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/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.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/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/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-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/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/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/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/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/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-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 @@ -3448,59 +4190,58 @@ packages: - 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 +- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py312h90b7ffd_0.conda + sha256: e8c83696e6529ac1909a96690c58624bb376312fd0768409380cd9b05e248c9b + md5: 542da724e75cdeef19e29cca23935c25 depends: - python - libgcc >=14 - __glibc >=2.17,<3.0.a0 - - python_abi 3.12.* *_cp312 - 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: 237970 - timestamp: 1767045004512 -- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.3.0-py314h680f03e_0.conda + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 238360 + timestamp: 1777848717715 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda noarch: generic - sha256: c31ab719d256bc6f89926131e88ecd0f0c5d003fe8481852c6424f4ec6c7eb29 - md5: a2ac7763a9ac75055b68f325d3255265 + sha256: de1755a35258eb1b59f2288559bbf0b76da60bd2fa6cd6f768ead442f85bd666 + md5: b712198b257f378e9bd8cde277218296 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 + size: 7546 + timestamp: 1777848733980 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py312h87c4bb7_0.conda + sha256: 7dbd64d3f06622ef8286be6dfceeb8e6008450fb4e6d9309fbb908b12f3937ff + md5: 95a833465ec45ac1e8f2ed1aaba8ec37 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 + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 239305 + timestamp: 1777848727027 +- conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py312h06d0912_0.conda + sha256: 71caf40c0fdeb11fafaac639e6e6f9120112aa105a7a5e9dfb5b4b06db9ca97a + md5: 77d0a2bdd46dd8d502bb27eb80353fcd 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 + - python_abi 3.12.* *_cp312 license: BSD-3-Clause AND MIT AND EPL-2.0 purls: - - pkg:pypi/backports-zstd?source=hash-mapping - size: 236635 - timestamp: 1767045021157 + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 237107 + timestamp: 1777848740547 - pypi: https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl name: backrefs version: '7.0' @@ -4581,10 +5322,10 @@ packages: - importlib-metadata>=1.6.0 ; python_full_version < '3.8' - packaging>=20.9 requires_python: '>=3.5' -- pypi: ./ +- pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl name: easydiffraction - version: 0.15.0+devdirty9 - sha256: ea566085dad9a9e2b2a0916bd7af16841d1af477107460060da3446971d2c2ad + version: 0.16.0 + sha256: 2691a1e175974ca79e0ec3c219d92b77f277c38fb3b0b8d25f6f7e99696bf70f requires_dist: - asciichartpy - asteval @@ -4645,6 +5386,70 @@ packages: - validate-pyproject[all] ; extra == 'dev' - versioningit ; extra == 'dev' requires_python: '>=3.12' +- pypi: ./ + name: easydiffraction + version: 0.16.0+devdirty3 + sha256: f7b222922aa1bbe773d6384ee96acc456820490c46139a8f2598129cb6c4717e + requires_dist: + - asciichartpy + - asteval + - bumps + - colorama + - crysfml + - cryspy + - darkdetect + - dfo-ls + - diffpy-pdffit2 + - diffpy-utils + - gemmi + - lmfit + - numpy + - pandas + - plotly + - pooch + - py3dmol + - rich + - scipy + - sympy + - tabulate + - 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-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 @@ -5175,10 +5980,10 @@ packages: 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 +- pypi: https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl name: gitpython - version: 3.1.49 - sha256: 024b0422d7f84d15cd794844e029ffebd4c5d42a7eb9b936b458697ef550a02c + version: 3.1.50 + sha256: d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9 requires_dist: - gitdb>=4.0.1,<5 - typing-extensions>=3.10.0.2 ; python_full_version < '3.10' @@ -5900,9 +6705,9 @@ packages: - 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 +- 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 @@ -5925,11 +6730,10 @@ packages: - websocket-client >=1.7 - python license: BSD-3-Clause - license_family: BSD purls: - - pkg:pypi/jupyter-server?source=hash-mapping - size: 347094 - timestamp: 1755870522134 + - pkg:pypi/jupyter-server?source=compressed-mapping + size: 360522 + timestamp: 1778060967727 - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda sha256: 5eda79ed9f53f590031d29346abd183051263227dd9ee667b5ca1133ce297654 md5: 7b8bace4943e0dc345fc45938826f2b8 @@ -6179,7 +6983,7 @@ packages: license: MIT license_family: MIT purls: - - pkg:pypi/lark?source=compressed-mapping + - pkg:pypi/lark?source=hash-mapping size: 94312 timestamp: 1761596921009 - pypi: https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl @@ -6454,45 +7258,45 @@ packages: 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 +- 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.7.5.* + - expat 2.8.0.* 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 + size: 77241 + timestamp: 1777846112704 +- 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.7.5.* + - expat 2.8.0.* 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 + size: 68789 + timestamp: 1777846180142 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + sha256: 2d81d647c1f01108803457cac999b947456f44dd0a3c2325395677feacaeca67 + md5: 264e350e035092b5135a2147c238aec4 depends: - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 constrains: - - expat 2.7.5.* + - expat 2.8.0.* license: MIT license_family: MIT purls: [] - size: 70609 - timestamp: 1774719377850 + size: 71094 + timestamp: 1777846223617 - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 md5: a360c33a5abe61c07959e449fa1453eb @@ -6821,39 +7625,38 @@ packages: 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 +- 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 - - icu >=78.3,<79.0a0 - libgcc >=14 - libzlib >=1.3.2,<2.0a0 license: blessing 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: 954962 + timestamp: 1777986471789 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda + sha256: 49daec7c83e70d4efc17b813547824bc2bcf2f7256d84061d24fbfe537da9f74 + md5: 6681822ea9d362953206352371b6a904 depends: - __osx >=11.0 - libzlib >=1.3.2,<2.0a0 license: blessing purls: [] - size: 920039 - timestamp: 1775754485962 -- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.0-hf5d6505_0.conda - sha256: 7a6256ea136936df4c4f3b227ba1e273b7d61152f9811b52157af497f07640b0 - md5: 4152b5a8d2513fd7ae9fb9f221a5595d + size: 920047 + timestamp: 1777987051643 +- 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: 1301855 - timestamp: 1775753831574 + size: 1304178 + timestamp: 1777986510497 - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda sha256: 78668020064fdaa27e9ab65cd2997e2c837b564ab26ce3bf0e58a2ce1a525c6e md5: 1b08cd684f34175e4514474793d44bcb @@ -7086,10 +7889,10 @@ packages: - 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 +- pypi: https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl name: markdown-it-py - version: 4.0.0 - sha256: 87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 + version: 4.1.0 + sha256: d4939a62a2dd0cd9cb80a191a711ba1d39bac8ed5ef9e9966895b0171c01c46d requires_dist: - mdurl~=0.1 - psutil ; extra == 'benchmarking' @@ -7117,6 +7920,7 @@ packages: - pytest ; extra == 'testing' - pytest-cov ; extra == 'testing' - pytest-regressions ; extra == 'testing' + - pytest-timeout ; 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 @@ -7388,19 +8192,18 @@ packages: - 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 +- 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: 74250 - timestamp: 1766504456031 + - pkg:pypi/mistune?source=compressed-mapping + size: 74567 + timestamp: 1777824616382 - pypi: https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl name: mkdocs version: 1.6.1 @@ -7858,13 +8661,13 @@ packages: requires_dist: - nbformat requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/1a/66/8ce2af42feeba7a85b573e615a360ca1e204c75f6c8d4c5641140b1bfd17/ncrystal-4.3.4-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-py3-none-any.whl name: ncrystal - version: 4.3.4 - sha256: 87ea53b4e6937e4df9b2e0c71dfd88ff77de6fe8c8b4d204405d24d12143aba0 + version: 4.4.2 + sha256: e02fa7d743addc3fbea23287737a88b8d01192450fdca51554d3f9032fe4617c requires_dist: - - ncrystal-core==4.3.4 - - ncrystal-python==4.3.4 + - 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' @@ -7890,25 +8693,25 @@ packages: - 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 +- pypi: https://files.pythonhosted.org/packages/c7/6b/6c02f55c2ce2f137ccca0986be7dd89bea31d5bee4346b4377fa3b8586df/ncrystal_core-4.4.2-py3-none-win_amd64.whl name: ncrystal-core - version: 4.3.4 - sha256: d3b94528c5d237d43c64c18a2347b967445c43c31ade5c64716e171838b53f9e + version: 4.4.2 + sha256: 9b28a90b63849e6a3a807a0a59f7c2ee57e4c64f5643b2dcb6a798ac8ccf666a 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 +- 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.3.4 - sha256: 47e4441b65170f63acc6c25ea02c7827dbf76a5813f4bf01f9404a060bee6063 + version: 4.4.2 + sha256: b7e6101a6850aa18cf441825214381614db444ffcba648de8266fe1c4d1024ce requires_python: '>=3.8' -- 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/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.3.4 - sha256: 50642b491f1a9bbd4d37909dc11ef45ecf14c6272511ec5a1e1117ef7e51aa66 + version: 4.4.2 + sha256: d0d9c47cd017b7cefc52dde50546d7c151bfdd75d345e42e2b3e74ab5fe83c62 requires_python: '>=3.8' -- pypi: https://files.pythonhosted.org/packages/74/10/6434f57fa65651672534ce34a1f40ddbc7b880658ce50a8f9d7ab0830719/ncrystal_python-4.3.4-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl name: ncrystal-python - version: 4.3.4 - sha256: f7075904fa40c6a85ac9d792255ae0751b0d7059dd5297d54b1d7208e17be66e + version: 4.4.2 + sha256: f419318d088fade6bcff1e39e15baf6fe69fcf5306dd681fca1106d1f63a89ce requires_dist: - numpy>=1.22 requires_python: '>=3.8' @@ -8716,18 +9519,18 @@ packages: - 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 +- 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: 82287 - timestamp: 1770676243987 + - pkg:pypi/parso?source=compressed-mapping + size: 82472 + timestamp: 1777722955579 - pypi: https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl name: partd version: 1.4.2 @@ -8952,10 +9755,10 @@ packages: - 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 +- pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl name: pip - version: '26.1' - sha256: 4e8486d821d814b77319acb7b9e8bf5a4ee7590a643e7cb21029f209be8573c1 + version: 26.1.1 + sha256: 99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb requires_python: '>=3.10' - conda: https://conda.anaconda.org/conda-forge/noarch/pixi-kernel-0.7.1-pyhbbac1ac_0.conda sha256: 506c9330b8dc5ae98f4c32629fa59fa40e6bdd42a681c48d2f9554693dd01156 @@ -9377,57 +10180,57 @@ packages: - 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 +- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl name: pydantic - version: 2.13.3 - sha256: 6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927 + version: 2.13.4 + sha256: 45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba requires_dist: - annotated-types>=0.6.0 - - pydantic-core==2.46.3 + - 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/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl +- 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.3 - sha256: 8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e + version: 2.46.4 + sha256: 962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f 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 +- 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.3 - sha256: ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018 + version: 2.46.4 + sha256: e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 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 +- 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.3 - sha256: c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1 + version: 2.46.4 + sha256: 926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce 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 +- 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.3 - sha256: fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395 + version: 2.46.4 + sha256: 7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b 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 +- 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.3 - sha256: af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089 + version: 2.46.4 + sha256: 23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 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 +- 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.3 - sha256: ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f + version: 2.46.4 + sha256: 811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac requires_dist: - typing-extensions>=4.14.1 requires_python: '>=3.9' @@ -9781,10 +10584,10 @@ packages: - 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 +- pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl name: python-discovery - version: 1.2.2 - sha256: e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a + version: 1.3.0 + sha256: 441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f requires_dist: - filelock>=3.15.4 - platformdirs>=4.3.6,<5 @@ -10018,7 +10821,7 @@ packages: license: MIT license_family: MIT purls: - - pkg:pypi/pyyaml?source=compressed-mapping + - pkg:pypi/pyyaml?source=hash-mapping size: 202391 timestamp: 1770223462836 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda @@ -11350,19 +12153,18 @@ packages: requires_dist: - wcwidth ; extra == 'widechars' requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2022.3.0-h3155e25_2.conda - sha256: abd9a489f059fba85c8ffa1abdaa4d515d6de6a3325238b8e81203b913cf65a9 - md5: 0f9817ffbe25f9e69ceba5ea70c52606 +- conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-ha3553a1_1.conda + sha256: 5ff149ba6832bf4ded4b43bf0a41cde7be814802a95070553176c087f65b2a01 + md5: 34aa94d586fe95fa121966c0d4e73cf4 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 + size: 156910 + timestamp: 1777976465531 - conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyh6dadd2b_1.conda sha256: b375e8df0d5710717c31e7c8e93c025c37fa3504aea325c7a55509f64e5d4340 md5: e43ca10d61e55d0a8ec5d8c62474ec9e @@ -11562,17 +12364,17 @@ packages: - 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.15.0-pyhcf101f3_0.conda + sha256: dfb681579be59c2e790c95f7f49b7529a9b0511d6385ad276e3c8988cbd54d2c + md5: 4bada6a6d908a27262af8ebddf4f7492 depends: - - python >=3.9 + - python >=3.10 + - python license: BSD-3-Clause - license_family: BSD purls: - - pkg:pypi/traitlets?source=hash-mapping - size: 110051 - timestamp: 1733367480074 + - pkg:pypi/traitlets?source=compressed-mapping + size: 115165 + timestamp: 1778074251714 - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.py3-none-any.whl name: traittypes version: 0.2.3 @@ -11780,10 +12582,10 @@ packages: - 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 +- pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl name: virtualenv - version: 21.3.0 - sha256: 4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7 + version: 21.3.1 + sha256: d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35 requires_dist: - distlib>=0.3.7,<1 - filelock>=3.24.2,<4 ; python_full_version >= '3.10' @@ -11821,17 +12623,17 @@ packages: 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda + sha256: 1ee2d8384972ecbf8630ce8a3ea9d16858358ad3e8566675295e66996d5352da + md5: eb9538b8e55069434a18547f43b96059 depends: - python >=3.10 license: MIT license_family: MIT purls: - - pkg:pypi/wcwidth?source=hash-mapping - size: 71550 - timestamp: 1770634638503 + - pkg:pypi/wcwidth?source=compressed-mapping + size: 82917 + timestamp: 1777744489106 - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda sha256: 21f6c8a20fe050d09bfda3fb0a9c3493936ce7d6e1b3b5f8b01319ee46d6c6f6 md5: 6639b6b0d8b5a284f027a2003669aa65 diff --git a/pixi.toml b/pixi.toml index 546620d4a..958851cf2 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 diff --git a/pyproject.toml b/pyproject.toml index c0943384c..5faffe6b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,6 @@ 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 @@ -53,6 +52,7 @@ dependencies = [ [project.optional-dependencies] dev = [ + 'essdiffraction', # ESS-specific diffraction library 'GitPython', # Interact with Git repositories 'build', # Building the package 'pre-commit', # Pre-commit hooks 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}' - ) From 3467c721df60fe2fb94889ee75c644bf6a5fde8f Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 13 May 2026 23:26:45 +0200 Subject: [PATCH 02/10] Add initial Bayesian analysis implementation with BUMPS DREAM (#170) * Add Bayesian analysis design note * Add arviz for Bayesian analysis * Remove old package structure documentation * Update package structure docs with refln reorganization * Plan Bayesian DREAM implementation * Register BUMPS DREAM minimizer * Add Bayesian fit result models * Thread random seed through fitting * Implement DREAM sampling * Add Bayesian fit result display * Plot Bayesian parameter correlations * Add posterior ArviZ plots * Add posterior predictive plots * Refine Bayesian fit UX * Fix DREAM sampler progress reporting * Fix posterior pair plot axis sharing * Improve Bayesian demo parameter choices * Expose DREAM sampler settings * Render posterior marginals as KDE curves * Add posterior pair density contours * Switch Bayesian demo to correlated pair * Add Bayesian tracking and posterior plot updates * Add posterior draws and log-posterior tracking * Use population mean log-posterior for progress tracking * Polish Bayesian progress and posterior plots * Fix DREAM diagnostics and posterior KDEs * Refine posterior plot styling * Improve posterior pair plot UX * Add uncertainty-based fit bounds method * Tighten posterior pair plot layout * Make fit bounds helper a setter * Refine posterior contour layering * Fix posterior contour layering * Preserve pair plot sample hover * Enforce pair plot trace z-order * Restore pair plot axis frames * Split posterior diagnostic warnings * Strengthen pair plot axis borders * Free broad_gauss and set bounds from uncertainty * Draw explicit pair plot borders * Align pair plot y-axis titles * Unify plot axis border colors * Lock posterior pair axis ranges * Align pair plot title with modebar * Update diagonal subplot axis styling * Expose DREAM init and align pair labels * Tighten fit bounds multiplier from 5 to 4 * Match pair plot axis title font sizes * Disable Rich highlighting in Bayesian summaries * Tune DREAM and model parameters in fit * Expand Bayesian tutorial walkthrough * Apply linting rules and formatting * Add LBCO Bayesian tutorial * Make posterior pair plots responsive * Add DREAM bound validation and flexible parameter selection * Adopt fixed-aspect CSS for posterior pair plots * Add posterior pair plot rendering modes * Add multiline axis titles to posterior pair plots * Redesign correlation heatmap display * Simplify tutorial parameter loops and bounds * Improve hover template readability * Improve posterior plot binning and styling * Refactor posterior x-axis range to static method * Change posterior predictive default style to band * Add separate caching for band summaries * Reduce sampling steps and adjust plot ranges * Update posterior pair plot marker and background * Update notebooks * Fix docstring errors in Plotter class * Standardize axis title font size * Improve Bayesian fit summary display * Improve Bayesian fit result display * Adjust posterior and band fill transparency * Update fit bounds and plot range in ed-21 * Improve posterior table diagnostic notes * Add parallel DREAM sampling * Add parallel=0 description to DREAM tutorial * Fix test expectations for r-hat and parallel=0 * Add Bayesian DREAM to architecture * Clean up * Fix Bayesian diagnostics and DREAM cleanup edge cases * Fix square matrix plot spacing and aspect ratio * Apply the latest templates * Bump setup-pixi to v0.9.5 * Bump dependencies * Bump pixi.lock from v6 to v7 * Add correlation-based limits to posterior pair plots * Improve posterior summary plot styling * Add single-crystal posterior predictive scatter plot * Add Tb2TiO7 Bayesian tutorial * Set default correlation and pair-plot limit to six * Bump copier template to v0.11.3 * Update tutorials * Adjust posterior pair plot margins and title shift * Generalize pair-plot layout to square matrix * Switch DREAM defaults and refine Bayesian fit displays * Show uncertainty multiplier in pair plot titles * Refine Bayesian tutorial examples * Shorten fit-bounds multiplier constant name * Bump dependencies * Add ADR documents * Improve CI and codecov settings * Fix structure factor support for X-ray * Increase integration coverage for Bayesian fit helpers * Remove unused benchmark dependency and track serial benchmarks * Normalize styled console output in Windows integration tests --- .codecov.yml | 26 + .copier-answers.yml | 2 +- .github/actions/setup-pixi/action.yml | 2 +- .github/copilot-instructions.md | 13 +- .github/workflows/coverage.yml | 2 - .github/workflows/dashboard.yml | 6 +- .github/workflows/issues-labels.yml | 149 +- .github/workflows/pr-labels.yml | 74 +- .github/workflows/test.yml | 6 +- CONTRIBUTING.md | 6 + codecov.yml | 13 - docs/architecture/package-structure-full.md | 425 - docs/architecture/package-structure-short.md | 224 - docs/dev/adr_analysis-cif-fit-state.md | 220 + .../adr_parameter-correlation-persistence.md | 114 + docs/dev/adr_parameter-posterior-summary.md | 681 + docs/dev/adr_undo-fit.md | 162 + docs/dev/architecture.md | 103 +- docs/dev/issues_open.md | 24 + docs/dev/package-structure-full.md | 38 +- docs/dev/package-structure-short.md | 8 +- docs/docs/installation-and-setup/index.md | 13 +- docs/docs/tutorials/ed-13.ipynb | 2 +- docs/docs/tutorials/ed-21.ipynb | 730 + docs/docs/tutorials/ed-21.py | 354 + docs/docs/tutorials/ed-22.ipynb | 427 + docs/docs/tutorials/ed-22.py | 269 + docs/docs/tutorials/index.json | 14 + docs/docs/tutorials/index.md | 16 + docs/mkdocs.yml | 11 +- pixi.lock | 21172 ++++++++-------- pyproject.toml | 2 +- src/easydiffraction/analysis/analysis.py | 40 +- .../analysis/calculators/cryspy.py | 10 +- .../analysis/categories/fit/default.py | 16 +- .../analysis/fit_helpers/__init__.py | 6 + .../analysis/fit_helpers/bayesian.py | 777 + .../analysis/fit_helpers/reporting.py | 38 +- .../analysis/fit_helpers/tracking.py | 391 +- src/easydiffraction/analysis/fitting.py | 4 + .../analysis/minimizers/__init__.py | 2 + .../analysis/minimizers/base.py | 85 +- .../analysis/minimizers/bumps_dream.py | 948 + .../analysis/minimizers/enums.py | 11 + src/easydiffraction/core/variable.py | 78 + src/easydiffraction/display/plotters/ascii.py | 2 + src/easydiffraction/display/plotters/base.py | 5 + .../display/plotters/plotly.py | 549 +- src/easydiffraction/display/plotting.py | 3711 ++- src/easydiffraction/project/project.py | 5 + .../test_analysis_and_fit_category_support.py | 360 + .../fitting/test_bayesian_dream.py | 178 + .../fitting/test_bayesian_helper_support.py | 670 + .../fitting/test_bayesian_tracker_and_base.py | 451 + .../fitting/test_bumps_dream_support.py | 559 + .../fitting/test_cli_entrypoints.py | 173 + .../analysis/calculators/test_cryspy.py | 29 + .../analysis/fit_helpers/test_bayesian.py | 385 + .../analysis/fit_helpers/test_reporting.py | 66 + .../analysis/minimizers/test_base.py | 39 + .../analysis/minimizers/test_bumps.py | 12 + .../analysis/minimizers/test_bumps_dream.py | 362 + .../analysis/minimizers/test_enums.py | 7 + .../analysis/minimizers/test_factory.py | 12 +- .../easydiffraction/core/test_parameters.py | 84 + .../display/plotters/test_plotly.py | 152 +- .../easydiffraction/display/test_plotting.py | 1131 +- .../easydiffraction/project/test_project.py | 14 + tools/generate_package_docs.py | 4 +- 69 files changed, 25035 insertions(+), 11639 deletions(-) create mode 100644 .codecov.yml delete mode 100644 codecov.yml delete mode 100644 docs/architecture/package-structure-full.md delete mode 100644 docs/architecture/package-structure-short.md create mode 100644 docs/dev/adr_analysis-cif-fit-state.md create mode 100644 docs/dev/adr_parameter-correlation-persistence.md create mode 100644 docs/dev/adr_parameter-posterior-summary.md create mode 100644 docs/dev/adr_undo-fit.md create mode 100644 docs/docs/tutorials/ed-21.ipynb create mode 100644 docs/docs/tutorials/ed-21.py create mode 100644 docs/docs/tutorials/ed-22.ipynb create mode 100644 docs/docs/tutorials/ed-22.py create mode 100644 src/easydiffraction/analysis/fit_helpers/bayesian.py create mode 100644 src/easydiffraction/analysis/minimizers/bumps_dream.py create mode 100644 tests/integration/fitting/test_analysis_and_fit_category_support.py create mode 100644 tests/integration/fitting/test_bayesian_dream.py create mode 100644 tests/integration/fitting/test_bayesian_helper_support.py create mode 100644 tests/integration/fitting/test_bayesian_tracker_and_base.py create mode 100644 tests/integration/fitting/test_bumps_dream_support.py create mode 100644 tests/integration/fitting/test_cli_entrypoints.py create mode 100644 tests/unit/easydiffraction/analysis/fit_helpers/test_bayesian.py create mode 100644 tests/unit/easydiffraction/analysis/minimizers/test_bumps_dream.py 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 a807c84da..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.1-12-gbb9bb30 +_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..0a54cc941 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -77,7 +77,7 @@ ## Testing - Every new module, class, or bug fix ships with tests. See - `docs/architecture/architecture.md` §10 for the full strategy. + `docs/dev/architecture.md` §10 for the full strategy. - Unit tests mirror the source tree: `src/easydiffraction//.py` → `tests/unit/easydiffraction//test_.py`. Verify with @@ -102,8 +102,8 @@ - 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/architecture.md` and follow documented patterns. Localised + bug fixes or test updates need only this file. - 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; @@ -146,13 +146,12 @@ Non-trivial changes use a two-phase workflow: Notes: -- `pixi run fix` regenerates `docs/architecture/package-structure-*.md` +- `pixi run fix` regenerates `docs/dev/package-structure-*.md` automatically — never edit those by hand. Don't review auto-fixes; accept and move on. Then `pixi run check` until clean. - 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 `architecture.md` if affected. ### Planning 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/issues-labels.yml b/.github/workflows/issues-labels.yml index 56ab5b19a..4fbf4fab1 100644 --- a/.github/workflows/issues-labels.yml +++ b/.github/workflows/issues-labels.yml @@ -1,6 +1,5 @@ -# Verifies if the current issue has at least one real `[scope]` label and one -# real `[priority]` label. If either is missing, the workflow adds a reminder -# label with a warning emoji. +# 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 @@ -20,130 +19,22 @@ jobs: cancel-in-progress: true steps: - - name: Checkout repository - uses: actions/checkout@v6 - - - name: Sync missing-label reminders - uses: ./.github/actions/github-script + - name: Ensure [scope] label + uses: Rindrics/expect-label-prefix@v1.2.1 with: - script: | - const fs = require('fs'); - - const issueNumber = context.issue.number; - const action = context.payload.action; - const changedLabel = context.payload.label?.name ?? null; - const labels = context.payload.issue.labels.map(({ name }) => name); - const requirements = [ - { - prefix: '[scope] ', - reminder: '[scope] ⚠️ label needed', - }, - { - prefix: '[priority] ', - reminder: '[priority] ⚠️ label needed', - }, - ]; - - const labelsToAdd = []; - const labelsToRemove = []; - const evaluations = []; - - console.log(`::group::Issue label check for #${issueNumber}`); - console.log(`Event action: ${action}`); - if (changedLabel) { - console.log(`Event label: ${changedLabel}`); - } - console.log( - `Current labels: ${labels.length > 0 ? labels.join(', ') : '(none)'}`, - ); - - for (const { prefix, reminder } of requirements) { - const matchingRealLabels = labels.filter( - (name) => name.startsWith(prefix) && name !== reminder, - ); - const hasRealLabel = matchingRealLabels.length > 0; - const hasReminderLabel = labels.includes(reminder); - - evaluations.push({ - prefix, - reminder, - matchingRealLabels, - hasReminderLabel, - }); - - if (hasRealLabel && hasReminderLabel) { - labelsToRemove.push(reminder); - } else if (!hasRealLabel && !hasReminderLabel) { - labelsToAdd.push(reminder); - } - } - - for (const evaluation of evaluations) { - if (evaluation.matchingRealLabels.length > 0) { - console.log( - `Found required ${evaluation.prefix}label(s): ${evaluation.matchingRealLabels.join(', ')}`, - ); - } else { - console.log(`Missing required ${evaluation.prefix}label.`); - } - - if (evaluation.hasReminderLabel) { - console.log(`Reminder label already present: ${evaluation.reminder}`); - } - } - - if (labelsToAdd.length > 0) { - console.log(`Adding reminder labels: ${labelsToAdd.join(', ')}`); - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - labels: labelsToAdd, - }); - } - - for (const name of labelsToRemove) { - console.log(`Removing reminder label: ${name}`); - await github.rest.issues.removeLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - name, - }); - } - - if (labelsToAdd.length === 0 && labelsToRemove.length === 0) { - console.log('No label changes required.'); - } - - console.log('::endgroup::'); - - if (process.env.GITHUB_STEP_SUMMARY) { - const summaryLines = [ - '### Issue Label Check', - `- Issue: #${issueNumber}`, - `- Event: ${action}`, - `- Trigger label: ${changedLabel ?? '(none)'}`, - `- Current labels: ${labels.length > 0 ? labels.join(', ') : '(none)'}`, - '', - '#### Requirement status', - ...evaluations.map((evaluation) => { - const status = - evaluation.matchingRealLabels.length > 0 - ? `found ${evaluation.matchingRealLabels.join(', ')}` - : 'missing'; - const reminder = evaluation.hasReminderLabel - ? `reminder present: ${evaluation.reminder}` - : `reminder absent: ${evaluation.reminder}`; - return `- ${evaluation.prefix}: ${status}; ${reminder}`; - }), - '', - `- Labels to add: ${labelsToAdd.length > 0 ? labelsToAdd.join(', ') : '(none)'}`, - `- Labels to remove: ${labelsToRemove.length > 0 ? labelsToRemove.join(', ') : '(none)'}`, - ]; - - fs.appendFileSync( - process.env.GITHUB_STEP_SUMMARY, - `${summaryLines.join('\n')}\n`, - ); - } + 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: + 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 25710633b..fcb8a78cb 100644 --- a/.github/workflows/pr-labels.yml +++ b/.github/workflows/pr-labels.yml @@ -1,17 +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 `concurrency` configuration below ensures that only the latest run -# for the same PR remains active, canceling any previous in-progress -# run. +# 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 @@ -19,45 +11,35 @@ on: pull_request_target: types: [opened, labeled, unlabeled, synchronize] -concurrency: - group: pr-labels-${{ github.event.pull_request.number }} - cancel-in-progress: true - permissions: - pull-requests: read + 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/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/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/architecture/package-structure-full.md b/docs/architecture/package-structure-full.md deleted file mode 100644 index be35bb076..000000000 --- a/docs/architecture/package-structure-full.md +++ /dev/null @@ -1,425 +0,0 @@ -# Package Structure (full) - -``` -📦 easydiffraction -├── 📁 analysis -│ ├── 📁 calculators -│ │ ├── 📄 __init__.py -│ │ ├── 📄 base.py -│ │ │ ├── 🏷️ class PowderReflnRecord -│ │ │ └── 🏷️ 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 -│ │ │ │ ├── 📄 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 -│ │ │ ├── 📁 refln -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 bragg_pd.py -│ │ │ │ │ ├── 🏷️ class PowderReflnBase -│ │ │ │ │ ├── 🏷️ class PowderCwlRefln -│ │ │ │ │ ├── 🏷️ class PowderTofRefln -│ │ │ │ │ ├── 🏷️ class PowderReflnDataBase -│ │ │ │ │ ├── 🏷️ class PowderCwlReflnData -│ │ │ │ │ └── 🏷️ class PowderTofReflnData -│ │ │ │ ├── 📄 bragg_sc.py -│ │ │ │ │ ├── 🏷️ class Refln -│ │ │ │ │ └── 🏷️ class ReflnData -│ │ │ │ └── 📄 factory.py -│ │ │ │ └── 🏷️ class ReflnFactory -│ │ │ └── 📄 __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 _PowderMeasVsCalcSeries -│ │ ├── 🏷️ 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/architecture/package-structure-short.md b/docs/architecture/package-structure-short.md deleted file mode 100644 index ba2a0b33b..000000000 --- a/docs/architecture/package-structure-short.md +++ /dev/null @@ -1,224 +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 -│ │ │ │ ├── 📄 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 -│ │ │ ├── 📁 refln -│ │ │ │ ├── 📄 __init__.py -│ │ │ │ ├── 📄 bragg_pd.py -│ │ │ │ ├── 📄 bragg_sc.py -│ │ │ │ └── 📄 factory.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/dev/adr_analysis-cif-fit-state.md b/docs/dev/adr_analysis-cif-fit-state.md new file mode 100644 index 000000000..c39fc192b --- /dev/null +++ b/docs/dev/adr_analysis-cif-fit-state.md @@ -0,0 +1,220 @@ +# ADR: Analysis CIF Fit State + +**Status:** Proposed +**Date:** 2026-05-13 + +## Context + +`analysis/analysis.cif` currently persists analysis configuration such +as `_fit.minimizer_type`, `_fit.mode`, aliases, constraints, and +joint-fit weights. It does not persist analysis-owned fit state such as +fit bounds, bound provenance, pre-fit scalar snapshots, or latest +fit-status metadata. + +At the same time, parameter CIF serialization already carries the +committed parameter `value`, the current `free` state, and the current +`uncertainty` via CIF bracket notation. That data belongs to the model +and should remain in structure or experiment CIF files. + +The missing piece is analysis-owned fit state: + +- fit controls that apply to parameters during fitting but are not part + of the model itself +- latest fit-status metadata shown by `display.fit_results()` +- deterministic and Bayesian fit metadata that should survive project + reloads and command-line workflows + +This separation matters because: + +- Bayesian plotting after reload needs `fit_min`, `fit_max`, and bound + provenance even when raw posterior arrays are absent +- command-line users need a saved pre-fit starting state to recover from + a poor minimization run +- `analysis.fit_results` already changes by fit type, but its persisted + projection should have a stable analysis-owned home + +The current architecture document still describes fit results as +runtime-only. This ADR proposes a narrower persisted projection of the +latest fit state, not a direct dump of backend runtime objects. + +## Decision + +### 1. `analysis/analysis.cif` becomes the home of analysis-owned fit state + +The analysis CIF file will persist: + +- fit configuration +- aliases and constraints +- joint-fit weights +- per-parameter fit controls owned by analysis +- latest fit-status metadata common to deterministic and Bayesian fits +- fit-type-specific extensions defined in separate ADRs + +Committed parameter values remain in structure or experiment CIF files. +They are not duplicated into `analysis/analysis.cif`. + +### 2. Add a real `_fit_parameter` loop + +Introduce a new analysis-owned loop category: + +```cif +loop_ +_fit_parameter.param_unique_name +_fit_parameter.fit_min +_fit_parameter.fit_max +_fit_parameter.fit_bounds_uncertainty_multiplier +_fit_parameter.start_value +_fit_parameter.start_uncertainty +cosio.atom_site.Co1.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 +cosio.atom_site.Co2.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 +``` + +Fields: + +- `param_unique_name` +- `fit_min` +- `fit_max` +- `fit_bounds_uncertainty_multiplier` +- `start_value` +- `start_uncertainty` + +Rationale: + +- `fit_min` and `fit_max` are required to restore deterministic and + Bayesian fit controls faithfully. +- `fit_bounds_uncertainty_multiplier` preserves the provenance of + uncertainty-derived bounds for restored Bayesian plot annotations. +- `start_value` and `start_uncertainty` capture the last committed + pre-fit scalar state and enable fit recovery workflows, especially in + command-line usage. +- `start_uncertainty` preserves a user-visible pre-fit uncertainty + instead of treating it as disposable fit residue. + +### 3. Add a generic `_fit_result` single-item category + +Persist the latest fit-status metadata shared across fit types in a +single analysis-owned category: + +- `result_kind` +- `success` +- `message` +- `iterations` +- `fitting_time` +- `reduced_chi_square` + +Suggested CIF fragment: + +```cif +_fit_result.result_kind deterministic +_fit_result.success yes +_fit_result.message "Fit converged" +_fit_result.iterations 37 +_fit_result.fitting_time 1.82 +_fit_result.reduced_chi_square 1.031 +``` + +`result_kind` distinguishes the latest persisted fit projection, for +example `deterministic` or `bayesian`. + +### 4. Persist only stable fit-status fields here + +The `_fit_result` category is for generic status fields that are stable +across engines and already belong to the result model. + +It should not persist backend runtime objects or arbitrary engine +payloads. + +Metrics such as R-factors shown by `display.fit_results()` are derived +from observed and calculated data and can be recomputed after load when +needed. They do not need to be part of the first persisted fit-state +schema. + +### 5. Fit-type-specific extensions build on this ADR + +This ADR defines the common `analysis.cif` contract for deterministic +and Bayesian fitting. + +Fit-type-specific extensions are layered on top: + +- Bayesian persistence extends this with `_bayesian_*` categories and an + HDF5 sidecar, as described in `adr_parameter-posterior-summary.md`. +- Future fit-specific summaries should follow the same pattern: generic + shared fields in `_fit_result`, specialized fields in separate + categories. + +### 6. Restore order is analysis-first, fit-type-second + +Load order should be: + +1. standard analysis configuration +2. aliases and constraints +3. joint-fit weights +4. `_fit_parameter` +5. `_fit_result` +6. fit-type-specific extensions such as `_bayesian_*` + +This ensures that generic fit controls are available before restoring +specialized fit summaries. + +### 7. Suggested full `analysis.cif` example + +```cif +_fit.minimizer_type "bumps (dream)" +_fit.mode single + +loop_ +_alias.label +_alias.param_unique_name +biso_Co1 cosio.atom_site.Co1.adp_iso +biso_Co2 cosio.atom_site.Co2.adp_iso + +loop_ +_constraint.expression +"biso_Co2 = biso_Co1" + +loop_ +_fit_parameter.param_unique_name +_fit_parameter.fit_min +_fit_parameter.fit_max +_fit_parameter.fit_bounds_uncertainty_multiplier +_fit_parameter.start_value +_fit_parameter.start_uncertainty +cosio.atom_site.Co1.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 +cosio.atom_site.Co2.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 + +_fit_result.result_kind bayesian +_fit_result.success yes +_fit_result.message "Sampler converged" +_fit_result.iterations 3000 +_fit_result.fitting_time 82.4 +_fit_result.reduced_chi_square 1.031 + +# Bayesian-specific extension categories follow here. +``` + +## Consequences + +### Positive + +- `analysis.cif` becomes the single analysis-owned source of fit state. +- Deterministic and Bayesian persistence share one common base schema. +- Fit bounds, bound provenance, and start values survive project + reloads. +- Command-line workflows gain a persisted pre-fit starting state. + +### Trade-offs + +- The architecture document must be updated because fit state is no + longer entirely runtime-only. +- Analysis persistence becomes more stateful and must be kept in sync + with live parameter objects. +- Some existing serializer assumptions will need refactoring so that + `analysis.cif` owns fit metadata rather than individual parameters. + +## Deferred Work + +- Bayesian-specific categories and HDF5 sidecar details remain in + `adr_parameter-posterior-summary.md`. +- Undo semantics for `start_value` and `start_uncertainty` are defined + in a separate ADR. +- Correlation-matrix persistence is defined in a separate ADR. diff --git a/docs/dev/adr_parameter-correlation-persistence.md b/docs/dev/adr_parameter-correlation-persistence.md new file mode 100644 index 000000000..9c494908b --- /dev/null +++ b/docs/dev/adr_parameter-correlation-persistence.md @@ -0,0 +1,114 @@ +# ADR: Parameter Correlation Persistence + +**Status:** Proposed +**Date:** 2026-05-13 + +## Context + +`plot_param_correlations()` can currently visualize either: + +- deterministic parameter correlations derived from engine covariance +- Bayesian correlations derived from posterior samples + +After project reload, this correlation information is not available +unless the underlying runtime objects are rebuilt. For Bayesian fits, +full posterior samples may not always be restored. For deterministic +fits, engine covariance is typically not persisted at all. + +The correlation matrix is an analysis-owned summary, not model state. It +therefore belongs in `analysis/analysis.cif`, not in structure or +experiment CIF files. + +## Decision + +### 1. Add a `_fit_parameter_correlation` loop category + +Persist pairwise parameter correlations in a new analysis-owned loop: + +- `source_kind` +- `param_unique_name_i` +- `param_unique_name_j` +- `correlation` + +Suggested example: + +```cif +loop_ +_fit_parameter_correlation.source_kind +_fit_parameter_correlation.param_unique_name_i +_fit_parameter_correlation.param_unique_name_j +_fit_parameter_correlation.correlation +posterior cosio.atom_site.Co1.adp_iso cosio.atom_site.Co2.adp_iso 0.87 +``` + +`source_kind` records how the correlation was obtained, for example: + +- `deterministic` +- `posterior` + +### 2. Store only the upper triangle excluding the diagonal + +Each row stores one unordered parameter pair with +`param_unique_name_i < param_unique_name_j` in a stable ordering. + +The diagonal is omitted because it is always 1.0 and can be rebuilt on +load. + +This keeps the CIF loop compact while remaining lossless for the +correlation matrix. + +### 3. Treat the loop as a summary, not a replacement for raw samples + +For Bayesian fits, `_fit_parameter_correlation` is a persisted summary. +It does not replace posterior samples or posterior pair data. + +This means: + +- correlation heatmaps can be restored from the loop alone +- posterior pair plots still require posterior samples + +### 4. Deterministic and Bayesian fits share the same loop schema + +The same loop category is used for both deterministic and Bayesian fit +results. The distinction is carried by `source_kind`, not by separate +loop names. + +### 5. Suggested restore behavior + +On load: + +- if `_fit_parameter_correlation` is present, correlation summaries are + restored into a lightweight analysis-owned correlation structure +- if it is absent, correlation plots fall back to whatever live runtime + information is available + +### 6. Suggested user-facing behavior + +```python +# Restored from analysis.cif when available +project.display.plotter.plot_param_correlations() +``` + +If only the correlation loop is restored, the user still gets the +correlation heatmap without needing raw posterior samples. + +## Consequences + +### Positive + +- Deterministic and Bayesian correlation summaries survive reload. +- Correlation heatmaps no longer depend entirely on runtime-only data. +- The schema is compact and fit-type-agnostic. + +### Trade-offs + +- The correlation loop is a derived summary, so it must be kept in sync + with the latest fit result. +- Restored correlation data is not enough for posterior pair plots or + predictive summaries. + +## 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/adr_parameter-posterior-summary.md b/docs/dev/adr_parameter-posterior-summary.md new file mode 100644 index 000000000..dd7999ba1 --- /dev/null +++ b/docs/dev/adr_parameter-posterior-summary.md @@ -0,0 +1,681 @@ +# ADR: Parameter-Level Posterior Projection and Bayesian Persistence + +**Status:** Proposed +**Date:** 2026-05-13 + +## 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 current +architecture document describes this state as runtime-only and not +serialized. + +`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`, +`map_estimate`, `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 later persistence. + +The summary object currently provides the right level of detail: + +- `map_estimate` +- `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: + +- `map_estimate` -> `MAP estimate` +- `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: + map_estimate = param.posterior.map_estimate + 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 MAP to `parameter.value` after Bayesian fits + +After a posterior-capable fit, `parameter.value` is committed from the +maximum-a-posteriori estimate. + +MAP 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. Persist canonical Bayesian state at analysis level + +Canonical Bayesian state is owned by `analysis.fit_results`, not by +individual parameters. + +When the active result is a `BayesianFitResults` instance, persistence +must save enough data to restore two distinct capability levels: + +- summary-only restore for parameter inspection and tables +- full restore for posterior plots and predictive plots + +`parameter.posterior` is never serialized as a per-parameter property. +It is always rebuilt from the canonical analysis-level persisted data. + +### 11. Persist fit-control and Bayesian metadata in `analysis/analysis.cif` + +The existing `analysis/analysis.cif` file remains the text metadata +entry point for analysis persistence. + +The persisted fit-control and Bayesian metadata is split into explicit +CIF categories. + +#### 11.1 `_fit_parameter` loop + +Stores analysis-owned per-parameter fit metadata that is not currently +covered by parameter CIF serialization. + +This loop exists because the committed parameter CIF representation +already carries the active `value`, current `free` state, and current +`uncertainty`, but it does not carry fit bounds, bound provenance, or +the pre-fit uncertainty snapshot needed by undo. Those fields are +required for Bayesian plot ranges, pair-plot bound annotations, and +clean fit rollback after project reload. + +Fields: + +- `param_unique_name` +- `fit_min` +- `fit_max` +- `fit_bounds_uncertainty_multiplier` +- `start_value` +- `start_uncertainty` + +`fit_min` and `fit_max` are required for restored Bayesian plotting. +`fit_bounds_uncertainty_multiplier` is required if restored plots should +preserve the uncertainty-derived bound annotation exactly. `start_value` +and `start_uncertainty` are required for clean cross-session undo. If +omitted, restored fit reports may show `N/A` for `start` and `change`, +and undo may need to clear uncertainty as a compatibility fallback. + +#### 11.2 `_bayesian_result` single item + +Stores one saved Bayesian result header with these fields: + +- `schema_version` +- `sampler_name` +- `point_estimate_name` +- `success` +- `sampler_completed` +- `reduced_chi_square` +- `fitting_time` +- `best_log_posterior` +- `credible_interval_inner` +- `credible_interval_outer` +- `has_posterior_samples` +- `has_posterior_predictive` +- `sidecar_file` + +For the current design, `point_estimate_name` is always `map`. + +#### 11.3 `_bayesian_sampler` single item + +Stores resolved sampler settings actually used for the run: + +- `steps` +- `burn` +- `thin` +- `pop` +- `parallel` +- `init` +- `random_seed` + +This persists the existing runtime `sampler_settings` in an explicit, +schema-driven form rather than as an open-ended key/value mapping. + +#### 11.4 `_bayesian_convergence` single item + +Stores top-level convergence and shape metadata: + +- `converged` +- `max_r_hat` +- `min_ess_bulk` +- `n_draws` +- `n_chains` +- `n_parameters` + +Per-parameter `r_hat` and `ess_bulk` remain in the parameter summary +loop described below. + +#### 11.5 `_bayesian_parameter_posterior` loop + +Stores one canonical posterior summary row per sampled parameter. These +rows are the source used to rebuild `parameter.posterior` on load. + +Fields: + +- `order_index` +- `unique_name` +- `display_name` +- `map_estimate` +- `median` +- `uncertainty` +- `interval_68_lower` +- `interval_68_upper` +- `interval_95_lower` +- `interval_95_upper` +- `ess_bulk` +- `r_hat` + +`order_index` defines the parameter order used by posterior sample +columns in the sidecar arrays. + +#### 11.6 `_bayesian_predictive_dataset` loop + +Stores one manifest row per persisted posterior predictive summary. + +Fields: + +- `experiment_name` +- `x_axis_name` +- `x_path` +- `map_prediction_path` +- `lower_95_path` +- `upper_95_path` +- `lower_68_path` +- `upper_68_path` +- `draws_path` +- `n_x` +- `n_draws_cached` + +This loop tells the loader which arrays to read from the sidecar file +for each experiment-level predictive summary. + +#### 11.7 Suggested CIF fragments + +The active parameter value remains in the structure or experiment CIF as +it does today, for example: + +```cif +_atom_site_U_iso_or_equiv 0.0319(21) +``` + +Analysis-owned fit-control and Bayesian metadata then lives in +`analysis/analysis.cif`, for example: + +```cif +_fit.minimizer_type "bumps (dream)" +_fit.mode single + +loop_ +_fit_parameter.param_unique_name +_fit_parameter.fit_min +_fit_parameter.fit_max +_fit_parameter.fit_bounds_uncertainty_multiplier +_fit_parameter.start_value +_fit_parameter.start_uncertainty +cosio.atom_site.Co1.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 +cosio.atom_site.Co2.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 + +_bayesian_result.schema_version 1 +_bayesian_result.sampler_name dream +_bayesian_result.point_estimate_name map +_bayesian_result.success yes +_bayesian_result.sampler_completed yes +_bayesian_result.reduced_chi_square 1.031 +_bayesian_result.fitting_time 82.4 +_bayesian_result.best_log_posterior -1542.77 +_bayesian_result.credible_interval_inner 0.68 +_bayesian_result.credible_interval_outer 0.95 +_bayesian_result.has_posterior_samples yes +_bayesian_result.has_posterior_predictive yes +_bayesian_result.sidecar_file "bayesian_data.h5" + +_bayesian_sampler.steps 3000 +_bayesian_sampler.burn 600 +_bayesian_sampler.thin 1 +_bayesian_sampler.pop 20 +_bayesian_sampler.parallel 0 +_bayesian_sampler.init lhs +_bayesian_sampler.random_seed 12345 + +_bayesian_convergence.converged yes +_bayesian_convergence.max_r_hat 1.01 +_bayesian_convergence.min_ess_bulk 812.4 +_bayesian_convergence.n_draws 2400 +_bayesian_convergence.n_chains 20 +_bayesian_convergence.n_parameters 2 + +loop_ +_bayesian_parameter_posterior.order_index +_bayesian_parameter_posterior.unique_name +_bayesian_parameter_posterior.display_name +_bayesian_parameter_posterior.map_estimate +_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 +0 cosio.atom_site.Co1.adp_iso "Co1 ADP" 0.0319 0.0317 0.0021 0.0298 0.0339 0.0278 0.0361 812.4 1.01 +1 cosio.atom_site.Co2.adp_iso "Co2 ADP" 0.0320 0.0318 0.0020 0.0300 0.0338 0.0281 0.0359 830.7 1.00 + +loop_ +_bayesian_predictive_dataset.experiment_name +_bayesian_predictive_dataset.x_axis_name +_bayesian_predictive_dataset.x_path +_bayesian_predictive_dataset.map_prediction_path +_bayesian_predictive_dataset.lower_95_path +_bayesian_predictive_dataset.upper_95_path +_bayesian_predictive_dataset.lower_68_path +_bayesian_predictive_dataset.upper_68_path +_bayesian_predictive_dataset.draws_path +_bayesian_predictive_dataset.n_x +_bayesian_predictive_dataset.n_draws_cached +hrpt ttheta /predictive/hrpt/x /predictive/hrpt/map_prediction /predictive/hrpt/lower_95 /predictive/hrpt/upper_95 /predictive/hrpt/lower_68 /predictive/hrpt/upper_68 /predictive/hrpt/draws 2500 200 +``` + +### 12. Persist bulk arrays in `analysis/bayesian_data.h5` + +Large numerical arrays are stored outside CIF text in a single sidecar +file: + +- `analysis/bayesian_data.h5` + +The sidecar is optional. Summary-only restore remains valid without it. + +HDF5 is selected instead of NPZ because the persisted Bayesian payload +is a structured collection of named datasets with heterogeneous shapes, +optional groups, and potentially large predictive arrays. HDF5 provides +explicit hierarchical storage, dataset metadata, selective reads, and a +better long-term path for compression or chunking. NPZ is simpler, but +it is flatter and less suitable once the saved Bayesian state grows +beyond a small set of arrays. + +#### 12.1 Required core HDF5 dataset paths when posterior samples are saved + +- `posterior_parameter_samples` +- `posterior_log_posterior` +- `posterior_draw_index` + +Expected array shapes: + +- `posterior_parameter_samples`: `(n_draws, n_chains, n_parameters)` +- `posterior_log_posterior`: `(n_draws, n_chains)` when available +- `posterior_draw_index`: `(n_draws,)` when available + +If `posterior_log_posterior` or `posterior_draw_index` are unavailable, +their corresponding header flags remain false and the arrays may be +omitted. + +#### 12.2 Predictive dataset keys + +Posterior predictive arrays are addressed through the +`_bayesian_predictive_dataset` manifest rather than inferred from file +ordering. + +Recommended HDF5 dataset naming is: + +- `predictive____x` +- `predictive____map_prediction` +- `predictive____lower_95` +- `predictive____upper_95` +- `predictive____lower_68` +- `predictive____upper_68` +- `predictive____draws` + +The manifest, not the naming convention, is the source of truth. + +Recommended HDF5 group layout is: + +- `/posterior/parameter_samples` +- `/posterior/log_posterior` +- `/posterior/draw_index` +- `/predictive//x` +- `/predictive//map_prediction` +- `/predictive//lower_95` +- `/predictive//upper_95` +- `/predictive//lower_68` +- `/predictive//upper_68` +- `/predictive//draws` + +The manifest remains the canonical mapping used by the loader, so this +layout is recommended rather than mandatory. + +#### 12.3 What is not persisted in the sidecar + +Do not persist backend-specific runtime objects such as `engine_result`, +the DREAM driver, or ArviZ `InferenceData`. + +Those objects can be rebuilt from canonical saved arrays when needed, or +left unavailable after load. + +### 13. Restore flow and partial-availability policy + +Persistence must support both full and partial restore. + +#### 13.1 Save flow + +When `Project.save()` sees `analysis.fit_results` as a +`BayesianFitResults` instance: + +1. It writes standard analysis configuration to `analysis/analysis.cif`. +2. It appends `_fit_parameter`, `_bayesian_result`, `_bayesian_sampler`, + `_bayesian_convergence`, and `_bayesian_parameter_posterior`. +3. If posterior predictive summaries are available, it also writes the + `_bayesian_predictive_dataset` manifest. +4. If posterior sample arrays or predictive arrays are available, it + writes `analysis/bayesian_data.h5`. + +#### 13.2 Load flow + +When `Project.load()` restores `analysis/analysis.cif`: + +1. Standard analysis configuration is restored first. +2. If `_fit_parameter` is present, fit bounds, bound provenance, and + optional pre-fit scalar snapshots are restored by matching + `param_unique_name` to live parameters. +3. If Bayesian categories are present, a lightweight + `BayesianFitResults` instance is rebuilt from persisted metadata and + parameter summary rows. +4. `parameter.posterior` is rebuilt by matching summary rows to live + parameters via `unique_name`. +5. `parameter.value` and `parameter.uncertainty` continue to come from + the normal project serialization path; Bayesian restore does not + overwrite them. +6. If `analysis/bayesian_data.h5` exists and matches the manifest, + `posterior_samples` and `posterior_predictive` are restored. +7. If the sidecar is missing or incomplete, the restore degrades to + summary-only mode without failing the whole project load. + +#### 13.3 Partial restore behavior + +The chosen partial-restore policy is: + +- `parameter.posterior` and `display.fit_results()` remain available + from saved metadata and summaries. +- Bayesian-only plots requiring canonical posterior arrays remain + unavailable unless those arrays were restored successfully. +- Missing sidecar data should produce a clear warning, not a hard load + failure. + +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.map_estimate) + print(posterior.uncertainty) + print(posterior.interval_68) + +project.analysis.display.fit_results() +``` + +Example plotting behavior after load: + +```python +# Works with summary-only restore +project.analysis.display.fit_results() + +# Requires canonical posterior arrays from analysis/bayesian_data.h5 +project.display.plotter.plot_posterior_pairs() +project.display.plotter.plot_param_distribution(param) +project.display.plotter.plot_posterior_predictive(expt_name='hrpt') +``` + +### 14. Keep parameter posterior data rebuilt, not duplicated + +`parameter.posterior` is always rebuilt from the canonical +`_bayesian_parameter_posterior` loop in `analysis/analysis.cif`. + +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 source for full Bayesian + state. +- `analysis/analysis.cif` becomes the home for fit-control metadata that + does not belong in structure or experiment CIF files. +- Bayesian save/load gains a clear split between text metadata in + `analysis/analysis.cif` and bulk numerical arrays in + `analysis/bayesian_data.h5`. +- Partial restore works for summaries even when full posterior arrays + are absent. +- 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. +- Bayesian persistence now spans both CIF metadata and a binary sidecar, + so save/load code must validate consistency between the two. + +## 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 now defines persistence for one canonical saved Bayesian run. + +It still defers: + +- support for multiple saved Bayesian runs per project +- plot-ready cache layers beyond canonical posterior and predictive data +- persistence-time compression or chunking strategies beyond the first + HDF5 sidecar implementation +- 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 restored canonical + Bayesian data rather than serialized redundantly at parameter level. +- The sidecar reader and writer should be isolated behind explicit + serializer helpers instead of being implemented inline in + `Project.save()` and `Project.load()`. + +## Chosen Defaults + +- `parameter.value` remains committed to MAP after posterior fits. +- If a project is loaded without full posterior arrays, restoring only + `parameter.posterior` is acceptable for table display and parameter + inspection. +- Posterior plotting remains unavailable unless the canonical Bayesian + containers needed by those plots are also restored. diff --git a/docs/dev/adr_undo-fit.md b/docs/dev/adr_undo-fit.md new file mode 100644 index 000000000..299741308 --- /dev/null +++ b/docs/dev/adr_undo-fit.md @@ -0,0 +1,162 @@ +# ADR: Undo Fit + +**Status:** Proposed +**Date:** 2026-05-13 + +## Context + +The new `_fit_parameter.start_value` and +`_fit_parameter.start_uncertainty` fields in `analysis/analysis.cif` +capture the last committed pre-fit scalar state for each fitted +parameter. This is useful when a minimization run produces a poor result +and the user wants to return to the state from immediately before the +fit. + +This need is especially important in command-line workflows, where the +user may save a project after a bad fit and reopen it later expecting a +simple way to roll back to the pre-fit state. + +However, these snapshots alone do not define undo semantics. The API +owner, rollback scope, and interaction with fit-derived metadata must be +explicit. + +## Decision + +### 1. Add an analysis-owned `undo_fit()` operation + +The rollback operation belongs on `Analysis`, for example: + +```python +project.analysis.undo_fit() +``` + +`Analysis` owns the fit lifecycle, fit metadata, and persisted +`analysis.cif` state, so it is the correct owner for this operation. + +### 2. Initial undo scope is scalar rollback plus posterior clear + +The first undo implementation restores each fitted parameter's saved +pre-fit scalar state and clears fit state that belongs only to the +discarded fit. + +It does not attempt to restore every possible runtime detail of the +previous fit result. + +After `undo_fit()`: + +- `parameter.value` is restored from `_fit_parameter.start_value` +- `parameter.uncertainty` is restored from + `_fit_parameter.start_uncertainty` +- `parameter.posterior` is cleared +- `analysis.fit_results` is cleared + +This gives the user a safe, predictable return to the pre-fit visible +parameter state without pretending to restore a full historical result. + +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 are analysis configuration, not fit output. + +### 4. Undo is single-level for now + +Only the latest saved pre-fit state is addressable. + +The initial API does not create a stack of historical fits. Supporting +multiple undo levels would require a dedicated snapshot history design +and is deferred. + +### 5. Persisted scalar snapshots are the rollback anchors + +The minimum persisted state required for clean cross-session undo is the +pair of `_fit_parameter.start_value` and +`_fit_parameter.start_uncertainty` defined in +`adr_analysis-cif-fit-state.md`. + +If a parameter has no saved `start_value`, `undo_fit()` leaves that +parameter unchanged. + +If a parameter has no saved `start_uncertainty`, `undo_fit()` may clear +that parameter's uncertainty as a compatibility fallback for older saved +projects. + +### 6. Suggested user flow + +```python +project.analysis.fit() + +# Decide that the latest fit should be discarded. +project.analysis.undo_fit() + +# Save the recovered state if desired. +project.save() +``` + +### 7. Add a top-level `undo-fit` CLI command + +Because command-line recovery is one of the main motivations for this +feature, undo must also be exposed through the existing top-level CLI. + +Suggested command: + +```bash +python -m easydiffraction undo-fit PROJECT_DIR +``` + +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 perform the rollback in memory without overwriting + project files +- emit a clear message describing whether the latest fit snapshot was + successfully discarded + +Suggested dry-run form: + +```bash +python -m easydiffraction undo-fit PROJECT_DIR --dry +``` + +If the project does not contain a usable undo snapshot, the command +should fail with a clear non-zero exit status instead of silently doing +nothing. + +## Consequences + +### Positive + +- Users gain a simple recovery path after a poor fit. +- The feature works naturally with saved projects and both Python and + command-line workflows. +- The initial scope stays small and does not require full historical fit + snapshots. + +### Trade-offs + +- Undo restores pre-fit scalar parameter state, not a full historical + `fit_results` object. +- Older saved projects that do not carry `start_uncertainty` may still + fall back to clearing uncertainty. +- Multi-level undo remains unsupported. + +## Deferred Work + +- exact restoration of previous posterior-derived projections beyond the + scalar parameter snapshot +- multi-level undo / redo +- user-facing confirmation or preview APIs +- rollback of fit-type-specific persisted summaries beyond parameter + values diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 390aa77bd..48a411619 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -527,28 +527,28 @@ 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` | +| 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`, `BumpsDreamMinimizer`, `BumpsAmoebaMinimizer`, `BumpsDEMinimizer` | > **Note:** `ExperimentFactory` and `StructureFactory` are _builder_ > factories with `from_cif_path`, `from_cif_str`, `from_data_path`, and @@ -675,6 +675,7 @@ the choice. | `dfols` | `DfolsMinimizer` | | `bumps` | `BumpsMinimizer` | | `bumps (lm)` | `BumpsLmMinimizer` | +| `bumps (dream)` | `BumpsDreamMinimizer` | | `bumps (amoeba)` | `BumpsAmoebaMinimizer` | | `bumps (de)` | `BumpsDEMinimizer` | @@ -825,14 +826,45 @@ workflow: only `single`; >1 → all three). - Joint-fit weights: `joint_fit_experiments` (`CategoryCollection` of per-experiment weight entries); sibling of `fit`, not a child. +- Fit results: `analysis.fit_results` stores the latest runtime result + object. This is `FitResults` for deterministic fits and + `BayesianFitResults` for Bayesian DREAM runs. - 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. + to `'sequential'` internally). `fit()` accepts optional `random_seed` + for stochastic minimizers; deterministic minimizers reject non-`None` + seeds. `display.fit_results()` dispatches through the active runtime + result object. - Aliases and constraints (single-type categories; no public `_type` getter or setter) +#### 6.4.1 Bayesian DREAM Runtime Results + +Bayesian sampling is integrated as a normal minimizer selection with tag +`'bumps (dream)'`. It does not create a parallel `Analysis` stack or a +new persisted results category. + +- `BayesianFitResults` extends `FitResults` with runtime-only posterior + state such as `posterior_samples`, `posterior_parameter_summaries`, + `posterior_predictive`, `diagnostics`, and `sampler_settings`. +- Posterior arrays and predictive caches remain runtime-only; they are + not serialized into CIF or project directories. +- `sampler_settings` records the resolved stochastic settings actually + used for the run, including `random_seed`, `steps`, `burn`, `thin`, + `pop`, and `parallel`. +- The current user-facing DREAM controls live on the active minimizer + object, for example `project.analysis.fit.minimizer.steps`, `burn`, + `thin`, `pop`, `parallel`, and `init`. +- `plot_param_correlations()` uses posterior samples when available and + otherwise falls back to deterministic covariance or engine-derived + correlations. +- Bayesian-only plotting methods are exposed explicitly rather than by + overloading deterministic plot calls: `plot_posterior_pairs()`, + `plot_param_distribution(param)`, and + `plot_posterior_predictive(expt_name, ...)`. + --- ## 7. Project — The Top-Level Façade @@ -894,7 +926,9 @@ project_dir/ 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`. +`_fit.mode`) lives in `analysis/analysis.cif`. Runtime fit outputs, +including `analysis.fit_results`, posterior chains, posterior predictive +summaries, and convergence diagnostics, are not serialized. ### 7.3 Verbosity @@ -1052,6 +1086,27 @@ project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True) project.save() ``` +### 8.4.1 Bayesian Refinement + +```python +# Deterministic pre-fit remains explicit +project.analysis.fit.minimizer_type = 'bumps (lm)' +project.analysis.fit() + +# Switch to Bayesian sampling using the same entry point +project.analysis.fit.minimizer_type = 'bumps (dream)' +project.analysis.fit.minimizer.steps = 1000 +project.analysis.fit.minimizer.parallel = 0 +project.analysis.fit(random_seed=11) + +# Runtime-only Bayesian summaries and plots +project.analysis.display.fit_results() +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') +``` + ### 8.5 TOF Experiment (tutorial ed-7) ```python diff --git a/docs/dev/issues_open.md b/docs/dev/issues_open.md index 014aa4675..21976afde 100644 --- a/docs/dev/issues_open.md +++ b/docs/dev/issues_open.md @@ -180,6 +180,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 diff --git a/docs/dev/package-structure-full.md b/docs/dev/package-structure-full.md index ae233b21d..8083f39b0 100644 --- a/docs/dev/package-structure-full.md +++ b/docs/dev/package-structure-full.md @@ -6,6 +6,7 @@ │ ├── 📁 calculators │ │ ├── 📄 __init__.py │ │ ├── 📄 base.py +│ │ │ ├── 🏷️ class PowderReflnRecord │ │ │ └── 🏷️ class CalculatorBase │ │ ├── 📄 crysfml.py │ │ │ └── 🏷️ class CrysfmlCalculator @@ -48,10 +49,16 @@ │ │ └── 📄 __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 SamplerProgressUpdate │ │ ├── 🏷️ class _TerminalLiveHandle │ │ └── 🏷️ class FitProgressTracker │ ├── 📁 minimizers @@ -65,12 +72,18 @@ │ │ │ └── 🏷️ 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 @@ -169,9 +182,6 @@ │ │ │ │ │ ├── 🏷️ class PdDataBase │ │ │ │ │ ├── 🏷️ class PdCwlData │ │ │ │ │ └── 🏷️ class PdTofData -│ │ │ │ ├── 📄 bragg_sc.py -│ │ │ │ │ ├── 🏷️ class Refln -│ │ │ │ │ └── 🏷️ class ReflnData │ │ │ │ ├── 📄 factory.py │ │ │ │ │ └── 🏷️ class DataFactory │ │ │ │ └── 📄 total_pd.py @@ -257,6 +267,20 @@ │ │ │ │ │ └── 🏷️ class TotalGaussianDampedSinc │ │ │ │ └── 📄 total_mixins.py │ │ │ │ └── 🏷️ class TotalBroadeningMixin +│ │ │ ├── 📁 refln +│ │ │ │ ├── 📄 __init__.py +│ │ │ │ ├── 📄 bragg_pd.py +│ │ │ │ │ ├── 🏷️ class PowderReflnBase +│ │ │ │ │ ├── 🏷️ class PowderCwlRefln +│ │ │ │ │ ├── 🏷️ class PowderTofRefln +│ │ │ │ │ ├── 🏷️ class PowderReflnDataBase +│ │ │ │ │ ├── 🏷️ class PowderCwlReflnData +│ │ │ │ │ └── 🏷️ class PowderTofReflnData +│ │ │ │ ├── 📄 bragg_sc.py +│ │ │ │ │ ├── 🏷️ class Refln +│ │ │ │ │ └── 🏷️ class ReflnData +│ │ │ │ └── 📄 factory.py +│ │ │ │ └── 🏷️ class ReflnFactory │ │ │ └── 📄 __init__.py │ │ ├── 📁 item │ │ │ ├── 📄 __init__.py @@ -352,7 +376,13 @@ │ │ └── 🏷️ class RendererFactoryBase │ ├── 📄 plotting.py │ │ ├── 🏷️ class PlotterEngineEnum +│ │ ├── 🏷️ class PosteriorPairPlotStyleEnum │ │ ├── 🏷️ class _MeasVsCalcPlotOptions +│ │ ├── 🏷️ class _PowderMeasVsCalcSeries +│ │ ├── 🏷️ class _PosteriorDistributionContext +│ │ ├── 🏷️ class _PosteriorPairsContext +│ │ ├── 🏷️ class _CorrelationHeatmapContext +│ │ ├── 🏷️ class _PosteriorPairsLegendState │ │ ├── 🏷️ class Plotter │ │ └── 🏷️ class PlotterFactory │ ├── 📄 tables.py diff --git a/docs/dev/package-structure-short.md b/docs/dev/package-structure-short.md index 30b4daf72..9ce283eb5 100644 --- a/docs/dev/package-structure-short.md +++ b/docs/dev/package-structure-short.md @@ -31,6 +31,7 @@ │ │ └── 📄 __init__.py │ ├── 📁 fit_helpers │ │ ├── 📄 __init__.py +│ │ ├── 📄 bayesian.py │ │ ├── 📄 metrics.py │ │ ├── 📄 reporting.py │ │ └── 📄 tracking.py @@ -40,6 +41,7 @@ │ │ ├── 📄 bumps.py │ │ ├── 📄 bumps_amoeba.py │ │ ├── 📄 bumps_de.py +│ │ ├── 📄 bumps_dream.py │ │ ├── 📄 bumps_lm.py │ │ ├── 📄 dfols.py │ │ ├── 📄 enums.py @@ -85,7 +87,6 @@ │ │ │ ├── 📁 data │ │ │ │ ├── 📄 __init__.py │ │ │ │ ├── 📄 bragg_pd.py -│ │ │ │ ├── 📄 bragg_sc.py │ │ │ │ ├── 📄 factory.py │ │ │ │ └── 📄 total_pd.py │ │ │ ├── 📁 diffrn @@ -128,6 +129,11 @@ │ │ │ │ ├── 📄 tof_mixins.py │ │ │ │ ├── 📄 total.py │ │ │ │ └── 📄 total_mixins.py +│ │ │ ├── 📁 refln +│ │ │ │ ├── 📄 __init__.py +│ │ │ │ ├── 📄 bragg_pd.py +│ │ │ │ ├── 📄 bragg_sc.py +│ │ │ │ └── 📄 factory.py │ │ │ └── 📄 __init__.py │ │ ├── 📁 item │ │ │ ├── 📄 __init__.py diff --git a/docs/docs/installation-and-setup/index.md b/docs/docs/installation-and-setup/index.md index 41036f1d9..ee86679ba 100644 --- a/docs/docs/installation-and-setup/index.md +++ b/docs/docs/installation-and-setup/index.md @@ -254,8 +254,8 @@ once using the command line, as shown below. ```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/ ``` @@ -278,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/tutorials/ed-13.ipynb b/docs/docs/tutorials/ed-13.ipynb index 4d605962c..69eae9aa9 100644 --- a/docs/docs/tutorials/ed-13.ipynb +++ b/docs/docs/tutorials/ed-13.ipynb @@ -2665,7 +2665,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-21.ipynb b/docs/docs/tutorials/ed-21.ipynb new file mode 100644 index 000000000..405bc8589 --- /dev/null +++ b/docs/docs/tutorials/ed-21.ipynb @@ -0,0 +1,730 @@ +{ + "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." + ] + }, + { + "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", + "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": "7", + "metadata": {}, + "outputs": [], + "source": [ + "project.structures.create(name='lbco')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "structure = project.structures['lbco']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "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": "10", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a = 3.88" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "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": "12", + "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": "13", + "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": "14", + "metadata": {}, + "source": [ + "#### Download the Measured Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "data_path = ed.download_data(id=3, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "#### Create the Experiment Object" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "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": "18", + "metadata": {}, + "outputs": [], + "source": [ + "experiment = project.experiments['hrpt']" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "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": "20", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.instrument.setup_wavelength = 1.494\n", + "experiment.instrument.calib_twotheta_offset = 0.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "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": "22", + "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": "23", + "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": "24", + "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": "25", + "metadata": {}, + "source": [ + "#### Link the Structural Phase to the Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_phases.create(id='lbco', scale=9.1351)" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "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": "28", + "metadata": {}, + "outputs": [], + "source": [ + "structure.cell.length_a.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "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": "30", + "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": "31", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit.show_minimizer_types()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit.minimizer_type = 'bumps (lm)'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.display.fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "35", + "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": "36", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.plotter.plot_param_correlations()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + ] + }, + { + "cell_type": "markdown", + "id": "38", + "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", + "Default `multiplier` is 8 to give a wide range for the sampler to\n", + "explore, but here we use 3 to speed up the tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.display.free_params()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": {}, + "outputs": [], + "source": [ + "for param in project.free_parameters:\n", + " param.set_fit_bounds_from_uncertainty(multiplier=3.5)" + ] + }, + { + "cell_type": "markdown", + "id": "41", + "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": "42", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.display.free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "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 default\n", + "also uses `parallel=0`, which tells BUMPS DREAM to use all available\n", + "CPUs for population evaluations.\n", + "\n", + "The default `steps` value is 1000, and real analyses often need more\n", + "to achieve good convergence and posterior sampling. Here we use a much\n", + "smaller value to keep the tutorial fast, but this is not recommended\n", + "for production analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit.show_minimizer_types()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit.minimizer_type = 'bumps (dream)'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "46", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit.minimizer.steps = 100 # lower than the default 1000" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "markdown", + "id": "48", + "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": "49", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.display.fit_results()" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.plotter.plot_param_correlations()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.plotter.plot_posterior_pairs()" + ] + }, + { + "cell_type": "markdown", + "id": "53", + "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": "54", + "metadata": {}, + "outputs": [], + "source": [ + "for param in project.free_parameters:\n", + " project.display.plotter.plot_param_distribution(param)" + ] + }, + { + "cell_type": "markdown", + "id": "55", + "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": "56", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.plotter.plot_posterior_predictive(expt_name='hrpt')" + ] + }, + { + "cell_type": "markdown", + "id": "57", + "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": "58", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.plotter.plot_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..d91173c37 --- /dev/null +++ b/docs/docs/tutorials/ed-21.py @@ -0,0 +1,354 @@ +# %% [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. + +# %% +project = ed.Project() + +# %% [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.fit.show_minimizer_types() + +# %% +project.analysis.fit.minimizer_type = 'bumps (lm)' + +# %% +project.analysis.fit() + +# %% +project.analysis.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.plotter.plot_param_correlations() + +# %% +project.display.plotter.plot_meas_vs_calc(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.analysis.display.free_params() + +# %% [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.analysis.display.free_params() + +# %% [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.fit.show_minimizer_types() + +# %% +project.analysis.fit.minimizer_type = 'bumps (dream)' + +# %% +project.analysis.fit.minimizer.steps = 300 # lower than the default 3000 + +# %% +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.analysis.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.plotter.plot_param_correlations() + +# %% +project.display.plotter.plot_posterior_pairs() + +# %% [markdown] +# The one-dimensional posterior distributions below make it easier to +# inspect individual parameters in isolation, including asymmetry or +# multimodality. + +# %% +for param in project.free_parameters: + project.display.plotter.plot_param_distribution(param) + +# %% [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.plotter.plot_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.plotter.plot_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..cc880314e --- /dev/null +++ b/docs/docs/tutorials/ed-22.ipynb @@ -0,0 +1,427 @@ +{ + "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." + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_crystal.id = 'tbti'\n", + "experiment.linked_crystal.scale = 1.0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.instrument.setup_wavelength = 0.793" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.extinction.mosaicity = 35000\n", + "experiment.extinction.radius = 10" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "## Step 4: Run an Initial Local Refinement" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "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": "19", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_crystal.scale.free = True\n", + "experiment.extinction.radius.free = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit.show_minimizer_types()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.display.fit_results()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.plotter.plot_param_correlations()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.plotter.plot_meas_vs_calc(expt_name='heidi')" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "## Step 5: Prepare for Bayesian Sampling" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.display.free_params()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "for param in project.free_parameters:\n", + " param.set_fit_bounds_from_uncertainty(multiplier=1.5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.display.free_params()" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "## Step 6: Configure and Run BUMPS-DREAM" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit.show_minimizer_types()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit.minimizer_type = 'bumps (dream)'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit.minimizer.steps = 500" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.display.fit_results()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.plotter.plot_param_correlations()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.plotter.plot_posterior_pairs()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": {}, + "outputs": [], + "source": [ + "for param in project.free_parameters:\n", + " project.display.plotter.plot_param_distribution(param)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.plotter.plot_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..aaa727180 --- /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.fit.show_minimizer_types() + +# %% +project.analysis.fit() + +# %% [markdown] +# The fit-results display summarizes the locally refined values and their +# estimated uncertainties. + +# %% +project.analysis.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.plotter.plot_param_correlations() + +# %% +project.display.plotter.plot_meas_vs_calc(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.analysis.display.free_params() + +# %% [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.analysis.display.free_params() + +# %% [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.fit.show_minimizer_types() + +# %% +project.analysis.fit.minimizer_type = 'bumps (dream)' + +# %% +project.analysis.fit.minimizer.steps = 500 # lower than the default 3000 + +# %% +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.analysis.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.plotter.plot_param_correlations() + +# %% +project.display.plotter.plot_posterior_pairs() + +# %% [markdown] +# The one-dimensional posterior distributions below make it easier to +# inspect individual parameters in isolation, including asymmetry or +# multimodality. + +# %% +for param in project.free_parameters: + project.display.plotter.plot_param_distribution(param) + +# %% [markdown] +# Finally, the posterior predictive plot propagates the sampled +# parameter uncertainty into the calculated single-crystal reflection +# intensities. + +# %% +project.display.plotter.plot_posterior_predictive(expt_name='heidi') 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..a11c73e60 100644 --- a/docs/docs/tutorials/index.md +++ b/docs/docs/tutorials/index.md @@ -98,6 +98,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/mkdocs.yml b/docs/mkdocs.yml index 45c2ce5fb..16091a5d6 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 @@ -214,6 +216,9 @@ 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 - API Reference: @@ -229,5 +234,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 + - Command-Line: + - Command-Line: cli/index.md diff --git a/pixi.lock b/pixi.lock index 4ff7fdb61..f1532e2b2 100644 --- a/pixi.lock +++ b/pixi.lock @@ -1,4 +1,8 @@ -version: 6 +version: 7 +platforms: +- name: linux-64 +- name: osx-arm64 +- name: win-64 environments: default: channels: @@ -6,49 +10,91 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple - options: - pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_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.4.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 @@ -71,47 +117,14 @@ environments: - 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.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/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.1-h0c1763c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-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/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/linux-64/msgspec-0.21.1-py314h5bd0f2a_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 @@ -121,29 +134,23 @@ environments: - 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.0-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 @@ -152,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.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/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/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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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/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/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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.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/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/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/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/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/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/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/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/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/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/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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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/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/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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.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/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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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/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/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/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-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/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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/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.4.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 @@ -409,43 +406,14 @@ environments: - 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.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_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.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.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/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/osx-arm64/msgspec-0.21.1-py314h6c2aa35_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 @@ -455,31 +423,23 @@ environments: - 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.0-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 @@ -488,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.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/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: . + - 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/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/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/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/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/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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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/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/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.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/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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-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/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/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/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/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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/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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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/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/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/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/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/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/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/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.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/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/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/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/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/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-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/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/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/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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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.4.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 @@ -741,36 +745,14 @@ environments: - 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.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.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.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.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/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/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/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 @@ -779,286 +761,361 @@ environments: - 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.0-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-2023.0.0-ha3553a1_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/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.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/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.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.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_905.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_905.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-ha3553a1_1.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_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - - conda: https://conda.anaconda.org/conda-forge/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/0c/53/b50773ecf1d1e4a5858ee13011e30317ba02639ae4a1411a34967951fc9b/crysfml-0.6.2-cp314-cp314-win_amd64.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/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/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/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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.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/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/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.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/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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/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/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/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/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/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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/8a/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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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/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/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/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/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/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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/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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a8/4e/c09876f08fa9faaa5e1178f3d77b7af3f343258689bd6f3b72593b2f74e3/mkdocs_markdownextradata_plugin-0.2.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/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/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/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/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/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/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/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/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/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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/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/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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/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/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/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/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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.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/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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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: ./ + - 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/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.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/linux-64/cffi-2.0.0-py312h460c074_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py312h8285ef7_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/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.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/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/linux-64/msgspec-0.21.1-py312h4c3975b_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-py312h5253ce2_0.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/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/linux-64/rpds-py-0.30.0-py312h868fb18_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-py312h4c3975b_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-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.4.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/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 @@ -1081,48 +1138,14 @@ environments: - 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.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/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/libsodium-1.0.21-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-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/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/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 @@ -1132,29 +1155,23 @@ environments: - 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/requests-2.34.0-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-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 @@ -1163,241 +1180,230 @@ 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-py312h4c3975b_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.6.3-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/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/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/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/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/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/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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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/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/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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/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/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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/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/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/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/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/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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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/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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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/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/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/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/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/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/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/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/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/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/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/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/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/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/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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/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/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/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/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-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/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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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: ./ + - 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/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.4.0-py312h87c4bb7_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 @@ -1420,42 +1426,14 @@ environments: - 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.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_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.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.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/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/osx-arm64/msgspec-0.21.1-py312h2bbb03f_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 @@ -1465,31 +1443,23 @@ environments: - 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.0-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/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda @@ -1498,231 +1468,274 @@ 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.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/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: 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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - 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/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/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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/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/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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-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/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/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/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/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/44/7b/537a61906eac58d94131273084d21d4eb219f5453f0ed30de3aca580a2b4/scipp-26.3.1-cp312-cp312-macosx_14_0_arm64.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/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/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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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/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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.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/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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/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/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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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: - - 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-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.4.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 + - 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/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.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/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/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-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/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/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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/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 @@ -1751,35 +1764,14 @@ environments: - 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.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.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.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.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/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/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/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 @@ -1788,286 +1780,360 @@ environments: - 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/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/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.0-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/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-2023.0.0-ha3553a1_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/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.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/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-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.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.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_905.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_905.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-ha3553a1_1.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_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - - conda: https://conda.anaconda.org/conda-forge/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/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/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/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/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/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/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/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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/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/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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/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/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.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/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.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/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/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-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/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/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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/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/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-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/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/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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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/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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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/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/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/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/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/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/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/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/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/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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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: ./ + - 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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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 py-314-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/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.4.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 @@ -2090,47 +2156,14 @@ environments: - 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.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/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.1-h0c1763c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuv-1.51.0-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-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/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/linux-64/msgspec-0.21.1-py314h5bd0f2a_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 @@ -2140,29 +2173,23 @@ environments: - 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.0-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 @@ -2171,241 +2198,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.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/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: 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/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: . + - 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/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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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/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/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/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/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/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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.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/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5b/69/93b34728cc386efdde0c342f8c680b9187dea7beb7adaf6b58a0713be101/mpld3-0.5.12-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2c/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/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/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/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/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/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/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/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/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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/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/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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/89/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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.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/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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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 - - 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.4.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 + - 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/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/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-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/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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/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/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 @@ -2428,43 +2445,14 @@ environments: - 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.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_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.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.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/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/osx-arm64/msgspec-0.21.1-py314h6c2aa35_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 @@ -2474,31 +2462,23 @@ environments: - 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.0-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 @@ -2507,231 +2487,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.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/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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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/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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/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/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.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/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/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/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/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/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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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/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/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/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/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/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/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/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/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.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/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/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/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/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/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-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/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/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/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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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.4.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 @@ -2760,36 +2784,14 @@ environments: - 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.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.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.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.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/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/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/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 @@ -2798,272 +2800,332 @@ environments: - 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.0-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-2023.0.0-ha3553a1_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/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.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/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 - - 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/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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-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 + - 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.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.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_905.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_905.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-ha3553a1_1.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_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + - pypi: . + - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0c/53/b50773ecf1d1e4a5858ee13011e30317ba02639ae4a1411a34967951fc9b/crysfml-0.6.2-cp314-cp314-win_amd64.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/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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.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/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/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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/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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8a/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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-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/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: ./ - user: - 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-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.4.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 + - 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/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/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/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/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/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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/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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.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/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/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 @@ -3097,33 +3159,14 @@ environments: - 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/libedit-3.1.20250104-pl5321h7949ede_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.0-hecca717_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-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/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/linux-64/msgspec-0.21.1-py314h5bd0f2a_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/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 @@ -3133,29 +3176,23 @@ environments: - 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.0-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 @@ -3164,152 +3201,142 @@ 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.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/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: 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/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/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/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/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.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/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/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/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/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/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/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.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/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-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/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/b2/e6/65abe97bbd42eb6ef73d3a58566ce89b097ae049511b7d9708288714a798/crysfml-0.6.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/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/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/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/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/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.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/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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/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/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/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-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/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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.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/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/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/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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/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/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/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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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/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/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/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.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/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/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-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/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/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/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/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/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/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.4.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/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 @@ -3343,28 +3370,14 @@ environments: - 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/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/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/noarch/matplotlib-inline-0.2.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/osx-arm64/msgspec-0.21.1-py314h6c2aa35_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/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 @@ -3374,31 +3387,23 @@ environments: - 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.0-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 @@ -3407,151 +3412,173 @@ 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.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/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/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/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/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/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/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/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/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/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.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/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/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/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.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/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-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/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/d0/42/bf7fb4d923a15b99b678ecb3bdcc02d336ee34baa876c6f41c5c55038b9c/crysfml-0.6.1-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/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-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/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/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/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/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/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.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/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-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/06/41/4e70dea1d0311016c0b0b1c53a24a266f9f8a34c6bc1af0f17cfca20aa1d/gemmi-0.7.5-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-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/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/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-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/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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.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/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.2-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/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/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-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/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/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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/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/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.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/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/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_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/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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.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/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/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-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/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/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/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/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.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/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-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/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.4.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 @@ -3585,25 +3612,14 @@ environments: - 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/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/noarch/matplotlib-inline-0.2.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/win-64/msgspec-0.21.1-py314h5a2d7ad_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/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 @@ -3612,29 +3628,22 @@ environments: - 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.0-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 @@ -3643,131 +3652,154 @@ 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-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.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/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_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda - - conda: https://conda.anaconda.org/conda-forge/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: 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/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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-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/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/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.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/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/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/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/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/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/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/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.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/ff/1c/a28b27effb13a381fe077ea3e3e78f6debd6315f2b3edff67bbb93d0ef51/gemmi-0.7.5-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/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/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-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/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a3/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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.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/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/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/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/f7/00/bbca25f8a2372465cdf93138c1e1e38dff045fb0afef1488f395d0afcb3b/ncrystal-4.4.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/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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-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/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.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/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.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/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/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/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.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/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.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/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-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/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/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/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.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/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/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/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/8d/c0/fdf9d3ee103ce66a55f0532835ad5e154226c5222423c6636ba049dc42fc/traittypes-0.2.3-py2.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/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/38/8b/7ec325b4e9e78beefc2d025b01ee8a2fde771ef7c957c3bff99b9e1fbffa/xraydb-4.5.8-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-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/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 @@ -3783,1391 +3815,1200 @@ packages: 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 +- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.conda + sha256: 7988c207b2b766dad5ebabf25a92b8d75cb8faed92f256fd7a4e0875c9ec6d58 + md5: 1567f06d717246abab170736af8bad1b 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 + - __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: - - cpython - - python-gil + - __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: [] - 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 + 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: - - exceptiongroup >=1.0.2 - - idna >=2.8 - - python >=3.10 - - typing_extensions >=4.5 - 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: - - trio >=0.32.0 - - uvloop >=0.22.1 - - winloop >=0.2.3 + - libbrotlicommon 1.2.0 hb03c661_1 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 + - 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: - - argon2-cffi-bindings - - python >=3.9 - - typing-extensions + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 constrains: - - argon2_cffi ==999 + - libbrotlicommon 1.2.0 hb03c661_1 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 + - 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 - - cffi >=1.0.1 - 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/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 + - 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 - - cffi >=2.0.0b1 + - 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/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 + - 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: - - __osx >=11.0 - - cffi >=1.0.1 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 - 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 + - 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: - - __osx >=11.0 - - cffi >=2.0.0b1 - - python >=3.14,<3.15.0a0 - - python >=3.14,<3.15.0a0 *_cp314 + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 - 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 + - 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: - - 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 + - __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: - - 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 + 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: - - 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 + - __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: - - 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 + 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: - - 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 + - __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: - - python >=3.10 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 constrains: - - astroid >=2,<5 + - libabseil-static =20260107.1=cxx17* + - abseil-cpp =20260107.1 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 + 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: - - python >=3.10 - - python + - libopenblas >=0.3.33,<0.3.34.0a0 + - libopenblas >=0.3.33,<1.0a0 constrains: - - pytz >=2015.7 + - 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: - - pkg:pypi/babel?source=compressed-mapping - size: 7684321 - timestamp: 1772555330347 -- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py312h90b7ffd_0.conda - sha256: e8c83696e6529ac1909a96690c58624bb376312fd0768409380cd9b05e248c9b - md5: 542da724e75cdeef19e29cca23935c25 + 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: - - 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: 238360 - timestamp: 1777848717715 -- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda - noarch: generic - sha256: de1755a35258eb1b59f2288559bbf0b76da60bd2fa6cd6f768ead442f85bd666 - md5: b712198b257f378e9bd8cde277218296 - depends: - - python >=3.14 - license: BSD-3-Clause AND MIT AND EPL-2.0 + - libgcc >=14 + license: MIT + license_family: MIT purls: [] - size: 7546 - timestamp: 1777848733980 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py312h87c4bb7_0.conda - sha256: 7dbd64d3f06622ef8286be6dfceeb8e6008450fb4e6d9309fbb908b12f3937ff - md5: 95a833465ec45ac1e8f2ed1aaba8ec37 - depends: - - python - - __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=compressed-mapping - size: 239305 - timestamp: 1777848727027 -- conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py312h06d0912_0.conda - sha256: 71caf40c0fdeb11fafaac639e6e6f9120112aa105a7a5e9dfb5b4b06db9ca97a - md5: 77d0a2bdd46dd8d502bb27eb80353fcd + 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: - - 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: 237107 - timestamp: 1777848740547 -- 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 + - __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: - - python >=3.10 - - soupsieve >=1.2 - - typing-extensions + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 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 + 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: - - python >=3.10 - - webencodings - - python + - libblas 3.11.0 7_h4a7cf45_openblas 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 + - 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: - - bleach ==6.3.0 pyhcf101f3_1 - - tinycss2 - license: Apache-2.0 AND MIT + - ncurses + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD 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 + 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 - - libstdcxx >=14 - - python >=3.12,<3.13.0a0 - - python_abi 3.12.* *_cp312 constrains: - - libbrotlicommon 1.2.0 hb03c661_1 + - expat 2.8.0.* 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 + 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 - - 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 + 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: - - __osx >=11.0 - - libcxx >=19 - - python >=3.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 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 + - 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: - - __osx >=11.0 - - libcxx >=19 - - python >=3.14,<3.15.0a0 - - python >=3.14,<3.15.0a0 *_cp314 - - python_abi 3.14.* *_cp314 + - 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: - - 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 + - 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: - - 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 + - __glibc >=2.17,<3.0.a0 + - libgcc >=15.2.0 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 + - 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: - - 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 + - __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: - - 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 + - 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: bzip2-1.0.6 + license: BSD-2-Clause 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 + 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: - - __osx >=11.0 - license: bzip2-1.0.6 - license_family: BSD + - __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: 124834 - timestamp: 1771350416561 -- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda - sha256: 76dfb71df5e8d1c4eded2dbb5ba15bb8fb2e2b0fe42d94145d5eed4c75c35902 - md5: 4cb8e6b48f67de0b018719cdf1136306 + 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: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: bzip2-1.0.6 - license_family: BSD + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-only + license_family: GPL 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 + 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 - license: MIT - license_family: MIT + - libgfortran + - libgfortran5 >=14.3.0 + constrains: + - openblas >=0.3.33,<0.3.34.0a0 + license: BSD-3-Clause + license_family: BSD 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 + 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: - - __osx >=11.0 - license: MIT - license_family: MIT + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: ISC 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 + 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: - - __win - license: ISC + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.2,<2.0a0 + license: blessing 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 + 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: - - __unix - license: ISC + - __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: 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 + size: 5852044 + timestamp: 1778269036376 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + sha256: bc1b08c92626c91500fd9f26f2c797f3eb153b627d53e9c13cd167f1e12b2829 + md5: 38ffe67b78c9d4de527be8315e5ada2c depends: - - cached_property >=1.5.2,<1.5.3.0a0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 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 + 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: - - python >=3.6 + - __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/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 + - 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: - - python >=3.10 - license: ISC + - __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/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 + - 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 - - 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 + license: BSD-3-Clause + license_family: BSD 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 + - 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 - - 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 + license: BSD-3-Clause + license_family: BSD 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 + - 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: - - __osx >=11.0 + - __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 - - pycparser + - 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 >=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/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 + - 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: - - __osx >=11.0 - - libffi >=3.5.2,<3.6.0a0 - - pycparser + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 - 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/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 + - 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: - - pycparser - - python >=3.12,<3.13.0a0 + - 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 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + constrains: + - __glibc >=2.17 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 + - 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: - - pycparser - - python >=3.14,<3.15.0a0 + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 - python_abi 3.14.* *_cp314 - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + constrains: + - __glibc >=2.17 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 + - 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/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 + - 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-3-Clause + license: BSD-2-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 + - 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/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 + - 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: d3e9bbd7340199527f28bbacf947702368f31de60c433a16446767d3c6aaf6fe - md5: f54c1ffb8ecedb85a8b7fcde3a187212 + sha256: a1c97297e867776760489537bc5ae36fa83a154be30e3b79385a39ca4cb058fe + md5: 1133126d840e75287d83947be3fc3e71 depends: - - python >=3.12,<3.13.0a0 - - python_abi * *_cp312 - license: Python-2.0 + - 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: 46463 - timestamp: 1772728929620 -- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.4-py314hd8ed1ab_100.conda - noarch: generic - sha256: 40dc224f2b718e5f034efd2332bc315a719063235f63673468d26a24770094ee - md5: f111d4cfaf1fe9496f386bc98ae94452 + 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: - - python >=3.14,<3.15.0a0 - - python_abi * *_cp314 - license: Python-2.0 + - __win + license: ISC 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 + 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: - - 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 + - __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: - - python - - libgcc >=14 - - libstdcxx >=14 - - __glibc >=2.17,<3.0.a0 - - python_abi 3.14.* *_cp314 - license: MIT - license_family: MIT + - 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/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 + - 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 - - __osx >=11.0 - - python 3.12.* *_cpython - - libcxx >=19 - - python_abi 3.12.* *_cp312 - license: MIT - license_family: MIT + - python >=3.10 + license: ISC 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 + - 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 - - python 3.14.* *_cp314 - - __osx >=11.0 - - libcxx >=19 - - python_abi 3.14.* *_cp314 + - python >=3.10 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 + - 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 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.12.* *_cp312 - license: MIT - license_family: MIT + - python >=3.9 + license: BSD-3-Clause + license_family: BSD 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 + - 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 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - - ucrt >=10.0.20348.0 - - python_abi 3.14.* *_cp314 - license: MIT - license_family: MIT + license: BSD-3-Clause + license_family: BSD purls: - - pkg:pypi/debugpy?source=hash-mapping - size: 4026404 - timestamp: 1769745008861 + - 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 @@ -5190,1764 +5031,1546 @@ packages: - 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: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl - name: easydiffraction - version: 0.16.0 - sha256: 2691a1e175974ca79e0ec3c219d92b77f277c38fb3b0b8d25f6f7e99696bf70f - requires_dist: - - asciichartpy - - asteval - - bumps - - colorama - - crysfml - - cryspy - - darkdetect - - dfo-ls - - diffpy-pdffit2 - - diffpy-utils - - essdiffraction - - gemmi - - lmfit - - numpy - - pandas - - plotly - - pooch - - py3dmol - - rich - - scipy - - sympy - - tabulate - - typeguard - - typer - - uncertainties - - varname - - build ; extra == 'dev' - - copier ; extra == 'dev' - - docstripy ; extra == 'dev' - - format-docstring ; extra == 'dev' - - gitpython ; extra == 'dev' - - interrogate ; extra == 'dev' - - jinja2 ; extra == 'dev' - - jupyterquiz ; extra == 'dev' - - jupytext ; extra == 'dev' - - mike ; extra == 'dev' - - mkdocs ; extra == 'dev' - - mkdocs-autorefs ; extra == 'dev' - - mkdocs-jupyter ; extra == 'dev' - - mkdocs-markdownextradata-plugin ; extra == 'dev' - - mkdocs-material ; extra == 'dev' - - mkdocs-plugin-inline-svg ; extra == 'dev' - - mkdocstrings-python ; extra == 'dev' - - nbmake ; extra == 'dev' - - nbqa ; extra == 'dev' - - nbstripout ; extra == 'dev' - - pre-commit ; extra == 'dev' - - pydoclint ; extra == 'dev' - - pytest ; extra == 'dev' - - pytest-benchmark ; extra == 'dev' - - pytest-cov ; extra == 'dev' - - pytest-xdist ; extra == 'dev' - - pyyaml ; extra == 'dev' - - radon ; extra == 'dev' - - ruff ; extra == 'dev' - - spdx-headers ; extra == 'dev' - - validate-pyproject[all] ; extra == 'dev' - - versioningit ; extra == 'dev' - requires_python: '>=3.12' -- pypi: ./ - name: easydiffraction - version: 0.16.0+devdirty3 - sha256: f7b222922aa1bbe773d6384ee96acc456820490c46139a8f2598129cb6c4717e - requires_dist: - - asciichartpy - - asteval - - bumps - - colorama - - crysfml - - cryspy - - darkdetect - - dfo-ls - - diffpy-pdffit2 - - diffpy-utils - - gemmi - - lmfit - - numpy - - pandas - - plotly - - pooch - - py3dmol - - rich - - scipy - - sympy - - tabulate - - 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-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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl - name: gitpython - version: 3.1.50 - sha256: d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9 - requires_dist: - - gitdb>=4.0.1,<5 - - typing-extensions>=3.10.0.2 ; python_full_version < '3.10' - - coverage[toml] ; extra == 'test' - - ddt>=1.1.1,!=1.4.3 ; extra == 'test' - - mock ; python_full_version < '3.8' and extra == 'test' - - mypy==1.18.2 ; python_full_version >= '3.9' and extra == 'test' - - pre-commit ; extra == 'test' - - pytest>=7.3.1 ; extra == 'test' - - pytest-cov ; extra == 'test' - - pytest-instafail ; extra == 'test' - - pytest-mock ; extra == 'test' - - pytest-sugar ; extra == 'test' - - typing-extensions ; python_full_version < '3.11' and extra == 'test' - - sphinx>=7.4.7,<8 ; extra == 'doc' - - sphinx-rtd-theme ; extra == 'doc' - - sphinx-autodoc-typehints ; extra == 'doc' - requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/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' -- conda: https://conda.anaconda.org/conda-forge/linux-64/gsl-2.8-hbf7d49c_1.conda - sha256: f923af07c3a3db746d3be8efebdaa9c819a6007ee3cc12445cee059641611e05 - md5: 04e128d2adafe3c844cde58f103c481b +- 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=compressed-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: - - __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/osx-arm64/gsl-2.8-h8d0574d_1.conda - sha256: f11d8f2007f6591022afa958d8fe15afbe4211198d1603c0eb886bc21a9eb19e - md5: cc261442bead590d89ca9f96884a344f + - 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: - - __osx >=11.0 - - libblas >=3.9.0,<4.0a0 - - libcblas >=3.9.0,<4.0a0 - license: GPL-3.0-or-later - license_family: GPL + - 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: 1862134 - timestamp: 1737621413640 -- conda: https://conda.anaconda.org/conda-forge/win-64/gsl-2.8-h5b8d9c4_1.conda - sha256: 87a3468e09cc1ee0268e8639debad6a5b440090ef8cb1d2ee5eed66c86085528 - md5: a47cf810b7c03955139a150b228b93ca + 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: - - 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 + - cpython 3.14.4.* + - python_abi * *_cp314 + license: Python-2.0 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: 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 - - python - license: MIT - license_family: MIT + license: BSD-2-Clause + license_family: BSD 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/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: 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 - - hyperframe >=6.1,<7 - - hpack >=4.1,<5 + - rpds-py >=0.7.0 + - typing_extensions >=4.4.0 - 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 - 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/referencing?source=hash-mapping + size: 51788 + timestamp: 1760379115194 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + sha256: 4487fdb341537e2df47159ed8e546add99080974c52d5b2dc2a710910619115a + md5: a5985537dab1ba7034b5ff4ea22e2fa9 depends: - - python >=3.9 - - h11 >=0.16 - - h2 >=3,<5 - - sniffio 1.* - - anyio >=4.0,<5.0 - - certifi + - python >=3.10 + - certifi >=2023.5.7 + - charset-normalizer >=2,<4 + - idna >=2.5,<4 + - urllib3 >=1.26,<3 - python - license: BSD-3-Clause - license_family: BSD + constrains: + - chardet >=3.0.2,<8 + license: Apache-2.0 + license_family: APACHE 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 + - pkg:pypi/requests?source=hash-mapping + size: 68658 + timestamp: 1778534036810 +- conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda + sha256: 3b45efeae771f1a20307b36ecdb3a8911a89c05382836b50c62b0a99d8d3dfd8 + md5: da94ff04d97ec5efc42cbe5da3c43a84 depends: - - anyio - - certifi - - httpcore 1.* - - idna - - python >=3.9 - license: BSD-3-Clause + - python >=3.11 + - typing_extensions >=4.0,<5.0 + - python + 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 + - 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.18.2-pyhcf101f3_0.conda - sha256: 04fb8ea7749f67abaf76df6257bf86688e1389ceed55eb4fb0176fd2e882dbd6 - md5: 5ee7945accf0f215ddd6055d25d7cd83 + - 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=compressed-mapping - size: 360522 - timestamp: 1778060967727 -- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda - sha256: 5eda79ed9f53f590031d29346abd183051263227dd9ee667b5ca1133ce297654 - md5: 7b8bace4943e0dc345fc45938826f2b8 + - 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 - license_family: BSD + - libbrotlicommon 1.2.0 hc919400_1 + 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/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: - - 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 + - __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-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/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: + - 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: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: LGPL-2.1-or-later + - 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: + - __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 @@ -6962,71 +6585,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=hash-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 @@ -7041,69 +6599,24 @@ 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 - depends: - - libopenblas >=0.3.32,<0.3.33.0a0 - - libopenblas >=0.3.32,<1.0a0 - constrains: - - blas 2.306 openblas - - liblapack 3.11.0 6*_openblas - - liblapacke 3.11.0 6*_openblas - - libcblas 3.11.0 6*_openblas - - mkl <2026 - 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 - depends: - - libopenblas >=0.3.32,<0.3.33.0a0 - - libopenblas >=0.3.32,<1.0a0 - 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 - 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 +- 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: - - mkl >=2025.3.1,<2026.0a0 + - libopenblas >=0.3.33,<0.3.34.0a0 + - libopenblas >=0.3.33,<1.0a0 constrains: - - blas 2.306 mkl - - liblapacke 3.11.0 6*_mkl - - liblapack 3.11.0 6*_mkl - - libcblas 3.11.0 6*_mkl + - 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: 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 + size: 18783 + timestamp: 1778489983152 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-hc919400_1.conda sha256: a7cb9e660531cf6fbd4148cff608c85738d0b76f0975c5fc3e7d5e92840b7229 md5: 006e7ddd8a110771134fcc4e1e3a6ffa @@ -7114,18 +6627,6 @@ packages: 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 - 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/osx-arm64/libbrotlidec-1.2.0-hc919400_1.conda sha256: 2eae444039826db0454b19b52a3390f63bfe24f6b3e63089778dd5a5bf48b6bf md5: 079e88933963f3f149054eec2c487bc2 @@ -7137,18 +6638,6 @@ packages: 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 - 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/osx-arm64/libbrotlienc-1.2.0-hc919400_1.conda sha256: 01436c32bb41f9cb4bcf07dda647ce4e5deb8307abfc3abdc8da5317db8189d1 md5: b2b7c8288ca1a2d71ff97a8e6a1e8883 @@ -7160,74 +6649,31 @@ packages: 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 - depends: - - libblas 3.11.0 6_h4a7cf45_openblas - constrains: - - blas 2.306 openblas - - liblapack 3.11.0 6*_openblas - - liblapacke 3.11.0 6*_openblas - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 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 - depends: - - libblas 3.11.0 6_h51639a9_openblas - constrains: - - liblapacke 3.11.0 6*_openblas - - blas 2.306 openblas - - liblapack 3.11.0 6*_openblas - license: BSD-3-Clause - license_family: BSD - purls: [] - size: 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 +- 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: - - libblas 3.11.0 6_hf2e6a31_mkl + - libblas 3.11.0 7_h51639a9_openblas constrains: - - blas 2.306 mkl - - liblapacke 3.11.0 6*_mkl - - liblapack 3.11.0 6*_mkl + - 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: 68221 - timestamp: 1774503722413 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda - sha256: 25a0d02148a39b665d9c2957676faf62a4d2a58494d53b201151199a197db4b0 - md5: 448a1af83a9205655ee1cf48d3875ca3 + 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: 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 - license_family: BSD - purls: [] - size: 134676 - timestamp: 1738479519902 + size: 569359 + timestamp: 1778191546305 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda sha256: 66aa216a403de0bb0c1340a88d1a06adaff66bae2cfd196731aa24db9859d631 md5: 44083d2d2c2025afca315c7a172eab2b @@ -7240,16 +6686,6 @@ packages: purls: [] size: 107691 timestamp: 1738479560845 -- 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/osx-arm64/libev-4.33-h93a5062_2.conda sha256: 95cecb3902fbe0399c3a7e67a5bed1db813e5ab0e22f4023a5e0f722f2cc214f md5: 36d33e440c31857372a72137f78bacf5 @@ -7258,56 +6694,18 @@ packages: purls: [] size: 107458 timestamp: 1702146414478 -- 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/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/win-64/libexpat-2.8.0-hac47afa_0.conda - sha256: 2d81d647c1f01108803457cac999b947456f44dd0a3c2325395677feacaeca67 - md5: 264e350e035092b5135a2147c238aec4 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + depends: + - __osx >=11.0 constrains: - expat 2.8.0.* license: MIT license_family: MIT purls: [] - size: 71094 - timestamp: 1777846223617 -- 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 + size: 68789 + timestamp: 1777846180142 - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7 md5: 43c04d9cb46ef176bb2a4c77e324d599 @@ -7318,2251 +6716,1777 @@ packages: 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 - depends: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: MIT - license_family: MIT - purls: [] - size: 45831 - timestamp: 1769456418774 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda - sha256: faf7d2017b4d718951e3a59d081eb09759152f93038479b768e3d612688f83f5 - md5: 0aa00f03f9e39fb9876085dee11a85d4 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_19.conda + sha256: 06644fa4d34d57c9e48f4d84b1256f9e5f654fdb37f43acc8a58a396952d42b7 + md5: 644058123986582db33aebd4ae2ca184 depends: - - __glibc >=2.17,<3.0.a0 - - _openmp_mutex >=4.5 + - _openmp_mutex constrains: - - libgcc-ng ==15.2.0=*_18 - - libgomp 15.2.0 he0feb66_18 + - 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: 1041788 - timestamp: 1771378212382 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_18.conda - sha256: 1d9c4f35586adb71bcd23e31b68b7f3e4c4ab89914c26bed5f2859290be5560e - md5: 92df6107310b1fff92c4cc84f0de247b + 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: - - _openmp_mutex + - libgfortran5 15.2.0 hdae7583_19 constrains: - - libgcc-ng ==15.2.0=*_18 - - libgomp 15.2.0 18 + - libgfortran-ng ==15.2.0=*_19 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 + 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 he0feb66_18 + - libgcc >=15.2.0 + constrains: + - libgfortran 15.2.0 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 + 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: - - libgfortran5 15.2.0 h68bc16d_18 + - __osx >=11.0 constrains: - - libgfortran-ng ==15.2.0=*_18 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - xz 5.8.3.* + license: 0BSD 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 + 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: - - 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 + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD 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 + 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: - - __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 + - __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: 2482475 - timestamp: 1771378241063 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_18.conda - sha256: 91033978ba25e6a60fb86843cf7e1f7dc8ad513f9689f991c9ddabfaf0361e7e - md5: c4a6f7989cffb0544bfd9207b6789971 + 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: - - libgcc >=15.2.0 + - __osx >=11.0 + - libgfortran + - libgfortran5 >=14.3.0 + - llvm-openmp >=19.1.7 constrains: - - libgfortran 15.2.0 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - openblas >=0.3.33,<0.3.34.0a0 + license: BSD-3-Clause + license_family: BSD 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 + 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: - - __glibc >=2.17,<3.0.a0 - license: GPL-3.0-only WITH GCC-exception-3.1 - license_family: GPL + - __osx >=11.0 + license: ISC 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 + 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: - - 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 + - __osx >=11.0 + - libzlib >=1.3.2,<2.0a0 + license: blessing purls: [] - size: 2411241 - timestamp: 1765104337762 -- conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda - sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 - md5: 64571d1dd6cdcfa25d0664a5950fdaa2 + 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: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: LGPL-2.1-only + - __osx >=11.0 + license: MIT + license_family: MIT 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 + 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 - - libgcc >=14 + - __osx >=11.0 constrains: - - xz 5.8.3.* - license: 0BSD + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other 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 + 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 constrains: - - xz 5.8.3.* - license: 0BSD + - openmp 22.1.5|22.1.5.* + - intel-openmp <0.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE 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 + 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: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 constrains: - - xz 5.8.3.* - license: 0BSD + - jinja2 >=3.0.0 + 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 + depends: + - __osx >=11.0 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - 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: 27256 + timestamp: 1772445397216 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/msgspec-0.21.1-py312h2bbb03f_0.conda + sha256: 50e284832520f08ef1e37e0ca20459f5df2c048f59dfba1f2e3ee0ccfe7be317 + md5: ae340bdc5bdf5abd3183c5962517cbde + depends: + - __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: + - 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 + - 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: 805509 + timestamp: 1777423252320 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nodejs-25.8.2-h7039424_0.conda + sha256: 4782b172b3b8a557b60bf5f591821cf100e2092ba7a5494ce047dfa41626de26 + md5: ca8277c52fdface8bb8ebff7cd9a6f56 + depends: + - libcxx >=19 + - __osx >=11.0 + - icu >=78.3,<79.0a0 + - libbrotlicommon >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libnghttp2 >=1.68.1,<2.0a0 + - libuv >=1.51.0,<2.0a0 + - libsqlite >=3.52.0,<4.0a0 + - libzlib >=1.3.2,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + - c-ares >=1.34.6,<2.0a0 + - libabseil >=20260107.1,<20260108.0a0 + - libabseil * cxx17* + license: MIT + license_family: MIT purls: [] - size: 106486 - timestamp: 1775825663227 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda - sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 - md5: 2c21e66f50753a083cbe6b80f38268fa + 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: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - license: BSD-2-Clause - license_family: BSD + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache 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: 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 - license: BSD-2-Clause + - python 3.12.* *_cpython + - python_abi 3.12.* *_cp312 + license: BSD-3-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 + 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: - - ucrt >=10.0.20348.0 - - vc >=14.3,<15 - - vc14_runtime >=14.44.35208 - license: BSD-2-Clause + - python + - __osx >=11.0 + - python 3.14.* *_cp314 + - python_abi 3.14.* *_cp314 + license: BSD-3-Clause license_family: BSD - 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 + 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: - - __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 + - 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: 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/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 - - c-ares >=1.34.6,<2.0a0 - - libcxx >=19 - - libev >=4.33,<4.34.0a0 - - libev >=4.33,<5.0a0 + - 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 +- 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/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: [] - 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: 187278 + timestamp: 1770223990452 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda + sha256: 95f385f9606e30137cf0b5295f63855fd22223a4cf024d306cf9098ea1c4a252 + md5: dcf51e564317816cb8d546891019b3ab depends: - - __glibc >=2.17,<3.0.a0 - - libgcc >=13 - license: LGPL-2.1-only + - __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/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda + noarch: python + sha256: 2f31f799a46ed75518fae0be75ecc8a1b84360dbfd55096bc2fe8bd9c797e772 + md5: 2f6b79700452ef1e91f45a99ab8ffe5a + depends: + - python + - libcxx >=19 + - __osx >=11.0 + - _python_abi3_support 1.* + - cpython >=3.12 + - zeromq >=4.3.5,<4.4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 191641 + timestamp: 1771717073430 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 + md5: f8381319127120ce51e081dce4865cf4 + depends: + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only license_family: GPL purls: [] - size: 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.1-h0c1763c_0.conda - sha256: 54cdcd3214313b62c2a8ee277e6f42150d9b748264c1b70d958bf735e420ef8d - md5: 7dc38adcbf71e6b38748e919e16e0dce + 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 - - 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: 954962 - timestamp: 1777986471789 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.1-h1b79a29_0.conda - sha256: 49daec7c83e70d4efc17b813547824bc2bcf2f7256d84061d24fbfe537da9f74 - md5: 6681822ea9d362953206352371b6a904 + 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: 920047 - timestamp: 1777987051643 -- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.1-hf5d6505_0.conda - sha256: e70562450332ca8954bc16f3455468cca5ef3695c7d7187ecc87f8fc3c70e9eb - md5: 7fea434a17c323256acc510a041b80d7 + 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: 1304178 - timestamp: 1777986510497 -- 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 + 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: - - 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 + - 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: - - 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 >=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: - - 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.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 - 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: + - 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 + license: bzip2-1.0.6 + license_family: BSD 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 + 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: - - libiconv >=1.18,<2.0a0 - - liblzma >=5.8.3,<6.0a0 - - libzlib >=1.3.2,<2.0a0 + - 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 - 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/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/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: - - zlib 1.3.2 *_2 - license: Zlib - license_family: Other - 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 - 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 + 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: + - 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 - constrains: - - intel-openmp <0.0a0 - - openmp 22.1.4|22.1.4.* - license: Apache-2.0 WITH LLVM-exception - license_family: APACHE + - 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/win-64/gsl-2.8-h5b8d9c4_1.conda + sha256: 87a3468e09cc1ee0268e8639debad6a5b440090ef8cb1d2ee5eed66c86085528 + md5: a47cf810b7c03955139a150b228b93ca + 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 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/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl - name: markdown-it-py - version: 4.1.0 - sha256: d4939a62a2dd0cd9cb80a191a711ba1d39bac8ed5ef9e9966895b0171c01c46d - requires_dist: - - mdurl~=0.1 - - psutil ; extra == 'benchmarking' - - pytest ; extra == 'benchmarking' - - pytest-benchmark ; extra == 'benchmarking' - - commonmark~=0.9 ; extra == 'compare' - - markdown~=3.4 ; extra == 'compare' - - mistletoe~=1.0 ; extra == 'compare' - - mistune~=3.0 ; extra == 'compare' - - panflute~=2.3 ; extra == 'compare' - - markdown-it-pyrs ; extra == 'compare' - - linkify-it-py>=1,<3 ; extra == 'linkify' - - mdit-py-plugins>=0.5.0 ; extra == 'plugins' - - gprof2dot ; extra == 'profiling' - - mdit-py-plugins>=0.5.0 ; extra == 'rtd' - - myst-parser ; extra == 'rtd' - - pyyaml ; extra == 'rtd' - - sphinx ; extra == 'rtd' - - sphinx-copybutton ; extra == 'rtd' - - sphinx-design ; extra == 'rtd' - - sphinx-book-theme~=1.0 ; extra == 'rtd' - - jupyter-sphinx ; extra == 'rtd' - - ipykernel ; extra == 'rtd' - - coverage ; extra == 'testing' - - pytest ; extra == 'testing' - - pytest-cov ; extra == 'testing' - - pytest-regressions ; extra == 'testing' - - pytest-timeout ; extra == 'testing' - - requests ; extra == 'testing' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda - sha256: 5f3aad1f3a685ed0b591faad335957dbdb1b73abfd6fc731a0d42718e0653b33 - md5: 93a4752d42b12943a355b682ee43285b + 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: - - __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 + - 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: - - __glibc >=2.17,<3.0.a0 - - libgcc >=14 - - python >=3.14,<3.15.0a0 - - python_abi 3.14.* *_cp314 + - 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: 27424 - timestamp: 1772445227915 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py312h04c11ed_1.conda - sha256: 330394fb9140995b29ae215a19fad46fcc6691bdd1b7654513d55a19aaa091c1 - md5: 11d95ab83ef0a82cc2de12c1e0b47fe4 + 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.12,<3.13.0a0 - - python >=3.12,<3.13.0a0 *_cpython - - python_abi 3.12.* *_cp312 + - 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=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: 68594 + timestamp: 1778490364980 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.0-hac47afa_0.conda + sha256: 2d81d647c1f01108803457cac999b947456f44dd0a3c2325395677feacaeca67 + md5: 264e350e035092b5135a2147c238aec4 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/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 + - 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.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: MIT + license_family: MIT + purls: [] + size: 45831 + timestamp: 1769456418774 +- 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: - - 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 + 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/win-64/liblzma-5.8.3-hfd05255_0.conda + sha256: d636d1a25234063642f9c531a7bb58d84c1c496411280a36ea000bd122f078f1 + md5: 8f83619ab1588b98dd99c90b0bfc5c6d 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 + - 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/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 + 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: - - python >=3.10 - - traitlets - 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.1-pyhcf101f3_0.conda - sha256: b52dc6c78fbbe7a3008535cb8bfd87d70d8053e9250bbe16e387470a9df07070 - md5: b97e84d1553b4a1c765b87fff83453ad + - 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: - - python >=3.10 - - typing_extensions - - python - license: BSD-3-Clause - purls: - - pkg:pypi/mistune?source=compressed-mapping - size: 74567 - timestamp: 1777824616382 -- 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 + - 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: + - 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 + constrains: + - icu <0.0a0 + license: MIT + license_family: MIT + purls: [] + 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: + - 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: - - llvm-openmp >=22.1.4 - - onemkl-license 2025.3.1 h57928b3_12 - - tbb >=2022.3.0 - ucrt >=10.0.20348.0 - vc >=14.3,<15 - vc14_runtime >=14.44.35208 - license: LicenseRef-IntelSimplifiedSoftwareOct2022 - license_family: Proprietary + constrains: + - openmp 22.1.5|22.1.5.* + - intel-openmp <0.0a0 + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE 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 - 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/osx-arm64/msgspec-0.21.1-py312h2bbb03f_0.conda - sha256: 50e284832520f08ef1e37e0ca20459f5df2c048f59dfba1f2e3ee0ccfe7be317 - md5: ae340bdc5bdf5abd3183c5962517cbde - depends: - - __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: - - 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 - - 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/win-64/msgspec-0.21.1-py312he06e257_0.conda - sha256: 003de3343b481937b5eb500ecdbfc882e87cea608be3741dc1fb13d22f8ed95e - md5: 1f32f4f6aa595377a7e651e67ba53d30 + 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: - 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/msgspec?source=hash-mapping - size: 199413 - timestamp: 1776337631789 -- conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py314h5a2d7ad_0.conda - sha256: 6a076225fa315d29c5d556e3912a6319aea60b4f458c23f23f5ce66495cb9414 - md5: a4b20f401c93cf8651093fcc8380e3c9 + - 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: - 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/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 - 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/markupsafe?source=hash-mapping + size: 30022 + timestamp: 1772445159549 +- conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_905.conda + sha256: 76a43359adae10aef8de7ff8e4fab70647bda928146374298506afab2e4a7b4f + md5: 7741affec1b3d2275586397ed4c91639 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 + - llvm-openmp >=22.1.4 + - onemkl-license 2026.0.0 h57928b3_905 + - 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: 114620200 + timestamp: 1778111077072 +- conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py312he06e257_0.conda + sha256: 003de3343b481937b5eb500ecdbfc882e87cea608be3741dc1fb13d22f8ed95e + md5: 1f32f4f6aa595377a7e651e67ba53d30 + 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: 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/msgspec?source=hash-mapping + size: 199413 + timestamp: 1776337631789 +- conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py314h5a2d7ad_0.conda + sha256: 6a076225fa315d29c5d556e3912a6319aea60b4f458c23f23f5ce66495cb9414 + md5: a4b20f401c93cf8651093fcc8380e3c9 depends: - - jsonschema >=2.6 - - jupyter_core >=4.12,!=5.0.* - - python >=3.9 - - python-fastjsonschema >=2.15 - - traitlets >=5.1 + - 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/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/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/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/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/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/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl - name: ncrystal-python - version: 4.4.2 - sha256: f419318d088fade6bcff1e39e15baf6fe69fcf5306dd681fca1106d1f63a89ce - requires_dist: - - numpy>=1.22 - requires_python: '>=3.8' -- 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 + - 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: 918956 - timestamp: 1777422145199 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda - sha256: 4ea6c620b87bd1d42bb2ccc2c87cd2483fa2d7f9e905b14c223f11ff3f4c455d - md5: 343d10ed5b44030a2f67193905aea159 + size: 31271315 + timestamp: 1774517904472 +- conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_905.conda + sha256: 848a7215e1ce227139074461664d01c00e7e1e8a367ccbd6581c0860d6ec4a19 + md5: fea22e21062046ba44336de37f4b6372 + license: LicenseRef-IntelSimplifiedSoftwareOct2022 + license_family: Proprietary + purls: [] + size: 41103 + timestamp: 1778110756075 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + sha256: feb5815125c60f2be4a411e532db1ed1cd2d7261a6a43c54cb6ae90724e2e154 + md5: 05c7d624cff49dbd8db1ad5ba537a8a3 depends: - - __osx >=11.0 - license: X11 AND BSD-3-Clause + - 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: 805509 - timestamp: 1777423252320 -- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda - sha256: bb7b21d7fd0445ddc0631f64e66d91a179de4ba920b8381f29b9d006a42788c0 - md5: 598fd7d4d0de2455fb74f56063969a97 + 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: - - python >=3.9 - license: BSD-2-Clause + - 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/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/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/win-64/python-3.12.13-h0159041_0_cpython.conda + sha256: a02b446d8b7b167b61733a3de3be5de1342250403e72a63b18dac89e99e6180e + md5: 2956dff38eb9f8332ad4caeba941cfe7 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 + - 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 - - icu >=78.3,<79.0a0 - libzlib >=1.3.2,<2.0a0 - - libabseil >=20260107.1,<20260108.0a0 - - libabseil * cxx17* + - 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 - - libbrotlicommon >=1.2.0,<1.3.0a0 - - libbrotlienc >=1.2.0,<1.3.0a0 - - libbrotlidec >=1.2.0,<1.3.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: [] - 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/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: - - 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 >=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: [] - size: 17101803 - timestamp: 1774517834028 -- conda: https://conda.anaconda.org/conda-forge/win-64/nodejs-25.8.2-h80d1838_0.conda - sha256: 5e38e51da1aa4bc352db9b4cec1c3e25811de0f4408edaa24e009a64de6dbfdf - md5: e626ee7934e4b7cb21ce6b721cff8677 + 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: + - 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: [] - size: 31271315 - timestamp: 1774517904472 -- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda - sha256: 7b920e46b9f7a2d2aa6434222e5c8d739021dbc5cc75f32d124a8191d86f9056 - md5: e7f89ea5f7ea9401642758ff50a2d9c1 + 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: - - jupyter_server >=1.8,<3 - - python >=3.9 + - 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/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 + - 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: - - __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/osx-arm64/openssl-3.6.2-hd24854e_0.conda - sha256: c91bf510c130a1ea1b6ff023e28bac0ccaef869446acd805e2016f69ebdc49ea - md5: 25dcccd4f80f1638428613e0d7c9b4e1 + - 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: - - __osx >=11.0 - - ca-certificates - license: Apache-2.0 - license_family: Apache - 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 + - 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-ha3553a1_1.conda + sha256: 5ff149ba6832bf4ded4b43bf0a41cde7be814802a95070553176c087f65b2a01 + md5: 34aa94d586fe95fa121966c0d4e73cf4 depends: - - ca-certificates + - 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 + license_family: APACHE 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: 156910 + timestamp: 1777976465531 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + sha256: 0e79810fae28f3b69fe7391b0d43f5474d6bd91d451d5f2bde02f55ae481d5e3 + md5: 0481bfd9814bf525bd4b3ee4b51494c4 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 + - 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.8 - - python + - 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/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 - 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 - requires_dist: - - numpy>=1.26.0 ; python_full_version < '3.14' - - numpy>=2.3.3 ; python_full_version >= '3.14' - - python-dateutil>=2.8.2 - - tzdata ; sys_platform == 'win32' - - tzdata ; sys_platform == 'emscripten' - - hypothesis>=6.116.0 ; extra == 'test' - - pytest>=8.3.4 ; extra == 'test' - - pytest-xdist>=3.6.1 ; extra == 'test' - - pyarrow>=13.0.0 ; extra == 'pyarrow' - - bottleneck>=1.4.2 ; extra == 'performance' - - numba>=0.60.0 ; extra == 'performance' - - numexpr>=2.10.2 ; extra == 'performance' - - scipy>=1.14.1 ; extra == 'computation' - - xarray>=2024.10.0 ; extra == 'computation' - - fsspec>=2024.10.0 ; extra == 'fss' - - s3fs>=2024.10.0 ; extra == 'aws' - - gcsfs>=2024.10.0 ; extra == 'gcp' - - odfpy>=1.4.1 ; extra == 'excel' - - openpyxl>=3.1.5 ; extra == 'excel' - - python-calamine>=0.3.0 ; extra == 'excel' - - pyxlsb>=1.0.10 ; extra == 'excel' - - xlrd>=2.0.1 ; extra == 'excel' - - xlsxwriter>=3.2.0 ; extra == 'excel' - - pyarrow>=13.0.0 ; extra == 'parquet' - - pyarrow>=13.0.0 ; extra == 'feather' - - pyiceberg>=0.8.1 ; extra == 'iceberg' - - tables>=3.10.1 ; extra == 'hdf5' - - pyreadstat>=1.2.8 ; extra == 'spss' - - sqlalchemy>=2.0.36 ; extra == 'postgresql' - - psycopg2>=2.9.10 ; extra == 'postgresql' - - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.36 ; extra == 'mysql' - - pymysql>=1.1.1 ; extra == 'mysql' - - sqlalchemy>=2.0.36 ; extra == 'sql-other' - - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' - - beautifulsoup4>=4.12.3 ; extra == 'html' - - html5lib>=1.1 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'xml' - - matplotlib>=3.9.3 ; extra == 'plot' - - jinja2>=3.1.5 ; extra == 'output-formatting' - - tabulate>=0.9.0 ; extra == 'output-formatting' - - pyqt5>=5.15.9 ; extra == 'clipboard' - - qtpy>=2.4.2 ; extra == 'clipboard' - - zstandard>=0.23.0 ; extra == 'compression' - - pytz>=2024.2 ; extra == 'timezone' - - adbc-driver-postgresql>=1.2.0 ; extra == 'all' - - adbc-driver-sqlite>=1.2.0 ; extra == 'all' - - beautifulsoup4>=4.12.3 ; extra == 'all' - - bottleneck>=1.4.2 ; extra == 'all' - - fastparquet>=2024.11.0 ; extra == 'all' - - fsspec>=2024.10.0 ; extra == 'all' - - gcsfs>=2024.10.0 ; extra == 'all' - - html5lib>=1.1 ; extra == 'all' - - hypothesis>=6.116.0 ; extra == 'all' - - jinja2>=3.1.5 ; extra == 'all' - - lxml>=5.3.0 ; extra == 'all' - - matplotlib>=3.9.3 ; extra == 'all' - - numba>=0.60.0 ; extra == 'all' - - numexpr>=2.10.2 ; extra == 'all' - - odfpy>=1.4.1 ; extra == 'all' - - openpyxl>=3.1.5 ; extra == 'all' - - psycopg2>=2.9.10 ; extra == 'all' - - pyarrow>=13.0.0 ; extra == 'all' - - pyiceberg>=0.8.1 ; extra == 'all' - - pymysql>=1.1.1 ; extra == 'all' - - pyqt5>=5.15.9 ; extra == 'all' - - pyreadstat>=1.2.8 ; extra == 'all' - - pytest>=8.3.4 ; extra == 'all' - - pytest-xdist>=3.6.1 ; extra == 'all' - - python-calamine>=0.3.0 ; extra == 'all' - - pytz>=2024.2 ; extra == 'all' - - pyxlsb>=1.0.10 ; extra == 'all' - - qtpy>=2.4.2 ; extra == 'all' - - scipy>=1.14.1 ; extra == 'all' - - s3fs>=2024.10.0 ; extra == 'all' - - sqlalchemy>=2.0.36 ; extra == 'all' - - tables>=3.10.1 ; extra == 'all' - - tabulate>=0.9.0 ; extra == 'all' - - xarray>=2024.10.0 ; extra == 'all' - - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.2.0 ; extra == 'all' - - zstandard>=0.23.0 ; extra == 'all' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl - name: pandas - version: 3.0.2 - sha256: 970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14 - requires_dist: - - numpy>=1.26.0 ; python_full_version < '3.14' - - numpy>=2.3.3 ; python_full_version >= '3.14' - - python-dateutil>=2.8.2 - - tzdata ; sys_platform == 'win32' - - tzdata ; sys_platform == 'emscripten' - - hypothesis>=6.116.0 ; extra == 'test' - - pytest>=8.3.4 ; extra == 'test' - - pytest-xdist>=3.6.1 ; extra == 'test' - - pyarrow>=13.0.0 ; extra == 'pyarrow' - - bottleneck>=1.4.2 ; extra == 'performance' - - numba>=0.60.0 ; extra == 'performance' - - numexpr>=2.10.2 ; extra == 'performance' - - scipy>=1.14.1 ; extra == 'computation' - - xarray>=2024.10.0 ; extra == 'computation' - - fsspec>=2024.10.0 ; extra == 'fss' - - s3fs>=2024.10.0 ; extra == 'aws' - - gcsfs>=2024.10.0 ; extra == 'gcp' - - odfpy>=1.4.1 ; extra == 'excel' - - openpyxl>=3.1.5 ; extra == 'excel' - - python-calamine>=0.3.0 ; extra == 'excel' - - pyxlsb>=1.0.10 ; extra == 'excel' - - xlrd>=2.0.1 ; extra == 'excel' - - xlsxwriter>=3.2.0 ; extra == 'excel' - - pyarrow>=13.0.0 ; extra == 'parquet' - - pyarrow>=13.0.0 ; extra == 'feather' - - pyiceberg>=0.8.1 ; extra == 'iceberg' - - tables>=3.10.1 ; extra == 'hdf5' - - pyreadstat>=1.2.8 ; extra == 'spss' - - sqlalchemy>=2.0.36 ; extra == 'postgresql' - - psycopg2>=2.9.10 ; extra == 'postgresql' - - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.36 ; extra == 'mysql' - - pymysql>=1.1.1 ; extra == 'mysql' - - sqlalchemy>=2.0.36 ; extra == 'sql-other' - - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' - - beautifulsoup4>=4.12.3 ; extra == 'html' - - html5lib>=1.1 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'xml' - - matplotlib>=3.9.3 ; extra == 'plot' - - jinja2>=3.1.5 ; extra == 'output-formatting' - - tabulate>=0.9.0 ; extra == 'output-formatting' - - pyqt5>=5.15.9 ; extra == 'clipboard' - - qtpy>=2.4.2 ; extra == 'clipboard' - - zstandard>=0.23.0 ; extra == 'compression' - - pytz>=2024.2 ; extra == 'timezone' - - adbc-driver-postgresql>=1.2.0 ; extra == 'all' - - adbc-driver-sqlite>=1.2.0 ; extra == 'all' - - beautifulsoup4>=4.12.3 ; extra == 'all' - - bottleneck>=1.4.2 ; extra == 'all' - - fastparquet>=2024.11.0 ; extra == 'all' - - fsspec>=2024.10.0 ; extra == 'all' - - gcsfs>=2024.10.0 ; extra == 'all' - - html5lib>=1.1 ; extra == 'all' - - hypothesis>=6.116.0 ; extra == 'all' - - jinja2>=3.1.5 ; extra == 'all' - - lxml>=5.3.0 ; extra == 'all' - - matplotlib>=3.9.3 ; extra == 'all' - - numba>=0.60.0 ; extra == 'all' - - numexpr>=2.10.2 ; extra == 'all' - - odfpy>=1.4.1 ; extra == 'all' - - openpyxl>=3.1.5 ; extra == 'all' - - psycopg2>=2.9.10 ; extra == 'all' - - pyarrow>=13.0.0 ; extra == 'all' - - pyiceberg>=0.8.1 ; extra == 'all' - - pymysql>=1.1.1 ; extra == 'all' - - pyqt5>=5.15.9 ; extra == 'all' - - pyreadstat>=1.2.8 ; extra == 'all' - - pytest>=8.3.4 ; extra == 'all' - - pytest-xdist>=3.6.1 ; extra == 'all' - - python-calamine>=0.3.0 ; extra == 'all' - - pytz>=2024.2 ; extra == 'all' - - pyxlsb>=1.0.10 ; extra == 'all' - - qtpy>=2.4.2 ; extra == 'all' - - scipy>=1.14.1 ; extra == 'all' - - s3fs>=2024.10.0 ; extra == 'all' - - sqlalchemy>=2.0.36 ; extra == 'all' - - tables>=3.10.1 ; extra == 'all' - - tabulate>=0.9.0 ; extra == 'all' - - xarray>=2024.10.0 ; extra == 'all' - - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.2.0 ; extra == 'all' - - zstandard>=0.23.0 ; extra == 'all' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl - name: pandas - version: 3.0.2 - sha256: a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4 + 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: 694692 + timestamp: 1756385147981 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + sha256: 9dc40c2610a6e6727d635c62cced5ef30b7b30123f5ef67d6139e23d21744b3a + md5: 1e610f2416b6acdd231c5f573d754a0f + depends: + - vc14_runtime >=14.44.35208 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 19356 + timestamp: 1767320221521 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + sha256: 02732f953292cce179de9b633e74928037fa3741eb5ef91c3f8bae4f761d32a5 + md5: 37eb311485d2d8b2c419449582046a42 + depends: + - ucrt >=10.0.20348.0 + - vcomp14 14.44.35208 h818238b_34 + constrains: + - vs2015_runtime 14.44.35208.* *_34 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 683233 + timestamp: 1767320219644 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + sha256: 878d5d10318b119bd98ed3ed874bd467acbe21996e1d81597a1dbf8030ea0ce6 + md5: 242d9f25d2ae60c76b38a5e42858e51d + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.44.35208.* *_34 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 115235 + timestamp: 1767320173250 +- conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 + sha256: 9df10c5b607dd30e05ba08cbd940009305c75db242476f4e845ea06008b0a283 + md5: 1cee351bf20b830d991dbe0bc8cd7dfe + license: MIT + license_family: MIT + purls: [] + size: 1176306 +- conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + sha256: 80ee68c1e7683a35295232ea79bcc87279d31ffeda04a1665efdb43cbd50a309 + md5: 433699cba6602098ae8957a323da2664 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + purls: [] + size: 63944 + timestamp: 1753484092156 +- conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-h507cc87_10.conda + sha256: b8568dfde46edf3455458912ea6ffb760e4456db8230a0cf34ecbc557d3c275f + md5: 1ab0237036bfb14e923d6107473b0021 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libsodium >=1.0.21,<1.0.22.0a0 + - krb5 >=1.22.2,<1.23.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 265665 + timestamp: 1772476832995 +- conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + sha256: 368d8628424966fd8f9c8018326a9c779e06913dd39e646cf331226acc90e5b2 + md5: 053b84beec00b71ea8ff7a4f84b55207 + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 388453 + timestamp: 1764777142545 +- pypi: . + name: easydiffraction requires_dist: - - numpy>=1.26.0 ; python_full_version < '3.14' - - numpy>=2.3.3 ; python_full_version >= '3.14' - - python-dateutil>=2.8.2 - - tzdata ; sys_platform == 'win32' - - tzdata ; sys_platform == 'emscripten' - - hypothesis>=6.116.0 ; extra == 'test' - - pytest>=8.3.4 ; extra == 'test' - - pytest-xdist>=3.6.1 ; extra == 'test' - - pyarrow>=13.0.0 ; extra == 'pyarrow' - - bottleneck>=1.4.2 ; extra == 'performance' - - numba>=0.60.0 ; extra == 'performance' - - numexpr>=2.10.2 ; extra == 'performance' - - scipy>=1.14.1 ; extra == 'computation' - - xarray>=2024.10.0 ; extra == 'computation' - - fsspec>=2024.10.0 ; extra == 'fss' - - s3fs>=2024.10.0 ; extra == 'aws' - - gcsfs>=2024.10.0 ; extra == 'gcp' - - odfpy>=1.4.1 ; extra == 'excel' - - openpyxl>=3.1.5 ; extra == 'excel' - - python-calamine>=0.3.0 ; extra == 'excel' - - pyxlsb>=1.0.10 ; extra == 'excel' - - xlrd>=2.0.1 ; extra == 'excel' - - xlsxwriter>=3.2.0 ; extra == 'excel' - - pyarrow>=13.0.0 ; extra == 'parquet' - - pyarrow>=13.0.0 ; extra == 'feather' - - pyiceberg>=0.8.1 ; extra == 'iceberg' - - tables>=3.10.1 ; extra == 'hdf5' - - pyreadstat>=1.2.8 ; extra == 'spss' - - sqlalchemy>=2.0.36 ; extra == 'postgresql' - - psycopg2>=2.9.10 ; extra == 'postgresql' - - adbc-driver-postgresql>=1.2.0 ; extra == 'postgresql' - - sqlalchemy>=2.0.36 ; extra == 'mysql' - - pymysql>=1.1.1 ; extra == 'mysql' - - sqlalchemy>=2.0.36 ; extra == 'sql-other' - - adbc-driver-postgresql>=1.2.0 ; extra == 'sql-other' - - adbc-driver-sqlite>=1.2.0 ; extra == 'sql-other' - - beautifulsoup4>=4.12.3 ; extra == 'html' - - html5lib>=1.1 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'html' - - lxml>=5.3.0 ; extra == 'xml' - - matplotlib>=3.9.3 ; extra == 'plot' - - jinja2>=3.1.5 ; extra == 'output-formatting' - - tabulate>=0.9.0 ; extra == 'output-formatting' - - pyqt5>=5.15.9 ; extra == 'clipboard' - - qtpy>=2.4.2 ; extra == 'clipboard' - - zstandard>=0.23.0 ; extra == 'compression' - - pytz>=2024.2 ; extra == 'timezone' - - adbc-driver-postgresql>=1.2.0 ; extra == 'all' - - adbc-driver-sqlite>=1.2.0 ; extra == 'all' - - beautifulsoup4>=4.12.3 ; extra == 'all' - - bottleneck>=1.4.2 ; extra == 'all' - - fastparquet>=2024.11.0 ; extra == 'all' - - fsspec>=2024.10.0 ; extra == 'all' - - gcsfs>=2024.10.0 ; extra == 'all' - - html5lib>=1.1 ; extra == 'all' - - hypothesis>=6.116.0 ; extra == 'all' - - jinja2>=3.1.5 ; extra == 'all' - - lxml>=5.3.0 ; extra == 'all' - - matplotlib>=3.9.3 ; extra == 'all' - - numba>=0.60.0 ; extra == 'all' - - numexpr>=2.10.2 ; extra == 'all' - - odfpy>=1.4.1 ; extra == 'all' - - openpyxl>=3.1.5 ; extra == 'all' - - psycopg2>=2.9.10 ; extra == 'all' - - pyarrow>=13.0.0 ; extra == 'all' - - pyiceberg>=0.8.1 ; extra == 'all' - - pymysql>=1.1.1 ; extra == 'all' - - pyqt5>=5.15.9 ; extra == 'all' - - pyreadstat>=1.2.8 ; extra == 'all' - - pytest>=8.3.4 ; extra == 'all' - - pytest-xdist>=3.6.1 ; extra == 'all' - - python-calamine>=0.3.0 ; extra == 'all' - - pytz>=2024.2 ; extra == 'all' - - pyxlsb>=1.0.10 ; extra == 'all' - - qtpy>=2.4.2 ; extra == 'all' - - scipy>=1.14.1 ; extra == 'all' - - s3fs>=2024.10.0 ; extra == 'all' - - sqlalchemy>=2.0.36 ; extra == 'all' - - tables>=3.10.1 ; extra == 'all' - - tabulate>=0.9.0 ; extra == 'all' - - xarray>=2024.10.0 ; extra == 'all' - - xlrd>=2.0.1 ; extra == 'all' - - xlsxwriter>=3.2.0 ; extra == 'all' - - zstandard>=0.23.0 ; extra == 'all' - requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl - name: pandas - version: 3.0.2 - sha256: 0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288 + - arviz + - asciichartpy + - asteval + - bumps + - colorama + - crysfml + - cryspy + - darkdetect + - dfo-ls + - diffpy-pdffit2 + - diffpy-utils + - gemmi + - lmfit + - numpy + - pandas + - plotly + - pooch + - py3dmol + - rich + - scipy + - sympy + - tabulate + - 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: - - 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' + - 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/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/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl + name: h5py + version: 3.16.0 + sha256: 96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6 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.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: + - 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.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/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/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/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/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/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.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.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' -- 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=compressed-mapping - size: 82472 - timestamp: 1777722955579 -- 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/0c/53/b50773ecf1d1e4a5858ee13011e30317ba02639ae4a1411a34967951fc9b/crysfml-0.6.2-cp314-cp314-win_amd64.whl + name: crysfml + version: 0.6.2 + sha256: 4278178f2028360f489f2cdfda7f2f7f26e4f1674b50eb934f403bb443a8f00a 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/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/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/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/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl + name: pyparsing + version: 3.3.2 + sha256: 850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d requires_dist: - - hyperscan>=0.7 ; extra == 'hyperscan' - - typing-extensions>=4 ; extra == 'optional' - - google-re2>=1.1 ; extra == 're2' + - railroad-diagrams ; extra == 'diagrams' + - jinja2 ; extra == 'diagrams' 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/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 @@ -9595,326 +8519,488 @@ packages: - 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 +- pypi: https://files.pythonhosted.org/packages/12/aa/fb2a0649fdeef5ab7072d221e8f4df164098792c813af6c87e2581cfa860/mpltoolbox-26.2.0-py3-none-any.whl + name: mpltoolbox + version: 26.2.0 + sha256: cd2668db4216fc4d7c2ba37974961aa61445f1517527b645b6082930e35ba7f0 + requires_dist: + - matplotlib + - ipympl ; extra == 'test' + - pytest>=8.0 ; extra == 'test' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl + name: interrogate + version: 1.7.0 + sha256: b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12 + requires_dist: + - attrs + - click>=7.1 + - colorama + - py + - tabulate + - tomli ; python_full_version < '3.11' + - cairosvg ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-autobuild ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-mock ; extra == 'dev' + - coverage[toml] ; extra == 'dev' + - wheel ; extra == 'dev' + - pre-commit ; extra == 'dev' + - sphinx ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - cairosvg ; extra == 'png' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-mock ; extra == 'tests' + - coverage[toml] ; extra == 'tests' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/13/95/cf3f7fe4910cf0365fa8ea0c731f4b8a624d97cd76ea777913ac8d0868e2/mkdocs_jupyter-0.26.3-py3-none-any.whl + name: mkdocs-jupyter + version: 0.26.3 + sha256: cd6644fb578131157194d750fd4d10fc2fd8f1e84e00036ee62df3b5b4b84c82 + requires_dist: + - ipykernel>6.0.0,<8 + - jupytext>1.13.8,<2 + - mkdocs-material>9.0.0 + - mkdocs>=1.4.0,<2 + - nbconvert>=7.2.9,<8 + - pygments>2.12.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/14/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/1a/1f/86b4d15221096cb5500bcd73bf350745749e3ba056cdd7a7f75f126f154e/scipp-26.3.1-cp312-cp312-win_amd64.whl + name: scipp + version: 26.3.1 + sha256: 8b036876edf7895d17644f59711037d2d7d9ad048b1a503200646d8229fb1ad7 + 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/1a/c7/78200c18404ded028758b28b588aa1f4f3acd851271a74156a2a3db9eadf/crysfml-0.6.2-cp312-cp312-win_amd64.whl + name: crysfml + version: 0.6.2 + sha256: cd2027d98252a138bd7260b57f77c8d3c69e0da95454a44a9b80551198e8a327 + requires_dist: + - 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/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl + name: versioningit + version: 3.3.0 + sha256: 23b1db3c4756cded9bd6b0ddec6643c261e3d0c471707da3e0b230b81ce53e4b + requires_dist: + - importlib-metadata>=3.6 ; python_full_version < '3.10' + - packaging>=17.1 + - tomli>=1.2,<3.0 ; python_full_version < '3.11' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl + name: dill + version: 0.4.1 + sha256: 1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d + requires_dist: + - objgraph>=1.7.2 ; extra == 'graph' + - gprof2dot>=2022.7.29 ; extra == 'profile' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl + name: annotated-doc + version: 0.0.4 + sha256: 571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/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: + - 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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl + name: virtualenv + version: 21.3.2 + sha256: c58ea748fa50bb2a4367da5ba3d30b02458ed40b4ea888faad94021f3309f764 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 + - 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/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/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl - name: pillow - version: 12.2.0 - sha256: f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421 + - 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/3a/eb/fea4d1d51c49832120f7f285d07306db3960f423a2612c6057caf3e8196f/pip-26.1.1-py3-none-any.whl - name: pip - version: 26.1.1 - sha256: 99cb1c2899893b075ff56e4ed0af55669a955b49ad7fb8d8603ecdaf4ed653fb + - 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: + - 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 +- 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: - - 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/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl - name: plotly - version: 6.7.0 - sha256: ac8aca1c25c663a59b5b9140a549264a5badde2e057d79b8c772ae2920e32ff0 + - 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 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 + - prettytable + - ply + - numpy +- 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: - - 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/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/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl + name: fonttools + version: 4.62.1 + sha256: 9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42 requires_dist: - - pywin32 ; platform_python_implementation != 'PyPy' and sys_platform == 'win32' - - paramiko ; extra == 'ssh' + - 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/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/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 @@ -9929,212 +9015,244 @@ 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/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/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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + name: python-discovery + version: 1.3.0 + sha256: 441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f requires_dist: - - prettytable - - ply - - numpy + - 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/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl + name: mkdocstrings-python + version: 2.0.3 + sha256: 0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12 + requires_dist: + - mkdocstrings>=0.30 + - mkdocs-autorefs>=1.4 + - griffelib>=2.0 + - typing-extensions>=4.0 ; python_full_version < '3.11' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/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/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: + - 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 @@ -10143,1484 +9261,2154 @@ packages: - 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 +- 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: + - 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/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl + name: format-docstring + version: 0.2.7 + sha256: c9d50eafebe0f260e3270ca662ff3a0ed4050f64d95e352f8c5f88d9aede42d6 + requires_dist: + - click>=8.0 + - jupyter-notebook-parser>=0.1.4 + - tomli>=1.1.0 ; python_full_version < '3.11' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/33/eb/f9f1ded8e4db9638f9530c3782eb01f5ab04945f4cb9e597a51c203fa4c5/diffpy_pdffit2-1.6.0.tar.gz + name: diffpy-pdffit2 + version: 1.6.0 + sha256: 11a65466f8790f5ac7ae45f2f3fc0d5d116d156d274bcfc079df653123d080e2 + requires_dist: + - diffpy-structure + requires_python: '>=3.12,<3.15' +- pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl + name: tokenize-rt + version: 6.2.0 + sha256: a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/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/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl + name: coverage + version: 7.14.0 + sha256: 829994cfe1aeb773ca27bf246d4badc1e764893e3bfb98fff820fcecd1ca4662 + requires_dist: + - 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/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/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/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl + name: pandas + version: 3.0.3 + sha256: 3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5 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 - name: pycifrw - version: 5.0.1 - sha256: 379801e71509d0f9c59b56edc5ceb6600796eaf2b84ee5e0f5a256c76542047d + - 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: - - 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 + - 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' -- 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/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - name: pydantic - version: 2.13.4 - sha256: 45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba +- 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: - - 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' + - 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/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl - name: pydantic-core - version: 2.46.4 - sha256: 962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f +- 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: - - typing-extensions>=4.14.1 + - prompt-toolkit>=2.0,<4.0 requires_python: '>=3.9' -- 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 +- 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: - - typing-extensions>=4.14.1 - requires_python: '>=3.9' -- 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 + - 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: - - typing-extensions>=4.14.1 + - 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/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/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' +- 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: + - setuptools + - flake8 ; extra == 'qa' +- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl + name: typer + version: 0.25.1 + sha256: 75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89 + requires_dist: + - click>=8.2.1 + - shellingham>=1.3.0 + - rich>=13.8.0 + - annotated-doc>=0.0.2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl name: pydantic-core version: 2.46.4 - sha256: 7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b + sha256: e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89 requires_dist: - typing-extensions>=4.14.1 requires_python: '>=3.9' -- 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 +- 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: - - typing-extensions>=4.14.1 - 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 + - 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: - - typing-extensions>=4.14.1 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - name: pydoclint - version: 0.8.3 - sha256: 5fc9b82d0d515afce0908cb70e8ff695a68b19042785c248c4f227ad66b4a164 + - 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/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' + - 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: + - numpy + 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' +- 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/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.0 - - docstring-parser-fork>=0.0.12 - - tomli>=2.0.1 ; python_full_version < '3.11' - - flake8>=4 ; extra == 'flake8' + - 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/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/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl + name: scipy + version: 1.17.1 + sha256: 3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19 requires_dist: - - markdown>=3.6 + - 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 - - pygments>=2.19.1 ; extra == 'extra' + - 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/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/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: - - railroad-diagrams ; extra == 'diagrams' - - jinja2 ; extra == 'diagrams' + - 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: + - 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: + - 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/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - name: pyproject-hooks - version: 1.2.0 - sha256: 9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913 +- 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' -- 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/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl + name: ipywidgets + version: 8.1.8 + sha256: ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e 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/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' + - comm>=0.1.3 + - ipython>=6.1.0 + - traitlets>=4.3.1 + - widgetsnbextension~=4.0.14 + - jupyterlab-widgets~=3.0.15 + - jsonschema ; extra == 'test' + - ipykernel ; extra == 'test' + - pytest>=3.6.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytz ; extra == 'test' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/57/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/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/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/58/e0/f1871f520c359e4e3a2eb7437c9e7e792bb6c356414e8617937561167caf/pycifrw-5.0.1.tar.gz + name: pycifrw + version: 5.0.1 + sha256: e636b80be6a2be15b215e69ecec0c0a784ebcbfed8b1e3bac4bcc6e6ba9a75e0 requires_dist: - - coverage[toml]>=7.10.6 - - pluggy>=1.2 - - pytest>=7 - - process-tests ; extra == 'testing' - - pytest-xdist ; extra == 'testing' - - virtualenv ; extra == 'testing' + - 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/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/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: - - execnet>=2.1 - - pytest>=7.0.0 - - filelock ; extra == 'testing' - - psutil>=3.0 ; extra == 'psutil' - - setproctitle ; extra == 'setproctitle' + - 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' -- 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - name: python-discovery - version: 1.3.0 - sha256: 441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f +- 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: - - 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 + - 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: - - 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 + - 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: - - 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' -- 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 + - 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' + - 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/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: - - 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' -- 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=hash-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 + - 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/69/d1/705e6c19b437a4105bf3b9ae7945fcfc3ad2abb73d14bae0a3f2d58b305b/arviz_base-1.1.0-py3-none-any.whl + name: arviz-base + version: 1.1.0 + sha256: 4f97016f697751038f45d144331a1830c921f0ebc2739d5df343120fba453e83 requires_dist: - - pyyaml - 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 + - numpy>=2 + - xarray>=2024.11.0 + - typing-extensions>=3.10 + - lazy-loader>=0.4 + - build ; extra == 'check' + - pre-commit ; extra == 'check' + - docstub==0.4 ; extra == 'check' + - mypy ; extra == 'check' + - pre-commit ; extra == 'ci' + - cloudpickle ; extra == 'ci' + - sphinx-book-theme ; extra == 'doc' + - myst-parser[linkify] ; extra == 'doc' + - myst-nb ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - numpydoc ; extra == 'doc' + - sphinx>=5 ; extra == 'doc' + - sphinx-design ; extra == 'doc' + - jupyter-sphinx ; extra == 'doc' + - h5netcdf[h5py] ; extra == 'doc' + - h5netcdf[h5py] ; extra == 'h5netcdf' + - netcdf4 ; extra == 'netcdf4' + - xarray!=2025.8.0 ; extra == 'test' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - scipy ; extra == 'test' + - zarr ; extra == 'zarr' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/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: - - prompt-toolkit>=2.0,<4.0 + - 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/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - name: radon - version: 6.0.1 - sha256: 632cc032364a6f8bb1010a2f6a12d0f14bc7e5ede76585ef29dc0cecf4cd8859 +- 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: - - 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 + - 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/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: - - 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 - 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 + - 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: - - cyclebane>=24.6.0 + - diffpy-structure + requires_python: '>=3.12,<3.15' +- pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + name: mdit-py-plugins + version: 0.6.0 + sha256: f7e7a25d8b616fee99cb1e330da73451d11a8061baf39bb9663ab9ce0e005b90 + 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/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' - - 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/0f/0e/0eb94e64f5badef67f11fe1e448dde2a44f00940d8949f4adf71d560552e/scipp-26.3.1-cp314-cp314-macosx_14_0_arm64.whl - name: scipp - version: 26.3.1 - sha256: 1f103f6c5a33b08773206c613fe2dd9c02585f5c4e44b77311c54b7828a758ed + - 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/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/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/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl + name: multidict + version: 6.7.1 + sha256: fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19 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/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 + - 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: - - 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 + - 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>=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/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 + - 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: - - 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 + - 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: - - 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 + - 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/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl + name: pydoclint + version: 0.8.3 + sha256: 5fc9b82d0d515afce0908cb70e8ff695a68b19042785c248c4f227ad66b4a164 + requires_dist: + - click>=8.1.0 + - docstring-parser-fork>=0.0.12 + - tomli>=2.0.1 ; python_full_version < '3.11' + - flake8>=4 ; extra == 'flake8' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/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: 3b9865ebdb7923eb25739b7cda624555d45275341000f099c1dcf371b1dd7e35 + sha256: 1758a18fffca9c7c2a6fa9547cf87bf45f9d52fc3ccbdffcf7524f71bc060424 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' + - 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' - - pytest-xdist>=3.0 ; extra == 'test' - - pythreejs>=2.4.1 ; extra == 'test' - - sciline>=25.1.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/5c/01/6cb4d63c6b6933be4b7945b2f64638336420f04ea71ca5b9a7539c008bc5/scippnexus-26.1.1-py3-none-any.whl - name: scippnexus - version: 26.1.1 - sha256: 899a0a5e71291b7809d902c17b6c74addf5a805397eabcec557491ff74eead12 +- 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: - - 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 + - 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/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 + - 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: + - 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: - - numpy>=1.26.4,<2.7 - - pytest>=8.0.0 ; extra == 'test' + - 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' + - coverage ; extra == 'test' + - sphinx>=5,<7 ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx-rtd-theme>=0.2.5 ; extra == 'docs' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl + name: typeguard + version: 4.5.1 + sha256: 44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40 + requires_dist: + - importlib-metadata>=3.6 ; python_full_version < '3.10' + - typing-extensions>=4.14.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/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/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl - name: scipy - version: 1.17.1 - sha256: 3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19 +- 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' - - 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' + - 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/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/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl - name: scipy - version: 1.17.1 - sha256: 41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87 +- 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: - - numpy>=1.26.4,<2.7 - - pytest>=8.0.0 ; extra == 'test' + - 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/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' - - 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' + - 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/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/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/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/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl - name: scipy - version: 1.17.1 - sha256: cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086 +- 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: - - 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' + - 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' @@ -11661,954 +11449,1217 @@ 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 - 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 - requires_dist: - - wsproto - - tox ; extra == 'dev' - - flake8 ; extra == 'dev' - - pytest ; extra == 'dev' - - pytest-cov ; extra == 'dev' +- 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' - 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 - 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 + - 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: - - 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' + - 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/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/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl + name: verspec + version: 0.1.0 + sha256: 741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31 requires_dist: - - numpy>=1.20,<3 - - importlib-resources ; python_full_version < '3.10' - - typing-extensions>=4.9.0 ; python_full_version < '3.13' + - coverage ; extra == 'test' + - flake8>=3.7 ; extra == 'test' + - mypy ; extra == 'test' + - pretend ; extra == 'test' - 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' +- 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/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/8f/82/b54e646be7b938fcbdda10030c6533bd2bb1a59930a1381cc83d6050a49c/spglib-2.6.0-cp312-cp312-win_amd64.whl - name: spglib - version: 2.6.0 - sha256: 86d0fd355689e58becd2cda609b03c3a0d9ad9d6f761cefd08b970db6f314eae +- 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: - - 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' + - 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/bd/8c/d4907ad4f6bdc5bf79462d8767728713a7b316918a7444df372958a0e417/spglib-2.6.0-cp312-cp312-macosx_11_0_arm64.whl - name: spglib - version: 2.6.0 - sha256: 83ea2e90addc7232017c793a32d94b47bc68040c595671d1cbb836ede4349510 +- 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: - - 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' + - 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/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/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/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/bf/34/1fe99124be59579ebd24316522e1da780979c856977b142c0dcd878b0a2d/spglib-2.6.0.tar.gz - name: spglib - version: 2.6.0 - sha256: d66eda2ba00a1e14fd96ec9c3b4dbf8ab0fb3f124643e35785c71ee455b408eb +- 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: - - numpy>=1.20,<3 - - importlib-resources ; python_full_version < '3.10' - - typing-extensions>=4.9.0 ; python_full_version < '3.13' + - 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: + - 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' - - 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' + - 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/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 +- 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: - - 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' + - 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' -- 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/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: - - 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 + - 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: - - 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/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl - name: sqlalchemy - version: 2.0.49 - sha256: 4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b + - numpy + - pycifrw + - diffpy-utils + requires_python: '>=3.12,<3.15' +- pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl + name: watchdog + version: 6.0.0 + sha256: 20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2 + requires_dist: + - pyyaml>=3.10 ; extra == 'watchmedo' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b7/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/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl + name: validate-pyproject + version: '0.25' + sha256: f9d05e2686beff82f9ea954f582306b036ced3d3feb258c1110f2c2a495b1981 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 + - 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: - - 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 + - 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: - - 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' + - 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: + - 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' -- 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 +- 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 + sha256: 83ea2e90addc7232017c793a32d94b47bc68040c595671d1cbb836ede4349510 requires_dist: - - mpmath>=1.1.0,<1.4 - - pytest>=7.1.0 ; extra == 'dev' - - hypothesis>=6.70.0 ; extra == 'dev' + - 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/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/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: - - wcwidth ; extra == 'widechars' - requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-ha3553a1_1.conda - sha256: 5ff149ba6832bf4ded4b43bf0a41cde7be814802a95070553176c087f65b2a01 - md5: 34aa94d586fe95fa121966c0d4e73cf4 - 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 - purls: [] - size: 156910 - timestamp: 1777976465531 -- 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 + - 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 + sha256: d66eda2ba00a1e14fd96ec9c3b4dbf8ab0fb3f124643e35785c71ee455b408eb + 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/bf/50/98b146aea0f1cd7531d25f12bea69fa9ce8d1662124f93fb30dc4511b65e/docstring_parser_fork-0.0.14-py3-none-any.whl + name: docstring-parser-fork + version: 0.0.14 + sha256: 4c544f234ef2cc2749a3df32b70c437d77888b1099143a1ad5454452c574b9af + requires_dist: + - docstring-parser[docs] ; extra == 'dev' + - docstring-parser[test] ; extra == 'dev' + - pre-commit>=2.16.0 ; python_full_version >= '3.9' and extra == 'dev' + - pydoctor>=25.4.0 ; extra == 'docs' + - pytest ; extra == 'test' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/bf/e1/9e8e09ab8fc5c77f228e3271188dc4f569d12692548c42bff81b1fbba5e1/easydiffraction-0.16.0-py3-none-any.whl + name: easydiffraction + version: 0.16.0 + sha256: 2691a1e175974ca79e0ec3c219d92b77f277c38fb3b0b8d25f6f7e99696bf70f + requires_dist: + - asciichartpy + - asteval + - bumps + - colorama + - crysfml + - cryspy + - darkdetect + - dfo-ls + - diffpy-pdffit2 + - diffpy-utils + - essdiffraction + - gemmi + - lmfit + - numpy + - pandas + - plotly + - pooch + - py3dmol + - rich + - scipy + - sympy + - tabulate + - typeguard + - typer + - uncertainties + - varname + - build ; extra == 'dev' + - copier ; extra == 'dev' + - docstripy ; extra == 'dev' + - format-docstring ; extra == 'dev' + - gitpython ; extra == 'dev' + - interrogate ; extra == 'dev' + - jinja2 ; extra == 'dev' + - jupyterquiz ; extra == 'dev' + - jupytext ; extra == 'dev' + - mike ; extra == 'dev' + - mkdocs ; extra == 'dev' + - mkdocs-autorefs ; extra == 'dev' + - mkdocs-jupyter ; extra == 'dev' + - mkdocs-markdownextradata-plugin ; extra == 'dev' + - mkdocs-material ; extra == 'dev' + - mkdocs-plugin-inline-svg ; extra == 'dev' + - mkdocstrings-python ; extra == 'dev' + - nbmake ; extra == 'dev' + - nbqa ; extra == 'dev' + - nbstripout ; extra == 'dev' + - pre-commit ; extra == 'dev' + - pydoclint ; extra == 'dev' + - pytest ; extra == 'dev' + - pytest-benchmark ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - pyyaml ; extra == 'dev' + - radon ; extra == 'dev' + - ruff ; extra == 'dev' + - spdx-headers ; extra == 'dev' + - validate-pyproject[all] ; extra == 'dev' + - versioningit ; extra == 'dev' + requires_python: '>=3.12' +- pypi: https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl + name: smmap + version: 5.0.3 + sha256: c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/c2/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/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: - - 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' + - 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/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 +- pypi: https://files.pythonhosted.org/packages/c6/3d/020a6b6248c3d4a37797db068256f0b3f15b01bc481327ba888c50309aa8/mkdocs_plugin_inline_svg-0.1.0-py3-none-any.whl + name: mkdocs-plugin-inline-svg + version: 0.1.0 + sha256: a5aab2d98a19b24019f8e650f54fc647c2f31e7d0e36fc5cf2d2161acc0ea49a + requires_dist: + - mkdocs + requires_python: '>=3.5' +- pypi: https://files.pythonhosted.org/packages/c7/6b/6c02f55c2ce2f137ccca0986be7dd89bea31d5bee4346b4377fa3b8586df/ncrystal_core-4.4.2-py3-none-win_amd64.whl + name: ncrystal-core + version: 4.4.2 + sha256: 9b28a90b63849e6a3a807a0a59f7c2ee57e4c64f5643b2dcb6a798ac8ccf666a + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl + name: dfo-ls + version: 1.6.5 + sha256: d147d42e471e240f9abf8bc38351a88f555ea6a8fcfd83119bbbf93c36f75ab2 + requires_dist: + - setuptools + - numpy + - scipy>=1.11 + - pandas + - pytest ; extra == 'dev' + - sphinx ; extra == 'dev' + - sphinx-rtd-theme ; extra == 'dev' + - trustregion>=1.1 ; extra == 'trustregion' requires_python: '>=3.9' -- 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.15.0-pyhcf101f3_0.conda - sha256: dfb681579be59c2e790c95f7f49b7529a9b0511d6385ad276e3c8988cbd54d2c - md5: 4bada6a6d908a27262af8ebddf4f7492 - depends: - - python >=3.10 - - python - license: BSD-3-Clause - purls: - - pkg:pypi/traitlets?source=compressed-mapping - size: 115165 - timestamp: 1778074251714 -- 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/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl + name: narwhals + version: 2.21.0 + sha256: 1e6617d0fca68ae1fda29e5397c4eaacd3ffc9fffe6bcd6ded0c690475e853be requires_dist: - - traitlets>=4.2.2 - - 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 + - cudf-cu12>=24.10.0 ; extra == 'cudf' + - dask[dataframe]>=2024.8 ; extra == 'dask' + - duckdb>=1.1 ; extra == 'duckdb' + - ibis-framework>=6.0.0 ; extra == 'ibis' + - packaging ; extra == 'ibis' + - pyarrow-hotfix ; extra == 'ibis' + - rich ; extra == 'ibis' + - modin ; extra == 'modin' + - pandas>=1.1.3 ; extra == 'pandas' + - polars>=0.20.4 ; extra == 'polars' + - pyarrow>=13.0.0 ; extra == 'pyarrow' + - pyspark>=3.5.0 ; extra == 'pyspark' + - pyspark[connect]>=3.5.0 ; extra == 'pyspark-connect' + - duckdb>=1.1 ; extra == 'sql' + - sqlparse ; extra == 'sql' + - sqlframe>=3.22.0,!=3.39.3 ; extra == 'sqlframe' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/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: - - importlib-metadata>=3.6 ; python_full_version < '3.10' - - typing-extensions>=4.14.0 + - 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/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/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: - - click>=8.2.1 - - shellingham>=1.3.0 - - rich>=13.8.0 - - annotated-doc>=0.0.2 + - 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/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 -- pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - name: typing-inspection - version: 0.4.2 - sha256: 4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 +- 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: - - 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 + - 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/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 ; extra == 'arrays' - - pytest ; extra == 'test' - - pytest-codspeed ; extra == 'test' + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' - pytest-cov ; extra == 'test' - - scipy ; extra == 'test' - - sphinx ; extra == 'doc' + - 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' - - 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 + - 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/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: - - 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/a4/39/3a0ae5b0edb66e61bb0e8bc53a503495cba5892297ae21faf6ba0525e681/varname-1.0.0-py3-none-any.whl - name: varname - version: 1.0.0 - sha256: 1125bfe981c3bbbe56988f5cb85fdcd7cad923b153283c2d464aea8b4c833d51 + - six + - argparse ; python_full_version < '2.7' + - funcsigs ; python_full_version < '3.3' + - rst2ansi ; extra == 'restructuredtext' +- 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: - - 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 + - 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: - - 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/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - name: verspec - version: 0.1.0 - sha256: 741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31 + - 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/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: - - coverage ; extra == 'test' - - flake8>=3.7 ; extra == 'test' - - mypy ; extra == 'test' - - pretend ; extra == 'test' - - pytest ; extra == 'test' -- pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl - name: virtualenv - version: 21.3.1 - sha256: d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35 + - 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: - - 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 + - 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: - - pyyaml>=3.10 ; extra == 'watchmedo' - 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 + - 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: - - pyyaml>=3.10 ; extra == 'watchmedo' - requires_python: '>=3.9' + - 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 @@ -12616,6 +12667,11 @@ packages: requires_dist: - pyyaml>=3.10 ; extra == 'watchmedo' 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/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl name: watchdog version: 6.0.0 @@ -12623,178 +12679,279 @@ packages: requires_dist: - pyyaml>=3.10 ; extra == 'watchmedo' requires_python: '>=3.9' -- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - sha256: 1ee2d8384972ecbf8630ce8a3ea9d16858358ad3e8566675295e66996d5352da - md5: eb9538b8e55069434a18547f43b96059 - depends: - - python >=3.10 - license: MIT - license_family: MIT - purls: - - pkg:pypi/wcwidth?source=compressed-mapping - size: 82917 - timestamp: 1777744489106 -- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - sha256: 21f6c8a20fe050d09bfda3fb0a9c3493936ce7d6e1b3b5f8b01319ee46d6c6f6 - md5: 6639b6b0d8b5a284f027a2003669aa65 - depends: - - python >=3.10 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/webcolors?source=hash-mapping - size: 18987 - timestamp: 1761899393153 -- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda - sha256: 19ff205e138bb056a46f9e3839935a2e60bd1cf01c8241a5e172a422fed4f9c6 - md5: 2841eb5bfc75ce15e9a0054b98dcd64d - depends: - - python >=3.9 - license: BSD-3-Clause - license_family: BSD - purls: - - pkg:pypi/webencodings?source=hash-mapping - size: 15496 - timestamp: 1733236131358 -- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda - sha256: 42a2b61e393e61cdf75ced1f5f324a64af25f347d16c60b14117393a98656397 - md5: 2f1ed718fcd829c184a6d4f0f2e07409 - depends: - - python >=3.10 - license: Apache-2.0 - license_family: APACHE - purls: - - pkg:pypi/websocket-client?source=hash-mapping - size: 61391 - timestamp: 1759928175142 -- 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 +- 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 + sha256: 4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 + requires_dist: + - typing-extensions>=4.12.0 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/dc/b7/901d837999a9350a7773289f7760cb473d4ba01fdc6ebae0ff2553d269ac/ncrystal_python-4.4.2-py3-none-any.whl + name: ncrystal-python + version: 4.4.2 + sha256: f419318d088fade6bcff1e39e15baf6fe69fcf5306dd681fca1106d1f63a89ce + requires_dist: + - numpy>=1.22 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl + name: email-validator + version: 2.3.0 + sha256: 80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4 requires_dist: - - h11>=0.16.0,<1 + - dnspython>=2.0.0 + - idna>=2.0.0 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl + name: markdown + version: 3.10.2 + sha256: e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36 + requires_dist: + - coverage ; extra == 'testing' + - pyyaml ; extra == 'testing' + - mkdocs>=1.6 ; extra == 'docs' + - mkdocs-nature>=0.6 ; extra == 'docs' + - mdx-gh-links>=0.2 ; extra == 'docs' + - mkdocstrings[python]>=0.28.3 ; extra == 'docs' + - mkdocs-gen-files ; extra == 'docs' + - mkdocs-section-index ; extra == 'docs' + - mkdocs-literate-nav ; extra == 'docs' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/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/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl + name: multidict + version: 6.7.1 + sha256: 5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f 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/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: - - idna>=2.0 - - multidict>=4.0 - - propcache>=0.2.1 + - 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/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/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl - name: yarl - version: 1.23.0 - sha256: 23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719 +- 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: - - idna>=2.0 - - multidict>=4.0 - - propcache>=0.2.1 + - 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/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 +- 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 + 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/eb/be/b257e12f9710819fde40adc972578bee6b72c5992da1bc8369bef2597756/nbmake-1.5.5-py3-none-any.whl + name: nbmake + version: 1.5.5 + sha256: c6fbe6e48b60cacac14af40b38bf338a3b88f47f085c54ac5b8639ff0babaf4b requires_dist: - - idna>=2.0 - - multidict>=4.0 - - propcache>=0.2.1 + - 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/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl + name: prettytable + version: 3.17.0 + sha256: aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287 + requires_dist: + - wcwidth + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-lazy-fixtures ; extra == 'tests' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/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 +- 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: - - idna>=2.0 - - multidict>=4.0 - - propcache>=0.2.1 + - numpy>=1.21.2 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/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: + - typing-extensions>=4.1.0 ; python_full_version < '3.11' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f1/5b/e63c877c4c94382b66de5045e08ec8cd960e8a4d22f0d62a4dfb1f9e5ac6/ipydatawidgets-4.3.5-py2.py3-none-any.whl + name: ipydatawidgets + version: 4.3.5 + sha256: d590cdb7c364f2f6ab346f20b9d2dd661d27a834ef7845bc9d7113118f05ec87 + requires_dist: + - ipywidgets>=7.0.0 + - numpy + - traittypes>=0.2.0 + - sphinx ; extra == 'docs' + - recommonmark ; extra == 'docs' + - sphinx-rtd-theme ; extra == 'docs' + - pytest>=4 ; extra == 'test' + - pytest-cov ; extra == 'test' + - nbval>=0.9.2 ; extra == 'test' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl + name: pathspec + version: 1.1.1 + sha256: a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 + requires_dist: + - 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 + - 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: + - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f4/a4/61adb19f3c74b0dc0e411de4f06ebef564b1f179928f9dffcbd4b378f2ef/jupyter_notebook_parser-0.1.4-py2.py3-none-any.whl + name: jupyter-notebook-parser + version: 0.1.4 + sha256: 27b3b67cf898684e646d569f017cb27046774ad23866cb0bdf51d5f76a46476b + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/f5/57/2a154a69d6642860300bf8eb205d13131104991f2b1065bbb9075ac5c32e/tof-26.3.0-py3-none-any.whl + name: tof + version: 26.3.0 + sha256: e89783a072b05fdb53d9e76fbf919dc8935e75e118fdaf17ca5cc33727ef002b + requires_dist: + - plopp>=23.10.0 + - pooch>=1.5.0 + - scipp>=25.1.0 + - lazy-loader>=0.3 + - pytest>=8.0 ; extra == 'test' + - scippneutron>=24.12.0 ; extra == 'test' + requires_python: '>=3.11' - pypi: https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl name: yarl version: 1.23.0 @@ -12804,91 +12961,148 @@ 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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + name: pymdown-extensions + version: 10.21.2 + sha256: 5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638 + requires_dist: + - markdown>=3.6 + - pyyaml + - pygments>=2.19.1 ; extra == 'extra' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl + name: ghp-import + version: 2.1.0 + sha256: 8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 + requires_dist: + - python-dateutil>=2.8.1 + - twine ; extra == 'dev' + - markdown ; extra == 'dev' + - flake8 ; extra == 'dev' + - wheel ; extra == 'dev' +- pypi: https://files.pythonhosted.org/packages/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/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl + name: numpy + version: 2.4.4 + sha256: 715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74 + requires_python: '>=3.11' +- 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/pyproject.toml b/pyproject.toml index 5faffe6b6..f81f23031 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ dependencies = [ '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 ] @@ -63,7 +64,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 diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index c132e409c..23b9fcf3f 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -184,9 +184,11 @@ 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.') @@ -513,7 +515,13 @@ def joint_fit_experiments(self) -> object: """Per-experiment weight collection for joint fitting.""" return self._joint_fit_experiments - def _run_fit(self, verbosity: str | None = None, *, use_physical_limits: bool = False) -> None: + def _run_fit( + self, + verbosity: str | None = None, + *, + use_physical_limits: bool = False, + random_seed: int | None = None, + ) -> None: """ Execute fitting for all experiments. @@ -542,6 +550,8 @@ def _run_fit(self, verbosity: str | None = None, *, use_physical_limits: bool = 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. """ verb = VerbosityEnum(verbosity if verbosity is not None else self.project.verbosity) @@ -563,10 +573,20 @@ def _run_fit(self, verbosity: str | None = None, *, use_physical_limits: bool = # 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) + self._fit_joint( + verb, + structures, + experiments, + use_physical_limits=use_physical_limits, + random_seed=random_seed, + ) elif mode is FitModeEnum.SINGLE: self._fit_single( - verb, structures, experiments, use_physical_limits=use_physical_limits + verb, + structures, + experiments, + use_physical_limits=use_physical_limits, + random_seed=random_seed, ) elif mode is FitModeEnum.SEQUENTIAL: log.error( @@ -585,6 +605,7 @@ def _fit_joint( experiments: object, *, use_physical_limits: bool, + random_seed: int | None, ) -> None: """ Run joint fitting across all experiments with weights. @@ -599,6 +620,8 @@ 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 @@ -622,6 +645,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 +658,7 @@ def _fit_single( experiments: object, *, use_physical_limits: bool, + random_seed: int | None, ) -> None: """ Run single-mode fitting for each experiment independently. @@ -648,6 +673,8 @@ 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 @@ -666,6 +693,7 @@ def _fit_single( analysis=self, verbosity=verb, use_physical_limits=use_physical_limits, + random_seed=random_seed, ) # After fitting, snapshot parameter values before 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/fit/default.py b/src/easydiffraction/analysis/categories/fit/default.py index c002fe399..7a9ebcb12 100644 --- a/src/easydiffraction/analysis/categories/fit/default.py +++ b/src/easydiffraction/analysis/categories/fit/default.py @@ -148,6 +148,7 @@ def run( verbosity: str | None = None, *, use_physical_limits: bool = False, + random_seed: int | None = None, ) -> None: """ Execute fitting for the owning analysis. @@ -158,6 +159,8 @@ def run( Console output verbosity override. use_physical_limits : bool, default=False Whether to fall back to physical limits as fit bounds. + random_seed : int | None, default=None + Optional random seed passed to stochastic minimizers. Raises ------ @@ -168,16 +171,25 @@ def run( 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) + parent._run_fit( + verbosity=verbosity, + use_physical_limits=use_physical_limits, + random_seed=random_seed, + ) def __call__( self, verbosity: str | None = None, *, use_physical_limits: bool = False, + random_seed: int | None = None, ) -> None: """Execute :meth:`run` for convenience.""" - self.run(verbosity=verbosity, use_physical_limits=use_physical_limits) + self.run( + verbosity=verbosity, + use_physical_limits=use_physical_limits, + random_seed=random_seed, + ) def from_cif(self, block: object, idx: int = 0) -> None: """ 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..e10d9c698 --- /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. + map_value : float + Maximum-a-posteriori or best-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 + map_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. + map_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 + map_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='map' + 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 = 'map' + 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, + map_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. + map_values : np.ndarray + MAP or best-sampled parameter 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, + map_value=float(map_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 == 'map': + return 'Max posterior' + 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', + 'max posterior', + '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..b5cfb11a2 100644 --- a/src/easydiffraction/analysis/fit_helpers/tracking.py +++ b/src/easydiffraction/analysis/fit_helpers/tracking.py @@ -3,6 +3,7 @@ import time from contextlib import suppress +from dataclasses import dataclass import numpy as np @@ -29,8 +30,29 @@ from easydiffraction.utils.logging import ConsoleManager SIGNIFICANT_CHANGE_THRESHOLD = 0.01 # 1% threshold -DEFAULT_HEADERS = ['iteration', 'χ²', 'improvement [%]'] -DEFAULT_ALIGNMENTS = ['center', 'center', 'center'] +SAMPLER_PROGRESS_UPDATE_SECONDS = 5.0 +TRACKING_MODE_FIT = 'fit' +TRACKING_MODE_SAMPLER = 'sampling' +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'] + + +@dataclass(frozen=True, slots=True) +class SamplerProgressUpdate: + """ + Normalized sampler progress payload forwarded by monitor hooks. + """ + + iteration: int + total_iterations: int + phase: str + progress_percent: float + log_posterior: float + reduced_chi2: float + elapsed_time: float + force_report: bool = False class _TerminalLiveHandle: @@ -97,10 +119,20 @@ 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._df_rows: list[list[str]] = [] self._display_handle: object | None = None @@ -112,9 +144,19 @@ def reset(self) -> None: 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 def track( self, @@ -140,6 +182,16 @@ 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 + row: list[str] = [] # First iteration, initialize tracking @@ -150,6 +202,7 @@ def track( row = [ str(self._iteration), + self._format_elapsed_time(), f'{reduced_chi2:.2f}', '', ] @@ -164,6 +217,7 @@ def track( row = [ str(self._iteration), + self._format_elapsed_time(), f'{reduced_chi2:.2f}', f'{change_in_percent:.1f}% ↓', ] @@ -175,7 +229,7 @@ def track( 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 @@ -185,6 +239,46 @@ def track( 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 + + 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 + @property def best_chi2(self) -> float | None: """Best recorded reduced chi-square value or None.""" @@ -208,13 +302,17 @@ 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 start_tracking(self, minimizer_name: str, *, mode: str = TRACKING_MODE_FIT) -> None: """ Initialize display and headers and announce the minimizer. @@ -222,14 +320,23 @@ 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 + ) + 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._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 = [] @@ -237,8 +344,8 @@ def start_tracking(self, minimizer_name: str) -> None: # Initial empty table; subsequent updates will reuse the handle render_table( - columns_headers=DEFAULT_HEADERS, - columns_alignment=DEFAULT_ALIGNMENTS, + columns_headers=self._headers(), + columns_alignment=self._alignments(), columns_data=self._df_rows, display_handle=self._display_handle, ) @@ -250,41 +357,291 @@ 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_headers=self._headers(), + columns_alignment=self._alignments(), columns_data=self._df_rows, display_handle=self._display_handle, ) 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 not VerbosityEnum.FULL: + return + + self._close_display_handle() + self._print_completion_summary() + + def _initial_sampler_progress_row( + self, + *, + update: SamplerProgressUpdate, + clamped_iteration: int, + clamped_progress: float, + ) -> list[str]: + if self._previous_chi2 is not None and self._best_chi2 is not None: + 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 + 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 _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() + 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 'sampling', + ] + + 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: - return + 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}' - # Close terminal live if used + def _close_display_handle(self) -> None: if self._display_handle is not None and hasattr(self._display_handle, 'close'): with suppress(Exception): self._display_handle.close() - # Print best result + def _print_completion_summary(self) -> None: + if self._tracking_mode == TRACKING_MODE_SAMPLER: + console.print('✅ Bayesian sampling complete.') + return + 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}' + + @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 not VerbosityEnum.FULL: + return + + render_table( + columns_headers=self._headers(), + columns_alignment=self._alignments(), + columns_data=self._df_rows, + display_handle=self._display_handle, + ) diff --git a/src/easydiffraction/analysis/fitting.py b/src/easydiffraction/analysis/fitting.py index 9b0360143..db5590e1b 100644 --- a/src/easydiffraction/analysis/fitting.py +++ b/src/easydiffraction/analysis/fitting.py @@ -39,6 +39,7 @@ def fit( verbosity: VerbosityEnum = VerbosityEnum.FULL, *, use_physical_limits: bool = False, + random_seed: int | None = None, ) -> None: """ Run the fitting process. @@ -65,6 +66,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 @@ -118,6 +121,7 @@ def objective_function(engine_params: dict[str, Any]) -> np.ndarray: objective_function, verbosity=verbosity, use_physical_limits=use_physical_limits, + random_seed=random_seed, ) def _process_fit_results( 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..bbc639f10 100644 --- a/src/easydiffraction/analysis/minimizers/base.py +++ b/src/easydiffraction/analysis/minimizers/base.py @@ -41,6 +41,7 @@ def __init__( 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.tracker: FitProgressTracker = FitProgressTracker() def _start_tracking( @@ -60,7 +61,7 @@ def _start_tracking( """ self.tracker.reset() self.tracker._verbosity = verbosity - self.tracker.start_tracking(minimizer_name) + self.tracker.start_tracking(minimizer_name, mode=self._tracking_mode()) self.tracker.start_timer() def _stop_tracking(self) -> None: @@ -68,6 +69,11 @@ def _stop_tracking(self) -> None: self.tracker.stop_timer() self.tracker.finish_tracking() + @staticmethod + def _tracking_mode() -> str: + """Return the tracker mode for the current minimizer.""" + return 'fit' + @abstractmethod def _prepare_solver_args(self, parameters: list[Any]) -> dict[str, Any]: """ @@ -124,7 +130,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 +169,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,6 +267,33 @@ 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], @@ -238,6 +301,7 @@ def fit( verbosity: VerbosityEnum = VerbosityEnum.FULL, *, use_physical_limits: bool = False, + random_seed: int | None = None, ) -> FitResults: """ Run the full minimization workflow. @@ -255,6 +319,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. Returns ------- @@ -264,16 +330,21 @@ 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() + 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) + finally: + self._stop_tracking() return self._finalize_fit(parameters, raw_result) diff --git a/src/easydiffraction/analysis/minimizers/bumps_dream.py b/src/easydiffraction/analysis/minimizers/bumps_dream.py new file mode 100644 index 000000000..c9af1012b --- /dev/null +++ b/src/easydiffraction/analysis/minimizers/bumps_dream.py @@ -0,0 +1,948 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bumps minimizer variant using the DREAM sampler.""" + +from __future__ import annotations + +import random +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 + +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 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``. + """ + 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, + ) + + 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 not can_pickle(problem): + log.warning( + 'DREAM parallel evaluation requires a picklable ' + 'problem; falling back to serial execution.' + ) + return None + + return MPMapper.start_mapper(problem, [], cpus=self.parallel) + + @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] + map_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) + posterior_parameter_summaries = summarize_posterior_parameters( + parameter_names=context.parameter_names, + posterior_samples=posterior_samples, + map_values=map_values, + parameter_display_names=context.parameter_display_names, + convergence_diagnostics=convergence_diagnostics, + ) + posterior_standard_deviations = standard_deviations_from_summaries( + posterior_parameter_summaries + ) + + if not convergence_diagnostics.get('converged', True): + log.warning('Convergence diagnostics indicate the posterior may be poorly mixed.') + + return OptimizeResult( + x=map_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: + """ + Commit MAP values on success and restore starts on failure. + + 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='map', + 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/core/variable.py b/src/easydiffraction/core/variable.py index 84bbd2f42..303c5bbe5 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): """ @@ -280,6 +282,7 @@ 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 @@ -414,6 +417,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 +430,80 @@ 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_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 # ====================================================================== diff --git a/src/easydiffraction/display/plotters/ascii.py b/src/easydiffraction/display/plotters/ascii.py index a0c95fe11..4aeb5577a 100644 --- a/src/easydiffraction/display/plotters/ascii.py +++ b/src/easydiffraction/display/plotters/ascii.py @@ -131,6 +131,8 @@ def plot_powder_meas_vs_calc( title=plot_spec.title, height=plot_spec.height, ) + 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.') diff --git a/src/easydiffraction/display/plotters/base.py b/src/easydiffraction/display/plotters/base.py index b1a19a274..d4be0d67a 100644 --- a/src/easydiffraction/display/plotters/base.py +++ b/src/easydiffraction/display/plotters/base.py @@ -60,6 +60,11 @@ 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 class XAxisType(StrEnum): diff --git a/src/easydiffraction/display/plotters/plotly.py b/src/easydiffraction/display/plotters/plotly.py index 887879dd3..cb4914c3f 100644 --- a/src/easydiffraction/display/plotters/plotly.py +++ b/src/easydiffraction/display/plotters/plotly.py @@ -65,6 +65,15 @@ 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)' +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 +166,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 +222,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 +475,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 +489,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 +793,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 +954,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 +962,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 +998,7 @@ def _get_layout( }, title={ 'text': title, + 'font': {'size': TITLE_FONT_SIZE}, }, legend={ 'bgcolor': cls._legend_background_color(), @@ -909,14 +1008,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, }, @@ -1165,11 +1272,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 +1283,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 +1367,40 @@ 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._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 +1408,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 +1556,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 +1569,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 +1637,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 +1646,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% 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..34b0da107 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -16,6 +16,7 @@ import numpy as np import pandas as pd +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 +31,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 +68,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 +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_INTERVAL_68_FILL_COLOR = 'rgba(214, 39, 40, 0.26)' +POSTERIOR_MEDIAN_LINE_COLOR = 'rgb(80, 80, 80)' +POSTERIOR_POINT_ESTIMATE_LINE_COLOR = 'rgb(214, 39, 40)' +POSTERIOR_POINT_ESTIMATE_TRACE_NAME = 'Max posterior' +POSTERIOR_POINT_ESTIMATE_LINE_DASH = 'dot' +POSTERIOR_PREDICTIVE_INTERVAL_TRACE_NAME = '95% 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) @@ -90,6 +191,70 @@ 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 + 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.""" @@ -562,6 +727,9 @@ 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 +738,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,11 +799,439 @@ def plot_param_correlations( display_corr_df, row_numbers=row_numbers, col_numbers=col_numbers, - threshold=threshold, + threshold=resolved_threshold, precision=precision, ) ) + @classmethod + def _resolve_correlation_filter( + cls, + corr_df: pd.DataFrame, + *, + threshold: float | 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: + """ + 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 + ---------- + 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``. + """ + 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. + """ + 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, + 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. 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. + x : object | None, default=None + Optional explicit x-axis data to override stored values. + + Raises + ------ + ValueError + If ``style`` is not one of ``'band'``, ``'draws'``, or + ``'band+draws'``. + """ + if style not in {'band', 'draws', 'band+draws'}: + msg = "style must be 'band', 'draws', or 'band+draws'." + raise ValueError(msg) + + if self._project is None: + log.warning('Plotter is not attached to a project.') + return + + if self.engine != PlotterEngineEnum.PLOTLY.value: + log.warning('Posterior predictive plots currently require the Plotly backend.') + return + + self._update_project_categories(expt_name) + experiment = self._project.experiments[expt_name] + x_axis, _, sample_form, scattering_type, _ = self._resolve_x_axis(experiment.type, x) + + plot_options = _MeasVsCalcPlotOptions( + x_min=x_min, + x_max=x_max, + show_residual=show_residual, + x=x, + ) + + if sample_form == SampleFormEnum.SINGLE_CRYSTAL: + 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 + + 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% 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.map_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.""" + 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=style in {'draws', 'band+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=style in {'draws', 'band+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'], + ) + + 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'}, + ) + @staticmethod def _filter_correlation_dataframe( corr_df: pd.DataFrame, @@ -697,6 +1306,7 @@ 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. @@ -711,6 +1321,8 @@ def _trim_correlation_display_dataframe( 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 ------- @@ -722,7 +1334,7 @@ def _trim_correlation_display_dataframe( row_numbers = list(range(1, num_rows + 1)) col_numbers = list(range(1, num_cols + 1)) - if min(num_rows, num_cols) <= 1: + if show_diagonal or min(num_rows, num_cols) <= 1: return corr_df, row_numbers, col_numbers if preserve_all_rows: @@ -739,16 +1351,19 @@ def _get_param_correlation_dataframe(self) -> pd.DataFrame | None: Square correlation matrix labeled by parameter unique names, or ``None`` if unavailable. """ - result = self._get_fit_result_for_correlation() - if result is None: + fit_results = self._get_fit_result_for_correlation() + if fit_results is None: return None - raw_result, var_names, fit_results = result - covar = getattr(raw_result, 'covar', None) - if covar is not None: - return self._correlation_from_covariance(covar, var_names, fit_results.parameters) + corr_df = self._posterior_correlation_dataframe(fit_results) + if corr_df is not None: + return corr_df - corr_df = self._get_param_correlation_dataframe_from_engine_params( + raw_result = self._raw_fit_result_for_correlation(fit_results) + if raw_result is None: + return None + + corr_df = self._correlation_dataframe_from_engine_result( raw_result=raw_result, parameters=fit_results.parameters, ) @@ -757,21 +1372,2587 @@ def _get_param_correlation_dataframe(self) -> pd.DataFrame | None: log.warning( 'Correlation matrix is unavailable for this fit. ' - 'Use the lmfit minimizer and ensure covariance estimation succeeds.' + 'Use a minimizer that returns covariance information or posterior samples.' ) return None - def _get_fit_result_for_correlation( + def _posterior_correlation_dataframe( self, - ) -> tuple[object, list[str], object] | None: - """ - Validate and return the raw fit result for correlation. + 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) - Returns + @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: + log.warning('No raw fit result available. Correlation matrix cannot be plotted.') + 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.') + return None + return raw_result + + 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, + ), + 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, + density_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], + density_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( + density_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.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 _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']) + + 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 + fill_colorscale, line_colorscale = self._posterior_pair_contour_colorscales( + x_values, + y_values, + ) + contour_start = float(np.max(density) * 0.20) + contour_end = float(np.max(density) * 0.95) + contour_size = float(np.max(density) * 0.15) + fill_density = np.array(density, copy=True) + fill_density[fill_density < contour_start] = np.nan + fill_trace = go.Contour( + x=x_grid, + y=y_grid, + z=fill_density, + contours={ + '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 _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 _posterior_distribution_context( + self, + param: object, + ) -> _PosteriorDistributionContext | None: + """Return the context for a posterior distribution plot.""" + posterior_samples, fit_results = self._get_posterior_samples_and_fit_results() + if posterior_samples is None or fit_results is None: + 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, + ) + ) + fig.add_trace( + self._posterior_interval_band_trace( + x0=summary.interval_68[0], + x1=summary.interval_68[1], + y_axis_range=y_axis_range, + trace_name='68% credible interval', + color=POSTERIOR_INTERVAL_68_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.map_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._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) + posterior_samples = getattr(fit_results, 'posterior_samples', None) + if posterior_predictive is None or posterior_samples 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 not None: + return summary + + 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 + + map_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), + map_prediction=np.asarray(map_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 MAP 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: + map_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 map_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 != map_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(map_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, + ) -> None: + """Render posterior predictive summaries using Plotly.""" + 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.map_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, + ) + ) + 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_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 + + map_prediction = np.asarray(summary.map_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 != map_prediction.shape or upper_95.shape != map_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=map_prediction, + y_meas=y_meas, + y_meas_su=y_meas_su, + ) + trace.error_x = { + 'type': 'data', + 'array': np.maximum(0.0, upper_95 - map_prediction), + 'arrayminus': np.maximum(0.0, map_prediction - lower_95), + 'visible': True, + } + trace.customdata = np.column_stack((lower_95, upper_95, y_meas_su)) + trace.hovertemplate = ( + 'Predicted I²: %{x:,.2f}
' + '95% 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, + map_prediction=self._filtered_y_array(summary.map_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.""" + 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=style in {'draws', 'band+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_raw = getattr(pattern, 'intensity_bkg', None) + y_bkg = ( + self._filtered_y_array(y_bkg_raw, ctx['x_array'], ctx['x_min'], ctx['x_max']) + if y_bkg_raw is not None + else None + ) + y_calc = self._filtered_y_array( + summary.map_prediction, summary.x, ctx['x_min'], ctx['x_max'] + ) + show_residual = True if plot_options.show_residual is None else plot_options.show_residual + y_resid = y_meas - y_calc if show_residual 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 style in {'draws', 'band+draws'}: + draws = getattr(summary, 'draws', None) + if 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 draws + ], + dtype=float, + ) + + if np.asarray(ctx['x_filtered']).size == 0: + 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'], + ) + + 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, + ) + self._backend.plot_powder_meas_vs_calc(plot_spec=plot_spec) + + @staticmethod + def _resolve_posterior_parameter_names( + *, + fit_results: object, + parameters: list[object] | None, + ) -> list[str] | None: + """ + Resolve posterior parameter names from descriptors. + + Parameters + ---------- + fit_results : object + Bayesian fit result exposing posterior samples. + parameters : list[object] | None + Optional parameter subset. + + Returns + ------- + list[str] | None + Posterior parameter names in plotting order, or ``None`` if + the selection cannot be resolved. + """ + 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 parameters is None: + return list(available_names) + + 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 + + @staticmethod + def _resolve_posterior_parameter_name( + *, + fit_results: object, + available_names: list[str], + parameter: object, + ) -> str | None: + """ + Resolve one posterior parameter selection into a unique name. + """ + 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 + 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) + + @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 + + selection_candidates = Plotter._posterior_parameter_selection_candidates( + fit_results=fit_results, + available_names=available_names, + ) + 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( + 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, + ) -> object | None: + """ + 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 +3962,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 +4147,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( @@ -1283,6 +4917,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, diff --git a/src/easydiffraction/project/project.py b/src/easydiffraction/project/project.py index 7dc42cf6d..58d7e97fb 100644 --- a/src/easydiffraction/project/project.py +++ b/src/easydiffraction/project/project.py @@ -171,6 +171,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.""" 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..7a52c63d8 --- /dev/null +++ b/tests/integration/fitting/test_analysis_and_fit_category_support.py @@ -0,0 +1,360 @@ +# 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.categories.fit.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_fit_instantiation_defaults_and_run_paths(): + from easydiffraction.analysis.categories.fit.default import Fit + import easydiffraction.analysis.categories.fit.default as fit_mod + + fit = Fit() + + assert fit._identity.category_code == 'fit' + assert fit.mode.value == 'single' + assert fit.minimizer_type.value == 'lmfit (leastsq)' + assert fit.minimizer is None + + with pytest.raises( + RuntimeError, + match=r'Fit category is not attached to an Analysis object\.', + ): + fit.run() + + calls: list[tuple[str | None, bool, int | None]] = [] + + class Parent: + fitter = object() + + @staticmethod + def _run_fit( + verbosity: str | None = None, + *, + use_physical_limits: bool = False, + random_seed: int | None = None, + ) -> None: + calls.append((verbosity, use_physical_limits, random_seed)) + + fit._parent = Parent() + fit(verbosity='silent', use_physical_limits=True, random_seed=7) + + assert calls == [('silent', True, 7)] + + class ParentWithMinimizer: + fitter = type('FitterHolder', (), {'minimizer': 'MIN'})() + + fit._parent = ParentWithMinimizer() + assert fit.minimizer == 'MIN' + + shown: list[str] = [] + monkeypatch = pytest.MonkeyPatch() + monkeypatch.setattr(fit_mod.MinimizerFactory, 'show_supported', lambda: shown.append('shown')) + Fit.show_available_minimizers() + monkeypatch.undo() + + assert shown == ['shown'] + + +def test_fit_from_cif_warns_on_invalid_minimizer(monkeypatch): + import easydiffraction.analysis.categories.fit.default as fit_mod + from easydiffraction.analysis.categories.fit.default import Fit + + fit = Fit() + fit._minimizer_type._value = 'bad-minimizer' + + class Parent: + fitter = None + + warnings: list[str] = [] + fit._parent = Parent() + monkeypatch.setattr(fit_mod.CategoryItem, 'from_cif', lambda self, block, idx=0: None) + monkeypatch.setattr( + fit_mod, + 'Fitter', + lambda value: (_ for _ in ()).throw(ValueError('bad minimizer')), + ) + monkeypatch.setattr(fit_mod.log, 'warning', lambda message: warnings.append(message)) + + fit.from_cif(object()) + + assert warnings == ['bad minimizer'] + + +def test_fit_fallback_paths_without_parent_or_project(capsys, monkeypatch): + import easydiffraction.analysis.categories.fit.default as fit_mod + from easydiffraction.analysis.categories.fit.default import Fit + + fit = Fit() + + assert fit.minimizer is None + + fit.show_modes() + out = capsys.readouterr().out + assert 'single' in out + assert 'joint' in out + assert 'sequential' in out + + monkeypatch.setattr(fit_mod.CategoryItem, 'from_cif', lambda self, block, idx=0: None) + fit.from_cif(object()) + + +def test_fit_show_modes_for_single_and_multiple_experiments(capsys): + from easydiffraction.analysis.analysis import Analysis + + single = Analysis(project=_make_project_with_names(['e1'])) + single.fit.show_modes() + out_single = capsys.readouterr().out + assert 'Fit modes' in out_single + assert 'single' in out_single + assert 'joint' not in out_single + + multi = Analysis(project=_make_project_with_names(['e1', 'e2'])) + multi.fit.show_modes() + 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.fit.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.fit.mode.value == 'single' + analysis.fit.mode = 'joint' + assert analysis.fit.mode.value == 'joint' + assert len(analysis.joint_fit_experiments) == 0 + + analysis.help() + out = _unstyled_output(capsys.readouterr().out) + assert "Help for 'Analysis'" in out + assert 'fit' in out + assert 'display' in out + assert 'Properties' in out + assert 'Methods' in out + assert 'fit_sequential()' 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 + 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 FakeExpr: + value = 'x = y + 1' + + class FakeConstraint: + expression = FakeExpr() + + analysis.constraints._items = [FakeConstraint()] + captured: dict[str, object] = {} + monkeypatch.setattr(analysis_mod, 'render_table', lambda **kwargs: captured.update(kwargs)) + analysis.display.constraints() + out = capsys.readouterr().out + assert 'User defined constraints' in out + assert captured['columns_data'][0][0] == '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_bayesian_dream.py b/tests/integration/fitting/test_bayesian_dream.py new file mode 100644 index 000000000..23e0de589 --- /dev/null +++ b/tests/integration/fitting/test_bayesian_dream.py @@ -0,0 +1,178 @@ +# 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.fit.minimizer_type = 'bumps (dream)' + minimizer = project.analysis.fit.minimizer + minimizer.steps = 20 + minimizer.burn = 5 + minimizer.thin = 1 + minimizer.pop = 4 + minimizer.init = 'lhs' + + +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) + project.analysis.fit(verbosity='silent', 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.fit.minimizer_type = 'bumps (lm)' + project.analysis.fit(verbosity='silent') + + 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) + project.analysis.fit(verbosity='silent', 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_are_runtime_only_after_save_load(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) + project.analysis.fit(verbosity='silent', 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' + assert analysis_cif.is_file() + assert 'posterior' not in analysis_cif.read_text().lower() + + loaded = Project.load(str(proj_dir)) + assert loaded.analysis.fit_results is None 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..a4eef9007 --- /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, + map_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, + map_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', + map_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', + map_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') == 'Max posterior' + assert _format_point_estimate_name('best_sample') == 'Best 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', + map_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', + map_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', + 'max posterior', + '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', + map_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', + map_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', + map_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..f63cb111f --- /dev/null +++ b/tests/integration/fitting/test_bayesian_tracker_and_base.py @@ -0,0 +1,451 @@ +# 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 + + monkeypatch.setattr(tracking_mod, 'in_jupyter', lambda: False) + + 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 + + +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 + + monkeypatch.setattr(tracking_mod, 'in_jupyter', lambda: False) + + 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 + + +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 + + monkeypatch.setattr(tracking_mod, 'in_jupyter', lambda: False) + + 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 + + +def test_tracker_final_sampler_row_replaces_last_row(monkeypatch): + import easydiffraction.analysis.fit_helpers.tracking as tracking_mod + from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker + + monkeypatch.setattr(tracking_mod, 'render_table', lambda **kwargs: None) + + 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 + + class FakeLive: + def __init__(self, *, console, auto_refresh): + self.console = console + self.auto_refresh = auto_refresh + self.started = False + self.stopped = False + + def start(self): + self.started = True + + def stop(self): + self.stopped = True + + monkeypatch.setattr(tracking_mod, 'in_jupyter', lambda: False) + monkeypatch.setattr(tracking_mod, 'Live', FakeLive) + monkeypatch.setattr(tracking_mod.ConsoleManager, 'get', lambda: 'console') + + handle = tracking_mod._make_display_handle() + + assert isinstance(handle, tracking_mod._TerminalLiveHandle) + assert handle._live.console == 'console' + assert handle._live.auto_refresh is True + assert handle._live.started is True + + handle.close() + + assert handle._live.stopped is True + + +def test_tracker_misc_helper_paths(monkeypatch): + import easydiffraction.analysis.fit_helpers.tracking as tracking_mod + from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker + + render_calls: list[dict[str, object]] = [] + monkeypatch.setattr(tracking_mod, 'render_table', lambda **kwargs: render_calls.append(kwargs)) + monkeypatch.setattr( + tracking_mod, 'calculate_reduced_chi_square', lambda residuals, n_params: 3.0 + ) + + 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._replace_last_tracking_row(['1']) + + assert tracker._df_rows == [['1']] + assert len(render_calls) == 1 + + +def test_tracker_final_rows_cover_fallbacks_and_close_suppression(): + 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', '', ''] + + class BadHandle: + @staticmethod + def close() -> None: + message = 'boom' + raise RuntimeError(message) + + tracker._display_handle = BadHandle() + tracker._close_display_handle() + + +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..bf87549a4 --- /dev/null +++ b/tests/integration/fitting/test_bumps_dream_support.py @@ -0,0 +1,559 @@ +# 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: + 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_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 + monkeypatch.setattr( + 'easydiffraction.analysis.minimizers.bumps_dream.can_pickle', lambda problem: False + ) + monkeypatch.setattr( + 'easydiffraction.analysis.minimizers.bumps_dream.log.warning', + lambda message: warnings.append(message), + ) + + assert minimizer._build_mapper('problem') is None + assert any('falling back to serial execution' in message for message in warnings) + + +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', + map_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', + map_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', + map_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..636c0ca4c --- /dev/null +++ b/tests/integration/fitting/test_cli_entrypoints.py @@ -0,0 +1,173 @@ +# 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 _plotter: + @staticmethod + def plot_param_correlations() -> None: + calls.append('PLOT_CORR') + + @staticmethod + def plot_meas_vs_calc(expt_name: str, *, show_residual: bool = False) -> None: + calls.append(f'PLOT_{expt_name}_{show_residual}') + + plotter = _plotter() + + 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_True'] + + +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 _plotter: + @staticmethod + def plot_param_correlations() -> None: + return None + + @staticmethod + def plot_meas_vs_calc(expt_name: str, *, show_residual: bool = False) -> None: + return None + + plotter = _plotter() + + 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/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/fit_helpers/test_bayesian.py b/tests/unit/easydiffraction/analysis/fit_helpers/test_bayesian.py new file mode 100644 index 000000000..3b641e6d6 --- /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, + map_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', + map_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', + map_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', + 'max posterior', + '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', + map_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', + map_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..bb824f829 100644 --- a/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py +++ b/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py @@ -59,3 +59,69 @@ def __init__(self, start, value, uncertainty, name='p', units='u'): assert 'Fitted parameters:' in out # Table border: accept common border glyphs from Rich/tabulate 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/minimizers/test_base.py b/tests/unit/easydiffraction/analysis/minimizers/test_base.py index 23d385e7c..0ad0ca127 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: @@ -119,3 +120,41 @@ def _compute_residuals( ) out = f({}) assert np.allclose(out, 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 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 diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_bumps.py b/tests/unit/easydiffraction/analysis/minimizers/test_bumps.py index 668524eaf..1a73fdbcd 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 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..eea9f0898 --- /dev/null +++ b/tests/unit/easydiffraction/analysis/minimizers/test_bumps_dream.py @@ -0,0 +1,362 @@ +# 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_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', + map_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', + map_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_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/core/test_parameters.py b/tests/unit/easydiffraction/core/test_parameters.py index e7d102d9d..a6c5b7adf 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 diff --git a/tests/unit/easydiffraction/display/plotters/test_plotly.py b/tests/unit/easydiffraction/display/plotters/test_plotly.py index 3905ee278..5c3c8dd50 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,49 @@ 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='Max posterior', + y_calc_line_dash='dot', + ), + ) + + fig = captured['fig'] + predictive_band_trace = next(trace for trace in fig.data if trace.name == '95% interval') + max_posterior_trace = next(trace for trace in fig.data if trace.name == 'Max posterior') + 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/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py index 5e1cc4daf..c0ef16158 100644 --- a/tests/unit/easydiffraction/display/test_plotting.py +++ b/tests/unit/easydiffraction/display/test_plotting.py @@ -2,6 +2,10 @@ # SPDX-License-Identifier: BSD-3-Clause import re +from types import MethodType +from types import SimpleNamespace + +import numpy as np import pytest @@ -233,6 +237,897 @@ 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, + map_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.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_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_INTERVAL_68_FILL_COLOR + 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', + '68% credible interval', + '95% credible interval', + 'Median', + 'Max posterior', + } + 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_68_trace = next( + trace for trace in figure.data if trace.name == '68% credible interval' + ) + 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 == 'Max posterior') + 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 interval_68_trace.fillcolor == POSTERIOR_INTERVAL_68_FILL_COLOR + 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_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._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]), + map_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 == 'Max posterior') + + assert upper_band_trace.name == '95% 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]), + map_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, x=None), + x_axis=XAxisType.TWO_THETA, + style='band', + ) + + plot_spec = captured['plot_spec'] + assert plot_spec.y_calc_name == 'Max posterior' + assert plot_spec.y_calc_line_dash == 'dot' + + +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', + map_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', + map_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.map_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]), + map_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, + ): + 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 + + 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'].map_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 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 +1536,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 +1555,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 +1594,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 +1711,128 @@ 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_filters_by_default_threshold(monkeypatch): +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_param_correlations_shows_full_table_when_threshold_is_zero(monkeypatch): from easydiffraction.display.plotting import Plotter from easydiffraction.display.tables import TableRenderer @@ -843,18 +1880,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 +1956,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/project/test_project.py b/tests/unit/easydiffraction/project/test_project.py index 92907f66c..090ce5404 100644 --- a/tests/unit/easydiffraction/project/test_project.py +++ b/tests/unit/easydiffraction/project/test_project.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.project.project as MUT @@ -49,3 +51,15 @@ 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] diff --git a/tools/generate_package_docs.py b/tools/generate_package_docs.py index 671a4f0ec..1d860557e 100644 --- a/tools/generate_package_docs.py +++ b/tools/generate_package_docs.py @@ -3,7 +3,7 @@ """Generate project package structure markdown files. -Outputs two docs under docs/architecture/: +Outputs two docs under docs/dev/: - package-structure-short.md (folders/files only) - package-structure-full.md (folders/files and classes) @@ -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' IGNORE_DIRS = { From efadbcdc94c56bcffff9987c70ed61a32b1a162a Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 14 May 2026 17:35:55 +0200 Subject: [PATCH 03/10] Improve chart and table display API (#171) * Add display UX ADR and plan * Rename display settings category to rendering * Add grouped display facade * Move constraint display and standardize CIF helpers * Refine display pattern routing * Implement state-aware pattern display * Update display UX documentation * Complete display UX verification and test updates * Address display UX review fixes * Fix single-crystal display availability checks --- docs/dev/adr_display-ux.md | 227 ++++++ docs/dev/architecture.md | 59 +- docs/dev/package-structure-full.md | 12 +- docs/dev/package-structure-short.md | 3 +- docs/dev/plan_display-ux.md | 238 ++++++ docs/docs/tutorials/ed-1.ipynb | 10 +- docs/docs/tutorials/ed-1.py | 10 +- docs/docs/tutorials/ed-10.ipynb | 6 +- docs/docs/tutorials/ed-10.py | 6 +- docs/docs/tutorials/ed-11.ipynb | 12 +- docs/docs/tutorials/ed-11.py | 12 +- docs/docs/tutorials/ed-12.ipynb | 12 +- docs/docs/tutorials/ed-12.py | 12 +- docs/docs/tutorials/ed-13.ipynb | 100 ++- docs/docs/tutorials/ed-13.py | 98 ++- docs/docs/tutorials/ed-14.ipynb | 12 +- docs/docs/tutorials/ed-14.py | 12 +- docs/docs/tutorials/ed-15.ipynb | 14 +- docs/docs/tutorials/ed-15.py | 14 +- docs/docs/tutorials/ed-16.ipynb | 14 +- docs/docs/tutorials/ed-16.py | 14 +- docs/docs/tutorials/ed-17.ipynb | 34 +- docs/docs/tutorials/ed-17.py | 34 +- docs/docs/tutorials/ed-18.ipynb | 6 +- docs/docs/tutorials/ed-18.py | 6 +- docs/docs/tutorials/ed-2.ipynb | 18 +- docs/docs/tutorials/ed-2.py | 18 +- docs/docs/tutorials/ed-20.ipynb | 18 +- docs/docs/tutorials/ed-20.py | 18 +- docs/docs/tutorials/ed-21.ipynb | 162 ++-- docs/docs/tutorials/ed-21.py | 22 +- docs/docs/tutorials/ed-22.ipynb | 266 +++++-- docs/docs/tutorials/ed-22.py | 20 +- docs/docs/tutorials/ed-3.ipynb | 66 +- docs/docs/tutorials/ed-3.py | 66 +- docs/docs/tutorials/ed-4.ipynb | 4 +- docs/docs/tutorials/ed-4.py | 4 +- docs/docs/tutorials/ed-5.ipynb | 14 +- docs/docs/tutorials/ed-5.py | 14 +- docs/docs/tutorials/ed-6.ipynb | 38 +- docs/docs/tutorials/ed-6.py | 38 +- docs/docs/tutorials/ed-7.ipynb | 48 +- docs/docs/tutorials/ed-7.py | 48 +- docs/docs/tutorials/ed-8.ipynb | 12 +- docs/docs/tutorials/ed-8.py | 12 +- docs/docs/tutorials/ed-9.ipynb | 10 +- docs/docs/tutorials/ed-9.py | 10 +- .../user-guide/analysis-workflow/analysis.md | 8 +- .../user-guide/analysis-workflow/project.md | 4 +- docs/docs/user-guide/first-steps.md | 27 +- src/easydiffraction/__main__.py | 6 +- src/easydiffraction/analysis/analysis.py | 30 +- .../categories/constraints/default.py | 19 + src/easydiffraction/display/plotters/ascii.py | 9 + src/easydiffraction/display/plotters/base.py | 4 + .../display/plotters/plotly.py | 35 + src/easydiffraction/display/plotting.py | 269 +++++-- src/easydiffraction/io/cif/serialize.py | 14 +- .../project/categories/display/__init__.py | 8 - .../project/categories/display/default.py | 117 --- .../project/categories/rendering/__init__.py | 8 + .../project/categories/rendering/default.py | 131 ++++ .../{display => rendering}/factory.py | 6 +- src/easydiffraction/project/display.py | 689 ++++++++++++++++++ src/easydiffraction/project/project.py | 21 +- src/easydiffraction/project/project_info.py | 5 +- .../test_analysis_and_fit_category_support.py | 3 +- .../fitting/test_analysis_display.py | 20 +- .../fitting/test_cli_entrypoints.py | 31 +- tests/integration/fitting/test_plotting.py | 22 +- .../analysis/test_analysis_coverage.py | 4 +- .../easydiffraction/display/test_plotting.py | 134 +++- .../categories/display/test_default.py | 55 -- .../categories/display/test_factory.py | 23 - .../categories/rendering/test_default.py | 55 ++ .../categories/rendering/test_factory.py | 23 + .../easydiffraction/project/test_display.py | 479 ++++++++++++ .../easydiffraction/project/test_project.py | 11 + .../project/test_project_load.py | 10 +- .../project/test_project_save.py | 4 +- tests/unit/easydiffraction/test___main__.py | 31 +- 81 files changed, 3228 insertions(+), 990 deletions(-) create mode 100644 docs/dev/adr_display-ux.md create mode 100644 docs/dev/plan_display-ux.md delete mode 100644 src/easydiffraction/project/categories/display/__init__.py delete mode 100644 src/easydiffraction/project/categories/display/default.py create mode 100644 src/easydiffraction/project/categories/rendering/__init__.py create mode 100644 src/easydiffraction/project/categories/rendering/default.py rename src/easydiffraction/project/categories/{display => rendering}/factory.py (70%) create mode 100644 src/easydiffraction/project/display.py delete mode 100644 tests/unit/easydiffraction/project/categories/display/test_default.py delete mode 100644 tests/unit/easydiffraction/project/categories/display/test_factory.py create mode 100644 tests/unit/easydiffraction/project/categories/rendering/test_default.py create mode 100644 tests/unit/easydiffraction/project/categories/rendering/test_factory.py create mode 100644 tests/unit/easydiffraction/project/test_display.py diff --git a/docs/dev/adr_display-ux.md b/docs/dev/adr_display-ux.md new file mode 100644 index 000000000..264092e22 --- /dev/null +++ b/docs/dev/adr_display-ux.md @@ -0,0 +1,227 @@ +# ADR: Display UX Facade + +## Status + +Accepted. + +## Context + +The current user-facing display API mixes 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 existing `project.display` category is 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() +``` + +Suggested 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=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: + +| 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` should 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 a model/calculation is available +- background if defined and relevant +- Bragg ticks if phases/reflections are available +- residual if both measured and calculated data are available and the + experiment type supports a residual panel +- excluded regions if available +- uncertainty bands where posterior predictive data exists + +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 should show option name, description, availability for the +experiment, whether `include='auto'` includes it, and the reason an +option is unavailable. + +Initial option names: + +- `auto` +- `measured` +- `calculated` +- `background` +- `residual` +- `bragg` +- `excluded` +- `uncertainty` + +`uncertainty` should be implemented immediately where posterior +predictive data exists. It should be unavailable, with a clear reason, +when no posterior predictive data is present. + +## 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. +- `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=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` need CIF access cleanup for + consistency with structure and experiment objects. diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 48a411619..85fc0ad4e 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -879,15 +879,16 @@ 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) | +| 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.rendering` | `Rendering` | Plot/table engine selection | +| `project.display` | `ProjectDisplay` | Pattern/report facade | +| `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 @@ -922,7 +923,7 @@ project_dir/ ``` `project.cif` carries both the `_project.*` metadata and the -`_display.*` engine preferences (`plotter_type`, `tabler_type`), so a +`_rendering.*` engine preferences (`chart_engine`, `table_engine`), 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`, @@ -1064,7 +1065,7 @@ 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) +project.display.pattern(expt_name='hrpt') # Select free parameters project.structures['lbco'].cell.length_a.free = True @@ -1073,14 +1074,14 @@ 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() +project.display.parameters.free() # Fit and show results project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # Plot after fitting -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True) +project.display.pattern(expt_name='hrpt') # Save project.save() @@ -1100,11 +1101,11 @@ project.analysis.fit.minimizer.parallel = 0 project.analysis.fit(random_seed=11) # Runtime-only Bayesian summaries and plots -project.analysis.display.fit_results() -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.display.fit.results() +project.display.fit.correlations() +project.display.posterior.pairs() +project.display.posterior.distribution(param) +project.display.posterior.predictive(expt_name='hrpt') ``` ### 8.5 TOF Experiment (tutorial ed-7) @@ -1225,8 +1226,8 @@ 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). +applies to `display` on `Project`, which owns `chart_engine` and +`table_engine` (see §9.4.1). **Design decisions:** @@ -1248,14 +1249,14 @@ 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` | +| Family | User intent | Examples | CIF | +| ---------------------------------- | ------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| Backend selector | Pick an execution backend | `fit.minimizer_type`, `calculation.calculator_type`, `rendering.chart_engine` | `_fit.minimizer_type`, `_calculation.calculator_type`, `_rendering.chart_engine` | +| 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- +configuration category (`fit`, `calculation`, `rendering`). Switchable- category implementation selectors are owned by the host (typically the experiment) because switching them replaces the category instance, as described in §9.3. @@ -1272,8 +1273,8 @@ 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() +project.rendering.show_chart_engines() +project.rendering.show_table_engines() ``` Available calculators are filtered by `engine_imported` (whether the diff --git a/docs/dev/package-structure-full.md b/docs/dev/package-structure-full.md index 8083f39b0..e71f099de 100644 --- a/docs/dev/package-structure-full.md +++ b/docs/dev/package-structure-full.md @@ -402,14 +402,20 @@ │ └── 📄 ascii.py ├── 📁 project │ ├── 📁 categories -│ │ ├── 📁 display +│ │ ├── 📁 rendering │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py -│ │ │ │ └── 🏷️ class Display +│ │ │ │ └── 🏷️ class Rendering │ │ │ └── 📄 factory.py -│ │ │ └── 🏷️ class DisplayFactory +│ │ │ └── 🏷️ class RenderingFactory │ │ └── 📄 __init__.py │ ├── 📄 __init__.py +│ ├── 📄 display.py +│ │ ├── 🏷️ class PatternOptionStatus +│ │ ├── 🏷️ class ParameterDisplay +│ │ ├── 🏷️ class FitDisplay +│ │ ├── 🏷️ class PosteriorDisplay +│ │ └── 🏷️ class ProjectDisplay │ ├── 📄 project.py │ │ └── 🏷️ class Project │ └── 📄 project_info.py diff --git a/docs/dev/package-structure-short.md b/docs/dev/package-structure-short.md index 9ce283eb5..573142dae 100644 --- a/docs/dev/package-structure-short.md +++ b/docs/dev/package-structure-short.md @@ -198,12 +198,13 @@ │ └── 📄 ascii.py ├── 📁 project │ ├── 📁 categories -│ │ ├── 📁 display +│ │ ├── 📁 rendering │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py │ │ │ └── 📄 factory.py │ │ └── 📄 __init__.py │ ├── 📄 __init__.py +│ ├── 📄 display.py │ ├── 📄 project.py │ └── 📄 project_info.py ├── 📁 summary diff --git a/docs/dev/plan_display-ux.md b/docs/dev/plan_display-ux.md new file mode 100644 index 000000000..4088ac657 --- /dev/null +++ b/docs/dev/plan_display-ux.md @@ -0,0 +1,238 @@ +# Plan: Display UX Facade + +## Goal + +Implement the display UX approach described in +`docs/dev/adr_display-ux.md`. + +The end-user API should become: + +```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=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') +``` + +Renderer configuration should move to: + +```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() +``` + +## Branch + +Use implementation branch: + +```text +feature/display-ux +``` + +## Decisions + +- Use grouped display namespaces under `project.display`. +- Use `project.display.fit.series(param, versus=...)` for sequential fit + parameter plots. +- Use `pattern(..., include='auto')` as the default experiment chart. +- Use `include` rather than `layers`, `components`, `content`, `view`, + `series`, or boolean flags. +- Do not add `project.display.constraints()`. +- Move constraint reporting to `project.analysis.constraints.show()`. +- Rename the serialized project display category to `rendering`. +- Do not add legacy CIF loading for `_display.plotter_type` or + `_display.tabler_type`. +- Standardize CIF display helpers on `project.analysis` and + `project.info`: `as_cif` should be a read-only property returning CIF + text, and `show_as_cif()` should pretty-print CIF text with a header. +- Implement `include='uncertainty'` immediately where posterior + predictive data exists. +- No compatibility aliases or deprecation warnings are required unless + release policy separately requires them. + +## Likely Files To Change + +- `src/easydiffraction/project/project.py` +- `src/easydiffraction/project/project_info.py` +- `src/easydiffraction/project/categories/display/default.py` +- `src/easydiffraction/project/categories/display/factory.py` +- `src/easydiffraction/project/categories/display/__init__.py` +- new `src/easydiffraction/project/categories/rendering/...` +- `src/easydiffraction/display/plotting.py` +- `src/easydiffraction/display/tables.py` +- `src/easydiffraction/analysis/analysis.py` +- `src/easydiffraction/analysis/categories/constraints/default.py` +- `src/easydiffraction/__main__.py` +- `docs/dev/architecture.md` +- `docs/docs/user-guide/**/*.md` +- `docs/docs/tutorials/*.py` +- tests under `tests/unit/easydiffraction/` +- tests under `tests/integration/fitting/` + +Do not edit generated tutorial notebooks directly. Update tutorial `.py` +files and regenerate notebooks in Phase 2 if required. + +## Resolved Questions + +- Legacy CIF `_display.plotter_type` and `_display.tabler_type` do not + need to load into `project.rendering`. No legacy code is required. +- `project.analysis` and `project.info` need consistency with structure + and experiment objects: `as_cif` should be a read-only property that + returns CIF text, and `show_as_cif()` should pretty-print CIF text + with a header. +- `include='uncertainty'` should be implemented immediately where + posterior predictive data exists. + +## Phase 1 - Implementation + +Do not create or run tests in Phase 1 unless explicitly requested. Every +completed Phase 1 implementation step must be staged with explicit paths +and committed locally before moving to the next implementation step or +the Phase 1 review gate. Use atomic commits, inspect the worktree before +each commit, and stage only the files changed for that step. + +- [x] Rename the serialized project display category to rendering. + - Move or recreate the category package as + `src/easydiffraction/project/categories/rendering/`. + - Rename user-facing settings from `plotter_type` and `tabler_type` to + `chart_engine` and `table_engine`. + - Add `show_chart_engines()`, `show_table_engines()`, and + `show_config()`. + - Update CIF names to `_rendering.chart_engine` and + `_rendering.table_engine`. + - Do not add legacy loading for `_display.plotter_type` or + `_display.tabler_type`. + +- [x] Add the new `project.display` facade. + - Add a facade object that is not the serialized rendering category. + - Add `pattern(...)` and `show_pattern_options(...)`. + - Add `parameters`, `fit`, and `posterior` namespace objects. + +- [x] Implement `pattern(..., include='auto')`. + - Replace common user-facing calls to `plot_meas`, `plot_calc`, and + `plot_meas_vs_calc` with one state-aware method. + - Support explicit includes for `measured`, `calculated`, + `background`, `residual`, `bragg`, `excluded`, and `uncertainty` + where data is available. + - Implement `uncertainty` immediately for experiments with posterior + predictive data. + - Render a clear warning or error when a requested include is not + available. + +- [x] Move parameter table displays under `project.display.parameters`. + - Implement `all()`, `fittable()`, `free()`, `access()`, and + `cif_uids()`. + - Remove the primary public need for `project.analysis.display`. + +- [x] Move fit displays under `project.display.fit`. + - Implement `results()`. + - Implement `correlations()`. + - Implement `series(param, versus=...)`. + +- [x] Move Bayesian displays under `project.display.posterior`. + - Implement `pairs()`. + - Implement `distribution(param)`. + - Implement `predictive(expt_name=...)`. + +- [x] Move constraint reporting to + `project.analysis.constraints.show()`. + - Do not add `project.display.constraints()`. + +- [x] Standardize CIF display helpers. + - Convert `project.analysis.as_cif()` to a read-only + `project.analysis.as_cif` property. + - Ensure `project.analysis.show_as_cif()` pretty-prints CIF text with + a header. + - Convert `project.info.as_cif()` to a read-only `project.info.as_cif` + property. + - Ensure `project.info.show_as_cif()` pretty-prints CIF text with a + header. + +- [x] Update docs, tutorials, and architecture text. + - Replace old public display examples with the selected API. + - Update `docs/dev/architecture.md`. + - Update tutorial `.py` files only; regenerate notebooks in Phase 2 if + required. + +- [x] Stop at the Phase 1 review gate. + - Summarize changed files and open questions. + - Suggest next verification commands. + - Wait for user approval before Phase 2. + +Suggested Phase 1 commit messages: + +```text +Rename display settings category to rendering +Add grouped display facade +Implement state-aware pattern display +Move analysis display reports to display facade +Standardize CIF display helpers +Update display UX documentation +``` + +## Phase 2 - Verification + +After Phase 1 is reviewed and approved: + +- [x] Add or update unit tests for the rendering category. +- [x] Add or update unit tests for the display facade namespaces. +- [x] Add or update plotting integration tests for `pattern(...)`. +- [x] Add or update analysis display integration tests for parameter and + fit report methods. +- [x] Regenerate tutorial notebooks if tutorial `.py` files changed. +- [x] Run formatting and checks. +- [x] Run unit tests. +- [x] Run integration tests. +- [x] Run script tests. + +Verification commands: + +```sh +pixi run notebook-prepare +pixi run fix +pixi run check +pixi run unit-tests +pixi run integration-tests +pixi run script-tests +``` + +Run `pixi run notebook-prepare` only if tutorial `.py` files changed. +Run `pixi run integration-tests` only after the relevant unit and +focused integration tests are passing. + +## Suggested Commit Message + +```text +Plan display UX facade implementation +``` + +## Suggested Pull Request + +Title: + +```text +Improve chart and table display API +``` + +Description: + +This change makes display commands easier to discover and use in +notebooks. Experiment patterns, parameter tables, fit reports, and +Bayesian plots are grouped under `project.display`, while renderer +settings move to `project.rendering`. 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 69eae9aa9..616c536c5 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": "title,tags,-all", + "cell_metadata_filter": "tags,title,-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..ab93cd536 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')" ] }, { @@ -262,7 +262,7 @@ "outputs": [], "source": [ "# Show fit results summary\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -292,7 +292,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='senju')" + "project.display.pattern(expt_name='senju')" ] }, { @@ -344,7 +344,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -364,7 +364,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -374,7 +374,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -384,7 +384,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='senju')" + "project.display.pattern(expt_name='senju')" ] }, { diff --git a/docs/docs/tutorials/ed-15.py b/docs/docs/tutorials/ed-15.py index c198fc234..659af7214 100644 --- a/docs/docs/tutorials/ed-15.py +++ b/docs/docs/tutorials/ed-15.py @@ -67,7 +67,7 @@ # ## 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 @@ -86,7 +86,7 @@ # %% # Show fit results summary -project.analysis.display.fit_results() +project.display.fit.results() # %% structure.show_as_cif() @@ -95,7 +95,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 +114,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..5da587e9d 100644 --- a/docs/docs/tutorials/ed-16.ipynb +++ b/docs/docs/tutorials/ed-16.ipynb @@ -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..bde9af470 100644 --- a/docs/docs/tutorials/ed-16.py +++ b/docs/docs/tutorials/ed-16.py @@ -190,10 +190,10 @@ # #### 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..d370ca274 100644 --- a/docs/docs/tutorials/ed-17.ipynb +++ b/docs/docs/tutorials/ed-17.ipynb @@ -569,7 +569,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -587,7 +587,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "project.display.pattern(expt_name='d20')" ] }, { @@ -678,7 +678,7 @@ "outputs": [], "source": [ "project.apply_params_from_csv(row_index=0)\n", - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "project.display.pattern(expt_name='d20')" ] }, { @@ -698,7 +698,7 @@ "outputs": [], "source": [ "project.apply_params_from_csv(row_index=-1)\n", - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "project.display.pattern(expt_name='d20')" ] }, { @@ -736,9 +736,9 @@ "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)" + "project.display.fit.series(structure.cell.length_a, versus=temperature)\n", + "project.display.fit.series(structure.cell.length_b, versus=temperature)\n", + "project.display.fit.series(structure.cell.length_c, versus=temperature)" ] }, { @@ -756,23 +756,23 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['Co1'].adp_iso,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['Si'].adp_iso,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['O1'].adp_iso,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['O2'].adp_iso,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['O3'].adp_iso,\n", " versus=temperature,\n", ")" @@ -793,23 +793,23 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['Co2'].fract_x,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['Co2'].fract_z,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['O1'].fract_z,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['O2'].fract_z,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['O3'].fract_z,\n", " versus=temperature,\n", ")" diff --git a/docs/docs/tutorials/ed-17.py b/docs/docs/tutorials/ed-17.py index f929cd47c..c1dc3a79a 100644 --- a/docs/docs/tutorials/ed-17.py +++ b/docs/docs/tutorials/ed-17.py @@ -280,13 +280,13 @@ # #### Show parameter correlations # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% [markdown] # #### Compare measured and calculated patterns for the first fit. # %% -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +project.display.pattern(expt_name='d20') # %% [markdown] # #### Run Sequential Fitting @@ -329,7 +329,7 @@ def extract_diffrn(file_path): # %% project.apply_params_from_csv(row_index=0) -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +project.display.pattern(expt_name='d20') # %% [markdown] # @@ -337,7 +337,7 @@ def extract_diffrn(file_path): # %% project.apply_params_from_csv(row_index=-1) -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +project.display.pattern(expt_name='d20') # %% [markdown] # #### Plot Parameter Evolution @@ -351,31 +351,31 @@ def extract_diffrn(file_path): # 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) +project.display.fit.series(structure.cell.length_a, versus=temperature) +project.display.fit.series(structure.cell.length_b, versus=temperature) +project.display.fit.series(structure.cell.length_c, versus=temperature) # %% [markdown] # Plot isotropic displacement parameters vs. temperature. # %% -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['Co1'].adp_iso, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['Si'].adp_iso, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['O1'].adp_iso, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['O2'].adp_iso, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['O3'].adp_iso, versus=temperature, ) @@ -384,23 +384,23 @@ def extract_diffrn(file_path): # Plot selected fractional coordinates vs. temperature. # %% -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['Co2'].fract_x, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['Co2'].fract_z, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['O1'].fract_z, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['O2'].fract_z, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.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..c1200010a 100644 --- a/docs/docs/tutorials/ed-18.ipynb +++ b/docs/docs/tutorials/ed-18.ipynb @@ -144,7 +144,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -154,7 +154,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -164,7 +164,7 @@ "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..7c4fff9fc 100644 --- a/docs/docs/tutorials/ed-18.py +++ b/docs/docs/tutorials/ed-18.py @@ -47,10 +47,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..927087e9d 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')" ] }, { @@ -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,7 @@ "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-2.py b/docs/docs/tutorials/ed-2.py index fa4918c05..c2772ba6b 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) @@ -199,13 +199,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 6: Switch calculator engine @@ -220,10 +220,10 @@ 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') diff --git a/docs/docs/tutorials/ed-20.ipynb b/docs/docs/tutorials/ed-20.ipynb index b9a47a281..4ad0e666c 100644 --- a/docs/docs/tutorials/ed-20.ipynb +++ b/docs/docs/tutorials/ed-20.ipynb @@ -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')" ] }, { @@ -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..e48e860ba 100644 --- a/docs/docs/tutorials/ed-20.py +++ b/docs/docs/tutorials/ed-20.py @@ -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 @@ -268,7 +268,7 @@ # #### 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 index 405bc8589..33f77196f 100644 --- a/docs/docs/tutorials/ed-21.ipynb +++ b/docs/docs/tutorials/ed-21.ipynb @@ -218,7 +218,9 @@ "id": "14", "metadata": {}, "source": [ - "#### Download the Measured Data" + "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." ] }, { @@ -236,7 +238,8 @@ "id": "16", "metadata": {}, "source": [ - "#### Create the Experiment Object" + "Create the experiment object and specify the sample form, beam mode,\n", + "and radiation probe." ] }, { @@ -270,7 +273,25 @@ "id": "19", "metadata": {}, "source": [ - "#### Set Instrument and Peak-Profile Parameters\n", + "Link the structural phase to the experiment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_phases.create(id='lbco', scale=9.1351)" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "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." @@ -279,7 +300,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -290,7 +311,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -302,10 +323,10 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "24", "metadata": {}, "source": [ - "#### Add Background Points and Excluded Regions\n", + "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." @@ -314,7 +335,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -327,7 +348,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -335,24 +356,6 @@ "experiment.excluded_regions.create(id='2', start=100, end=180)" ] }, - { - "cell_type": "markdown", - "id": "25", - "metadata": {}, - "source": [ - "#### Link the Structural Phase to the Experiment" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "26", - "metadata": {}, - "outputs": [], - "source": [ - "experiment.linked_phases.create(id='lbco', scale=9.1351)" - ] - }, { "cell_type": "markdown", "id": "27", @@ -442,7 +445,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -463,7 +466,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -473,7 +476,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -491,8 +494,10 @@ "on the current parameter value and expands them by a chosen multiple of\n", "the reported uncertainty.\n", "\n", - "Default `multiplier` is 8 to give a wide range for the sampler to\n", - "explore, but here we use 3 to speed up the tutorial." + "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." ] }, { @@ -502,23 +507,34 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" + ] + }, + { + "cell_type": "markdown", + "id": "40", + "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": "40", + "id": "41", "metadata": {}, "outputs": [], "source": [ "for param in project.free_parameters:\n", - " param.set_fit_bounds_from_uncertainty(multiplier=3.5)" + " param.set_fit_bounds_from_uncertainty()" ] }, { "cell_type": "markdown", - "id": "41", + "id": "42", "metadata": {}, "source": [ "Displaying the free parameters again is a convenient way to confirm\n", @@ -529,16 +545,16 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "43", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { "cell_type": "markdown", - "id": "43", + "id": "44", "metadata": {}, "source": [ "## Step 6: Configure and Run DREAM\n", @@ -550,20 +566,21 @@ "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 default\n", - "also uses `parallel=0`, which tells BUMPS DREAM to use all available\n", - "CPUs for population evaluations.\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 default `steps` value is 1000, and real analyses often need more\n", - "to achieve good convergence and posterior sampling. Here we use a much\n", - "smaller value to keep the tutorial fast, but this is not recommended\n", - "for production analysis." + "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": "44", + "id": "45", "metadata": {}, "outputs": [], "source": [ @@ -573,7 +590,7 @@ { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -583,17 +600,17 @@ { "cell_type": "code", "execution_count": null, - "id": "46", + "id": "47", "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer.steps = 100 # lower than the default 1000" + "project.analysis.fit.minimizer.steps = 300 # lower than the default 3000" ] }, { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "48", "metadata": {}, "outputs": [], "source": [ @@ -602,7 +619,7 @@ }, { "cell_type": "markdown", - "id": "48", + "id": "49", "metadata": {}, "source": [ "## Step 7: Inspect Bayesian Results\n", @@ -615,16 +632,16 @@ { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "50", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { "cell_type": "markdown", - "id": "50", + "id": "51", "metadata": {}, "source": [ "The correlation and posterior-pair plots are complementary:\n", @@ -632,32 +649,35 @@ "- `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." + " 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": "51", + "id": "52", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { "cell_type": "code", "execution_count": null, - "id": "52", + "id": "53", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_posterior_pairs()" + "project.display.posterior.pairs()" ] }, { "cell_type": "markdown", - "id": "53", + "id": "54", "metadata": {}, "source": [ "The one-dimensional posterior distributions below make it easier to\n", @@ -668,17 +688,17 @@ { "cell_type": "code", "execution_count": null, - "id": "54", + "id": "55", "metadata": {}, "outputs": [], "source": [ "for param in project.free_parameters:\n", - " project.display.plotter.plot_param_distribution(param)" + " project.display.posterior.distribution(param)" ] }, { "cell_type": "markdown", - "id": "55", + "id": "56", "metadata": {}, "source": [ "Finally, the posterior predictive plot propagates the sampled parameter\n", @@ -690,16 +710,16 @@ { "cell_type": "code", "execution_count": null, - "id": "56", + "id": "57", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_posterior_predictive(expt_name='hrpt')" + "project.display.posterior.predictive(expt_name='hrpt')" ] }, { "cell_type": "markdown", - "id": "57", + "id": "58", "metadata": {}, "source": [ "A final zoomed measured-vs-calculated plot is useful for checking how\n", @@ -710,11 +730,15 @@ { "cell_type": "code", "execution_count": null, - "id": "58", + "id": "59", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_posterior_predictive(expt_name='hrpt', x_min=92, x_max=93)" + "project.display.posterior.predictive(\n", + " expt_name='hrpt',\n", + " x_min=92,\n", + " x_max=93,\n", + ")" ] } ], diff --git a/docs/docs/tutorials/ed-21.py b/docs/docs/tutorials/ed-21.py index d91173c37..e890f55bc 100644 --- a/docs/docs/tutorials/ed-21.py +++ b/docs/docs/tutorials/ed-21.py @@ -213,7 +213,7 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # The correlation plot shows how strongly the fitted parameters move @@ -222,10 +222,10 @@ # region. # %% -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: Prepare for Bayesian Sampling @@ -244,7 +244,7 @@ # Show unset fit bounds before setting them from the local refinement uncertainties. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # Set fit bounds for all free parameters using the default multiplier of @@ -262,7 +262,7 @@ # sampler. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # ## Step 6: Configure and Run DREAM @@ -304,7 +304,7 @@ # statistics. # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # The correlation and posterior-pair plots are complementary: @@ -318,10 +318,10 @@ # keep the grid readable. # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_posterior_pairs() +project.display.posterior.pairs() # %% [markdown] # The one-dimensional posterior distributions below make it easier to @@ -330,7 +330,7 @@ # %% for param in project.free_parameters: - project.display.plotter.plot_param_distribution(param) + project.display.posterior.distribution(param) # %% [markdown] # Finally, the posterior predictive plot propagates the sampled parameter @@ -339,7 +339,7 @@ # model family explains the data in the region of interest. # %% -project.display.plotter.plot_posterior_predictive(expt_name='hrpt') +project.display.posterior.predictive(expt_name='hrpt') # %% [markdown] # A final zoomed measured-vs-calculated plot is useful for checking how @@ -347,7 +347,7 @@ # after the Bayesian run. # %% -project.display.plotter.plot_posterior_predictive( +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 index cc880314e..965e4ed82 100644 --- a/docs/docs/tutorials/ed-22.ipynb +++ b/docs/docs/tutorials/ed-22.ipynb @@ -35,7 +35,16 @@ "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." + "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?" ] }, { @@ -61,7 +70,11 @@ "id": "4", "metadata": {}, "source": [ - "## Step 1: Create a Project Container" + "## 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." ] }, { @@ -79,7 +92,12 @@ "id": "6", "metadata": {}, "source": [ - "## Step 2: Build the Structural Model" + "## 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." ] }, { @@ -117,7 +135,11 @@ "id": "10", "metadata": {}, "source": [ - "## Step 3: Define the Diffraction Experiment" + "## 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." ] }, { @@ -156,10 +178,18 @@ "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": "14", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -167,10 +197,20 @@ "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": "15", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -180,7 +220,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -190,16 +230,27 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "19", "metadata": {}, "source": [ - "## Step 4: Run an Initial Local Refinement" + "## 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": "18", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -218,7 +269,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -226,10 +277,20 @@ "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": "20", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -239,65 +300,111 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "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": "22", + "id": "26", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "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": "23", + "id": "28", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "29", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='heidi')" + "project.display.pattern(expt_name='heidi')" ] }, { "cell_type": "markdown", - "id": "25", + "id": "30", "metadata": {}, "source": [ - "## Step 5: Prepare for Bayesian Sampling" + "## 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": "26", + "id": "31", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "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": "27", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -305,28 +412,53 @@ " 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": "28", + "id": "35", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { "cell_type": "markdown", - "id": "29", + "id": "36", "metadata": {}, "source": [ - "## Step 6: Configure and Run BUMPS-DREAM" + "## 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": "30", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -336,7 +468,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -346,72 +478,120 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "39", "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer.steps = 500" + "project.analysis.fit.minimizer.steps = 500 # lower than the default 3000" ] }, { "cell_type": "code", "execution_count": null, - "id": "33", + "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": "34", + "id": "42", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "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": "35", + "id": "44", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "45", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_posterior_pairs()" + "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": "37", + "id": "47", "metadata": {}, "outputs": [], "source": [ "for param in project.free_parameters:\n", - " project.display.plotter.plot_param_distribution(param)" + " project.display.posterior.distribution(param)" + ] + }, + { + "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": "38", + "id": "49", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_posterior_predictive(expt_name='heidi')" + "project.display.posterior.predictive(expt_name='heidi')" ] } ], diff --git a/docs/docs/tutorials/ed-22.py b/docs/docs/tutorials/ed-22.py index aaa727180..bf0f2e19e 100644 --- a/docs/docs/tutorials/ed-22.py +++ b/docs/docs/tutorials/ed-22.py @@ -141,7 +141,7 @@ # estimated uncertainties. # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # The correlation plot shows how strongly the refined parameters move @@ -150,10 +150,10 @@ # intensities. # %% -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') # %% [markdown] # ## Step 5: Prepare for Bayesian Sampling @@ -174,7 +174,7 @@ # uncertainties. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # Set fit bounds for all free parameters using `multiplier=1.5`. In this @@ -192,7 +192,7 @@ # sampler. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # ## Step 6: Configure and Run DREAM @@ -232,7 +232,7 @@ # statistics. # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # The correlation and posterior-pair plots are complementary: @@ -246,10 +246,10 @@ # keep the grid readable. # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_posterior_pairs() +project.display.posterior.pairs() # %% [markdown] # The one-dimensional posterior distributions below make it easier to @@ -258,7 +258,7 @@ # %% for param in project.free_parameters: - project.display.plotter.plot_param_distribution(param) + project.display.posterior.distribution(param) # %% [markdown] # Finally, the posterior predictive plot propagates the sampled @@ -266,4 +266,4 @@ # intensities. # %% -project.display.plotter.plot_posterior_predictive(expt_name='heidi') +project.display.posterior.predictive(expt_name='heidi') diff --git a/docs/docs/tutorials/ed-3.ipynb b/docs/docs/tutorials/ed-3.ipynb index f99298a2f..03fba5148 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()" ] }, { @@ -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,7 +1061,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)" ] }, { @@ -1120,7 +1120,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -1139,7 +1139,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -1157,7 +1157,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -1167,7 +1167,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)" ] }, { @@ -1226,7 +1226,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -1245,7 +1245,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -1263,7 +1263,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -1273,7 +1273,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)" ] }, { @@ -1356,7 +1356,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.constraints()" + "project.analysis.constraints.show()" ] }, { @@ -1374,7 +1374,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -1393,7 +1393,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -1411,7 +1411,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -1421,7 +1421,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)" ] }, { @@ -1508,7 +1508,7 @@ }, "outputs": [], "source": [ - "project.analysis.display.constraints()" + "project.analysis.constraints.show()" ] }, { @@ -1544,7 +1544,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -1563,8 +1563,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()" ] }, { @@ -1582,7 +1582,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -1592,7 +1592,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)" ] }, { diff --git a/docs/docs/tutorials/ed-3.py b/docs/docs/tutorials/ed-3.py index de1f74c63..271b8e8f0 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 @@ -417,23 +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) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Save Project State @@ -456,23 +456,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) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Save Project State @@ -495,23 +495,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) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Save Project State @@ -546,29 +546,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) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Save Project State @@ -605,7 +605,7 @@ # Show defined constraints. # %% -project.analysis.display.constraints() +project.analysis.constraints.show() # %% [markdown] @@ -618,24 +618,24 @@ # 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 diff --git a/docs/docs/tutorials/ed-4.ipynb b/docs/docs/tutorials/ed-4.ipynb index 3a77ab8bb..bdf0343c4 100644 --- a/docs/docs/tutorials/ed-4.ipynb +++ b/docs/docs/tutorials/ed-4.ipynb @@ -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..a0ea3f623 100644 --- a/docs/docs/tutorials/ed-4.py +++ b/docs/docs/tutorials/ed-4.py @@ -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..248d4f59e 100644 --- a/docs/docs/tutorials/ed-8.ipynb +++ b/docs/docs/tutorials/ed-8.ipynb @@ -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..82beedd7b 100644 --- a/docs/docs/tutorials/ed-8.py +++ b/docs/docs/tutorials/ed-8.py @@ -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/user-guide/analysis-workflow/analysis.md b/docs/docs/user-guide/analysis-workflow/analysis.md index bf1d4ee87..b3c9e3d53 100644 --- a/docs/docs/user-guide/analysis-workflow/analysis.md +++ b/docs/docs/user-guide/analysis-workflow/analysis.md @@ -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: diff --git a/docs/docs/user-guide/analysis-workflow/project.md b/docs/docs/user-guide/analysis-workflow/project.md index 7959f9f85..5e8e8b417 100644 --- a/docs/docs/user-guide/analysis-workflow/project.md +++ b/docs/docs/user-guide/analysis-workflow/project.md @@ -115,8 +115,8 @@ data_La0.5Ba0.5CoO3 _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 +_display.chart_engine asciichartpy +_display.table_engine rich diff --git a/docs/docs/user-guide/first-steps.md b/docs/docs/user-guide/first-steps.md index b8f69070d..66f96e908 100644 --- a/docs/docs/user-guide/first-steps.md +++ b/docs/docs/user-guide/first-steps.md @@ -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/src/easydiffraction/__main__.py b/src/easydiffraction/__main__.py index bd5ae9be5..18220904f 100644 --- a/src/easydiffraction/__main__.py +++ b/src/easydiffraction/__main__.py @@ -99,10 +99,10 @@ def fit( if dry: project.info._path = None project.analysis.fit() - project.analysis.display.fit_results() - project.display.plotter.plot_param_correlations() + project.display.fit.results() + project.display.fit.correlations() for expt in project.experiments: - project.display.plotter.plot_meas_vs_calc(expt_name=expt.name, show_residual=True) + project.display.pattern(expt_name=expt.name) # project.summary.show_report() diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index 23b9fcf3f..e3c01607f 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + from contextlib import suppress import numpy as np @@ -102,7 +104,7 @@ class AnalysisDisplay: Accessed via ``analysis.display``. """ - def __init__(self, analysis: 'Analysis') -> None: + def __init__(self, analysis: Analysis) -> None: self._analysis = analysis def _flush_structure_categories(self) -> None: @@ -342,20 +344,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: """ @@ -381,10 +370,7 @@ 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 Analysis: @@ -900,6 +886,7 @@ def _update_categories( 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. @@ -911,3 +898,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/categories/constraints/default.py b/src/easydiffraction/analysis/categories/constraints/default.py index 63a4264d4..fc1d74fde 100644 --- a/src/easydiffraction/analysis/categories/constraints/default.py +++ b/src/easydiffraction/analysis/categories/constraints/default.py @@ -18,6 +18,9 @@ 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): @@ -132,3 +135,19 @@ def create(self, *, expression: str) -> None: item.expression = expression self.add(item) self._enabled = True + + 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.expression.value] for constraint in self] + + console.paragraph('User defined constraints') + render_table( + columns_headers=['expression'], + columns_alignment=['left'], + columns_data=rows, + ) + console.print(f'Constraints enabled: {self.enabled}') diff --git a/src/easydiffraction/display/plotters/ascii.py b/src/easydiffraction/display/plotters/ascii.py index 4aeb5577a..c81af0b0e 100644 --- a/src/easydiffraction/display/plotters/ascii.py +++ b/src/easydiffraction/display/plotters/ascii.py @@ -61,6 +61,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 +84,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 @@ -100,6 +103,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,6 +138,7 @@ 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.') diff --git a/src/easydiffraction/display/plotters/base.py b/src/easydiffraction/display/plotters/base.py index d4be0d67a..4cf43d669 100644 --- a/src/easydiffraction/display/plotters/base.py +++ b/src/easydiffraction/display/plotters/base.py @@ -65,6 +65,7 @@ class PowderMeasVsCalcSpec: 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): @@ -229,6 +230,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. @@ -250,6 +252,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 cb4914c3f..d41c1f9eb 100644 --- a/src/easydiffraction/display/plotters/plotly.py +++ b/src/easydiffraction/display/plotters/plotly.py @@ -70,6 +70,7 @@ 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' @@ -1038,6 +1039,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. @@ -1059,6 +1061,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 @@ -1075,8 +1079,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, @@ -1386,6 +1415,12 @@ def plot_powder_meas_vs_calc( 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, diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index 34b0da107..a258579e8 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -179,6 +179,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 @@ -373,6 +376,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, @@ -574,6 +589,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. @@ -588,16 +605,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( @@ -606,6 +630,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. @@ -620,16 +646,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( @@ -639,6 +672,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: """ @@ -656,17 +690,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, @@ -990,6 +1036,7 @@ def plot_posterior_predictive( x_max: float | None = None, *, show_residual: bool | None = None, + show_excluded: bool = False, x: object | None = None, ) -> None: """ @@ -1011,6 +1058,8 @@ def plot_posterior_predictive( 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. @@ -1024,6 +1073,28 @@ def plot_posterior_predictive( msg = "style must be 'band', 'draws', or 'band+draws'." raise ValueError(msg) + plot_options = _MeasVsCalcPlotOptions( + x_min=x_min, + x_max=x_max, + show_residual=show_residual, + show_excluded=show_excluded, + x=x, + ) + + self._plot_posterior_predictive_request( + expt_name=expt_name, + style=style, + plot_options=plot_options, + ) + + 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 @@ -1034,13 +1105,9 @@ def plot_posterior_predictive( self._update_project_categories(expt_name) experiment = self._project.experiments[expt_name] - x_axis, _, sample_form, scattering_type, _ = self._resolve_x_axis(experiment.type, x) - - plot_options = _MeasVsCalcPlotOptions( - x_min=x_min, - x_max=x_max, - show_residual=show_residual, - x=x, + x_axis, _, sample_form, scattering_type, _ = self._resolve_x_axis( + experiment.type, + plot_options.x, ) if sample_form == SampleFormEnum.SINGLE_CRYSTAL: @@ -1221,6 +1288,15 @@ def _plot_non_bragg_posterior_predictive( 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( @@ -1230,6 +1306,7 @@ def _plot_non_bragg_posterior_predictive( axes_labels=axes_labels, show_band=style in {'band', 'band+draws'}, show_draws=style in {'draws', 'band+draws'}, + excluded_ranges=excluded_ranges, ) @staticmethod @@ -3377,6 +3454,7 @@ def _plot_posterior_predictive_summary( axes_labels: list[str], show_band: bool, show_draws: bool, + excluded_ranges: tuple[tuple[float, float], ...] = (), ) -> None: """Render posterior predictive summaries using Plotly.""" go = __import__('plotly.graph_objects', fromlist=['Figure', 'Scatter']) @@ -3452,6 +3530,15 @@ def _plot_posterior_predictive_summary( 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}'", @@ -3627,12 +3714,14 @@ def _plot_posterior_predictive_data( ctx['x_min'], ctx['x_max'], ) - y_bkg_raw = getattr(pattern, 'intensity_bkg', None) - y_bkg = ( - self._filtered_y_array(y_bkg_raw, ctx['x_array'], ctx['x_min'], ctx['x_max']) - if y_bkg_raw is not None - else None + 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.map_prediction, summary.x, ctx['x_min'], ctx['x_max'] ) @@ -3669,7 +3758,7 @@ def _plot_posterior_predictive_data( dtype=float, ) - 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( @@ -3679,6 +3768,15 @@ def _plot_posterior_predictive_data( x_min=ctx['x_min'], x_max=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 () + ) plot_spec = PowderMeasVsCalcSpec( x=ctx['x_filtered'], @@ -3697,6 +3795,7 @@ def _plot_posterior_predictive_data( 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, ) self._backend.plot_powder_meas_vs_calc(plot_spec=plot_spec) @@ -4646,18 +4745,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. @@ -4665,20 +4765,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 @@ -4689,6 +4785,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'], @@ -4697,22 +4802,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. @@ -4720,20 +4827,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 @@ -4744,6 +4847,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'], @@ -4752,6 +4864,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( @@ -4837,12 +4950,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( @@ -4852,6 +4976,7 @@ def _plot_meas_vs_calc_data( series=powder_series, plot_options=plot_options, title=title, + excluded_ranges=excluded_ranges, ) return @@ -4863,6 +4988,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( @@ -4901,13 +5027,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( @@ -4930,9 +5057,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], @@ -4941,6 +5089,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. @@ -4958,8 +5107,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, diff --git a/src/easydiffraction/io/cif/serialize.py b/src/easydiffraction/io/cif/serialize.py index 0b868b6c1..b71e69e22 100644 --- a/src/easydiffraction/io/cif/serialize.py +++ b/src/easydiffraction/io/cif/serialize.py @@ -334,9 +334,9 @@ def _as_cif_text(section: object) -> str: def project_config_to_cif(project: object) -> str: """Render project-level configuration to ``project.cif`` text.""" 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 +350,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]) @@ -455,9 +455,9 @@ def project_config_from_cif(project: object, cif_text: str) -> None: _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) def analysis_from_cif(analysis: object, cif_text: str) -> None: 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/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..f8970cb55 --- /dev/null +++ b/src/easydiffraction/project/categories/rendering/default.py @@ -0,0 +1,131 @@ +# 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 + + +@RenderingFactory.register +class Rendering(CategoryItem): + """Chart and table engine selection for a project.""" + + type_info = TypeInfo( + tag='default', + description='Project rendering category', + ) + + def __init__(self) -> None: + super().__init__() + + self._plotter = Plotter() + self._tabler = TableRenderer.get() + + self._chart_engine = StringDescriptor( + name='chart_engine', + description='Chart renderer backend type', + value_spec=AttributeSpec( + default=self._plotter.engine, + validator=MembershipValidator( + allowed=[member.value for member in PlotterEngineEnum], + ), + ), + cif_handler=CifHandler(names=['_rendering.chart_engine']), + ) + self._table_engine = StringDescriptor( + name='table_engine', + 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=['_rendering.table_engine']), + ) + + self._identity.category_code = 'rendering' + + @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._plotter.engine = value + self._chart_engine.value = self._plotter.engine + + @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._tabler.engine = value + self._table_engine.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_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: + if chart_engine == self._plotter.engine: + self._chart_engine.value = chart_engine + else: + self.chart_engine = chart_engine + + table_engine = read_cif_str(block, '_rendering.table_engine') + if table_engine is not None: + if table_engine == self._tabler.engine: + self._table_engine.value = table_engine + else: + self.table_engine = table_engine diff --git a/src/easydiffraction/project/categories/display/factory.py b/src/easydiffraction/project/categories/rendering/factory.py similarity index 70% rename from src/easydiffraction/project/categories/display/factory.py rename to src/easydiffraction/project/categories/rendering/factory.py index 0d1be80a9..c2bdf7c5f 100644 --- a/src/easydiffraction/project/categories/display/factory.py +++ b/src/easydiffraction/project/categories/rendering/factory.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Factory for project display categories.""" +"""Factory for project rendering categories.""" from __future__ import annotations @@ -9,8 +9,8 @@ from easydiffraction.core.factory import FactoryBase -class DisplayFactory(FactoryBase): - """Create project display category instances.""" +class RenderingFactory(FactoryBase): + """Create project rendering 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..74c510fac --- /dev/null +++ b/src/easydiffraction/project/display.py @@ -0,0 +1,689 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Project display facade grouping charts and reports.""" + +from __future__ import annotations + +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.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() + + +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, + versus: object | None = None, + ) -> None: + """Plot one fitted parameter across sequential results.""" + self._project.rendering.plotter.plot_param_series(param=param, versus=versus) + + +class PosteriorDisplay: + """Posterior-plot namespace under ``project.display``.""" + + def __init__(self, project: Project) -> None: + self._project = project + + 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.""" + self._project.rendering.plotter.plot_posterior_pairs( + parameters=parameters, + style=style, + threshold=threshold, + max_parameters=max_parameters, + ) + + def distribution(self, param: object) -> None: + """Plot one sampled parameter's posterior distribution.""" + self._project.rendering.plotter.plot_param_distribution(param) + + 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.""" + 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, + ) + + +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 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: + 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: + 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_samples = getattr(fit_results, 'posterior_samples', None) + posterior_predictive = getattr(fit_results, 'posterior_predictive', None) + if posterior_samples is None or posterior_predictive is None: + return False, 'Posterior predictive data is unavailable.' + + if self._project.rendering.chart_engine.value != 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 58d7e97fb..3f3ac4fb4 100644 --- a/src/easydiffraction/project/project.py +++ b/src/easydiffraction/project/project.py @@ -16,8 +16,9 @@ from easydiffraction.datablocks.structure.collection import Structures 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.categories.rendering import Rendering +from easydiffraction.project.categories.rendering import RenderingFactory +from easydiffraction.project.display import ProjectDisplay from easydiffraction.project.project_info import ProjectInfo from easydiffraction.summary.summary import Summary from easydiffraction.utils.enums import VerbosityEnum @@ -82,8 +83,9 @@ def __init__( self._info: ProjectInfo = ProjectInfo(name, title, description) self._structures = Structures() self._experiments = Experiments() - self._display = DisplayFactory.create('default') - self._display._parent = self + self._rendering = RenderingFactory.create('default') + self._rendering._parent = self + self._display = ProjectDisplay(self) self._analysis = Analysis(self) self._summary = Summary(self) self._saved = False @@ -152,8 +154,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 @@ -373,7 +380,7 @@ def save(self) -> None: 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') diff --git a/src/easydiffraction/project/project_info.py b/src/easydiffraction/project/project_info.py index 94247f331..9bdfac7b0 100644 --- a/src/easydiffraction/project/project_info.py +++ b/src/easydiffraction/project/project_info.py @@ -2,6 +2,8 @@ # SPDX-License-Identifier: BSD-3-Clause """Project metadata container used by Project.""" +from __future__ import annotations + import datetime import pathlib @@ -121,6 +123,7 @@ def parameters(self) -> None: """List parameters (not implemented).""" # TODO: Consider moving to io.cif.serialize + @property def as_cif(self) -> str: """Export project metadata to CIF.""" return project_info_to_cif(self) @@ -129,6 +132,6 @@ def as_cif(self) -> str: 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() + cif_text: str = self.as_cif console.paragraph(paragraph_title) render_cif(cif_text) diff --git a/tests/integration/fitting/test_analysis_and_fit_category_support.py b/tests/integration/fitting/test_analysis_and_fit_category_support.py index 7a52c63d8..0ed65e4fc 100644 --- a/tests/integration/fitting/test_analysis_and_fit_category_support.py +++ b/tests/integration/fitting/test_analysis_and_fit_category_support.py @@ -282,6 +282,7 @@ def free_parameters(self): 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()) @@ -302,7 +303,7 @@ class FakeConstraint: analysis.constraints._items = [FakeConstraint()] captured: dict[str, object] = {} - monkeypatch.setattr(analysis_mod, 'render_table', lambda **kwargs: captured.update(kwargs)) + monkeypatch.setattr(constraints_mod, 'render_table', lambda **kwargs: captured.update(kwargs)) analysis.display.constraints() out = capsys.readouterr().out assert 'User defined constraints' in out diff --git a/tests/integration/fitting/test_analysis_display.py b/tests/integration/fitting/test_analysis_display.py index 2f5b0e450..bc549ee42 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 diff --git a/tests/integration/fitting/test_cli_entrypoints.py b/tests/integration/fitting/test_cli_entrypoints.py index 636c0ca4c..34cc85ad8 100644 --- a/tests/integration/fitting/test_cli_entrypoints.py +++ b/tests/integration/fitting/test_cli_entrypoints.py @@ -92,16 +92,21 @@ def fit_results() -> None: analysis = _analysis() class _display: - class _plotter: + class _fit: @staticmethod - def plot_param_correlations() -> None: - calls.append('PLOT_CORR') + def results() -> None: + calls.append('DISPLAY') @staticmethod - def plot_meas_vs_calc(expt_name: str, *, show_residual: bool = False) -> None: - calls.append(f'PLOT_{expt_name}_{show_residual}') + def correlations() -> None: + calls.append('PLOT_CORR') - plotter = _plotter() + fit = _fit() + + @staticmethod + def pattern(expt_name: str, **kwargs) -> None: + del kwargs + calls.append(f'PLOT_{expt_name}_False') display = _display() @@ -116,7 +121,7 @@ def plot_meas_vs_calc(expt_name: str, *, show_residual: bool = False) -> None: result = runner.invoke(main_mod.app, ['fit', str(project_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_dry_clears_path(monkeypatch, tmp_path): @@ -146,16 +151,20 @@ def fit_results() -> None: analysis = _analysis() class _display: - class _plotter: + class _fit: @staticmethod - def plot_param_correlations() -> None: + def results() -> None: return None @staticmethod - def plot_meas_vs_calc(expt_name: str, *, show_residual: bool = False) -> None: + def correlations() -> None: return None - plotter = _plotter() + fit = _fit() + + @staticmethod + def pattern(expt_name: str, **kwargs) -> None: + del expt_name, kwargs display = _display() 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/unit/easydiffraction/analysis/test_analysis_coverage.py b/tests/unit/easydiffraction/analysis/test_analysis_coverage.py index 089e1f6b8..9ff2926ea 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis_coverage.py +++ b/tests/unit/easydiffraction/analysis/test_analysis_coverage.py @@ -72,7 +72,7 @@ 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()) @@ -91,7 +91,7 @@ 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 diff --git a/tests/unit/easydiffraction/display/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py index c0ef16158..08540bf07 100644 --- a/tests/unit/easydiffraction/display/test_plotting.py +++ b/tests/unit/easydiffraction/display/test_plotting.py @@ -94,19 +94,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 @@ -154,13 +178,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) @@ -178,9 +213,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(): @@ -742,7 +784,15 @@ class Experiment: plotter._plot_posterior_predictive_data( experiment=Experiment(), expt_name='hrpt', - plot_options=SimpleNamespace(x_min=None, x_max=None, show_residual=None, x=None), + 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', ) @@ -752,6 +802,73 @@ class Experiment: assert plot_spec.y_calc_line_dash == 'dot' +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' @@ -1105,6 +1222,7 @@ def fake_plot_summary( axes_labels, show_band, show_draws, + excluded_ranges, ): captured['expt_name'] = expt_name captured['summary'] = summary @@ -1112,6 +1230,7 @@ def fake_plot_summary( 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) @@ -1125,6 +1244,7 @@ def fake_plot_summary( 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) 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/rendering/test_default.py b/tests/unit/easydiffraction/project/categories/rendering/test_default.py new file mode 100644 index 000000000..edb884272 --- /dev/null +++ b/tests/unit/easydiffraction/project/categories/rendering/test_default.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import gemmi + + +def test_rendering_defaults(): + 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 == rendering.plotter.engine + assert rendering.table_engine.value == rendering.tabler.engine + + +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.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' + + +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/test_display.py b/tests/unit/easydiffraction/project/test_display.py new file mode 100644 index 000000000..46f22296a --- /dev/null +++ b/tests/unit/easydiffraction/project/test_display.py @@ -0,0 +1,479 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for project/display.py.""" + +from __future__ import annotations + +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.plotting import _MeasVsCalcPlotOptions +from easydiffraction.project.display import PatternOptionStatus +from easydiffraction.project.display import ProjectDisplay + + +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_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'), + ) + project = SimpleNamespace( + analysis=SimpleNamespace(display=analysis_display), + rendering=SimpleNamespace(plotter=plotter), + ) + 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_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='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': 'temperature'}, + ) + + +def test_posterior_display_delegates_to_rendering_plotter(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + + 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', + }, + ) + + +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(): + 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, + ) + + 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, + ), + }, + ) + ] + + +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 090ce5404..e0ff677dd 100644 --- a/tests/unit/easydiffraction/project/test_project.py +++ b/tests/unit/easydiffraction/project/test_project.py @@ -63,3 +63,14 @@ def test_project_free_params_aggregate_structures_and_experiments(): 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) diff --git a/tests/unit/easydiffraction/project/test_project_load.py b/tests/unit/easydiffraction/project/test_project_load.py index c788f0a74..448345777 100644 --- a/tests/unit/easydiffraction/project/test_project_load.py +++ b/tests/unit/easydiffraction/project/test_project_load.py @@ -81,16 +81,16 @@ def test_round_trips_fit_mode(self, tmp_path): assert loaded.analysis.fit.mode.value == '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') diff --git a/tests/unit/easydiffraction/project/test_project_save.py b/tests/unit/easydiffraction/project/test_project_save.py index bf632e11c..08292525d 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') diff --git a/tests/unit/easydiffraction/test___main__.py b/tests/unit/easydiffraction/test___main__.py index 76d150c6d..9edd2bc27 100644 --- a/tests/unit/easydiffraction/test___main__.py +++ b/tests/unit/easydiffraction/test___main__.py @@ -95,16 +95,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 +124,7 @@ 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_dry_clears_path(monkeypatch, tmp_path): @@ -149,16 +154,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() From a6e80995b5805a19c1a9876d3aa9b3589f692ca1 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 14 May 2026 21:49:59 +0200 Subject: [PATCH 04/10] Improve parameter discoverability, constraint semantics, and CIF output (#172) * Reorganize dev documentation files * Hide refln in parameter summary displays * Exclude symmetry-fixed parameters from fittable displays * Rename parameter constraint state flags * Adopt bulk-created loop items into collections * Add help methods to display facades * Add help discovery helpers to analysis display * Add activity indicator implementation plan * Omit empty CIF fragments from serialization * Add Quick Reference page to docs * Uncomment display calls, simplify save in tutorial --- .../adr_analysis-cif-fit-state.md | 0 .../adr_parameter-correlation-persistence.md | 0 .../adr_parameter-posterior-summary.md | 0 .../dev/{ => ADR-suggestions}/adr_undo-fit.md | 0 docs/dev/{ => ADRs}/adr_display-ux.md | 52 ++- docs/dev/ADRs/adr_help-discoverability.md | 65 +++ docs/dev/{ => Issues}/issues_closed.md | 12 + docs/dev/{ => Issues}/issues_open.md | 31 +- docs/dev/architecture.md | 44 +- .../progress-activity-indicator.md | 407 ++++++++++++++++++ docs/dev/plan_display-ux.md | 238 ---------- docs/docs/quick-reference/index.md | 371 ++++++++++++++++ docs/docs/tutorials/ed-3.ipynb | 166 +++---- docs/docs/tutorials/ed-3.py | 26 +- docs/mkdocs.yml | 6 +- src/easydiffraction/analysis/analysis.py | 165 +++---- src/easydiffraction/analysis/fitting.py | 2 +- src/easydiffraction/analysis/sequential.py | 6 +- src/easydiffraction/core/collection.py | 13 + src/easydiffraction/core/datablock.py | 8 +- src/easydiffraction/core/singleton.py | 8 +- src/easydiffraction/core/variable.py | 44 +- .../crystallography/crystallography.py | 46 +- .../experiment/categories/data/bragg_pd.py | 4 +- .../experiment/categories/data/total_pd.py | 2 +- .../experiment/categories/refln/bragg_sc.py | 2 +- .../categories/atom_sites/default.py | 30 +- .../structure/categories/cell/default.py | 6 +- src/easydiffraction/io/cif/serialize.py | 30 +- src/easydiffraction/project/display.py | 17 + src/easydiffraction/project/project.py | 2 +- src/easydiffraction/summary/summary.py | 5 + src/easydiffraction/utils/utils.py | 85 ++++ tests/functional/test_structure_workflow.py | 24 +- .../fitting/test_exploration_help.py | 21 + .../easydiffraction/analysis/test_analysis.py | 12 + .../analysis/test_analysis_access_params.py | 257 ++++++++++- .../easydiffraction/core/test_datablock.py | 65 ++- .../easydiffraction/core/test_parameters.py | 28 +- .../test_crystallography_coverage.py | 26 +- .../test_crystallography_wyckoff.py | 14 +- .../categories/data/test_bragg_pd.py | 25 ++ .../categories/data/test_total_pd.py | 25 ++ .../categories/refln/test_bragg_sc.py | 29 ++ .../io/cif/test_serialize_more.py | 44 ++ .../easydiffraction/project/test_display.py | 35 ++ .../easydiffraction/summary/test_summary.py | 15 + .../unit/easydiffraction/utils/test_utils.py | 27 ++ 48 files changed, 1836 insertions(+), 704 deletions(-) rename docs/dev/{ => ADR-suggestions}/adr_analysis-cif-fit-state.md (100%) rename docs/dev/{ => ADR-suggestions}/adr_parameter-correlation-persistence.md (100%) rename docs/dev/{ => ADR-suggestions}/adr_parameter-posterior-summary.md (100%) rename docs/dev/{ => ADR-suggestions}/adr_undo-fit.md (100%) rename docs/dev/{ => ADRs}/adr_display-ux.md (80%) create mode 100644 docs/dev/ADRs/adr_help-discoverability.md rename docs/dev/{ => Issues}/issues_closed.md (90%) rename docs/dev/{ => Issues}/issues_open.md (97%) create mode 100644 docs/dev/implementation-plans/progress-activity-indicator.md delete mode 100644 docs/dev/plan_display-ux.md create mode 100644 docs/docs/quick-reference/index.md diff --git a/docs/dev/adr_analysis-cif-fit-state.md b/docs/dev/ADR-suggestions/adr_analysis-cif-fit-state.md similarity index 100% rename from docs/dev/adr_analysis-cif-fit-state.md rename to docs/dev/ADR-suggestions/adr_analysis-cif-fit-state.md diff --git a/docs/dev/adr_parameter-correlation-persistence.md b/docs/dev/ADR-suggestions/adr_parameter-correlation-persistence.md similarity index 100% rename from docs/dev/adr_parameter-correlation-persistence.md rename to docs/dev/ADR-suggestions/adr_parameter-correlation-persistence.md diff --git a/docs/dev/adr_parameter-posterior-summary.md b/docs/dev/ADR-suggestions/adr_parameter-posterior-summary.md similarity index 100% rename from docs/dev/adr_parameter-posterior-summary.md rename to docs/dev/ADR-suggestions/adr_parameter-posterior-summary.md diff --git a/docs/dev/adr_undo-fit.md b/docs/dev/ADR-suggestions/adr_undo-fit.md similarity index 100% rename from docs/dev/adr_undo-fit.md rename to docs/dev/ADR-suggestions/adr_undo-fit.md diff --git a/docs/dev/adr_display-ux.md b/docs/dev/ADRs/adr_display-ux.md similarity index 80% rename from docs/dev/adr_display-ux.md rename to docs/dev/ADRs/adr_display-ux.md index 264092e22..47e9b5c28 100644 --- a/docs/dev/adr_display-ux.md +++ b/docs/dev/ADRs/adr_display-ux.md @@ -2,12 +2,12 @@ ## Status -Accepted. +Accepted and implemented. ## Context -The current user-facing display API mixes presentation actions, analysis -reports, and renderer configuration: +The previous user-facing display API mixed presentation actions, +analysis reports, and renderer configuration: ```python project.display.plotter.plot_meas(expt_name='hrpt') @@ -32,7 +32,7 @@ This has several UX problems: - `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 existing `project.display` category is serialized to CIF, so it +- 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 @@ -55,7 +55,7 @@ project.rendering.show_table_engines() project.rendering.show_config() ``` -Suggested CIF names: +CIF names: - `_rendering.chart_engine` - `_rendering.table_engine` @@ -87,7 +87,8 @@ 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: +current responsibilities move to clearer homes, while the implementation +may keep the existing helpers as internal delegation targets: | Current method | New home | | ---------------------------- | -------------------------------------------------------------- | @@ -100,7 +101,7 @@ current responsibilities move to clearer homes: | `constraints()` | `project.analysis.constraints.show()` | | `as_cif()` | `project.analysis.as_cif` and `project.analysis.show_as_cif()` | -`project.analysis` and `project.info` should follow the same CIF display +`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. @@ -119,13 +120,17 @@ By default, `pattern()` uses `include='auto'` and displays as much useful information as the project state supports: - measured data if present -- calculated data if a model/calculation is available -- background if defined and relevant -- Bragg ticks if phases/reflections are available +- 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 -- uncertainty bands where posterior predictive data exists +- 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`: @@ -156,11 +161,11 @@ Add discovery for supported pattern content: project.display.show_pattern_options(expt_name='hrpt') ``` -The table should show option name, description, availability for the +The table shows option name, description, availability for the experiment, whether `include='auto'` includes it, and the reason an option is unavailable. -Initial option names: +Pattern option names: - `auto` - `measured` @@ -171,9 +176,17 @@ Initial option names: - `excluded` - `uncertainty` -`uncertainty` should be implemented immediately where posterior -predictive data exists. It should be unavailable, with a clear reason, -when no posterior predictive data is present. +`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 @@ -223,5 +236,8 @@ they duplicate `pattern(..., include=...)`. - 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` need CIF access cleanup for +- `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/adr_help-discoverability.md b/docs/dev/ADRs/adr_help-discoverability.md new file mode 100644 index 000000000..8fd019f39 --- /dev/null +++ b/docs/dev/ADRs/adr_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/issues_closed.md b/docs/dev/Issues/issues_closed.md similarity index 90% rename from docs/dev/issues_closed.md rename to docs/dev/Issues/issues_closed.md index a5ae2621a..fee4f327b 100644 --- a/docs/dev/issues_closed.md +++ b/docs/dev/Issues/issues_closed.md @@ -4,6 +4,18 @@ 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 `docs/dev/ADRs/adr_help-discoverability.md`. + +--- + ## Restore Minimiser Variant Support Used thin subclasses (approach A) to restore lmfit algorithm variants. diff --git a/docs/dev/issues_open.md b/docs/dev/Issues/issues_open.md similarity index 97% rename from docs/dev/issues_open.md rename to docs/dev/Issues/issues_open.md index 21976afde..bb41887f7 100644 --- a/docs/dev/issues_open.md +++ b/docs/dev/Issues/issues_open.md @@ -689,12 +689,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:** @@ -1263,27 +1264,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 @@ -1518,7 +1498,7 @@ operation is possible (e.g. in automated pipelines or tests). | 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 | +| 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 | @@ -1555,7 +1535,6 @@ operation is possible (e.g. in automated pipelines or tests). | 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 | diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 85fc0ad4e..7cc3abe92 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -187,16 +187,16 @@ 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 | +| 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 | `Parameter`s not blocked by user or symmetry constraints | +| 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`. @@ -210,7 +210,7 @@ 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 + └── GenericParameter # + free, uncertainty, fit_min, fit_max, user_constrained, symmetry_constrained ``` CIF-bound concrete classes add a `CifHandler` for serialisation: @@ -325,14 +325,15 @@ 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. +to zero by site symmetry) are flagged as `symmetry_constrained = 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 symmetry constrained. Surface helpers +`cell_symmetry_constrained_flags(...)` and +`atom_site_symmetry_constrained_flags(...)` in `crystallography` expose +the per-key flags. ### 4.2 Atomic Displacement Parameters (ADP) @@ -830,7 +831,10 @@ workflow: object. This is `FitResults` for deterministic fits and `BayesianFitResults` for Bayesian DREAM runs. - Parameter tables: `show_all_params()`, `show_fittable_params()`, - `show_free_params()`, `how_to_access_parameters()` + `show_free_params()`, `how_to_access_parameters()` Compact + summary-style parameter displays intentionally hide the large + loop-backed experiment categories `pd_data`, `total_data`, and `refln` + in `all()`, `access()`, and `cif_uids()` so the output stays readable. - Fitting: `fit()` dispatches single/joint through the callable `fit` category; `fit_sequential()` handles sequential mode (sets `fit.mode` to `'sequential'` internally). `fit()` accepts optional `random_seed` diff --git a/docs/dev/implementation-plans/progress-activity-indicator.md b/docs/dev/implementation-plans/progress-activity-indicator.md new file mode 100644 index 000000000..e776e0127 --- /dev/null +++ b/docs/dev/implementation-plans/progress-activity-indicator.md @@ -0,0 +1,407 @@ +# Progress Activity Indicator Implementation Plan + +**Status:** Proposed +**Date:** 2026-05-14 + +## Goal + +Add a small activity indicator for fitting and other long-running +calculations. The indicator should read as the same feature in terminal +and Jupyter, while using environment-appropriate rendering underneath. + +This is an activity indicator, not a numeric progress bar. Most +deterministic minimizers do not expose a reliable total work estimate, +and the existing fit progress table updates only when meaningful fit +state changes. A spinner-style indicator communicates that work is +continuing without implying a percentage that may be unavailable. + +## User-Facing Behavior + +### Visibility + +The indicator is controlled by existing verbosity: + +| Verbosity | Behavior | +| --------- | ---------------------------------------------------------------------------------------------------- | +| `silent` | Show nothing. No table, no activity indicator, no status line. | +| `short` | Show the activity indicator, but not the detailed fit progress table. Keep existing short summaries. | +| `full` | Show the detailed progress table and the activity indicator below it. | + +This changes the current fit tracker behavior where `short` returns +before creating any live progress output. + +### Labels + +Use the following labels: + +| Work type | Label | +| --------------------------------- | ------------ | +| Deterministic single fit | `fitting` | +| Sequential fit | `fitting` | +| Bayesian DREAM burn phase | `burn-in` | +| Bayesian DREAM sampling phase | `sampling` | +| Posterior predictive plots/checks | `processing` | +| Posterior pair plots | `processing` | +| Other long calculations | `processing` | + +The label must be updateable while work is running. DREAM should switch +from `burn-in` to `sampling` when sampler progress reports the phase +change. + +### Visual Style + +Use compact Unicode spinner frames: + +```text +⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ +``` + +Examples: + +```text +⠋ fitting +⠴ burn-in +⠇ sampling +⠙ processing +``` + +The indicator should be a single line. In `full` mode it appears below +the progress table. In `short` mode it appears as the only live progress +element. + +## Current Code Paths + +### Fit Progress + +The single-fit and sampler progress lifecycle is owned by +`src/easydiffraction/analysis/fit_helpers/tracking.py`. + +Important methods: + +- `FitProgressTracker.start_tracking(...)` +- `FitProgressTracker.add_tracking_info(...)` +- `FitProgressTracker.track_sampler_progress(...)` +- `FitProgressTracker.finish_tracking(...)` +- `_make_display_handle()` +- `_TerminalLiveHandle` + +The existing table update path uses `render_table(...)`, which delegates +to `TableRenderer` and then to either: + +- `PandasTableBackend` in Jupyter +- `RichTableBackend` in terminal + +### Sequential Fit + +Sequential fitting currently has separate progress output in +`src/easydiffraction/analysis/sequential.py`. + +Important functions: + +- `fit_sequential(...)` +- `_run_fit_loop(...)` +- `_report_chunk_progress(...)` + +Sequential fit should reuse the same activity indicator abstraction +rather than adding a separate spinner implementation. + +### Posterior And Other Long Calculations + +Posterior display work is routed through: + +- `src/easydiffraction/project/display.py` +- `src/easydiffraction/display/plotting.py` + +Important entry points include: + +- `PosteriorDisplay.pairs(...)` +- `PosteriorDisplay.predictive(...)` +- `Plotter.plot_posterior_pairs(...)` +- `Plotter.plot_posterior_predictive(...)` + +These should use the generic `processing` label if an activity indicator +is added to those paths. + +## Architecture + +### Add A Shared Activity Indicator + +Add a small shared display helper, preferably: + +```text +src/easydiffraction/display/progress.py +``` + +Recommended public/internal shape: + +```python +class ActivityIndicator: + def __init__(self, label: str = "processing", *, verbosity: VerbosityEnum) -> None: ... + def start(self) -> None: ... + def update(self, *, label: str | None = None, content: object | None = None) -> None: ... + def stop(self, *, final_label: str | None = None) -> None: ... +``` + +The exact class name can change during implementation, but it should +provide these capabilities: + +- no output in `silent` +- live output in `short` and `full` +- label updates while running +- optional table/content rendering above the indicator in `full` +- terminal and Jupyter implementations behind one API +- safe cleanup on exceptions + +### Terminal Rendering + +Use Rich for terminal rendering. + +Preferred implementation: + +- keep using `rich.live.Live` +- render a `rich.console.Group` +- group content should be: + - the progress table renderable, when present + - the activity indicator line + +The indicator line can be either: + +- Rich's built-in `Spinner`, if it works cleanly inside the existing + `Live` setup, or +- a local unicode-frame renderable driven by the same frame list. + +Do not create a second independent `Live` instance for the same output +area. The table and spinner should be refreshed together through one +live handle. + +### Jupyter Rendering + +Use an IPython `DisplayHandle` and HTML. + +The Jupyter spinner should be browser-driven CSS animation, not a +Python-loop animation. This matters because Python may not regain +control during expensive calculations, but CSS keeps animating once the +HTML has been displayed. + +Recommended HTML structure: + +```html +
+ + fitting +
+``` + +The table HTML and spinner HTML can be updated together in the same +display handle, or the spinner can have its own display handle below the +table. Prefer a single display handle if it keeps table-and-spinner +replacement simpler and avoids duplicated output cells. + +### Table Rendering Refactor + +The existing table backends mostly print/update directly. For a clean +combined table-plus-spinner render, add a way to build table renderables +without immediately displaying them. + +Possible approach: + +1. Keep `render_table(...)` working for existing callers. +2. Add a backend method that returns a renderable representation: + - Rich: return `rich.table.Table` + - Pandas: return HTML from `Styler.to_html()` +3. Let the activity indicator compose that renderable with the spinner. + +This avoids hard-coding table internals in the tracker and keeps normal +table rendering backwards compatible. + +## Fit Tracker Integration + +### State + +Add fields to `FitProgressTracker`: + +- `_activity_indicator` +- `_activity_label` + +The label is derived from tracking mode: + +- fit mode -> `fitting` +- sampler mode -> initial label from sampler phase if known, otherwise + `sampling` + +### `start_tracking(...)` + +Update behavior: + +1. Set tracking mode. +2. Return immediately only for `silent`. +3. Print the existing start/header messages only where appropriate: + - keep current full messages + - keep short mode concise +4. Create the activity indicator for `short` and `full`. +5. In `full`, render the initial empty progress table plus the + indicator. +6. In `short`, render only the indicator. + +### `add_tracking_info(...)` + +Update behavior: + +1. Always store row state for `full` mode. +2. In `full`, refresh the table plus the indicator. +3. In `short`, do not render the table; keep the indicator running. + +### `track_sampler_progress(...)` + +Update the activity label from `SamplerProgressUpdate.phase`: + +- phase `burn-in` -> label `burn-in` +- phase `sampling` -> label `sampling` +- any other phase -> normalized phase string if user-facing, otherwise + `processing` + +The existing sampler table's `phase` column remains unchanged. + +### `finish_tracking(...)` + +Update behavior: + +1. Finalize the last table row as today. +2. Stop the activity indicator for `short` and `full`. +3. In `full`, print the current completion summary. +4. In `short`, keep or add only a concise completion line if the current + short behavior expects one. +5. In `silent`, print nothing. + +Use `try/finally` in minimizer execution paths so the indicator is +stopped when a solver raises. + +## Sequential Fit Integration + +Sequential fit should use the same `ActivityIndicator`. + +Recommended behavior: + +- `silent`: no output. +- `short`: show one activity indicator labelled `fitting`; keep concise + chunk summaries when chunks finish. +- `full`: show one activity indicator labelled `fitting`; keep detailed + chunk summaries. + +Implementation points: + +1. Create the indicator in `fit_sequential(...)` after preflight checks + and before `_run_fit_loop(...)`. +2. Pass it into `_run_fit_loop(...)`, or wrap `_run_fit_loop(...)` in a + context manager. +3. Update the indicator content after each chunk if the implementation + supports content text, for example: + + ```text + ⠼ fitting chunk 3/20 + ``` + +4. Stop the indicator in a `finally` block before printing final + completion output. + +Do not duplicate spinner frame logic in `sequential.py`. + +## Posterior And Generic Processing Integration + +The first implementation can focus on fitting and sequential fitting. +After that, add a small context helper for generic long calculations: + +```python +with activity_indicator("processing", verbosity=VerbosityEnum(project.verbosity)): + ... +``` + +Use this for: + +- posterior predictive summary generation +- posterior pair plot construction when sample thinning, density grids, + or figure construction take noticeable time +- any future calculation where total progress is unknown + +The generic helper should default to `processing`. + +## Testing Plan + +### Unit Tests + +Add tests for the shared progress helper: + +- `silent` does not create display handles or print output. +- `short` starts the indicator. +- `full` can compose content plus indicator. +- label updates replace the visible label. +- `stop()` suppresses cleanup errors. + +Extend tracker tests: + +- `FitProgressTracker.start_tracking(...)` starts the indicator in + `short`. +- `silent` still shows nothing. +- full fit mode uses label `fitting`. +- sampler updates switch labels from `burn-in` to `sampling`. +- finalization stops the indicator on success. +- finalization stops the indicator when solver preparation or solver + execution raises. + +Extend sequential tests: + +- `fit_sequential(..., verbosity="short")` starts and stops the shared + indicator. +- `fit_sequential(..., verbosity="silent")` does not start it. +- chunk progress does not create a separate spinner. + +### Rendering Tests + +Terminal: + +- fake or monkeypatch `Live` and assert one live handle receives grouped + table-plus-indicator content. + +Jupyter: + +- fake `DisplayHandle` and assert generated HTML contains the activity + container and the selected label. +- assert CSS animation is included once, not duplicated on every table + update if avoidable. + +### Regression Tests + +Keep existing tests passing: + +- `tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py` +- `tests/integration/fitting/test_bayesian_tracker_and_base.py` +- sequential tests under `tests/integration/fitting/test_sequential.py` + +## Implementation Sequence + +1. Add `display/progress.py` with a minimal `ActivityIndicator`. +2. Add tests for verbosity behavior and label updates. +3. Refactor table rendering just enough to allow Rich renderable and + Jupyter HTML composition. +4. Wire `FitProgressTracker` to the activity indicator. +5. Update tracker tests for `short`, `full`, `silent`, and sampler phase + labels. +6. Wire sequential fitting to the shared activity indicator. +7. Update sequential tests. +8. Add generic `processing` context helper. +9. Add the helper to posterior predictive and posterior pairs if + profiling or user feedback shows those operations need visible + activity feedback. +10. Run focused unit tests, then the relevant integration tests. + +## Open Design Checks + +- Whether `short` mode should print the existing start line before the + spinner or show only the spinner until completion. +- Whether terminal output should use Rich's built-in `Spinner` or the + explicit EasyDiffraction frame list. Prefer the explicit list if + consistency with Jupyter matters more than Rich defaults. +- Whether the table and spinner should share one Jupyter display handle. + Prefer one handle unless it complicates the existing pandas backend. +- Whether generic display/plot operations need a public verbosity + argument, or should only read `project.verbosity`. diff --git a/docs/dev/plan_display-ux.md b/docs/dev/plan_display-ux.md deleted file mode 100644 index 4088ac657..000000000 --- a/docs/dev/plan_display-ux.md +++ /dev/null @@ -1,238 +0,0 @@ -# Plan: Display UX Facade - -## Goal - -Implement the display UX approach described in -`docs/dev/adr_display-ux.md`. - -The end-user API should become: - -```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=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') -``` - -Renderer configuration should move to: - -```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() -``` - -## Branch - -Use implementation branch: - -```text -feature/display-ux -``` - -## Decisions - -- Use grouped display namespaces under `project.display`. -- Use `project.display.fit.series(param, versus=...)` for sequential fit - parameter plots. -- Use `pattern(..., include='auto')` as the default experiment chart. -- Use `include` rather than `layers`, `components`, `content`, `view`, - `series`, or boolean flags. -- Do not add `project.display.constraints()`. -- Move constraint reporting to `project.analysis.constraints.show()`. -- Rename the serialized project display category to `rendering`. -- Do not add legacy CIF loading for `_display.plotter_type` or - `_display.tabler_type`. -- Standardize CIF display helpers on `project.analysis` and - `project.info`: `as_cif` should be a read-only property returning CIF - text, and `show_as_cif()` should pretty-print CIF text with a header. -- Implement `include='uncertainty'` immediately where posterior - predictive data exists. -- No compatibility aliases or deprecation warnings are required unless - release policy separately requires them. - -## Likely Files To Change - -- `src/easydiffraction/project/project.py` -- `src/easydiffraction/project/project_info.py` -- `src/easydiffraction/project/categories/display/default.py` -- `src/easydiffraction/project/categories/display/factory.py` -- `src/easydiffraction/project/categories/display/__init__.py` -- new `src/easydiffraction/project/categories/rendering/...` -- `src/easydiffraction/display/plotting.py` -- `src/easydiffraction/display/tables.py` -- `src/easydiffraction/analysis/analysis.py` -- `src/easydiffraction/analysis/categories/constraints/default.py` -- `src/easydiffraction/__main__.py` -- `docs/dev/architecture.md` -- `docs/docs/user-guide/**/*.md` -- `docs/docs/tutorials/*.py` -- tests under `tests/unit/easydiffraction/` -- tests under `tests/integration/fitting/` - -Do not edit generated tutorial notebooks directly. Update tutorial `.py` -files and regenerate notebooks in Phase 2 if required. - -## Resolved Questions - -- Legacy CIF `_display.plotter_type` and `_display.tabler_type` do not - need to load into `project.rendering`. No legacy code is required. -- `project.analysis` and `project.info` need consistency with structure - and experiment objects: `as_cif` should be a read-only property that - returns CIF text, and `show_as_cif()` should pretty-print CIF text - with a header. -- `include='uncertainty'` should be implemented immediately where - posterior predictive data exists. - -## Phase 1 - Implementation - -Do not create or run tests in Phase 1 unless explicitly requested. Every -completed Phase 1 implementation step must be staged with explicit paths -and committed locally before moving to the next implementation step or -the Phase 1 review gate. Use atomic commits, inspect the worktree before -each commit, and stage only the files changed for that step. - -- [x] Rename the serialized project display category to rendering. - - Move or recreate the category package as - `src/easydiffraction/project/categories/rendering/`. - - Rename user-facing settings from `plotter_type` and `tabler_type` to - `chart_engine` and `table_engine`. - - Add `show_chart_engines()`, `show_table_engines()`, and - `show_config()`. - - Update CIF names to `_rendering.chart_engine` and - `_rendering.table_engine`. - - Do not add legacy loading for `_display.plotter_type` or - `_display.tabler_type`. - -- [x] Add the new `project.display` facade. - - Add a facade object that is not the serialized rendering category. - - Add `pattern(...)` and `show_pattern_options(...)`. - - Add `parameters`, `fit`, and `posterior` namespace objects. - -- [x] Implement `pattern(..., include='auto')`. - - Replace common user-facing calls to `plot_meas`, `plot_calc`, and - `plot_meas_vs_calc` with one state-aware method. - - Support explicit includes for `measured`, `calculated`, - `background`, `residual`, `bragg`, `excluded`, and `uncertainty` - where data is available. - - Implement `uncertainty` immediately for experiments with posterior - predictive data. - - Render a clear warning or error when a requested include is not - available. - -- [x] Move parameter table displays under `project.display.parameters`. - - Implement `all()`, `fittable()`, `free()`, `access()`, and - `cif_uids()`. - - Remove the primary public need for `project.analysis.display`. - -- [x] Move fit displays under `project.display.fit`. - - Implement `results()`. - - Implement `correlations()`. - - Implement `series(param, versus=...)`. - -- [x] Move Bayesian displays under `project.display.posterior`. - - Implement `pairs()`. - - Implement `distribution(param)`. - - Implement `predictive(expt_name=...)`. - -- [x] Move constraint reporting to - `project.analysis.constraints.show()`. - - Do not add `project.display.constraints()`. - -- [x] Standardize CIF display helpers. - - Convert `project.analysis.as_cif()` to a read-only - `project.analysis.as_cif` property. - - Ensure `project.analysis.show_as_cif()` pretty-prints CIF text with - a header. - - Convert `project.info.as_cif()` to a read-only `project.info.as_cif` - property. - - Ensure `project.info.show_as_cif()` pretty-prints CIF text with a - header. - -- [x] Update docs, tutorials, and architecture text. - - Replace old public display examples with the selected API. - - Update `docs/dev/architecture.md`. - - Update tutorial `.py` files only; regenerate notebooks in Phase 2 if - required. - -- [x] Stop at the Phase 1 review gate. - - Summarize changed files and open questions. - - Suggest next verification commands. - - Wait for user approval before Phase 2. - -Suggested Phase 1 commit messages: - -```text -Rename display settings category to rendering -Add grouped display facade -Implement state-aware pattern display -Move analysis display reports to display facade -Standardize CIF display helpers -Update display UX documentation -``` - -## Phase 2 - Verification - -After Phase 1 is reviewed and approved: - -- [x] Add or update unit tests for the rendering category. -- [x] Add or update unit tests for the display facade namespaces. -- [x] Add or update plotting integration tests for `pattern(...)`. -- [x] Add or update analysis display integration tests for parameter and - fit report methods. -- [x] Regenerate tutorial notebooks if tutorial `.py` files changed. -- [x] Run formatting and checks. -- [x] Run unit tests. -- [x] Run integration tests. -- [x] Run script tests. - -Verification commands: - -```sh -pixi run notebook-prepare -pixi run fix -pixi run check -pixi run unit-tests -pixi run integration-tests -pixi run script-tests -``` - -Run `pixi run notebook-prepare` only if tutorial `.py` files changed. -Run `pixi run integration-tests` only after the relevant unit and -focused integration tests are passing. - -## Suggested Commit Message - -```text -Plan display UX facade implementation -``` - -## Suggested Pull Request - -Title: - -```text -Improve chart and table display API -``` - -Description: - -This change makes display commands easier to discover and use in -notebooks. Experiment patterns, parameter tables, fit reports, and -Bayesian plots are grouped under `project.display`, while renderer -settings move to `project.rendering`. diff --git a/docs/docs/quick-reference/index.md b/docs/docs/quick-reference/index.md new file mode 100644 index 000000000..e0d3fd202 --- /dev/null +++ b/docs/docs/quick-reference/index.md @@ -0,0 +1,371 @@ +--- +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 +structure_path = ed.download_data(id=1, destination='data') +data_path = ed.download_data(id=3, destination='data') +``` + +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.fit.show_modes() +project.analysis.fit.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.fit.mode = 'single' +project.analysis.fit.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.fit.show_modes() +project.analysis.fit.mode = 'single' + +project.analysis.fit.show_minimizer_types() +project.analysis.fit.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') +``` + +## 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 fit lbco_hrpt +python -m easydiffraction fit lbco_hrpt --dry +``` + +## Command-Line Reminders + +```bash +python -m easydiffraction --help +python -m easydiffraction --version +python -m easydiffraction list-tutorials +python -m easydiffraction download-tutorial 1 --destination tutorials +python -m easydiffraction download-all-tutorials --destination tutorials +python -m easydiffraction fit PROJECT_DIR +``` diff --git a/docs/docs/tutorials/ed-3.ipynb b/docs/docs/tutorials/ed-3.ipynb index 03fba5148..0f103f8aa 100644 --- a/docs/docs/tutorials/ed-3.ipynb +++ b/docs/docs/tutorials/ed-3.ipynb @@ -822,7 +822,7 @@ "metadata": {}, "outputs": [], "source": [ - "# project.display.parameters.all()" + "project.display.parameters.all()" ] }, { @@ -876,7 +876,7 @@ "metadata": {}, "outputs": [], "source": [ - "# project.display.parameters.access()" + "project.display.parameters.access()" ] }, { @@ -1068,24 +1068,6 @@ "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,7 +1098,7 @@ { "cell_type": "code", "execution_count": null, - "id": "108", + "id": "106", "metadata": {}, "outputs": [], "source": [ @@ -1125,7 +1107,7 @@ }, { "cell_type": "markdown", - "id": "109", + "id": "107", "metadata": {}, "source": [ "#### Run Fitting" @@ -1134,7 +1116,7 @@ { "cell_type": "code", "execution_count": null, - "id": "110", + "id": "108", "metadata": {}, "outputs": [], "source": [ @@ -1144,7 +1126,7 @@ }, { "cell_type": "markdown", - "id": "111", + "id": "109", "metadata": {}, "source": [ "#### Plot Measured vs Calculated" @@ -1153,7 +1135,7 @@ { "cell_type": "code", "execution_count": null, - "id": "112", + "id": "110", "metadata": {}, "outputs": [], "source": [ @@ -1163,7 +1145,7 @@ { "cell_type": "code", "execution_count": null, - "id": "113", + "id": "111", "metadata": {}, "outputs": [], "source": [ @@ -1172,7 +1154,7 @@ }, { "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,7 +1204,7 @@ { "cell_type": "code", "execution_count": null, - "id": "119", + "id": "117", "metadata": {}, "outputs": [], "source": [ @@ -1231,7 +1213,7 @@ }, { "cell_type": "markdown", - "id": "120", + "id": "118", "metadata": {}, "source": [ "#### Run Fitting" @@ -1240,7 +1222,7 @@ { "cell_type": "code", "execution_count": null, - "id": "121", + "id": "119", "metadata": {}, "outputs": [], "source": [ @@ -1250,7 +1232,7 @@ }, { "cell_type": "markdown", - "id": "122", + "id": "120", "metadata": {}, "source": [ "#### Plot Measured vs Calculated" @@ -1259,7 +1241,7 @@ { "cell_type": "code", "execution_count": null, - "id": "123", + "id": "121", "metadata": {}, "outputs": [], "source": [ @@ -1269,7 +1251,7 @@ { "cell_type": "code", "execution_count": null, - "id": "124", + "id": "122", "metadata": {}, "outputs": [], "source": [ @@ -1278,25 +1260,7 @@ }, { "cell_type": "markdown", - "id": "125", - "metadata": {}, - "source": [ - "#### Save Project State" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "126", - "metadata": {}, - "outputs": [], - "source": [ - "project.save_as(dir_path='lbco_hrpt', temporary=True)" - ] - }, - { - "cell_type": "markdown", - "id": "127", + "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,7 +1316,7 @@ { "cell_type": "code", "execution_count": null, - "id": "132", + "id": "128", "metadata": {}, "outputs": [], "source": [ @@ -1361,7 +1325,7 @@ }, { "cell_type": "markdown", - "id": "133", + "id": "129", "metadata": {}, "source": [ "Show free parameters." @@ -1370,7 +1334,7 @@ { "cell_type": "code", "execution_count": null, - "id": "134", + "id": "130", "metadata": {}, "outputs": [], "source": [ @@ -1379,7 +1343,7 @@ }, { "cell_type": "markdown", - "id": "135", + "id": "131", "metadata": {}, "source": [ "#### Run Fitting" @@ -1388,7 +1352,7 @@ { "cell_type": "code", "execution_count": null, - "id": "136", + "id": "132", "metadata": {}, "outputs": [], "source": [ @@ -1398,7 +1362,7 @@ }, { "cell_type": "markdown", - "id": "137", + "id": "133", "metadata": {}, "source": [ "#### Plot Measured vs Calculated" @@ -1407,7 +1371,7 @@ { "cell_type": "code", "execution_count": null, - "id": "138", + "id": "134", "metadata": {}, "outputs": [], "source": [ @@ -1417,7 +1381,7 @@ { "cell_type": "code", "execution_count": null, - "id": "139", + "id": "135", "metadata": {}, "outputs": [], "source": [ @@ -1426,25 +1390,7 @@ }, { "cell_type": "markdown", - "id": "140", - "metadata": {}, - "source": [ - "#### Save Project State" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "141", - "metadata": {}, - "outputs": [], - "source": [ - "project.save_as(dir_path='lbco_hrpt', temporary=True)" - ] - }, - { - "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,7 +1448,7 @@ { "cell_type": "code", "execution_count": null, - "id": "147", + "id": "141", "metadata": { "lines_to_next_cell": 2 }, @@ -1513,7 +1459,7 @@ }, { "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,7 +1486,7 @@ { "cell_type": "code", "execution_count": null, - "id": "151", + "id": "145", "metadata": {}, "outputs": [], "source": [ @@ -1549,7 +1495,7 @@ }, { "cell_type": "markdown", - "id": "152", + "id": "146", "metadata": {}, "source": [ "#### Run Fitting" @@ -1558,7 +1504,7 @@ { "cell_type": "code", "execution_count": null, - "id": "153", + "id": "147", "metadata": {}, "outputs": [], "source": [ @@ -1569,7 +1515,7 @@ }, { "cell_type": "markdown", - "id": "154", + "id": "148", "metadata": {}, "source": [ "#### Plot Measured vs Calculated" @@ -1578,7 +1524,7 @@ { "cell_type": "code", "execution_count": null, - "id": "155", + "id": "149", "metadata": {}, "outputs": [], "source": [ @@ -1588,7 +1534,7 @@ { "cell_type": "code", "execution_count": null, - "id": "156", + "id": "150", "metadata": {}, "outputs": [], "source": [ @@ -1597,7 +1543,7 @@ }, { "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 271b8e8f0..fcccb5968 100644 --- a/docs/docs/tutorials/ed-3.py +++ b/docs/docs/tutorials/ed-3.py @@ -345,7 +345,7 @@ # Show all parameters of the project. # %% -# project.display.parameters.all() +project.display.parameters.all() # %% [markdown] # Show all fittable parameters. @@ -363,7 +363,7 @@ # Show how to access parameters in the code. # %% -# project.display.parameters.access() +project.display.parameters.access() # %% [markdown] # #### Set Fit Mode @@ -435,12 +435,6 @@ # %% 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) - # %% [markdown] # ### Perform Fit 2/5 # @@ -478,7 +472,7 @@ # #### Save Project State # %% -project.save_as(dir_path='lbco_hrpt', temporary=True) +project.save() # %% [markdown] # ### Perform Fit 3/5 @@ -513,12 +507,6 @@ # %% 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) - # %% [markdown] # ### Perform Fit 4/5 # @@ -570,12 +558,6 @@ # %% 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) - # %% [markdown] # ### Perform Fit 5/5 # @@ -641,7 +623,7 @@ # #### Save Project State # %% -project.save_as(dir_path='lbco_hrpt', temporary=True) +project.save() # %% [markdown] # ## Step 5: Summary diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 16091a5d6..fdd6a30b0 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -176,6 +176,8 @@ nav: - Introduction: introduction/index.md - Installation & Setup: - Installation & Setup: installation-and-setup/index.md + - Quick Reference: + - Quick Reference: quick-reference/index.md - User Guide: - User Guide: user-guide/index.md - Glossary: user-guide/glossary.md @@ -221,6 +223,8 @@ nav: - 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 @@ -234,5 +238,3 @@ nav: - project: api-reference/project.md - summary: api-reference/summary.md - utils: api-reference/utils.md - - Command-Line: - - Command-Line: cli/index.md diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index e3c01607f..484c3e03d 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -16,7 +16,6 @@ from easydiffraction.analysis.categories.joint_fit_experiments import JointFitExperiments from easydiffraction.analysis.fit_helpers.tracking import _make_display_handle from easydiffraction.analysis.fitting import Fitter -from easydiffraction.core.guard import GuardedBase from easydiffraction.core.singleton import ConstraintsHandler from easydiffraction.core.variable import NumericDescriptor from easydiffraction.core.variable import Parameter @@ -26,75 +25,23 @@ 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'}) -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: @@ -107,6 +54,10 @@ class AnalysisDisplay: 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. @@ -116,12 +67,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.') @@ -138,15 +100,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.""" @@ -172,15 +136,17 @@ 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.""" @@ -225,14 +191,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 @@ -291,14 +257,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 @@ -412,27 +378,7 @@ def display(self) -> AnalysisDisplay: def help(self) -> None: """Print a summary of analysis properties and methods.""" - console.paragraph("Help for 'Analysis'") - - cls = type(self) - - prop_rows = _discover_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 = _discover_method_rows(cls) - if method_rows: - console.paragraph('Methods') - render_table( - columns_headers=['#', 'Name', 'Description'], - columns_alignment=['right', 'left', 'left'], - columns_data=method_rows, - ) + render_object_help(self) # ------------------------------------------------------------------ # Parameter helpers @@ -475,7 +421,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, @@ -551,7 +498,7 @@ def _run_fit( log.warning('No experiments found in the project. Cannot run fit.') return - # 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._update_categories() diff --git a/src/easydiffraction/analysis/fitting.py b/src/easydiffraction/analysis/fitting.py index db5590e1b..343c15742 100644 --- a/src/easydiffraction/analysis/fitting.py +++ b/src/easydiffraction/analysis/fitting.py @@ -81,7 +81,7 @@ def fit( expt_free_params.extend( p for p in expt.parameters - if isinstance(p, Parameter) and not p.constrained and p.free + if isinstance(p, Parameter) and not p.user_constrained and p.free ) params = structures.free_parameters + expt_free_params diff --git a/src/easydiffraction/analysis/sequential.py b/src/easydiffraction/analysis/sequential.py index 220179f89..25feb184e 100644 --- a/src/easydiffraction/analysis/sequential.py +++ b/src/easydiffraction/analysis/sequential.py @@ -452,7 +452,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 @@ -612,7 +612,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.' 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..0ac201760 100644 --- a/src/easydiffraction/core/datablock.py +++ b/src/easydiffraction/core/datablock.py @@ -187,8 +187,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/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/variable.py b/src/easydiffraction/core/variable.py index 303c5bbe5..55af6cba9 100644 --- a/src/easydiffraction/core/variable.py +++ b/src/easydiffraction/core/variable.py @@ -285,10 +285,10 @@ def __init__( 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: """ @@ -325,22 +325,22 @@ 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()``. + physical-range validators. Flags the parameter as user + constrained. Used exclusively by ``ConstraintsHandler.apply()``. """ self._value = v - self._constrained = True + self._user_constrained = True parent_datablock = self._datablock_item() if parent_datablock is not None: parent_datablock._need_categories_update = True @@ -356,24 +356,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 @@ -383,14 +383,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 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/data/bragg_pd.py b/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py index 0f8633010..b1e30aa0a 100644 --- a/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py +++ b/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py @@ -572,7 +572,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 +651,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..31ac92e76 100644 --- a/src/easydiffraction/datablocks/experiment/categories/data/total_pd.py +++ b/src/easydiffraction/datablocks/experiment/categories/data/total_pd.py @@ -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/refln/bragg_sc.py b/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py index 52329abb0..ada32c70f 100644 --- a/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py +++ b/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py @@ -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/structure/categories/atom_sites/default.py b/src/easydiffraction/datablocks/structure/categories/atom_sites/default.py index 562ad18d6..6c91a8561 100644 --- a/src/easydiffraction/datablocks/structure/categories/atom_sites/default.py +++ b/src/easydiffraction/datablocks/structure/categories/atom_sites/default.py @@ -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..712629501 100644 --- a/src/easydiffraction/datablocks/structure/categories/cell/default.py +++ b/src/easydiffraction/datablocks/structure/categories/cell/default.py @@ -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/io/cif/serialize.py b/src/easydiffraction/io/cif/serialize.py index b71e69e22..0527ac690 100644 --- a/src/easydiffraction/io/cif/serialize.py +++ b/src/easydiffraction/io/cif/serialize.py @@ -87,7 +87,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 +98,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 +108,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 @@ -279,13 +279,21 @@ def datablock_item_to_cif( parts: list[str] = [header] # First categories - parts.extend(v.as_cif for v in vars(datablock).values() if isinstance(v, CategoryItem)) + parts.extend( + cif_text + for cif_text in (v.as_cif for v in vars(datablock).values() if isinstance(v, CategoryItem)) + if cif_text + ) # Then collections parts.extend( - category_collection_to_cif(v, max_display=max_loop_display) - for v in vars(datablock).values() - if isinstance(v, CategoryCollection) + cif_text + for cif_text in ( + category_collection_to_cif(v, max_display=max_loop_display) + for v in vars(datablock).values() + if isinstance(v, CategoryCollection) + ) + if cif_text ) return '\n\n'.join(parts) @@ -715,11 +723,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): diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py index 74c510fac..2071463d5 100644 --- a/src/easydiffraction/project/display.py +++ b/src/easydiffraction/project/display.py @@ -13,6 +13,7 @@ from easydiffraction.display.plotting import PlotterEngineEnum from easydiffraction.display.plotting import PosteriorPairPlotStyleEnum from easydiffraction.display.plotting import _MeasVsCalcPlotOptions +from easydiffraction.utils.utils import render_object_help from easydiffraction.utils.utils import render_table if TYPE_CHECKING: @@ -68,6 +69,10 @@ 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``.""" @@ -103,6 +108,10 @@ def series( """Plot one fitted parameter across sequential results.""" 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``.""" @@ -150,6 +159,10 @@ def predictive( x=x, ) + def help(self) -> None: + """Print available posterior-display methods.""" + render_object_help(self) + class ProjectDisplay: """Grouped display facade exposed as ``project.display``.""" @@ -175,6 +188,10 @@ 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, diff --git a/src/easydiffraction/project/project.py b/src/easydiffraction/project/project.py index 3f3ac4fb4..e7508201f 100644 --- a/src/easydiffraction/project/project.py +++ b/src/easydiffraction/project/project.py @@ -342,7 +342,7 @@ 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() diff --git a/src/easydiffraction/summary/summary.py b/src/easydiffraction/summary/summary.py index 768abb4b2..27826dab7 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, diff --git a/src/easydiffraction/utils/utils.py b/src/easydiffraction/utils/utils.py index 0e301e30c..a81f1cb0b 100644 --- a/src/easydiffraction/utils/utils.py +++ b/src/easydiffraction/utils/utils.py @@ -563,6 +563,91 @@ def render_table( tabler.render(df, display_handle=display_handle) +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_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/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/unit/easydiffraction/analysis/test_analysis.py b/tests/unit/easydiffraction/analysis/test_analysis.py index a4d2c7faa..c5b2ef248 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis.py +++ b/tests/unit/easydiffraction/analysis/test_analysis.py @@ -67,6 +67,18 @@ def test_analysis_help(capsys): assert 'fit_sequential()' 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): """Test that display.fit_results logs a warning when fit() has not been run.""" from easydiffraction.analysis.analysis import Analysis 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/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 a6c5b7adf..f81e35e87 100644 --- a/tests/unit/easydiffraction/core/test_parameters.py +++ b/tests/unit/easydiffraction/core/test_parameters.py @@ -199,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/io/cif/test_serialize_more.py b/tests/unit/easydiffraction/io/cif/test_serialize_more.py index 0f98fe1a7..1add7692d 100644 --- a/tests/unit/easydiffraction/io/cif/test_serialize_more.py +++ b/tests/unit/easydiffraction/io/cif/test_serialize_more.py @@ -40,6 +40,50 @@ def __init__(self): 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 diff --git a/tests/unit/easydiffraction/project/test_display.py b/tests/unit/easydiffraction/project/test_display.py index 46f22296a..8e8f7695d 100644 --- a/tests/unit/easydiffraction/project/test_display.py +++ b/tests/unit/easydiffraction/project/test_display.py @@ -141,6 +141,41 @@ def test_parameter_display_delegates_to_analysis_display(): ] +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) diff --git a/tests/unit/easydiffraction/summary/test_summary.py b/tests/unit/easydiffraction/summary/test_summary.py index b8af02667..6c34baf11 100644 --- a/tests/unit/easydiffraction/summary/test_summary.py +++ b/tests/unit/easydiffraction/summary/test_summary.py @@ -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/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 From 2d759a95ed1b1fe0c0a7f04a84f20cfa967adeda Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 15 May 2026 07:34:42 +0200 Subject: [PATCH 05/10] Improve live fitting feedback and posterior uncertainty displays (#173) * Add shared activity indicator helper * Wire fit tracker activity indicator * Reuse activity indicator for sequential fitting * Restore shared display handle compatibility * Show processing indicator for posterior displays * Prevent notebook progress table stretching * Add activity label constants and styling * Refine progress indicator styling * Refactor progress indicator context manager * Format posterior predictive call * Rename 95% interval, remove 68% interval * Refactor progress indicator to use dynamic renderable * Share display handle across multi-experiment fits * Add timed progress updates for fit tracking * Prevent progress indicator errors on exception * Remove completed progress indicator plan --- .../progress-activity-indicator.md | 407 ---------------- docs/dev/package-structure-full.md | 5 +- docs/dev/package-structure-short.md | 1 + docs/docs/tutorials/ed-21.ipynb | 6 +- docs/docs/tutorials/ed-21.py | 6 +- src/easydiffraction/analysis/analysis.py | 62 +-- .../analysis/fit_helpers/tracking.py | 253 +++++----- src/easydiffraction/analysis/sequential.py | 49 +- .../display/plotters/plotly.py | 2 +- src/easydiffraction/display/plotting.py | 16 +- src/easydiffraction/display/progress.py | 439 ++++++++++++++++++ src/easydiffraction/display/tablers/base.py | 23 + src/easydiffraction/display/tablers/pandas.py | 36 +- src/easydiffraction/display/tablers/rich.py | 18 +- src/easydiffraction/display/tables.py | 56 ++- src/easydiffraction/project/display.py | 99 ++-- src/easydiffraction/utils/utils.py | 32 ++ .../fitting/test_bayesian_tracker_and_base.py | 145 ++++-- .../analysis/fit_helpers/test_tracking.py | 40 +- .../analysis/minimizers/test_base.py | 43 ++ .../easydiffraction/analysis/test_analysis.py | 95 ++++ .../analysis/test_sequential.py | 131 ++++++ .../display/plotters/test_plotly.py | 4 +- .../display/tablers/test_pandas.py | 12 + .../display/tablers/test_rich.py | 6 +- .../easydiffraction/display/test_plotting.py | 9 +- .../easydiffraction/display/test_progress.py | 210 +++++++++ .../easydiffraction/display/test_tables.py | 29 ++ .../easydiffraction/project/test_display.py | 33 +- 29 files changed, 1550 insertions(+), 717 deletions(-) delete mode 100644 docs/dev/implementation-plans/progress-activity-indicator.md create mode 100644 src/easydiffraction/display/progress.py create mode 100644 tests/unit/easydiffraction/display/test_progress.py diff --git a/docs/dev/implementation-plans/progress-activity-indicator.md b/docs/dev/implementation-plans/progress-activity-indicator.md deleted file mode 100644 index e776e0127..000000000 --- a/docs/dev/implementation-plans/progress-activity-indicator.md +++ /dev/null @@ -1,407 +0,0 @@ -# Progress Activity Indicator Implementation Plan - -**Status:** Proposed -**Date:** 2026-05-14 - -## Goal - -Add a small activity indicator for fitting and other long-running -calculations. The indicator should read as the same feature in terminal -and Jupyter, while using environment-appropriate rendering underneath. - -This is an activity indicator, not a numeric progress bar. Most -deterministic minimizers do not expose a reliable total work estimate, -and the existing fit progress table updates only when meaningful fit -state changes. A spinner-style indicator communicates that work is -continuing without implying a percentage that may be unavailable. - -## User-Facing Behavior - -### Visibility - -The indicator is controlled by existing verbosity: - -| Verbosity | Behavior | -| --------- | ---------------------------------------------------------------------------------------------------- | -| `silent` | Show nothing. No table, no activity indicator, no status line. | -| `short` | Show the activity indicator, but not the detailed fit progress table. Keep existing short summaries. | -| `full` | Show the detailed progress table and the activity indicator below it. | - -This changes the current fit tracker behavior where `short` returns -before creating any live progress output. - -### Labels - -Use the following labels: - -| Work type | Label | -| --------------------------------- | ------------ | -| Deterministic single fit | `fitting` | -| Sequential fit | `fitting` | -| Bayesian DREAM burn phase | `burn-in` | -| Bayesian DREAM sampling phase | `sampling` | -| Posterior predictive plots/checks | `processing` | -| Posterior pair plots | `processing` | -| Other long calculations | `processing` | - -The label must be updateable while work is running. DREAM should switch -from `burn-in` to `sampling` when sampler progress reports the phase -change. - -### Visual Style - -Use compact Unicode spinner frames: - -```text -⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ -``` - -Examples: - -```text -⠋ fitting -⠴ burn-in -⠇ sampling -⠙ processing -``` - -The indicator should be a single line. In `full` mode it appears below -the progress table. In `short` mode it appears as the only live progress -element. - -## Current Code Paths - -### Fit Progress - -The single-fit and sampler progress lifecycle is owned by -`src/easydiffraction/analysis/fit_helpers/tracking.py`. - -Important methods: - -- `FitProgressTracker.start_tracking(...)` -- `FitProgressTracker.add_tracking_info(...)` -- `FitProgressTracker.track_sampler_progress(...)` -- `FitProgressTracker.finish_tracking(...)` -- `_make_display_handle()` -- `_TerminalLiveHandle` - -The existing table update path uses `render_table(...)`, which delegates -to `TableRenderer` and then to either: - -- `PandasTableBackend` in Jupyter -- `RichTableBackend` in terminal - -### Sequential Fit - -Sequential fitting currently has separate progress output in -`src/easydiffraction/analysis/sequential.py`. - -Important functions: - -- `fit_sequential(...)` -- `_run_fit_loop(...)` -- `_report_chunk_progress(...)` - -Sequential fit should reuse the same activity indicator abstraction -rather than adding a separate spinner implementation. - -### Posterior And Other Long Calculations - -Posterior display work is routed through: - -- `src/easydiffraction/project/display.py` -- `src/easydiffraction/display/plotting.py` - -Important entry points include: - -- `PosteriorDisplay.pairs(...)` -- `PosteriorDisplay.predictive(...)` -- `Plotter.plot_posterior_pairs(...)` -- `Plotter.plot_posterior_predictive(...)` - -These should use the generic `processing` label if an activity indicator -is added to those paths. - -## Architecture - -### Add A Shared Activity Indicator - -Add a small shared display helper, preferably: - -```text -src/easydiffraction/display/progress.py -``` - -Recommended public/internal shape: - -```python -class ActivityIndicator: - def __init__(self, label: str = "processing", *, verbosity: VerbosityEnum) -> None: ... - def start(self) -> None: ... - def update(self, *, label: str | None = None, content: object | None = None) -> None: ... - def stop(self, *, final_label: str | None = None) -> None: ... -``` - -The exact class name can change during implementation, but it should -provide these capabilities: - -- no output in `silent` -- live output in `short` and `full` -- label updates while running -- optional table/content rendering above the indicator in `full` -- terminal and Jupyter implementations behind one API -- safe cleanup on exceptions - -### Terminal Rendering - -Use Rich for terminal rendering. - -Preferred implementation: - -- keep using `rich.live.Live` -- render a `rich.console.Group` -- group content should be: - - the progress table renderable, when present - - the activity indicator line - -The indicator line can be either: - -- Rich's built-in `Spinner`, if it works cleanly inside the existing - `Live` setup, or -- a local unicode-frame renderable driven by the same frame list. - -Do not create a second independent `Live` instance for the same output -area. The table and spinner should be refreshed together through one -live handle. - -### Jupyter Rendering - -Use an IPython `DisplayHandle` and HTML. - -The Jupyter spinner should be browser-driven CSS animation, not a -Python-loop animation. This matters because Python may not regain -control during expensive calculations, but CSS keeps animating once the -HTML has been displayed. - -Recommended HTML structure: - -```html -
- - fitting -
-``` - -The table HTML and spinner HTML can be updated together in the same -display handle, or the spinner can have its own display handle below the -table. Prefer a single display handle if it keeps table-and-spinner -replacement simpler and avoids duplicated output cells. - -### Table Rendering Refactor - -The existing table backends mostly print/update directly. For a clean -combined table-plus-spinner render, add a way to build table renderables -without immediately displaying them. - -Possible approach: - -1. Keep `render_table(...)` working for existing callers. -2. Add a backend method that returns a renderable representation: - - Rich: return `rich.table.Table` - - Pandas: return HTML from `Styler.to_html()` -3. Let the activity indicator compose that renderable with the spinner. - -This avoids hard-coding table internals in the tracker and keeps normal -table rendering backwards compatible. - -## Fit Tracker Integration - -### State - -Add fields to `FitProgressTracker`: - -- `_activity_indicator` -- `_activity_label` - -The label is derived from tracking mode: - -- fit mode -> `fitting` -- sampler mode -> initial label from sampler phase if known, otherwise - `sampling` - -### `start_tracking(...)` - -Update behavior: - -1. Set tracking mode. -2. Return immediately only for `silent`. -3. Print the existing start/header messages only where appropriate: - - keep current full messages - - keep short mode concise -4. Create the activity indicator for `short` and `full`. -5. In `full`, render the initial empty progress table plus the - indicator. -6. In `short`, render only the indicator. - -### `add_tracking_info(...)` - -Update behavior: - -1. Always store row state for `full` mode. -2. In `full`, refresh the table plus the indicator. -3. In `short`, do not render the table; keep the indicator running. - -### `track_sampler_progress(...)` - -Update the activity label from `SamplerProgressUpdate.phase`: - -- phase `burn-in` -> label `burn-in` -- phase `sampling` -> label `sampling` -- any other phase -> normalized phase string if user-facing, otherwise - `processing` - -The existing sampler table's `phase` column remains unchanged. - -### `finish_tracking(...)` - -Update behavior: - -1. Finalize the last table row as today. -2. Stop the activity indicator for `short` and `full`. -3. In `full`, print the current completion summary. -4. In `short`, keep or add only a concise completion line if the current - short behavior expects one. -5. In `silent`, print nothing. - -Use `try/finally` in minimizer execution paths so the indicator is -stopped when a solver raises. - -## Sequential Fit Integration - -Sequential fit should use the same `ActivityIndicator`. - -Recommended behavior: - -- `silent`: no output. -- `short`: show one activity indicator labelled `fitting`; keep concise - chunk summaries when chunks finish. -- `full`: show one activity indicator labelled `fitting`; keep detailed - chunk summaries. - -Implementation points: - -1. Create the indicator in `fit_sequential(...)` after preflight checks - and before `_run_fit_loop(...)`. -2. Pass it into `_run_fit_loop(...)`, or wrap `_run_fit_loop(...)` in a - context manager. -3. Update the indicator content after each chunk if the implementation - supports content text, for example: - - ```text - ⠼ fitting chunk 3/20 - ``` - -4. Stop the indicator in a `finally` block before printing final - completion output. - -Do not duplicate spinner frame logic in `sequential.py`. - -## Posterior And Generic Processing Integration - -The first implementation can focus on fitting and sequential fitting. -After that, add a small context helper for generic long calculations: - -```python -with activity_indicator("processing", verbosity=VerbosityEnum(project.verbosity)): - ... -``` - -Use this for: - -- posterior predictive summary generation -- posterior pair plot construction when sample thinning, density grids, - or figure construction take noticeable time -- any future calculation where total progress is unknown - -The generic helper should default to `processing`. - -## Testing Plan - -### Unit Tests - -Add tests for the shared progress helper: - -- `silent` does not create display handles or print output. -- `short` starts the indicator. -- `full` can compose content plus indicator. -- label updates replace the visible label. -- `stop()` suppresses cleanup errors. - -Extend tracker tests: - -- `FitProgressTracker.start_tracking(...)` starts the indicator in - `short`. -- `silent` still shows nothing. -- full fit mode uses label `fitting`. -- sampler updates switch labels from `burn-in` to `sampling`. -- finalization stops the indicator on success. -- finalization stops the indicator when solver preparation or solver - execution raises. - -Extend sequential tests: - -- `fit_sequential(..., verbosity="short")` starts and stops the shared - indicator. -- `fit_sequential(..., verbosity="silent")` does not start it. -- chunk progress does not create a separate spinner. - -### Rendering Tests - -Terminal: - -- fake or monkeypatch `Live` and assert one live handle receives grouped - table-plus-indicator content. - -Jupyter: - -- fake `DisplayHandle` and assert generated HTML contains the activity - container and the selected label. -- assert CSS animation is included once, not duplicated on every table - update if avoidable. - -### Regression Tests - -Keep existing tests passing: - -- `tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py` -- `tests/integration/fitting/test_bayesian_tracker_and_base.py` -- sequential tests under `tests/integration/fitting/test_sequential.py` - -## Implementation Sequence - -1. Add `display/progress.py` with a minimal `ActivityIndicator`. -2. Add tests for verbosity behavior and label updates. -3. Refactor table rendering just enough to allow Rich renderable and - Jupyter HTML composition. -4. Wire `FitProgressTracker` to the activity indicator. -5. Update tracker tests for `short`, `full`, `silent`, and sampler phase - labels. -6. Wire sequential fitting to the shared activity indicator. -7. Update sequential tests. -8. Add generic `processing` context helper. -9. Add the helper to posterior predictive and posterior pairs if - profiling or user feedback shows those operations need visible - activity feedback. -10. Run focused unit tests, then the relevant integration tests. - -## Open Design Checks - -- Whether `short` mode should print the existing start line before the - spinner or show only the spinner until completion. -- Whether terminal output should use Rich's built-in `Spinner` or the - explicit EasyDiffraction frame list. Prefer the explicit list if - consistency with Jupyter matters more than Rich defaults. -- Whether the table and spinner should share one Jupyter display handle. - Prefer one handle unless it complicates the existing pandas backend. -- Whether generic display/plot operations need a public verbosity - argument, or should only read `project.verbosity`. diff --git a/docs/dev/package-structure-full.md b/docs/dev/package-structure-full.md index e71f099de..dccf421a4 100644 --- a/docs/dev/package-structure-full.md +++ b/docs/dev/package-structure-full.md @@ -59,7 +59,6 @@ │ │ │ └── 🏷️ class FitResults │ │ └── 📄 tracking.py │ │ ├── 🏷️ class SamplerProgressUpdate -│ │ ├── 🏷️ class _TerminalLiveHandle │ │ └── 🏷️ class FitProgressTracker │ ├── 📁 minimizers │ │ ├── 📄 __init__.py @@ -385,6 +384,10 @@ │ │ ├── 🏷️ class _PosteriorPairsLegendState │ │ ├── 🏷️ class Plotter │ │ └── 🏷️ class PlotterFactory +│ ├── 📄 progress.py +│ │ ├── 🏷️ class _TerminalLiveHandle +│ │ ├── 🏷️ class ActivityIndicator +│ │ └── 🏷️ class _ActivityIndicatorContext │ ├── 📄 tables.py │ │ ├── 🏷️ class TableEngineEnum │ │ ├── 🏷️ class TableRenderer diff --git a/docs/dev/package-structure-short.md b/docs/dev/package-structure-short.md index 573142dae..b4e46ec73 100644 --- a/docs/dev/package-structure-short.md +++ b/docs/dev/package-structure-short.md @@ -186,6 +186,7 @@ │ ├── 📄 __init__.py │ ├── 📄 base.py │ ├── 📄 plotting.py +│ ├── 📄 progress.py │ ├── 📄 tables.py │ └── 📄 utils.py ├── 📁 io diff --git a/docs/docs/tutorials/ed-21.ipynb b/docs/docs/tutorials/ed-21.ipynb index 33f77196f..f0d5123d9 100644 --- a/docs/docs/tutorials/ed-21.ipynb +++ b/docs/docs/tutorials/ed-21.ipynb @@ -734,11 +734,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.posterior.predictive(\n", - " expt_name='hrpt',\n", - " x_min=92,\n", - " x_max=93,\n", - ")" + "project.display.posterior.predictive(expt_name='hrpt', x_min=92, x_max=93)" ] } ], diff --git a/docs/docs/tutorials/ed-21.py b/docs/docs/tutorials/ed-21.py index e890f55bc..7c6f9bdb5 100644 --- a/docs/docs/tutorials/ed-21.py +++ b/docs/docs/tutorials/ed-21.py @@ -347,8 +347,4 @@ # after the Bayesian run. # %% -project.display.posterior.predictive( - expt_name='hrpt', - x_min=92, - x_max=93, -) +project.display.posterior.predictive(expt_name='hrpt', x_min=92, x_max=93) diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index 484c3e03d..a2a4f7dee 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -14,12 +14,12 @@ 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.fitting import Fitter 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.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 @@ -614,37 +614,43 @@ def _fit_single( 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, - random_seed=random_seed, - ) + 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( @@ -680,7 +686,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: """ diff --git a/src/easydiffraction/analysis/fit_helpers/tracking.py b/src/easydiffraction/analysis/fit_helpers/tracking.py index b5cfb11a2..0915ae929 100644 --- a/src/easydiffraction/analysis/fit_helpers/tracking.py +++ b/src/easydiffraction/analysis/fit_helpers/tracking.py @@ -1,35 +1,29 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + +import sys import time -from contextlib import suppress from dataclasses import dataclass - -import numpy as np - -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 +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_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 +FIT_PROGRESS_UPDATE_SECONDS = 5.0 SAMPLER_PROGRESS_UPDATE_SECONDS = 5.0 TRACKING_MODE_FIT = 'fit' TRACKING_MODE_SAMPLER = 'sampling' @@ -38,6 +32,13 @@ SAMPLER_HEADERS = ['iteration', 'progress', 'time (s)', 'log posterior', 'phase'] SAMPLER_ALIGNMENTS = ['center', 'center', 'center', 'center', 'center'] +_TerminalLiveHandle = _SharedTerminalLiveHandle + + +def _make_display_handle() -> object | None: + """Return a backward-compatible generic live display handle.""" + return make_display_handle() + @dataclass(frozen=True, slots=True) class SamplerProgressUpdate: @@ -55,56 +56,6 @@ class SamplerProgressUpdate: force_report: bool = False -class _TerminalLiveHandle: - """ - Adapter that exposes update()/close() for terminal live updates. - - Wraps a rich.live.Live instance but keeps the tracker decoupled from - the underlying UI mechanism. - """ - - def __init__(self, live: 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: - """ - 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. - """ - 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 - - class FitProgressTracker: """ Track and report reduced chi-square during optimization. @@ -135,11 +86,13 @@ def __init__(self) -> None: self._last_sampler_elapsed_time: float | None = None 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 @@ -157,6 +110,8 @@ def reset(self) -> None: self._last_sampler_progress_percent = None self._last_sampler_log_posterior = None self._last_sampler_elapsed_time = None + self._df_rows = [] + self._activity_label = ACTIVITY_LABEL_FITTING def track( self, @@ -193,47 +148,51 @@ def track( return residuals row: list[str] = [] + elapsed_time = self._current_elapsed_time() - # 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(), + 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(), + 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 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 @@ -259,6 +218,7 @@ def track_sampler_progress(self, update: SamplerProgressUpdate) -> None: 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, @@ -326,29 +286,20 @@ def start_tracking(self, minimizer_name: str, *, mode: str = TRACKING_MODE_FIT) 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}'...") - 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() + 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:') - # Initial empty table; subsequent updates will reuse the handle - render_table( - columns_headers=self._headers(), - columns_alignment=self._alignments(), - columns_data=self._df_rows, - display_handle=self._display_handle, - ) + self._start_activity_indicator() def add_tracking_info(self, row: list[str]) -> None: """ @@ -364,16 +315,8 @@ def add_tracking_info(self, row: list[str]) -> None: 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=self._headers(), - columns_alignment=self._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.""" @@ -382,11 +325,19 @@ def finish_tracking(self) -> None: else: self._finalize_fit_tracking_row() - if self._verbosity is not VerbosityEnum.FULL: + if self._verbosity is VerbosityEnum.SILENT: return - self._close_display_handle() - self._print_completion_summary() + 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, @@ -511,7 +462,7 @@ def _final_sampler_tracking_row(self) -> list[str] | None: f'{final_progress:.1f}%', self._format_elapsed_time(elapsed_time), log_posterior, - self._last_sampler_phase or 'sampling', + self._last_sampler_phase or TRACKING_MODE_SAMPLER, ] def _finalize_fit_tracking_row(self) -> None: @@ -566,16 +517,14 @@ def _sampler_iteration_label(self, iteration: int) -> str: clamped_iteration = min(iteration, self._sampler_total_iterations) return f'{clamped_iteration}/{self._sampler_total_iterations}' - def _close_display_handle(self) -> None: - if self._display_handle is not None and hasattr(self._display_handle, 'close'): - with suppress(Exception): - self._display_handle.close() - def _print_completion_summary(self) -> None: if self._tracking_mode == TRACKING_MODE_SAMPLER: console.print('✅ Bayesian sampling complete.') return + if self._best_chi2 is None or self._best_iteration is None: + return + console.print( f'🏆 Best goodness-of-fit (reduced χ²) is {self._best_chi2:.2f} ' f'at iteration {self._best_iteration}' @@ -611,6 +560,11 @@ def _format_elapsed_time(self, elapsed_time: float | None = None) -> str: 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], @@ -636,12 +590,67 @@ def _replace_last_tracking_row(self, row: list[str]) -> None: return self._df_rows[-1] = row - if self._verbosity is not VerbosityEnum.FULL: + 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_SAMPLING + return ACTIVITY_LABEL_FITTING + + @staticmethod + def _activity_label_for_sampler_phase(phase: str) -> str: + normalized_phase = phase.strip().lower() + if normalized_phase == 'burn-in': + return ACTIVITY_LABEL_BURN_IN + 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 - render_table( + 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, - display_handle=self._display_handle, ) diff --git a/src/easydiffraction/analysis/sequential.py b/src/easydiffraction/analysis/sequential.py index 25feb184e..a52d38b1a 100644 --- a/src/easydiffraction/analysis/sequential.py +++ b/src/easydiffraction/analysis/sequential.py @@ -17,6 +17,8 @@ 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 @@ -529,9 +531,11 @@ def _report_chunk_progress( if verbosity is VerbosityEnum.SHORT: status = '✅' if successful else '❌' - print(f'{status} Chunk {chunk_idx}/{total_chunks}: {num_files} files, avg χ² = {chi2_str}') + console.print( + f'{status} Chunk {chunk_idx}/{total_chunks}: {num_files} files, avg χ² = {chi2_str}' + ) elif verbosity is VerbosityEnum.FULL: - print( + console.print( f'Chunk {chunk_idx}/{total_chunks}: ' f'{num_files} files, {len(successful)} succeeded, ' f'avg reduced χ² = {chi2_str}' @@ -540,7 +544,7 @@ def _report_chunk_progress( 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}') + console.print(f' {status} {Path(r["file_path"]).name}: χ² = {rchi2_str}') def _apply_diffrn_metadata( @@ -653,7 +657,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: @@ -755,6 +759,7 @@ def _run_fit_loop( csv_info: tuple[Path, list[str]], extract_diffrn: Callable | None, verb: VerbosityEnum, + indicator: ActivityIndicator | None, ) -> None: """ Execute the chunk-based fitting loop. @@ -773,6 +778,8 @@ def _run_fit_loop( User callback for diffrn metadata. verb : VerbosityEnum Output verbosity. + indicator : ActivityIndicator | None + Shared sequential-fit activity indicator. """ csv_path, header = csv_info total_chunks = len(chunks) @@ -789,6 +796,8 @@ def _run_fit_loop( _append_to_csv(csv_path, header, results) _report_chunk_progress(chunk_idx, total_chunks, results, verb) + if indicator is not None: + indicator.update() # Propagate last successful params last_ok = _find_last_successful(results) @@ -841,16 +850,15 @@ def fit_sequential( if mp.parent_process() is not None: return - project = analysis.project - verb = VerbosityEnum(project.verbosity) + verb = VerbosityEnum(analysis.project.verbosity) - _check_seq_preconditions(project) + _check_seq_preconditions(analysis.project) data_paths = extract_data_paths_from_dir(data_dir, file_pattern=file_pattern) - template = _build_template(project) + template = _build_template(analysis.project) csv_path, header, already_fitted, template = _setup_csv_and_recovery( - project, + analysis.project, template, verb, ) @@ -860,7 +868,7 @@ def fit_sequential( remaining.reverse() if not remaining: if verb is not VerbosityEnum.SILENT: - print('✅ All files already fitted. Nothing to do.') + console.print('✅ All files already fitted. Nothing to do.') return max_workers, chunk_size = _resolve_workers(max_workers, chunk_size) @@ -874,15 +882,30 @@ def fit_sequential( ) console.print('📈 Goodness-of-fit (reduced χ²):') + indicator = None + if verb is not VerbosityEnum.SILENT: + indicator = ActivityIndicator(ACTIVITY_LABEL_FITTING, verbosity=verb) + indicator.start() + pool_cm, main_mod, main_file_bak, main_spec_bak = _create_pool_context(max_workers) try: - _run_fit_loop(pool_cm, chunks, template, (csv_path, header), extract_diffrn, verb) + _run_fit_loop( + pool_cm, + chunks, + template, + (csv_path, header), + extract_diffrn, + verb, + indicator, + ) finally: + if indicator is not None: + indicator.stop() _restore_main_state(main_mod, main_file_bak, main_spec_bak) if verb is not VerbosityEnum.SILENT: - print( + console.print( f'✅ Sequential fitting complete: ' f'{len(already_fitted) + len(remaining)} files processed.' ) - print(f'📄 Results saved to: {csv_path}') + console.print(f'📄 Results saved to: {csv_path}') diff --git a/src/easydiffraction/display/plotters/plotly.py b/src/easydiffraction/display/plotters/plotly.py index d41c1f9eb..fbbda288f 100644 --- a/src/easydiffraction/display/plotters/plotly.py +++ b/src/easydiffraction/display/plotters/plotly.py @@ -1755,7 +1755,7 @@ def _get_predictive_band_traces( line={'color': PREDICTIVE_BAND_EDGE_COLOR, 'width': 1}, fill='tonexty', fillcolor=PREDICTIVE_BAND_COLOR, - name='95% interval', + name='95% credible interval', hoverinfo='skip', legendgroup='predictive_band', legendrank=35, diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index a258579e8..2ca6dff0f 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -97,12 +97,11 @@ class PosteriorPairPlotStyleEnum(StrEnum): 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_INTERVAL_68_FILL_COLOR = 'rgba(214, 39, 40, 0.26)' POSTERIOR_MEDIAN_LINE_COLOR = 'rgb(80, 80, 80)' POSTERIOR_POINT_ESTIMATE_LINE_COLOR = 'rgb(214, 39, 40)' POSTERIOR_POINT_ESTIMATE_TRACE_NAME = 'Max posterior' POSTERIOR_POINT_ESTIMATE_LINE_DASH = 'dot' -POSTERIOR_PREDICTIVE_INTERVAL_TRACE_NAME = '95% interval' +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 = [ @@ -1175,7 +1174,7 @@ def _plot_single_crystal_posterior_predictive( if style != 'band': log.warning( 'Single-crystal posterior predictive plots currently support ' - 'style="band" only; rendering the 95% interval.' + 'style="band" only; rendering the 95% credible interval.' ) summary = self._get_or_build_posterior_predictive_summary( @@ -2597,15 +2596,6 @@ def _add_posterior_distribution_interval_traces( color=POSTERIOR_INTERVAL_95_FILL_COLOR, ) ) - fig.add_trace( - self._posterior_interval_band_trace( - x0=summary.interval_68[0], - x1=summary.interval_68[1], - y_axis_range=y_axis_range, - trace_name='68% credible interval', - color=POSTERIOR_INTERVAL_68_FILL_COLOR, - ) - ) @staticmethod def _add_posterior_distribution_histogram( @@ -3614,7 +3604,7 @@ def _plot_single_crystal_posterior_predictive_summary( trace.customdata = np.column_stack((lower_95, upper_95, y_meas_su)) trace.hovertemplate = ( 'Predicted I²: %{x:,.2f}
' - '95% interval: [%{customdata[0]:,.2f}, %{customdata[1]:,.2f}]
' + '95% credible interval: [%{customdata[0]:,.2f}, %{customdata[1]:,.2f}]
' 'Measured I²: %{y:,.2f}
' 'su(I²meas): %{customdata[2]:,.2f}' ) diff --git a/src/easydiffraction/display/progress.py b/src/easydiffraction/display/progress.py new file mode 100644 index 000000000..1cde4d1c2 --- /dev/null +++ b/src/easydiffraction/display/progress.py @@ -0,0 +1,439 @@ +# 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_PROCESSING = 'Processing...' +ACTIVITY_LABEL_SAMPLING = 'Sampling...' +ACTIVITY_ACCENT_COLOR = '#d97706' +ACTIVITY_TERMINAL_STYLE = ACTIVITY_ACCENT_COLOR + +SPINNER_FRAMES: tuple[str, ...] = ( + '⠋', + '⠙', + '⠹', + '⠸', + '⠼', + '⠴', + '⠦', + '⠧', + '⠇', + '⠏', +) +_SPINNER_FRAME_SECONDS = 0.1 +_JUPYTER_SPINNER_SECONDS = 1.0 + + +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) -> None: + self._renderable: object = Text('') + self._live = Live( + console=console, + auto_refresh=True, + refresh_per_second=1 / _SPINNER_FRAME_SECONDS, + get_renderable=self._get_renderable, + ) + 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() -> object | None: + """ + Create a generic in-place display handle for the active environment. + + 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()) + + +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. + """ + + def __init__( + self, + label: str = ACTIVITY_LABEL_PROCESSING, + *, + verbosity: VerbosityEnum, + display_handle: object | None = None, + ) -> None: + self._label = label + self._verbosity = verbosity + self._content: object | None = None + self._provided_display_handle = display_handle + 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=True, + refresh_per_second=1 / _SPINNER_FRAME_SECONDS, + get_renderable=self._terminal_renderable, + ) + 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: + if self._running: + frame = self._current_frame() + return Text(f'{frame} {self._label}', style=ACTIVITY_TERMINAL_STYLE) + if self._keep_stopped_label: + return Text(self._label, style=ACTIVITY_TERMINAL_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: + 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/project/display.py b/src/easydiffraction/project/display.py
index 2071463d5..6bab7df3d 100644
--- a/src/easydiffraction/project/display.py
+++ b/src/easydiffraction/project/display.py
@@ -13,6 +13,9 @@
 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.utils import render_object_help
 from easydiffraction.utils.utils import render_table
 
@@ -128,12 +131,16 @@ def pairs(
         max_parameters: int = 6,
     ) -> None:
         """Plot posterior pair relationships for sampled parameters."""
-        self._project.rendering.plotter.plot_posterior_pairs(
-            parameters=parameters,
-            style=style,
-            threshold=threshold,
-            max_parameters=max_parameters,
-        )
+        with activity_indicator(
+            ACTIVITY_LABEL_PROCESSING,
+            verbosity=VerbosityEnum(self._project.verbosity),
+        ):
+            self._project.rendering.plotter.plot_posterior_pairs(
+                parameters=parameters,
+                style=style,
+                threshold=threshold,
+                max_parameters=max_parameters,
+            )
 
     def distribution(self, param: object) -> None:
         """Plot one sampled parameter's posterior distribution."""
@@ -150,14 +157,18 @@ def predictive(
         x: object | None = None,
     ) -> None:
         """Plot posterior predictive summaries for one experiment."""
-        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,
-        )
+        with activity_indicator(
+            ACTIVITY_LABEL_PROCESSING,
+            verbosity=VerbosityEnum(self._project.verbosity),
+        ):
+            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."""
@@ -213,19 +224,23 @@ def pattern(
                 msg = self._status_by_name(statuses, 'auto').reason
                 raise ValueError(msg)
             if 'uncertainty' in auto_include:
-                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,
-                    ),
-                )
+                with activity_indicator(
+                    ACTIVITY_LABEL_PROCESSING,
+                    verbosity=VerbosityEnum(self._project.verbosity),
+                ):
+                    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,
@@ -243,19 +258,23 @@ def pattern(
             raise ValueError(msg)
 
         if 'uncertainty' in normalized_include:
-            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,
-                ),
-            )
+            with activity_indicator(
+                ACTIVITY_LABEL_PROCESSING,
+                verbosity=VerbosityEnum(self._project.verbosity),
+            ):
+                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(
diff --git a/src/easydiffraction/utils/utils.py b/src/easydiffraction/utils/utils.py
index a81f1cb0b..2fe66e739 100644
--- a/src/easydiffraction/utils/utils.py
+++ b/src/easydiffraction/utils/utils.py
@@ -563,6 +563,38 @@ 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:
diff --git a/tests/integration/fitting/test_bayesian_tracker_and_base.py b/tests/integration/fitting/test_bayesian_tracker_and_base.py
index f63cb111f..e6988a0f3 100644
--- a/tests/integration/fitting/test_bayesian_tracker_and_base.py
+++ b/tests/integration/fitting/test_bayesian_tracker_and_base.py
@@ -27,7 +27,25 @@ 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
 
-    monkeypatch.setattr(tracking_mod, 'in_jupyter', lambda: False)
+    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')
@@ -46,6 +64,7 @@ def test_tracker_terminal_flow_prints_and_updates_best(monkeypatch, capsys):
     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):
@@ -53,7 +72,25 @@ def test_tracker_sampler_progress_renders_and_completes(monkeypatch, capsys):
     from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
     from easydiffraction.analysis.fit_helpers.tracking import SamplerProgressUpdate
 
-    monkeypatch.setattr(tracking_mod, 'in_jupyter', lambda: False)
+    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')
@@ -90,6 +127,8 @@ def test_tracker_sampler_progress_renders_and_completes(monkeypatch, capsys):
     assert 'Bayesian sampling complete.' in out
     assert tracker.best_chi2 == pytest.approx(1.0)
     assert tracker.best_iteration == 10
+    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):
@@ -97,7 +136,24 @@ def test_tracker_helper_error_paths_and_short_mode(monkeypatch):
     from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
     from easydiffraction.utils.enums import VerbosityEnum
 
-    monkeypatch.setattr(tracking_mod, 'in_jupyter', lambda: False)
+    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
@@ -111,14 +167,17 @@ def test_tracker_helper_error_paths_and_short_mode(monkeypatch):
         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_SAMPLING, VerbosityEnum.SHORT),
+        ('start', None),
+        ('update', tracking_mod.ACTIVITY_LABEL_SAMPLING),
+        ('stop', None),
+    ]
 
 
-def test_tracker_final_sampler_row_replaces_last_row(monkeypatch):
-    import easydiffraction.analysis.fit_helpers.tracking as tracking_mod
+def test_tracker_final_sampler_row_replaces_last_row():
     from easydiffraction.analysis.fit_helpers.tracking import FitProgressTracker
 
-    monkeypatch.setattr(tracking_mod, 'render_table', lambda **kwargs: None)
-
     tracker = FitProgressTracker()
     tracker._tracking_mode = 'sampling'
     tracker._sampler_total_iterations = 10
@@ -137,44 +196,32 @@ def test_tracker_final_sampler_row_replaces_last_row(monkeypatch):
 def test_make_display_handle_uses_terminal_live_when_available(monkeypatch):
     import easydiffraction.analysis.fit_helpers.tracking as tracking_mod
 
-    class FakeLive:
-        def __init__(self, *, console, auto_refresh):
-            self.console = console
-            self.auto_refresh = auto_refresh
-            self.started = False
-            self.stopped = False
-
-        def start(self):
-            self.started = True
-
-        def stop(self):
-            self.stopped = True
-
-    monkeypatch.setattr(tracking_mod, 'in_jupyter', lambda: False)
-    monkeypatch.setattr(tracking_mod, 'Live', FakeLive)
-    monkeypatch.setattr(tracking_mod.ConsoleManager, 'get', lambda: 'console')
-
-    handle = tracking_mod._make_display_handle()
+    sentinel = object()
 
-    assert isinstance(handle, tracking_mod._TerminalLiveHandle)
-    assert handle._live.console == 'console'
-    assert handle._live.auto_refresh is True
-    assert handle._live.started is True
+    monkeypatch.setattr(tracking_mod, 'make_display_handle', lambda: sentinel)
 
-    handle.close()
-
-    assert handle._live.stopped is True
+    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]] = []
-    monkeypatch.setattr(tracking_mod, 'render_table', lambda **kwargs: render_calls.append(kwargs))
+    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
@@ -197,13 +244,21 @@ def test_tracker_misc_helper_paths(monkeypatch):
     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_close_suppression():
+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
 
@@ -218,15 +273,19 @@ def test_tracker_final_rows_cover_fallbacks_and_close_suppression():
     tracker._tracking_mode = tracking_mod.TRACKING_MODE_FIT
     tracker._fitting_time = 1.5
     assert tracker._final_fit_tracking_row() == ['8', '1.50', '', '']
-
-    class BadHandle:
-        @staticmethod
-        def close() -> None:
-            message = 'boom'
-            raise RuntimeError(message)
-
-    tracker._display_handle = BadHandle()
-    tracker._close_display_handle()
+    tracker._tracking_mode = tracking_mod.TRACKING_MODE_SAMPLER
+    assert tracker._default_activity_label() == tracking_mod.ACTIVITY_LABEL_SAMPLING
+    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():
diff --git a/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py b/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py
index 561eb54c3..877a46b1a 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,37 @@ 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
diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_base.py b/tests/unit/easydiffraction/analysis/minimizers/test_base.py
index 0ad0ca127..3b18f5354 100644
--- a/tests/unit/easydiffraction/analysis/minimizers/test_base.py
+++ b/tests/unit/easydiffraction/analysis/minimizers/test_base.py
@@ -158,3 +158,46 @@ def _check_success(self, raw_result):
 
     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]))
diff --git a/tests/unit/easydiffraction/analysis/test_analysis.py b/tests/unit/easydiffraction/analysis/test_analysis.py
index c5b2ef248..637facc02 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
@@ -132,3 +134,96 @@ 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
diff --git a/tests/unit/easydiffraction/analysis/test_sequential.py b/tests/unit/easydiffraction/analysis/test_sequential.py
index 3179a0b13..5e01b586d 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
 
@@ -14,6 +16,8 @@
 from easydiffraction.analysis.sequential import _build_csv_header
 from easydiffraction.analysis.sequential import _read_csv_for_recovery
 from easydiffraction.analysis.sequential import _write_csv_header
+from easydiffraction.display.progress import ACTIVITY_LABEL_FITTING
+from easydiffraction.utils.enums import VerbosityEnum
 
 
 # ------------------------------------------------------------------
@@ -300,3 +304,130 @@ def test_fields_accessible(self):
         assert template.diffrn_field_names == ['temp']
         assert template.minimizer_tag == 'lmfit'
         assert template.calculator_tag == 'cryspy'
+
+
+def test_fit_sequential_short_starts_and_stops_shared_indicator(monkeypatch, tmp_path):
+    import easydiffraction.analysis.sequential as sequential_mod
+
+    events: list[tuple[object, ...]] = []
+    template = _minimal_template()
+
+    class FakeIndicator:
+        def __init__(self, label, *, verbosity):
+            events.append(('init', label, verbosity))
+
+        def start(self):
+            events.append(('start',))
+
+        def update(self):
+            events.append(('update',))
+
+        def stop(self):
+            events.append(('stop',))
+
+    def fake_run_fit_loop(
+        pool_cm, chunks, template_arg, csv_info, extract_diffrn, verb, indicator
+    ):
+        del pool_cm, csv_info, extract_diffrn
+        assert chunks == [['scan_001.xye']]
+        assert template_arg == template
+        assert verb is VerbosityEnum.SHORT
+        assert indicator is not None
+        indicator.update()
+
+    monkeypatch.setattr(sequential_mod, 'ActivityIndicator', FakeIndicator)
+    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='short'),
+        fitter=SimpleNamespace(selection='lmfit'),
+    )
+
+    sequential_mod.fit_sequential(analysis, data_dir=str(tmp_path))
+
+    assert events == [
+        ('init', ACTIVITY_LABEL_FITTING, VerbosityEnum.SHORT),
+        ('start',),
+        ('update',),
+        ('stop',),
+    ]
+
+
+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, extract_diffrn, verb, indicator
+    ):
+        del pool_cm, csv_info, extract_diffrn
+        assert chunks == [['scan_001.xye']]
+        assert template_arg == template
+        assert verb is VerbosityEnum.SILENT
+        assert 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='silent'),
+        fitter=SimpleNamespace(selection='lmfit'),
+    )
+
+    sequential_mod.fit_sequential(analysis, data_dir=str(tmp_path))
diff --git a/tests/unit/easydiffraction/display/plotters/test_plotly.py b/tests/unit/easydiffraction/display/plotters/test_plotly.py
index 5c3c8dd50..5b5836229 100644
--- a/tests/unit/easydiffraction/display/plotters/test_plotly.py
+++ b/tests/unit/easydiffraction/display/plotters/test_plotly.py
@@ -828,7 +828,9 @@ def fake_show_figure(self, fig):
     )
 
     fig = captured['fig']
-    predictive_band_trace = next(trace for trace in fig.data if trace.name == '95% interval')
+    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 == 'Max posterior')
     residual_trace = next(trace for trace in fig.data if trace.name == 'Residual (Imeas - Icalc)')
 
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 '= {
         'Posterior histogram',
         'Marginal density',
-        '68% credible interval',
         '95% credible interval',
         'Median',
         'Max posterior',
     }
     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_68_trace = next(
-        trace for trace in figure.data if trace.name == '68% credible interval'
-    )
     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 == 'Max posterior')
     assert marginal_trace.line.color == POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_COLOR
@@ -640,7 +635,7 @@ def test_build_param_distribution_plot_returns_plotly_figure():
     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 interval_68_trace.fillcolor == POSTERIOR_INTERVAL_68_FILL_COLOR + 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 @@ -685,7 +680,7 @@ def test_plot_posterior_predictive_summary_uses_consistent_labels_and_styles(mon 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 == 'Max posterior') - assert upper_band_trace.name == '95% interval' + 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 diff --git a/tests/unit/easydiffraction/display/test_progress.py b/tests/unit/easydiffraction/display/test_progress.py new file mode 100644 index 000000000..f690159ed --- /dev/null +++ b/tests/unit/easydiffraction/display/test_progress.py @@ -0,0 +1,210 @@ +# 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, + ): + 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_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_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, + ): + 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_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, + ): + 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/project/test_display.py b/tests/unit/easydiffraction/project/test_display.py index 8e8f7695d..b67d7c022 100644 --- a/tests/unit/easydiffraction/project/test_display.py +++ b/tests/unit/easydiffraction/project/test_display.py @@ -4,15 +4,18 @@ 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]]]: @@ -47,6 +50,7 @@ def _recorder(*args, **kwargs): project = SimpleNamespace( analysis=SimpleNamespace(display=analysis_display), rendering=SimpleNamespace(plotter=plotter), + verbosity='full', ) return project, calls @@ -207,9 +211,19 @@ def test_fit_display_delegates_to_analysis_and_rendering(): ) -def test_posterior_display_delegates_to_rendering_plotter(): +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') @@ -239,6 +253,10 @@ def test_posterior_display_delegates_to_rendering_plotter(): 'x': 'd_spacing', }, ) + assert indicator_calls == [ + (ACTIVITY_LABEL_PROCESSING, VerbosityEnum.FULL), + (ACTIVITY_LABEL_PROCESSING, VerbosityEnum.FULL), + ] def test_pattern_auto_routes_measured_and_excluded_to_plot_meas(): @@ -266,7 +284,9 @@ def test_pattern_auto_routes_measured_and_excluded_to_plot_meas(): ] -def test_pattern_uncertainty_routes_to_posterior_predictive(): +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( @@ -276,6 +296,14 @@ def test_pattern_uncertainty_routes_to_posterior_predictive(): 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', @@ -303,6 +331,7 @@ def test_pattern_uncertainty_routes_to_posterior_predictive(): }, ) ] + assert indicator_calls == [(ACTIVITY_LABEL_PROCESSING, VerbosityEnum.FULL)] def test_pattern_measured_and_calculated_suppresses_background_and_bragg(): From 1a94b5930f795448c6f76b1fb0baf62e24c5d213 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 15 May 2026 20:40:56 +0200 Subject: [PATCH 06/10] Improve Bayesian posterior displays and DREAM CLI robustness (#174) * Use full samples for pair-plot marginal KDEs * Support posterior distribution defaults * Support ASCII posterior predictive plots * Add ASCII posterior density plots * Update console heading color to deep_sky_blue3 * Guard DREAM multiprocessing for direct script entry points * Remove unused colorama and tabulate dependencies * Normalize docstring formatting * Add DREAM multiprocessing CLI workflow issue * Resample ASCII plots to terminal width * Simplify posterior distribution display in tutorials * Fit ASCII plot range to terminal width * Use 'processing' label for sampler tracking * Clean up code formatting * Add posterior display documentation * Refactor plotting and ASCII chart constants * Simplify posterior display calls in tutorials --- docs/dev/Issues/issues_open.md | 196 ++++++------ docs/docs/quick-reference/index.md | 12 + docs/docs/tutorials/ed-21.ipynb | 3 +- docs/docs/tutorials/ed-21.py | 3 +- docs/docs/tutorials/ed-22.ipynb | 3 +- docs/docs/tutorials/ed-22.py | 3 +- pixi.lock | 2 - pyproject.toml | 2 - .../analysis/fit_helpers/tracking.py | 3 +- .../analysis/minimizers/bumps_dream.py | 61 +++- src/easydiffraction/display/plotters/ascii.py | 52 +++- src/easydiffraction/display/plotters/base.py | 8 + .../display/plotters/plotly.py | 1 + src/easydiffraction/display/plotting.py | 217 +++++++++---- src/easydiffraction/project/display.py | 20 +- src/easydiffraction/utils/logging.py | 4 +- .../fitting/test_bayesian_tracker_and_base.py | 7 +- .../fitting/test_bumps_dream_support.py | 157 +++++++++- .../analysis/fit_helpers/test_reporting.py | 2 +- .../display/plotters/test_ascii.py | 94 ++++++ .../easydiffraction/display/test_plotting.py | 288 ++++++++++++++++++ .../display/test_plotting_coverage.py | 8 +- .../easydiffraction/project/test_display.py | 44 +++ 23 files changed, 1019 insertions(+), 171 deletions(-) diff --git a/docs/dev/Issues/issues_open.md b/docs/dev/Issues/issues_open.md index bb41887f7..8805fc8fb 100644 --- a/docs/dev/Issues/issues_open.md +++ b/docs/dev/Issues/issues_open.md @@ -1424,6 +1424,31 @@ threaded because each step's output is the next step's input. --- +## 95. 🟡 Re-Enable DREAM Multiprocessing in CLI Workflows + +**Type:** Performance / CLI robustness + +On macOS and other spawn-based platforms, running Bayesian scripts via +direct CLI entry points such as `python script.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 in terminal +workflows. + +**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 CLI 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 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 @@ -1464,88 +1489,89 @@ operation is possible (e.g. in automated pipelines or tests). ## 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 | 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 | 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 | -| 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 | 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 | +| 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 CLI workflows | 🟡 Med | Performance / CLI robustness | +| 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 | diff --git a/docs/docs/quick-reference/index.md b/docs/docs/quick-reference/index.md index e0d3fd202..fb6835d3e 100644 --- a/docs/docs/quick-reference/index.md +++ b/docs/docs/quick-reference/index.md @@ -306,6 +306,18 @@ project.display.fit.correlations() project.display.pattern(expt_name='hrpt') ``` +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 diff --git a/docs/docs/tutorials/ed-21.ipynb b/docs/docs/tutorials/ed-21.ipynb index f0d5123d9..d5fe45749 100644 --- a/docs/docs/tutorials/ed-21.ipynb +++ b/docs/docs/tutorials/ed-21.ipynb @@ -692,8 +692,7 @@ "metadata": {}, "outputs": [], "source": [ - "for param in project.free_parameters:\n", - " project.display.posterior.distribution(param)" + "project.display.posterior.distribution()" ] }, { diff --git a/docs/docs/tutorials/ed-21.py b/docs/docs/tutorials/ed-21.py index 7c6f9bdb5..8129f6e3e 100644 --- a/docs/docs/tutorials/ed-21.py +++ b/docs/docs/tutorials/ed-21.py @@ -329,8 +329,7 @@ # multimodality. # %% -for param in project.free_parameters: - project.display.posterior.distribution(param) +project.display.posterior.distribution() # %% [markdown] # Finally, the posterior predictive plot propagates the sampled parameter diff --git a/docs/docs/tutorials/ed-22.ipynb b/docs/docs/tutorials/ed-22.ipynb index 965e4ed82..c6de427d8 100644 --- a/docs/docs/tutorials/ed-22.ipynb +++ b/docs/docs/tutorials/ed-22.ipynb @@ -570,8 +570,7 @@ "metadata": {}, "outputs": [], "source": [ - "for param in project.free_parameters:\n", - " project.display.posterior.distribution(param)" + "project.display.posterior.distribution()" ] }, { diff --git a/docs/docs/tutorials/ed-22.py b/docs/docs/tutorials/ed-22.py index bf0f2e19e..da04c4b5d 100644 --- a/docs/docs/tutorials/ed-22.py +++ b/docs/docs/tutorials/ed-22.py @@ -257,8 +257,7 @@ # multimodality. # %% -for param in project.free_parameters: - project.display.posterior.distribution(param) +project.display.posterior.distribution() # %% [markdown] # Finally, the posterior predictive plot propagates the sampled diff --git a/pixi.lock b/pixi.lock index f1532e2b2..4d6614f72 100644 --- a/pixi.lock +++ b/pixi.lock @@ -8145,7 +8145,6 @@ packages: - asciichartpy - asteval - bumps - - colorama - crysfml - cryspy - darkdetect @@ -8162,7 +8161,6 @@ packages: - rich - scipy - sympy - - tabulate - typeguard - typer - uncertainties diff --git a/pyproject.toml b/pyproject.toml index f81f23031..71bf2f71b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,8 +24,6 @@ classifiers = [ requires-python = '>=3.12' dependencies = [ '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 diff --git a/src/easydiffraction/analysis/fit_helpers/tracking.py b/src/easydiffraction/analysis/fit_helpers/tracking.py index 0915ae929..0f8e4c970 100644 --- a/src/easydiffraction/analysis/fit_helpers/tracking.py +++ b/src/easydiffraction/analysis/fit_helpers/tracking.py @@ -11,6 +11,7 @@ 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_PROCESSING from easydiffraction.display.progress import ACTIVITY_LABEL_SAMPLING from easydiffraction.display.progress import ActivityIndicator from easydiffraction.display.progress import _TerminalLiveHandle as _SharedTerminalLiveHandle @@ -595,7 +596,7 @@ def _replace_last_tracking_row(self, row: list[str]) -> None: def _default_activity_label(self) -> str: if self._tracking_mode == TRACKING_MODE_SAMPLER: - return ACTIVITY_LABEL_SAMPLING + return ACTIVITY_LABEL_PROCESSING return ACTIVITY_LABEL_FITTING @staticmethod diff --git a/src/easydiffraction/analysis/minimizers/bumps_dream.py b/src/easydiffraction/analysis/minimizers/bumps_dream.py index c9af1012b..256ff9a55 100644 --- a/src/easydiffraction/analysis/minimizers/bumps_dream.py +++ b/src/easydiffraction/analysis/minimizers/bumps_dream.py @@ -4,7 +4,9 @@ from __future__ import annotations +import multiprocessing import random +import sys from dataclasses import dataclass import numpy as np @@ -726,14 +728,65 @@ def _build_mapper(self, problem: FitProblem) -> object | None: if self.parallel == 1: return None - if not can_pickle(problem): + if self._requires_serial_mapper_for_spawn_main_module(): log.warning( - 'DREAM parallel evaluation requires a picklable ' - 'problem; falling back to serial execution.' + 'DREAM parallel evaluation requires an import-safe main ' + 'module on spawn-based multiprocessing; falling back to ' + 'serial execution.' ) return None - return MPMapper.start_mapper(problem, [], cpus=self.parallel) + 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): + log.warning( + '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 + log.warning( + '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: diff --git a/src/easydiffraction/display/plotters/ascii.py b/src/easydiffraction/display/plotters/ascii.py index c81af0b0e..467467001 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,55 @@ 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 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 resampled to the available chart width.""" + target_point_count = cls._chart_point_count() + resampled_series: list[list[float]] = [] + for series in y_series: + series_array = np.ravel(np.asarray(series, dtype=float)) + if ( + series_array.size <= target_point_count + or series_array.size < ASCII_CHART_MIN_POINT_COUNT + ): + resampled_series.append(series_array.tolist()) + 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 + @staticmethod def _get_legend_item(label: str) -> str: """ @@ -94,8 +138,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) diff --git a/src/easydiffraction/display/plotters/base.py b/src/easydiffraction/display/plotters/base.py index 4cf43d669..6910321be 100644 --- a/src/easydiffraction/display/plotters/base.py +++ b/src/easydiffraction/display/plotters/base.py @@ -191,6 +191,14 @@ class XAxisType(StrEnum): 'mode': 'lines', 'name': 'Total calculated (Icalc)', }, + 'posterior': { + 'mode': 'lines', + 'name': 'Max posterior', + }, + 'density': { + 'mode': 'lines', + 'name': 'Marginal density', + }, 'bkg': { 'mode': 'lines', 'name': 'Background (Ibkg)', diff --git a/src/easydiffraction/display/plotters/plotly.py b/src/easydiffraction/display/plotters/plotly.py index fbbda288f..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)', } diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index 2ca6dff0f..3380141d3 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -214,6 +214,7 @@ class _PosteriorPairsContext: 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 @@ -330,10 +331,11 @@ def _auto_x_range_for_ascii( 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) + 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 @@ -998,6 +1000,9 @@ def plot_posterior_pairs( ``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)) + plot = self._build_posterior_pairs_plot( parameters=parameters, style=style, @@ -1022,6 +1027,10 @@ def plot_param_distribution( 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 @@ -1048,8 +1057,10 @@ def plot_posterior_predictive( style : str, default='band' ``'band'`` shows the 95% credible interval, ``'draws'`` shows sampled predictive curves, and ``'band+draws'`` shows - both together. Single-crystal plots currently render only - the interval-based reflection check. + 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 @@ -1098,10 +1109,6 @@ def _plot_posterior_predictive_request( log.warning('Plotter is not attached to a project.') return - if self.engine != PlotterEngineEnum.PLOTLY.value: - log.warning('Posterior predictive plots currently require the Plotly backend.') - return - self._update_project_categories(expt_name) experiment = self._project.experiments[expt_name] x_axis, _, sample_form, scattering_type, _ = self._resolve_x_axis( @@ -1110,6 +1117,12 @@ def _plot_posterior_predictive_request( ) 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, @@ -1236,6 +1249,10 @@ def _plot_non_bragg_posterior_predictive( 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: @@ -1263,7 +1280,7 @@ def _plot_non_bragg_posterior_predictive( experiment=experiment, expt_name=expt_name, x_axis=x_axis, - include_draws=style in {'draws', 'band+draws'}, + include_draws=show_draws, ) if summary is None: return @@ -1272,7 +1289,7 @@ def _plot_non_bragg_posterior_predictive( summary=summary, x_min=ctx['x_min'], x_max=ctx['x_max'], - include_draws=style in {'draws', 'band+draws'}, + include_draws=show_draws, ) if filtered_summary is None: log.warning( @@ -1632,6 +1649,7 @@ def _posterior_pairs_context( 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, @@ -1640,7 +1658,7 @@ def _posterior_pairs_context( axis_ranges=self._posterior_pair_axis_ranges( fit_results=fit_results, parameter_names=parameter_names, - density_samples=selected_samples, + samples=selected_samples, ), ) @@ -1681,7 +1699,7 @@ def _posterior_pair_axis_ranges( *, fit_results: object, parameter_names: list[str], - density_samples: np.ndarray, + samples: np.ndarray, ) -> list[tuple[float, float]]: """Return per-parameter axis ranges for a pair plot.""" axis_ranges: list[tuple[float, float]] = [] @@ -1692,7 +1710,7 @@ def _posterior_pair_axis_ranges( ) axis_ranges.append( self._posterior_axis_bounds( - density_samples[:, index], + samples[:, index], lower_bound=lower_bound, upper_bound=upper_bound, ) @@ -1777,7 +1795,7 @@ def _add_posterior_pair_diagonal( ) -> None: """Add the diagonal marginal-density panel.""" go = __import__('plotly.graph_objects', fromlist=['Histogram']) - density_values = context.density_samples[:, parameter_index] + 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], @@ -2457,13 +2475,52 @@ def _build_param_distribution_plot( ) 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._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 _posterior_distribution_context( self, param: object, ) -> _PosteriorDistributionContext | None: """Return the context for a posterior distribution plot.""" - posterior_samples, fit_results = self._get_posterior_samples_and_fit_results() - if posterior_samples is None or fit_results is None: + 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( @@ -3446,7 +3503,20 @@ def _plot_posterior_predictive_summary( show_draws: bool, excluded_ranges: tuple[tuple[float, float], ...] = (), ) -> None: - """Render posterior predictive summaries using Plotly.""" + """ + 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.map_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() @@ -3566,6 +3636,27 @@ def _plot_posterior_predictive_summary( ) 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, *, @@ -3677,6 +3768,10 @@ def _plot_posterior_predictive_data( 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, @@ -3693,7 +3788,7 @@ def _plot_posterior_predictive_data( experiment=experiment, expt_name=expt_name, x_axis=x_axis, - include_draws=style in {'draws', 'band+draws'}, + include_draws=show_draws, ) if summary is None: return @@ -3715,8 +3810,32 @@ def _plot_posterior_predictive_data( y_calc = self._filtered_y_array( summary.map_prediction, summary.x, ctx['x_min'], ctx['x_max'] ) - show_residual = True if plot_options.show_residual is None else plot_options.show_residual - y_resid = y_meas - y_calc if show_residual else None + 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 @@ -3735,15 +3854,14 @@ def _plot_posterior_predictive_data( ) predictive_draws = None - if style in {'draws', 'band+draws'}: - draws = getattr(summary, 'draws', None) - if draws is 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 draws + for draw in summary.draws ], dtype=float, ) @@ -3758,37 +3876,28 @@ def _plot_posterior_predictive_data( x_min=ctx['x_min'], x_max=ctx['x_max'], ) - excluded_ranges = ( - self._excluded_ranges( - experiment=experiment, - 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, ) - if plot_options.show_excluded - else () ) - 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, - ) - self._backend.plot_powder_meas_vs_calc(plot_spec=plot_spec) - @staticmethod def _resolve_posterior_parameter_names( *, diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py index 6bab7df3d..c248c9ad3 100644 --- a/src/easydiffraction/project/display.py +++ b/src/easydiffraction/project/display.py @@ -16,6 +16,7 @@ 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 @@ -142,9 +143,22 @@ def pairs( max_parameters=max_parameters, ) - def distribution(self, param: object) -> None: - """Plot one sampled parameter's posterior distribution.""" - self._project.rendering.plotter.plot_param_distribution(param) + 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, 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/tests/integration/fitting/test_bayesian_tracker_and_base.py b/tests/integration/fitting/test_bayesian_tracker_and_base.py index e6988a0f3..91ec0e572 100644 --- a/tests/integration/fitting/test_bayesian_tracker_and_base.py +++ b/tests/integration/fitting/test_bayesian_tracker_and_base.py @@ -127,6 +127,7 @@ def stop(self): 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 @@ -168,9 +169,9 @@ def stop(self): assert FitProgressTracker._rows_match_on_columns(['1', 'a'], ['1', 'b'], (0,)) is True assert events == [ - ('init', tracking_mod.ACTIVITY_LABEL_SAMPLING, VerbosityEnum.SHORT), + ('init', tracking_mod.ACTIVITY_LABEL_PROCESSING, VerbosityEnum.SHORT), ('start', None), - ('update', tracking_mod.ACTIVITY_LABEL_SAMPLING), + ('update', tracking_mod.ACTIVITY_LABEL_PROCESSING), ('stop', None), ] @@ -274,7 +275,7 @@ def test_tracker_final_rows_cover_fallbacks_and_activity_labels(): 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_SAMPLING + 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 ( diff --git a/tests/integration/fitting/test_bumps_dream_support.py b/tests/integration/fitting/test_bumps_dream_support.py index bf87549a4..4bcfed845 100644 --- a/tests/integration/fitting/test_bumps_dream_support.py +++ b/tests/integration/fitting/test_bumps_dream_support.py @@ -3,6 +3,7 @@ from __future__ import annotations +import threading from types import SimpleNamespace from unittest.mock import MagicMock from unittest.mock import patch @@ -33,6 +34,20 @@ 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 @@ -258,6 +273,7 @@ def test_build_mapper_falls_back_for_serial_and_unpicklable(monkeypatch): 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 ) @@ -267,7 +283,146 @@ def test_build_mapper_falls_back_for_serial_and_unpicklable(monkeypatch): ) assert minimizer._build_mapper('problem') is None - assert any('falling back to serial execution' in message for message in warnings) + 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(): diff --git a/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py b/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py index bb824f829..9758f70f8 100644 --- a/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py +++ b/tests/unit/easydiffraction/analysis/fit_helpers/test_reporting.py @@ -57,7 +57,7 @@ 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 ('╒', '┌', '+', '─')) diff --git a/tests/unit/easydiffraction/display/plotters/test_ascii.py b/tests/unit/easydiffraction/display/plotters/test_ascii.py index 49dd76537..a0e84dc42 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,28 @@ 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 'Max posterior' in out + + def test_ascii_plotter_plot_single_crystal(capsys): from easydiffraction.display.plotters.ascii import AsciiPlotter @@ -85,3 +109,73 @@ 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_resamples_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 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 diff --git a/tests/unit/easydiffraction/display/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py index 18f0afe81..c32424044 100644 --- a/tests/unit/easydiffraction/display/test_plotting.py +++ b/tests/unit/easydiffraction/display/test_plotting.py @@ -585,6 +585,7 @@ def test_posterior_pairs_context_thins_kde_samples_and_preserves_axis_ranges(): 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, @@ -596,6 +597,74 @@ def test_posterior_pairs_context_thins_kde_samples_and_preserves_axis_ranges(): 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, + map_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() @@ -646,6 +715,34 @@ def test_build_param_distribution_plot_returns_plotly_figure(): 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 @@ -657,6 +754,7 @@ def test_plot_posterior_predictive_summary_uses_consistent_labels_and_styles(mon captured: dict[str, object] = {} plotter = Plotter() + plotter.engine = 'plotly' plotter._backend = SimpleNamespace( _show_figure=lambda figure: captured.setdefault('fig', figure) ) @@ -797,6 +895,172 @@ class Experiment: 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]), + map_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]), + map_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 @@ -1947,6 +2211,30 @@ def fake_build(self, *, parameters, style, threshold, max_parameters): 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)) + ) + + 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 diff --git a/tests/unit/easydiffraction/display/test_plotting_coverage.py b/tests/unit/easydiffraction/display/test_plotting_coverage.py index 806e68da2..542dfa4f5 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,8 @@ 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_no_narrowing_when_limits_provided(self): from easydiffraction.display.plotting import Plotter diff --git a/tests/unit/easydiffraction/project/test_display.py b/tests/unit/easydiffraction/project/test_display.py index b67d7c022..b56f8fce0 100644 --- a/tests/unit/easydiffraction/project/test_display.py +++ b/tests/unit/easydiffraction/project/test_display.py @@ -50,6 +50,7 @@ def _recorder(*args, **kwargs): project = SimpleNamespace( analysis=SimpleNamespace(display=analysis_display), rendering=SimpleNamespace(plotter=plotter), + free_parameters=[], verbosity='full', ) return project, calls @@ -259,6 +260,49 @@ def fake_activity_indicator(label, *, verbosity): ] +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) From 6a4887e5025b16ab9ee4b23068a32dadfe4e182b Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Sun, 17 May 2026 09:45:00 +0200 Subject: [PATCH 07/10] Refactor fit modes and sequential analysis workflow (#175) * Rename Bayesian MAP labels to best posterior sample * Add ADR for fit mode categories * Refine fit-mode categories ADR with active-sibling pattern * Add open questions and drop fitting.mode mirror in ADR * Add implementation plan for fit-mode-categories ADR * Rename CIF field _fitting.mode to _fitting.mode_type * Tighten high-risk steps in fit-mode-categories plan * Add BoolDescriptor for CIF-bound boolean values * Add fitting category replacing fit configuration surface * Add fitting_mode_type selector and fitting accessor on Analysis * Rename joint_fit_experiments category to joint_fit * Add sequential_fit category with persisted scan settings * Add sequential_fit_extract category for scan metadata rules * Replace fit category with Analysis.fit() method * Drive sequential fitting from sequential_fit settings * Auto-populate joint_fit rows and validate before fitting * Add instance-aware help filter and hide inactive mode categories * Serialize only active mode-specific analysis categories * Restore mode before mode-specific analysis sections * Update tutorials, docs, and exports for new fitting API * Resolve ZIP extraction relative to saved project * Add live progress tables for sequential fitting * Set sequential fitting mode and save project * Complete fit-mode categories and refactor progress * Fix sequential replay, styling, and docs * Simplify sequential progress table headers * Add progress and time columns to sequential tables * Unify sequential and single fit spinners on ActivityIndicator * Fix spacing in chunk file range display * Render sequential progress as bordered table with index column * Render sequential progress with shared single-fit table renderable * Add fit.series_all to plot every fitted parameter * Unify fit.series to plot single or all fitted parameters * Encapsulate chunk progress in dataclass * Use string paths for versus parameter * Unify ASCII plot width handling across terminal charts * Sort ASCII parameter series by x before plotting * Skip fit reports for sequential mode * List all analysis files on save * Bump dependencies * Add resumed sequential-fit tutorial for Co2SiO4 * Update data index reference and hash * Clear sequential fit state and make ZIP extraction explicit * Update data index reference and hash * Normalize sequential CSV paths for resumed fits * Update data index reference and hash * Add resume fit tutorial and fix paths * Normalize CSV relative paths to POSIX style * Remove project save step from tutorial * Accept fit-mode categories ADR * Move Quick Reference to end of nav * Lower coverage threshold to 65% --- .../adr_parameter-posterior-summary.md | 43 +- docs/dev/ADRs/adr_display-ux.md | 7 +- docs/dev/ADRs/adr_fit-mode-categories.md | 781 +++++++++++++ docs/dev/Issues/issues_open.md | 247 +++- docs/dev/architecture.md | 273 +++-- docs/dev/package-structure-full.md | 40 +- docs/dev/package-structure-short.md | 14 +- docs/docs/quick-reference/index.md | 47 +- docs/docs/tutorials/ed-15.ipynb | 4 +- docs/docs/tutorials/ed-15.py | 4 +- docs/docs/tutorials/ed-16.ipynb | 6 +- docs/docs/tutorials/ed-16.py | 6 +- docs/docs/tutorials/ed-17.ipynb | 104 +- docs/docs/tutorials/ed-17.py | 51 +- docs/docs/tutorials/ed-2.ipynb | 4 +- docs/docs/tutorials/ed-2.py | 4 +- docs/docs/tutorials/ed-20.ipynb | 4 +- docs/docs/tutorials/ed-20.py | 4 +- docs/docs/tutorials/ed-21.ipynb | 10 +- docs/docs/tutorials/ed-21.py | 10 +- docs/docs/tutorials/ed-22.ipynb | 8 +- docs/docs/tutorials/ed-22.py | 8 +- docs/docs/tutorials/ed-23.ipynb | 241 ++++ docs/docs/tutorials/ed-23.py | 79 ++ docs/docs/tutorials/ed-3.ipynb | 8 +- docs/docs/tutorials/ed-3.py | 8 +- docs/docs/tutorials/ed-4.ipynb | 4 +- docs/docs/tutorials/ed-4.py | 4 +- docs/docs/tutorials/ed-8.ipynb | 4 +- docs/docs/tutorials/ed-8.py | 4 +- docs/docs/tutorials/index.md | 3 + .../user-guide/analysis-workflow/analysis.md | 18 +- docs/docs/user-guide/first-steps.md | 2 +- docs/mkdocs.yml | 5 +- pixi.lock | 1004 ++++++++--------- pyproject.toml | 2 +- src/easydiffraction/__main__.py | 5 +- src/easydiffraction/analysis/__init__.py | 14 + src/easydiffraction/analysis/analysis.py | 449 +++++--- .../analysis/categories/__init__.py | 13 + .../analysis/categories/fit/__init__.py | 6 - .../analysis/categories/fit/default.py | 212 ---- .../analysis/categories/fitting/__init__.py | 5 + .../analysis/categories/fitting/default.py | 125 ++ .../factory.py | 6 +- .../analysis/categories/joint_fit/__init__.py | 8 + .../default.py | 44 +- .../categories/{fit => joint_fit}/factory.py | 6 +- .../joint_fit_experiments/__init__.py | 5 - .../categories/sequential_fit/__init__.py | 7 + .../categories/sequential_fit/default.py | 124 ++ .../categories/sequential_fit/factory.py | 17 + .../sequential_fit_extract/__init__.py | 14 + .../sequential_fit_extract/default.py | 162 +++ .../sequential_fit_extract/factory.py | 17 + .../analysis/{categories/fit => }/enums.py | 8 +- .../analysis/fit_helpers/bayesian.py | 28 +- .../analysis/minimizers/bumps_dream.py | 13 +- src/easydiffraction/analysis/sequential.py | 705 +++++++++--- src/easydiffraction/core/__init__.py | 2 + src/easydiffraction/core/guard.py | 38 +- src/easydiffraction/core/variable.py | 49 + src/easydiffraction/display/plotters/ascii.py | 66 +- src/easydiffraction/display/plotters/base.py | 2 +- src/easydiffraction/display/plotting.py | 233 +++- src/easydiffraction/display/progress.py | 68 +- src/easydiffraction/io/ascii.py | 26 +- src/easydiffraction/io/cif/serialize.py | 201 +++- src/easydiffraction/project/display.py | 19 +- src/easydiffraction/project/project.py | 77 +- src/easydiffraction/summary/summary.py | 2 +- src/easydiffraction/utils/utils.py | 4 +- tests/functional/test_fitting_workflow.py | 12 +- .../functional/test_switchable_categories.py | 2 +- tests/integration/fitting/conftest.py | 5 +- .../test_analysis_and_fit_category_support.py | 116 +- .../fitting/test_analysis_display.py | 4 +- .../fitting/test_aniso_adp_fitting.py | 5 +- .../fitting/test_bayesian_dream.py | 28 +- .../fitting/test_bayesian_helper_support.py | 24 +- .../fitting/test_bumps_dream_support.py | 6 +- tests/integration/fitting/test_multi.py | 6 +- ..._powder-diffraction_constant-wavelength.py | 6 +- .../test_powder-diffraction_joint-fit.py | 16 +- .../test_powder-diffraction_time-of-flight.py | 4 +- .../integration/fitting/test_project_load.py | 13 +- tests/integration/fitting/test_sequential.py | 127 ++- .../categories/fitting/test_default.py | 37 + .../categories/fitting/test_factory.py | 24 + .../categories/sequential_fit/test_default.py | 35 + .../categories/sequential_fit/test_factory.py | 24 + .../sequential_fit_extract/test_default.py | 66 ++ .../sequential_fit_extract/test_factory.py | 32 + .../analysis/categories/test_fit.py | 73 +- .../categories/test_joint_fit_experiments.py | 16 +- .../analysis/fit_helpers/test_bayesian.py | 12 +- .../analysis/minimizers/test_bumps_dream.py | 4 +- .../easydiffraction/analysis/test_analysis.py | 88 +- .../analysis/test_analysis_coverage.py | 4 +- .../easydiffraction/analysis/test_enums.py | 22 + .../analysis/test_sequential.py | 418 ++++++- .../display/plotters/test_ascii.py | 131 ++- .../display/plotters/test_plotly.py | 6 +- .../easydiffraction/display/test_plotting.py | 37 +- .../display/test_plotting_coverage.py | 50 +- .../easydiffraction/display/test_progress.py | 105 ++ .../io/cif/test_serialize_more.py | 15 +- tests/unit/easydiffraction/io/test_ascii.py | 45 + .../easydiffraction/project/test_display.py | 11 +- .../easydiffraction/project/test_project.py | 47 + .../project/test_project_load.py | 10 +- .../project/test_project_save.py | 24 + .../easydiffraction/summary/test_summary.py | 4 +- .../summary/test_summary_details.py | 4 +- tests/unit/easydiffraction/test___main__.py | 57 + 115 files changed, 5770 insertions(+), 1850 deletions(-) create mode 100644 docs/dev/ADRs/adr_fit-mode-categories.md create mode 100644 docs/docs/tutorials/ed-23.ipynb create mode 100644 docs/docs/tutorials/ed-23.py delete mode 100644 src/easydiffraction/analysis/categories/fit/__init__.py delete mode 100644 src/easydiffraction/analysis/categories/fit/default.py create mode 100644 src/easydiffraction/analysis/categories/fitting/__init__.py create mode 100644 src/easydiffraction/analysis/categories/fitting/default.py rename src/easydiffraction/analysis/categories/{joint_fit_experiments => fitting}/factory.py (65%) create mode 100644 src/easydiffraction/analysis/categories/joint_fit/__init__.py rename src/easydiffraction/analysis/categories/{joint_fit_experiments => joint_fit}/default.py (64%) rename src/easydiffraction/analysis/categories/{fit => joint_fit}/factory.py (70%) delete mode 100644 src/easydiffraction/analysis/categories/joint_fit_experiments/__init__.py create mode 100644 src/easydiffraction/analysis/categories/sequential_fit/__init__.py create mode 100644 src/easydiffraction/analysis/categories/sequential_fit/default.py create mode 100644 src/easydiffraction/analysis/categories/sequential_fit/factory.py create mode 100644 src/easydiffraction/analysis/categories/sequential_fit_extract/__init__.py create mode 100644 src/easydiffraction/analysis/categories/sequential_fit_extract/default.py create mode 100644 src/easydiffraction/analysis/categories/sequential_fit_extract/factory.py rename src/easydiffraction/analysis/{categories/fit => }/enums.py (73%) create mode 100644 tests/unit/easydiffraction/analysis/categories/fitting/test_default.py create mode 100644 tests/unit/easydiffraction/analysis/categories/fitting/test_factory.py create mode 100644 tests/unit/easydiffraction/analysis/categories/sequential_fit/test_default.py create mode 100644 tests/unit/easydiffraction/analysis/categories/sequential_fit/test_factory.py create mode 100644 tests/unit/easydiffraction/analysis/categories/sequential_fit_extract/test_default.py create mode 100644 tests/unit/easydiffraction/analysis/categories/sequential_fit_extract/test_factory.py create mode 100644 tests/unit/easydiffraction/analysis/test_enums.py diff --git a/docs/dev/ADR-suggestions/adr_parameter-posterior-summary.md b/docs/dev/ADR-suggestions/adr_parameter-posterior-summary.md index dd7999ba1..f81e64472 100644 --- a/docs/dev/ADR-suggestions/adr_parameter-posterior-summary.md +++ b/docs/dev/ADR-suggestions/adr_parameter-posterior-summary.md @@ -54,7 +54,7 @@ model. ### 2. Reuse the existing Bayesian summary container Do not add separate flat parameter attributes such as `median`, -`map_estimate`, `interval_95`, `r_hat`, or `ess_bulk`. +`best_sample_value`, `interval_95`, `r_hat`, or `ess_bulk`. Instead, the parameter-level projection reuses the existing `PosteriorParameterSummary` object already produced for @@ -63,7 +63,7 @@ inspection, and later persistence. The summary object currently provides the right level of detail: -- `map_estimate` +- `best_sample_value` - `median` - `uncertainty` - `interval_68` @@ -87,7 +87,7 @@ The internal field names stay compact and code-oriented. User-facing tables, summaries, and plot annotations should use these friendly labels: -- `map_estimate` -> `MAP estimate` +- `best_sample_value` -> `Best posterior sample` - `median` -> `Median` - `uncertainty` -> `Standard uncertainty` - `interval_68` -> `68% credible interval` @@ -109,7 +109,7 @@ current_value = param.value current_uncertainty = param.uncertainty if param.posterior is not None: - map_estimate = param.posterior.map_estimate + best_sample_value = param.posterior.best_sample_value median = param.posterior.median uncertainty = param.posterior.uncertainty low95, high95 = param.posterior.interval_95 @@ -187,15 +187,15 @@ 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 MAP to `parameter.value` after Bayesian fits +### 8. Commit best posterior sample to `parameter.value` after Bayesian fits After a posterior-capable fit, `parameter.value` is committed from the -maximum-a-posteriori estimate. +best posterior sample. -MAP 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. +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 @@ -279,7 +279,7 @@ Stores one saved Bayesian result header with these fields: - `has_posterior_predictive` - `sidecar_file` -For the current design, `point_estimate_name` is always `map`. +For the current design, `point_estimate_name` is always `best_sample`. #### 11.3 `_bayesian_sampler` single item @@ -320,7 +320,7 @@ Fields: - `order_index` - `unique_name` - `display_name` -- `map_estimate` +- `best_sample_value` - `median` - `uncertainty` - `interval_68_lower` @@ -342,7 +342,7 @@ Fields: - `experiment_name` - `x_axis_name` - `x_path` -- `map_prediction_path` +- `best_sample_prediction_path` - `lower_95_path` - `upper_95_path` - `lower_68_path` @@ -382,7 +382,7 @@ cosio.atom_site.Co2.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 _bayesian_result.schema_version 1 _bayesian_result.sampler_name dream -_bayesian_result.point_estimate_name map +_bayesian_result.point_estimate_name best_sample _bayesian_result.success yes _bayesian_result.sampler_completed yes _bayesian_result.reduced_chi_square 1.031 @@ -413,7 +413,7 @@ loop_ _bayesian_parameter_posterior.order_index _bayesian_parameter_posterior.unique_name _bayesian_parameter_posterior.display_name -_bayesian_parameter_posterior.map_estimate +_bayesian_parameter_posterior.best_sample_value _bayesian_parameter_posterior.median _bayesian_parameter_posterior.uncertainty _bayesian_parameter_posterior.interval_68_lower @@ -429,7 +429,7 @@ loop_ _bayesian_predictive_dataset.experiment_name _bayesian_predictive_dataset.x_axis_name _bayesian_predictive_dataset.x_path -_bayesian_predictive_dataset.map_prediction_path +_bayesian_predictive_dataset.best_sample_prediction_path _bayesian_predictive_dataset.lower_95_path _bayesian_predictive_dataset.upper_95_path _bayesian_predictive_dataset.lower_68_path @@ -437,7 +437,7 @@ _bayesian_predictive_dataset.upper_68_path _bayesian_predictive_dataset.draws_path _bayesian_predictive_dataset.n_x _bayesian_predictive_dataset.n_draws_cached -hrpt ttheta /predictive/hrpt/x /predictive/hrpt/map_prediction /predictive/hrpt/lower_95 /predictive/hrpt/upper_95 /predictive/hrpt/lower_68 /predictive/hrpt/upper_68 /predictive/hrpt/draws 2500 200 +hrpt ttheta /predictive/hrpt/x /predictive/hrpt/best_sample_prediction /predictive/hrpt/lower_95 /predictive/hrpt/upper_95 /predictive/hrpt/lower_68 /predictive/hrpt/upper_68 /predictive/hrpt/draws 2500 200 ``` ### 12. Persist bulk arrays in `analysis/bayesian_data.h5` @@ -482,7 +482,7 @@ ordering. Recommended HDF5 dataset naming is: - `predictive____x` -- `predictive____map_prediction` +- `predictive____best_sample_prediction` - `predictive____lower_95` - `predictive____upper_95` - `predictive____lower_68` @@ -497,7 +497,7 @@ Recommended HDF5 group layout is: - `/posterior/log_posterior` - `/posterior/draw_index` - `/predictive//x` -- `/predictive//map_prediction` +- `/predictive//best_sample_prediction` - `/predictive//lower_95` - `/predictive//upper_95` - `/predictive//lower_68` @@ -573,7 +573,7 @@ param = project.phases['lbco'].cell.length_a posterior = param.posterior if posterior is not None: - print(posterior.map_estimate) + print(posterior.best_sample_value) print(posterior.uncertainty) print(posterior.interval_68) @@ -673,7 +673,8 @@ It still defers: ## Chosen Defaults -- `parameter.value` remains committed to MAP after posterior fits. +- `parameter.value` remains committed to the best posterior sample after + posterior fits. - If a project is loaded without full posterior arrays, restoring only `parameter.posterior` is acceptable for table display and parameter inspection. diff --git a/docs/dev/ADRs/adr_display-ux.md b/docs/dev/ADRs/adr_display-ux.md index 47e9b5c28..23a780812 100644 --- a/docs/dev/ADRs/adr_display-ux.md +++ b/docs/dev/ADRs/adr_display-ux.md @@ -77,7 +77,7 @@ project.display.parameters.cif_uids() project.display.fit.results() project.display.fit.correlations() -project.display.fit.series(param, versus=temperature) +project.display.fit.series(param, versus='diffrn.ambient_temperature') project.display.posterior.pairs() project.display.posterior.distribution(param) @@ -197,7 +197,8 @@ Use these naming rules: - `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. + 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 @@ -209,7 +210,7 @@ project.display.pattern(expt_name='hrpt') project.display.parameters(scope='free') project.display.fit_results() project.display.correlations() -project.display.parameter_series(param, versus=temperature) +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') diff --git a/docs/dev/ADRs/adr_fit-mode-categories.md b/docs/dev/ADRs/adr_fit-mode-categories.md new file mode 100644 index 000000000..278d3752a --- /dev/null +++ b/docs/dev/ADRs/adr_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/Issues/issues_open.md b/docs/dev/Issues/issues_open.md index 8805fc8fb..3e961f1cc 100644 --- a/docs/dev/Issues/issues_open.md +++ b/docs/dev/Issues/issues_open.md @@ -14,14 +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`. +**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. @@ -32,7 +32,7 @@ exactly match `project.experiments.names`. **Type:** Consistency `Analysis` owns categories (`Aliases`, `Constraints`, -`JointFitExperiments`) but does not extend `DatablockItem`. Its ad-hoc +`JointFitCollection`) 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. @@ -161,6 +161,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 architecture 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 @@ -784,18 +934,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. @@ -1487,6 +1637,72 @@ 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 | @@ -1529,7 +1745,7 @@ operation is possible (e.g. in automated pipelines or tests). | 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 | +| 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 | @@ -1575,3 +1791,4 @@ operation is possible (e.g. in automated pipelines or tests). | 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/architecture.md b/docs/dev/architecture.md index 7cc3abe92..d6a63b6d5 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -242,6 +242,34 @@ arguments so they can be used as mixins safely (e.g. | `MembershipValidator` | Value must be in an allowed set | | `RegexValidator` | Value must match a pattern | +### 2.7 String Paths vs Live Descriptors in Public APIs + +Public APIs reference parameters in one of two ways. The choice is not +stylistic — it follows the call site's role: + +- **Setup-time / schema-level APIs use string paths** (CIF-style + `'category.attribute'`). The targeted descriptor may not yet exist on + any concrete object (e.g. an extraction rule applies uniformly to + files about to be loaded), and the value must round-trip through CIF. + Examples: + `sequential_fit_extract.create(target='diffrn.ambient_temperature', ...)`, + alias/constraint definitions persisted in project CIF. +- **Cross-experiment selectors use the same string paths at runtime.** + `project.display.fit.series(..., versus=...)` selects a persisted + `diffrn.*` column in `analysis/results.csv` and the matching field + across experiments; it does not use one experiment's current live + descriptor value. Example: + `project.display.fit.series(param=structure.cell.length_a, versus='diffrn.ambient_temperature')`. +- **Concrete model parameters still use live descriptors.** The call + needs the parameter's `unique_name`, `description`, and `units`, and + it refers to one exact fitted quantity in the model. Example: + `param=structure.cell.length_a` in `project.display.fit.series(...)`. + +When adding a new public API, place it on one side of this rule rather +than accepting both. Use string paths when the value names a persisted +field or cross-experiment selector, and use live descriptors when the +value names one concrete model parameter. + --- ## 3. Experiment System @@ -528,28 +556,28 @@ 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`, `BumpsDreamMinimizer`, `BumpsAmoebaMinimizer`, `BumpsDEMinimizer` | +| 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` | +| `JointFitFactory` | Joint-fit weights | `JointFitCollection` | +| `CalculatorFactory` | Calculation engines | `CryspyCalculator`, `CrysfmlCalculator`, `PdffitCalculator` | +| `MinimizerFactory` | Minimisers | `LmfitMinimizer`, `LmfitLeastsqMinimizer`, `LmfitLeastSquaresMinimizer`, `DfolsMinimizer`, `BumpsMinimizer`, `BumpsLmMinimizer`, `BumpsDreamMinimizer`, `BumpsAmoebaMinimizer`, `BumpsDEMinimizer` | > **Note:** `ExperimentFactory` and `StructureFactory` are _builder_ > factories with `from_cif_path`, `from_cif_str`, `from_data_path`, and @@ -724,41 +752,41 @@ line-segment points. #### 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` | +| 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` | +| `JointFitCollection` | `JointFitFactory` | #### 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` | +| 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` | +| `JointFitItem` | `JointFitCollection` | #### Non-category classes — factory-created (get `type_info` only) @@ -825,8 +853,8 @@ workflow: 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. +- Joint-fit weights: `joint_fit` (`CategoryCollection` of per-experiment + weight entries); sibling of `fit`, not a child. - Fit results: `analysis.fit_results` stores the latest runtime result object. This is `FitResults` for deterministic fits and `BayesianFitResults` for Bayesian DREAM runs. @@ -835,12 +863,12 @@ workflow: summary-style parameter displays intentionally hide the large loop-backed experiment categories `pd_data`, `total_data`, and `refln` in `all()`, `access()`, and `cif_uids()` so the output stays readable. -- Fitting: `fit()` dispatches single/joint through the callable `fit` - category; `fit_sequential()` handles sequential mode (sets `fit.mode` - to `'sequential'` internally). `fit()` accepts optional `random_seed` - for stochastic minimizers; deterministic minimizers reject non-`None` - seeds. `display.fit_results()` dispatches through the active runtime - result object. +- Fitting: `fitting.minimizer_type` stores the shared minimizer + selection; `fitting_mode_type` stores the active mode on `Analysis` + itself; `fit()` dispatches to the current mode using the persisted + sibling categories `joint_fit`, `sequential_fit`, and + `sequential_fit_extract`. `display.fit_results()` dispatches through + the active runtime result object. - Aliases and constraints (single-type categories; no public `_type` getter or setter) @@ -859,8 +887,8 @@ new persisted results category. used for the run, including `random_seed`, `steps`, `burn`, `thin`, `pop`, and `parallel`. - The current user-facing DREAM controls live on the active minimizer - object, for example `project.analysis.fit.minimizer.steps`, `burn`, - `thin`, `pop`, `parallel`, and `init`. + object, for example `project.analysis.fitting.minimizer.steps`, + `burn`, `thin`, `pop`, `parallel`, and `init`. - `plot_param_correlations()` uses posterior samples when available and otherwise falls back to deterministic covariance or engine-derived correlations. @@ -930,10 +958,10 @@ project_dir/ `_rendering.*` engine preferences (`chart_engine`, `table_engine`), 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`. Runtime fit outputs, -including `analysis.fit_results`, posterior chains, posterior predictive -summaries, and convergence diagnostics, are not serialized. +experiment file, and fit configuration (`_fitting.minimizer_type`, +`_fitting.mode_type`) lives in `analysis/analysis.cif`. Runtime fit +outputs, including `analysis.fit_results`, posterior chains, posterior +predictive summaries, and convergence diagnostics, are not serialized. ### 7.3 Verbosity @@ -954,18 +982,19 @@ 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. +`analysis.fit()`, `experiments.add_from_data_path()`) read +`project.verbosity`. ```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 +# Override for one fit, then restore the project default +original_verbosity = project.verbosity +project.verbosity = 'silent' +project.analysis.fit() # → silent +project.verbosity = original_verbosity ``` **Output styles per level:** @@ -1066,7 +1095,7 @@ project.experiments['hrpt'].linked_phases.create(id='lbco', scale=10.0) # 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' +project.analysis.fitting.minimizer_type = 'lmfit' # Plot before fitting project.display.pattern(expt_name='hrpt') @@ -1095,14 +1124,14 @@ project.save() ```python # Deterministic pre-fit remains explicit -project.analysis.fit.minimizer_type = 'bumps (lm)' +project.analysis.fitting.minimizer_type = 'bumps (lm)' project.analysis.fit() # Switch to Bayesian sampling using the same entry point -project.analysis.fit.minimizer_type = 'bumps (dream)' -project.analysis.fit.minimizer.steps = 1000 -project.analysis.fit.minimizer.parallel = 0 -project.analysis.fit(random_seed=11) +project.analysis.fitting.minimizer_type = 'bumps (dream)' +project.analysis.fitting.minimizer.steps = 1000 +project.analysis.fitting.minimizer.parallel = 0 +project.analysis.fit() # Runtime-only Bayesian summaries and plots project.display.fit.results() @@ -1221,13 +1250,18 @@ 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 +- **Analysis:** `aliases`, `constraints`, `fitting`, `sequential_fit`. + +`fitting` is a dedicated analysis configuration category, but the fit +mode selector lives on the owner as `analysis.fitting_mode_type`. This +is the project's active-sibling selector pattern: the owner stores the +authoritative mode and decides which sibling categories are active, +shown in help, and serialized. `joint_fit`, `sequential_fit`, and +`sequential_fit_extract` remain direct `Analysis` siblings even when +inactive. See the fit-mode ADR for the full contract: +[`adr_fit-mode-categories.md`](ADRs/adr_fit-mode-categories.md). +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 `chart_engine` and @@ -1253,17 +1287,20 @@ 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`, `rendering.chart_engine` | `_fit.minimizer_type`, `_calculation.calculator_type`, `_rendering.chart_engine` | -| 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`, `rendering`). Switchable- -category implementation selectors are owned by the host (typically the -experiment) because switching them replaces the category instance, as -described in §9.3. +| Family | User intent | Examples | CIF | +| ---------------------------------- | ------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| Backend selector | Pick an execution backend | `fitting.minimizer_type`, `calculation.calculator_type`, `rendering.chart_engine` | `_fitting.minimizer_type`, `_calculation.calculator_type`, `_rendering.chart_engine` | +| Switchable-category impl. selector | Swap a category implementation | `experiment.background_type`, `experiment.peak_profile_type` | category-owned type tag such as `_peak.profile_type` | +| Active-sibling selector | Pick the active sibling surface | `analysis.fitting_mode_type` | owner-owned tag such as `_fitting.mode_type` | + +Backend selectors live on a dedicated configuration category (`fitting`, +`calculation`, `rendering`). Switchable-category implementation +selectors are owned by the host (typically the experiment) because +switching them replaces the category instance, as described in §9.3. +Active-sibling selectors are also owner-level, but they do not swap one +category implementation for another. Instead, they select which sibling +category family is authoritative while the shared configuration category +keeps a stable shape. ### 9.5 Discoverable Supported Options @@ -1275,8 +1312,8 @@ 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.analysis.fitting.show_minimizer_types() +project.analysis.show_fitting_mode_types() project.rendering.show_chart_engines() project.rendering.show_table_engines() ``` @@ -1316,10 +1353,10 @@ but internal dispatch always uses the enum: ```python # ✅ Correct — compare with enum -if self._fit.mode.value == FitModeEnum.JOINT: +if self._fitting_mode_type is FitModeEnum.JOINT: # ❌ Wrong — compare with raw string -if self._fit.mode.value == 'joint': +if self._fitting_mode_type == 'joint': ``` ### 9.7 Flat Category Structure — No Nested Categories @@ -1344,30 +1381,29 @@ Owner └── 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: +**Example — `fit` and `joint_fit`:** `fit` is a `CategoryItem` holding +the active minimizer and fitting mode. `joint_fit` 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 +project.analysis.fitting_mode_type = 'joint' +project.analysis.joint_fit['npd'].weight = 0.7 -# ❌ Wrong — joint_fit_experiments as a child of fit -project.analysis.fit.joint_fit_experiments['npd'].weight = 0.7 +# ❌ Wrong — joint_fit as a child of fit +project.analysis.fitting.joint_fit['npd'].weight = 0.7 ``` In CIF output, sibling categories appear as independent blocks: ``` -_fit.minimizer_type lmfit -_fit.mode joint +_fitting.mode_type joint +_fitting.minimizer_type lmfit loop_ -_joint_fit_experiment.id -_joint_fit_experiment.weight +_joint_fit.experiment_id +_joint_fit.weight npd 0.7 xrd 0.3 ``` @@ -1564,9 +1600,10 @@ 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. +- **Open:** [`issues_open.md`](Issues/issues_open.md) — prioritised + backlog. +- **Closed:** [`issues_closed.md`](Issues/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/package-structure-full.md b/docs/dev/package-structure-full.md index dccf421a4..c1728f1ba 100644 --- a/docs/dev/package-structure-full.md +++ b/docs/dev/package-structure-full.md @@ -31,21 +31,32 @@ │ │ │ │ └── 🏷️ class Constraints │ │ │ └── 📄 factory.py │ │ │ └── 🏷️ class ConstraintsFactory -│ │ ├── 📁 fit +│ │ ├── 📁 fitting │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py -│ │ │ │ └── 🏷️ class Fit -│ │ │ ├── 📄 enums.py -│ │ │ │ └── 🏷️ class FitModeEnum +│ │ │ │ └── 🏷️ class Fitting +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class FittingFactory +│ │ ├── 📁 joint_fit +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ ├── 🏷️ class JointFitItem +│ │ │ │ └── 🏷️ class JointFitCollection │ │ │ └── 📄 factory.py -│ │ │ └── 🏷️ class FitFactory -│ │ ├── 📁 joint_fit_experiments +│ │ │ └── 🏷️ class JointFitFactory +│ │ ├── 📁 sequential_fit │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py -│ │ │ │ ├── 🏷️ class JointFitExperiment -│ │ │ │ └── 🏷️ class JointFitExperiments +│ │ │ │ └── 🏷️ class SequentialFit │ │ │ └── 📄 factory.py -│ │ │ └── 🏷️ class JointFitExperimentsFactory +│ │ │ └── 🏷️ class SequentialFitFactory +│ │ ├── 📁 sequential_fit_extract +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ ├── 🏷️ class SequentialFitExtractItem +│ │ │ │ └── 🏷️ class SequentialFitExtractCollection +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class SequentialFitExtractFactory │ │ └── 📄 __init__.py │ ├── 📁 fit_helpers │ │ ├── 📄 __init__.py @@ -95,10 +106,17 @@ │ ├── 📄 analysis.py │ │ ├── 🏷️ class AnalysisDisplay │ │ └── 🏷️ class Analysis +│ ├── 📄 enums.py +│ │ └── 🏷️ class FitModeEnum │ ├── 📄 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 @@ -137,9 +155,11 @@ │ └── 📄 variable.py │ ├── 🏷️ class GenericDescriptorBase │ ├── 🏷️ class GenericStringDescriptor +│ ├── 🏷️ class GenericBoolDescriptor │ ├── 🏷️ class GenericNumericDescriptor │ ├── 🏷️ class GenericParameter │ ├── 🏷️ class StringDescriptor +│ ├── 🏷️ class BoolDescriptor │ ├── 🏷️ class NumericDescriptor │ └── 🏷️ class Parameter ├── 📁 crystallography diff --git a/docs/dev/package-structure-short.md b/docs/dev/package-structure-short.md index b4e46ec73..4c1c62f8d 100644 --- a/docs/dev/package-structure-short.md +++ b/docs/dev/package-structure-short.md @@ -19,12 +19,19 @@ │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py │ │ │ └── 📄 factory.py -│ │ ├── 📁 fit +│ │ ├── 📁 fitting +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 joint_fit │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py -│ │ │ ├── 📄 enums.py │ │ │ └── 📄 factory.py -│ │ ├── 📁 joint_fit_experiments +│ │ ├── 📁 sequential_fit +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 sequential_fit_extract │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py │ │ │ └── 📄 factory.py @@ -51,6 +58,7 @@ │ │ └── 📄 lmfit_leastsq.py │ ├── 📄 __init__.py │ ├── 📄 analysis.py +│ ├── 📄 enums.py │ ├── 📄 fitting.py │ └── 📄 sequential.py ├── 📁 core diff --git a/docs/docs/quick-reference/index.md b/docs/docs/quick-reference/index.md index fb6835d3e..c035d3fe3 100644 --- a/docs/docs/quick-reference/index.md +++ b/docs/docs/quick-reference/index.md @@ -179,8 +179,8 @@ experiment.show_peak_profile_types() experiment.show_background_types() experiment.calculation.show_calculator_types() -project.analysis.fit.show_modes() -project.analysis.fit.show_minimizer_types() +project.analysis.show_fitting_mode_types() +project.analysis.fitting.show_minimizer_types() project.rendering.show_chart_engines() project.rendering.show_table_engines() @@ -194,8 +194,8 @@ experiment.peak_profile_type = 'pseudo-voigt' experiment.background_type = 'line-segment' experiment.calculation.calculator_type = 'cryspy' -project.analysis.fit.mode = 'single' -project.analysis.fit.minimizer_type = 'lmfit' +project.analysis.fitting_mode_type = 'single' +project.analysis.fitting.minimizer_type = 'lmfit' project.rendering.chart_engine = 'plotly' project.rendering.table_engine = 'rich' @@ -289,11 +289,11 @@ Choose calculators and minimizers: experiment.calculation.show_calculator_types() experiment.calculation.calculator_type = 'cryspy' -project.analysis.fit.show_modes() -project.analysis.fit.mode = 'single' +project.analysis.show_fitting_mode_types() +project.analysis.fitting_mode_type = 'single' -project.analysis.fit.show_minimizer_types() -project.analysis.fit.minimizer_type = 'lmfit' +project.analysis.fitting.show_minimizer_types() +project.analysis.fitting.minimizer_type = 'lmfit' ``` Run a fit and inspect the result: @@ -306,6 +306,37 @@ 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 diff --git a/docs/docs/tutorials/ed-15.ipynb b/docs/docs/tutorials/ed-15.ipynb index ab93cd536..b6d82a319 100644 --- a/docs/docs/tutorials/ed-15.ipynb +++ b/docs/docs/tutorials/ed-15.ipynb @@ -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'" ] }, { diff --git a/docs/docs/tutorials/ed-15.py b/docs/docs/tutorials/ed-15.py index 659af7214..7359b712a 100644 --- a/docs/docs/tutorials/ed-15.py +++ b/docs/docs/tutorials/ed-15.py @@ -74,10 +74,10 @@ 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' # %% # Start refinement. All parameters, which have standard uncertainties diff --git a/docs/docs/tutorials/ed-16.ipynb b/docs/docs/tutorials/ed-16.ipynb index 5da587e9d..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)" ] }, { diff --git a/docs/docs/tutorials/ed-16.py b/docs/docs/tutorials/ed-16.py index bde9af470..e5520bfe3 100644 --- a/docs/docs/tutorials/ed-16.py +++ b/docs/docs/tutorials/ed-16.py @@ -182,9 +182,9 @@ # #### 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) diff --git a/docs/docs/tutorials/ed-17.ipynb b/docs/docs/tutorials/ed-17.ipynb index d370ca274..f32b0371f 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." ] }, @@ -87,7 +87,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.save_as('data/cosio_project', temporary=False)" + "project.save_as('projects/cosio', temporary=False)" ] }, { @@ -264,8 +264,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", + ")" ] }, { @@ -528,7 +531,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer_type = 'bumps (lm)'" + "project.analysis.fitting.minimizer_type = 'bumps (lm)'" ] }, { @@ -619,7 +622,8 @@ }, "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." ] }, { @@ -629,18 +633,49 @@ "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}" + "temperature = 'diffrn.ambient_temperature'" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "id": "52", "metadata": {}, + "outputs": [], + "source": [ + "project.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": "53", + "metadata": {}, + "source": [ + "Set the sequential fitting parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fitting_mode_type = 'sequential'\n", + "project.analysis.sequential_fit.data_dir = scan_data_dir\n", + "project.analysis.sequential_fit.max_workers = 'auto'\n", + "project.analysis.sequential_fit.reverse = True" + ] + }, + { + "cell_type": "markdown", + "id": "55", + "metadata": {}, "source": [ "Run the sequential fit over all data files in the scan directory." ] @@ -648,21 +683,16 @@ { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "56", "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", - ")" + "project.analysis.fit()" ] }, { "cell_type": "markdown", - "id": "54", + "id": "57", "metadata": {}, "source": [ "#### Replay a Dataset\n", @@ -673,7 +703,7 @@ { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "58", "metadata": {}, "outputs": [], "source": [ @@ -683,7 +713,7 @@ }, { "cell_type": "markdown", - "id": "56", + "id": "59", "metadata": {}, "source": [ "\n", @@ -693,7 +723,7 @@ { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "60", "metadata": {}, "outputs": [], "source": [ @@ -703,27 +733,17 @@ }, { "cell_type": "markdown", - "id": "58", + "id": "61", "metadata": {}, "source": [ "#### Plot Parameter Evolution\n", "\n", - "Define the quantity to use as the x-axis in the following plots." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "59", - "metadata": {}, - "outputs": [], - "source": [ - "temperature = expt.diffrn.ambient_temperature" + "Reuse the extracted diffrn path as the x-axis in the following plots." ] }, { "cell_type": "markdown", - "id": "60", + "id": "62", "metadata": {}, "source": [ "Plot unit cell parameters vs. temperature." @@ -732,7 +752,7 @@ { "cell_type": "code", "execution_count": null, - "id": "61", + "id": "63", "metadata": {}, "outputs": [], "source": [ @@ -743,7 +763,7 @@ }, { "cell_type": "markdown", - "id": "62", + "id": "64", "metadata": {}, "source": [ "Plot isotropic displacement parameters vs. temperature." @@ -752,7 +772,7 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "65", "metadata": {}, "outputs": [], "source": [ @@ -780,7 +800,7 @@ }, { "cell_type": "markdown", - "id": "64", + "id": "66", "metadata": {}, "source": [ "Plot selected fractional coordinates vs. temperature." @@ -789,7 +809,7 @@ { "cell_type": "code", "execution_count": null, - "id": "65", + "id": "67", "metadata": {}, "outputs": [], "source": [ diff --git a/docs/docs/tutorials/ed-17.py b/docs/docs/tutorials/ed-17.py index c1dc3a79a..2fd4b0f64 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] @@ -26,7 +26,7 @@ # results can be written to `analysis/results.csv`. # %% -project.save_as('data/cosio_project', temporary=False) +project.save_as('projects/cosio', temporary=False) # %% [markdown] # ## Step 2: Define Crystal Structure @@ -131,8 +131,11 @@ # #### 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 @@ -263,7 +266,7 @@ # #### Set Minimizer # %% -project.analysis.fit.minimizer_type = 'bumps (lm)' +project.analysis.fitting.minimizer_type = 'bumps (lm)' # %% [markdown] # #### Run Single Fitting @@ -299,28 +302,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' +# %% +project.analysis.sequential_fit_extract.create( + id='temperature', + target=temperature, + pattern=r'^TEMP\s+([0-9.]+)', + required=True, +) + +# %% [markdown] +# Set the sequential fitting parameters. + +# %% +project.analysis.fitting_mode_type = 'sequential' +project.analysis.sequential_fit.data_dir = scan_data_dir +project.analysis.sequential_fit.max_workers = 'auto' +project.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, -) +project.analysis.fit() # %% [markdown] # #### Replay a Dataset @@ -342,10 +352,7 @@ def extract_diffrn(file_path): # %% [markdown] # #### Plot Parameter Evolution # -# Define the quantity to use as the x-axis in the following plots. - -# %% -temperature = expt.diffrn.ambient_temperature +# Reuse the extracted diffrn path as the x-axis in the following plots. # %% [markdown] # Plot unit cell parameters vs. temperature. diff --git a/docs/docs/tutorials/ed-2.ipynb b/docs/docs/tutorials/ed-2.ipynb index 927087e9d..e3338eec8 100644 --- a/docs/docs/tutorials/ed-2.ipynb +++ b/docs/docs/tutorials/ed-2.ipynb @@ -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'" ] }, { diff --git a/docs/docs/tutorials/ed-2.py b/docs/docs/tutorials/ed-2.py index c2772ba6b..a8e9e3d22 100644 --- a/docs/docs/tutorials/ed-2.py +++ b/docs/docs/tutorials/ed-2.py @@ -192,8 +192,8 @@ 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() diff --git a/docs/docs/tutorials/ed-20.ipynb b/docs/docs/tutorials/ed-20.ipynb index 4ad0e666c..688fc593a 100644 --- a/docs/docs/tutorials/ed-20.ipynb +++ b/docs/docs/tutorials/ed-20.ipynb @@ -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'" ] }, { diff --git a/docs/docs/tutorials/ed-20.py b/docs/docs/tutorials/ed-20.py index e48e860ba..8c6a5b549 100644 --- a/docs/docs/tutorials/ed-20.py +++ b/docs/docs/tutorials/ed-20.py @@ -259,10 +259,10 @@ # #### 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 diff --git a/docs/docs/tutorials/ed-21.ipynb b/docs/docs/tutorials/ed-21.ipynb index d5fe45749..4905c6f09 100644 --- a/docs/docs/tutorials/ed-21.ipynb +++ b/docs/docs/tutorials/ed-21.ipynb @@ -415,7 +415,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.show_minimizer_types()" + "project.analysis.fitting.show_minimizer_types()" ] }, { @@ -425,7 +425,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer_type = 'bumps (lm)'" + "project.analysis.fitting.minimizer_type = 'bumps (lm)'" ] }, { @@ -584,7 +584,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.show_minimizer_types()" + "project.analysis.fitting.show_minimizer_types()" ] }, { @@ -594,7 +594,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer_type = 'bumps (dream)'" + "project.analysis.fitting.minimizer_type = 'bumps (dream)'" ] }, { @@ -604,7 +604,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer.steps = 300 # lower than the default 3000" + "project.analysis.fitting.minimizer.steps = 300 # lower than the default 3000" ] }, { diff --git a/docs/docs/tutorials/ed-21.py b/docs/docs/tutorials/ed-21.py index 8129f6e3e..87f003e46 100644 --- a/docs/docs/tutorials/ed-21.py +++ b/docs/docs/tutorials/ed-21.py @@ -204,10 +204,10 @@ # and uncertainty estimates for the Bayesian run. # %% -project.analysis.fit.show_minimizer_types() +project.analysis.fitting.show_minimizer_types() # %% -project.analysis.fit.minimizer_type = 'bumps (lm)' +project.analysis.fitting.minimizer_type = 'bumps (lm)' # %% project.analysis.fit() @@ -285,13 +285,13 @@ # this is not recommended for production analysis. # %% -project.analysis.fit.show_minimizer_types() +project.analysis.fitting.show_minimizer_types() # %% -project.analysis.fit.minimizer_type = 'bumps (dream)' +project.analysis.fitting.minimizer_type = 'bumps (dream)' # %% -project.analysis.fit.minimizer.steps = 300 # lower than the default 3000 +project.analysis.fitting.minimizer.steps = 300 # lower than the default 3000 # %% project.analysis.fit() diff --git a/docs/docs/tutorials/ed-22.ipynb b/docs/docs/tutorials/ed-22.ipynb index c6de427d8..bacb7c777 100644 --- a/docs/docs/tutorials/ed-22.ipynb +++ b/docs/docs/tutorials/ed-22.ipynb @@ -294,7 +294,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.show_minimizer_types()" + "project.analysis.fitting.show_minimizer_types()" ] }, { @@ -462,7 +462,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.show_minimizer_types()" + "project.analysis.fitting.show_minimizer_types()" ] }, { @@ -472,7 +472,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer_type = 'bumps (dream)'" + "project.analysis.fitting.minimizer_type = 'bumps (dream)'" ] }, { @@ -482,7 +482,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer.steps = 500 # lower than the default 3000" + "project.analysis.fitting.minimizer.steps = 500 # lower than the default 3000" ] }, { diff --git a/docs/docs/tutorials/ed-22.py b/docs/docs/tutorials/ed-22.py index da04c4b5d..da43c6d52 100644 --- a/docs/docs/tutorials/ed-22.py +++ b/docs/docs/tutorials/ed-22.py @@ -131,7 +131,7 @@ # and uncertainty estimates for the Bayesian run. # %% -project.analysis.fit.show_minimizer_types() +project.analysis.fitting.show_minimizer_types() # %% project.analysis.fit() @@ -213,13 +213,13 @@ # effective burn-in is recomputed automatically. # %% -project.analysis.fit.show_minimizer_types() +project.analysis.fitting.show_minimizer_types() # %% -project.analysis.fit.minimizer_type = 'bumps (dream)' +project.analysis.fitting.minimizer_type = 'bumps (dream)' # %% -project.analysis.fit.minimizer.steps = 500 # lower than the default 3000 +project.analysis.fitting.minimizer.steps = 500 # lower than the default 3000 # %% project.analysis.fit() diff --git a/docs/docs/tutorials/ed-23.ipynb b/docs/docs/tutorials/ed-23.ipynb new file mode 100644 index 000000000..d20045c6c --- /dev/null +++ b/docs/docs/tutorials/ed-23.ipynb @@ -0,0 +1,241 @@ +{ + "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 Archive\n", + "\n", + "The archive should contain a saved project directory with a partially\n", + "completed sequential fit, including `analysis/results.csv`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "zip_path = ed.download_data(id=34, destination='data')" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Extract Project\n", + "\n", + "Extract the saved project directory locally. For a project you\n", + "already have on disk, set `project_dir` directly instead." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "project_dir = ed.extract_project_from_zip(zip_path, destination='projects')" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Load Saved Project" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "project = ed.Project.load(project_dir)" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "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": "11", + "metadata": {}, + "outputs": [], + "source": [ + "project.analysis.fit()" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "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": "13", + "metadata": {}, + "outputs": [], + "source": [ + "project.apply_params_from_csv(row_index=0)\n", + "project.display.pattern(expt_name='d20')" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "\n", + "Apply fitted parameters from the last CSV row and plot the result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "project.apply_params_from_csv(row_index=-1)\n", + "project.display.pattern(expt_name='d20')" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## Plot Parameter Evolution\n", + "\n", + "Use the same persisted diffrn path stored in `analysis/results.csv`\n", + "for the x-axis. Omitting `param` plots every fitted parameter one\n", + "after another." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "temperature = 'diffrn.ambient_temperature'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "project.display.fit.series(versus=temperature)" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "## Save Project\n", + "\n", + "Save the updated project so the appended `analysis/results.csv` and\n", + "refreshed summary files remain on disk." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "project.save()" + ] + } + ], + "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..659cbcbb1 --- /dev/null +++ b/docs/docs/tutorials/ed-23.py @@ -0,0 +1,79 @@ +# %% [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 Archive +# +# The archive should contain a saved project directory with a partially +# completed sequential fit, including `analysis/results.csv`. + +# %% +zip_path = ed.download_data(id=34, destination='data') + +# %% [markdown] +# ## Extract Project +# +# Extract the saved project directory locally. For a project you +# already have on disk, set `project_dir` directly instead. + +# %% +project_dir = ed.extract_project_from_zip(zip_path, 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. Omitting `param` plots every fitted parameter one +# after another. + +# %% +temperature = 'diffrn.ambient_temperature' + +# %% +project.display.fit.series(versus=temperature) diff --git a/docs/docs/tutorials/ed-3.ipynb b/docs/docs/tutorials/ed-3.ipynb index 0f103f8aa..357607f26 100644 --- a/docs/docs/tutorials/ed-3.ipynb +++ b/docs/docs/tutorials/ed-3.ipynb @@ -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'" ] }, { diff --git a/docs/docs/tutorials/ed-3.py b/docs/docs/tutorials/ed-3.py index fcccb5968..7a194acd2 100644 --- a/docs/docs/tutorials/ed-3.py +++ b/docs/docs/tutorials/ed-3.py @@ -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 diff --git a/docs/docs/tutorials/ed-4.ipynb b/docs/docs/tutorials/ed-4.ipynb index bdf0343c4..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'" ] }, { diff --git a/docs/docs/tutorials/ed-4.py b/docs/docs/tutorials/ed-4.py index a0ea3f623..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 diff --git a/docs/docs/tutorials/ed-8.ipynb b/docs/docs/tutorials/ed-8.ipynb index 248d4f59e..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'" ] }, { diff --git a/docs/docs/tutorials/ed-8.py b/docs/docs/tutorials/ed-8.py index 82beedd7b..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 diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md index a11c73e60..a89601095 100644 --- a/docs/docs/tutorials/index.md +++ b/docs/docs/tutorials/index.md @@ -89,6 +89,9 @@ The tutorials are organized into the following categories: - [Co2SiO4 Temperature scan](ed-17.ipynb) – Sequential Rietveld refinement of Co2SiO4 using constant wavelength neutron powder diffraction data from D20 at ILL across a temperature scan. +- [Co2SiO4 Temperature scan, resumed](ed-23.ipynb) – Continue a saved + sequential refinement of Co2SiO4 from an existing + `analysis/results.csv` after an incomplete previous run. ## Simulated Data diff --git a/docs/docs/user-guide/analysis-workflow/analysis.md b/docs/docs/user-guide/analysis-workflow/analysis.md index b3c9e3d53..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 @@ -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/first-steps.md b/docs/docs/user-guide/first-steps.md index 66f96e908..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 diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index fdd6a30b0..c54ff9353 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -176,8 +176,6 @@ nav: - Introduction: introduction/index.md - Installation & Setup: - Installation & Setup: installation-and-setup/index.md - - Quick Reference: - - Quick Reference: quick-reference/index.md - User Guide: - User Guide: user-guide/index.md - Glossary: user-guide/glossary.md @@ -215,6 +213,7 @@ nav: - PbSO4 NPD+XRD: tutorials/ed-4.ipynb - Si Bragg+PDF: tutorials/ed-16.ipynb - Co2SiO4 T-scan: tutorials/ed-17.ipynb + - Co2SiO4 T-scan resumed: tutorials/ed-23.ipynb - Simulated Data: - LBCO+Si McStas: tutorials/ed-9.ipynb - BEER McStas: tutorials/ed-20.ipynb @@ -238,3 +237,5 @@ nav: - project: api-reference/project.md - summary: api-reference/summary.md - utils: api-reference/utils.md + - Quick Reference: + - Quick Reference: quick-reference/index.md diff --git a/pixi.lock b/pixi.lock index 4d6614f72..927dd5686 100644 --- a/pixi.lock +++ b/pixi.lock @@ -146,7 +146,7 @@ environments: - 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.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 @@ -177,6 +177,8 @@ environments: - 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 @@ -189,14 +191,13 @@ environments: - 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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.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 @@ -206,13 +207,13 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -224,6 +225,7 @@ environments: - 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 @@ -234,6 +236,7 @@ environments: - 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 @@ -243,7 +246,6 @@ environments: - 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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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 @@ -252,11 +254,11 @@ environments: - 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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl @@ -268,12 +270,9 @@ environments: - 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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.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/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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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 @@ -287,6 +286,7 @@ environments: - 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 @@ -299,6 +299,7 @@ environments: - 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 @@ -310,7 +311,6 @@ environments: - 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/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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 @@ -333,7 +333,7 @@ environments: - 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/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_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 @@ -341,11 +341,11 @@ environments: - 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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -435,7 +435,7 @@ environments: - 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.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 @@ -517,6 +517,8 @@ environments: - 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 @@ -530,27 +532,27 @@ environments: - 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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.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 @@ -572,6 +574,7 @@ environments: - 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 @@ -584,7 +587,6 @@ environments: - 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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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 @@ -593,11 +595,11 @@ environments: - 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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl @@ -608,12 +610,9 @@ environments: - 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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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 @@ -628,9 +627,9 @@ environments: - 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/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.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 @@ -640,6 +639,7 @@ environments: - 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 @@ -650,12 +650,12 @@ environments: - 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/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-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/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 @@ -680,11 +680,11 @@ environments: - 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/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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -772,7 +772,7 @@ environments: - 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.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 @@ -810,7 +810,7 @@ environments: - 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.12.2-default_h4379cf1_1000.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 @@ -822,10 +822,10 @@ environments: - 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_905.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_905.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 @@ -834,13 +834,13 @@ environments: - 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-ha3553a1_1.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_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/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 @@ -848,7 +848,9 @@ environments: - 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 @@ -860,11 +862,11 @@ environments: - 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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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 @@ -875,12 +877,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.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 @@ -902,6 +902,7 @@ environments: - 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 @@ -910,11 +911,9 @@ environments: - 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/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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 @@ -924,12 +923,12 @@ environments: - 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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl @@ -941,11 +940,9 @@ environments: - 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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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 @@ -959,11 +956,14 @@ environments: - 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 @@ -972,17 +972,18 @@ environments: - 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/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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 @@ -1010,14 +1011,13 @@ environments: - 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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.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/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 @@ -1167,7 +1167,7 @@ environments: - 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/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.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 @@ -1197,7 +1197,8 @@ environments: - 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/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/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 @@ -1210,11 +1211,11 @@ environments: - 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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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 @@ -1226,7 +1227,6 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -1252,6 +1252,7 @@ environments: - 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 @@ -1267,20 +1268,20 @@ environments: - 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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl @@ -1291,16 +1292,13 @@ environments: - 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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/43/53afb8ba17218f19b77c7834128566c5bbb100a0ad9ba2e8e89d089d7079/autopep8-2.3.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl @@ -1311,6 +1309,7 @@ environments: - 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 @@ -1323,6 +1322,7 @@ environments: - 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 @@ -1332,10 +1332,10 @@ environments: - 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/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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 @@ -1354,7 +1354,7 @@ environments: - 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/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_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/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 @@ -1363,11 +1363,11 @@ environments: - 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/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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -1455,7 +1455,7 @@ environments: - 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/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.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 @@ -1536,6 +1536,9 @@ environments: - 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 @@ -1550,10 +1553,10 @@ environments: - 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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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 @@ -1566,7 +1569,6 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -1586,7 +1588,7 @@ environments: - 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/44/7b/537a61906eac58d94131273084d21d4eb219f5453f0ed30de3aca580a2b4/scipp-26.3.1-cp312-cp312-macosx_14_0_arm64.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/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 @@ -1594,6 +1596,7 @@ environments: - 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 @@ -1604,7 +1607,6 @@ environments: - 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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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 @@ -1613,11 +1615,11 @@ environments: - 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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl @@ -1628,16 +1630,13 @@ environments: - 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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_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 @@ -1648,10 +1647,10 @@ environments: - 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/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/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.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 @@ -1663,6 +1662,7 @@ environments: - 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 @@ -1672,7 +1672,6 @@ environments: - 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/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-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 @@ -1680,6 +1679,7 @@ environments: - 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 @@ -1701,11 +1701,11 @@ environments: - 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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -1791,7 +1791,7 @@ environments: - 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/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.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 @@ -1830,7 +1830,7 @@ environments: - 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.12.2-default_h4379cf1_1000.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 @@ -1841,10 +1841,10 @@ environments: - 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_905.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_905.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 @@ -1853,13 +1853,13 @@ environments: - 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-ha3553a1_1.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_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/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 @@ -1869,6 +1869,8 @@ environments: - 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 @@ -1883,34 +1885,31 @@ environments: - 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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.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/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.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 @@ -1925,6 +1924,7 @@ environments: - 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 @@ -1937,7 +1937,6 @@ environments: - 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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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 @@ -1946,13 +1945,14 @@ environments: - 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/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-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 @@ -1965,11 +1965,9 @@ environments: - 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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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 @@ -1983,10 +1981,12 @@ environments: - 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 @@ -1995,6 +1995,7 @@ environments: - 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 @@ -2006,7 +2007,6 @@ environments: - 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/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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 @@ -2032,12 +2032,12 @@ environments: - 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/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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -2185,7 +2185,7 @@ environments: - 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.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 @@ -2216,6 +2216,8 @@ environments: - 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 @@ -2228,14 +2230,13 @@ environments: - 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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.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 @@ -2245,13 +2246,13 @@ environments: - 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/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -2263,6 +2264,7 @@ environments: - 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 @@ -2273,6 +2275,7 @@ environments: - 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 @@ -2282,7 +2285,6 @@ environments: - 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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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 @@ -2291,11 +2293,11 @@ environments: - 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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl @@ -2307,12 +2309,9 @@ environments: - 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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.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/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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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 @@ -2326,6 +2325,7 @@ environments: - 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 @@ -2338,6 +2338,7 @@ environments: - 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 @@ -2349,7 +2350,6 @@ environments: - 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/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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 @@ -2372,7 +2372,7 @@ environments: - 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/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_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 @@ -2380,11 +2380,11 @@ environments: - 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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -2474,7 +2474,7 @@ environments: - 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.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 @@ -2556,6 +2556,8 @@ environments: - 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 @@ -2569,27 +2571,27 @@ environments: - 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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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/2a/2d/d4bf65e47cea8ff2c794a600c4fd1273a7902f268757c531e0ee9f18aa58/pooch-1.9.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.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 @@ -2611,6 +2613,7 @@ environments: - 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 @@ -2623,7 +2626,6 @@ environments: - 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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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 @@ -2632,11 +2634,11 @@ environments: - 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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl @@ -2647,12 +2649,9 @@ environments: - 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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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 @@ -2667,9 +2666,9 @@ environments: - 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/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.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 @@ -2679,6 +2678,7 @@ environments: - 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 @@ -2689,12 +2689,12 @@ environments: - 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/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-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/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 @@ -2719,11 +2719,11 @@ environments: - 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/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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -2811,7 +2811,7 @@ environments: - 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.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 @@ -2849,7 +2849,7 @@ environments: - 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.12.2-default_h4379cf1_1000.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 @@ -2861,10 +2861,10 @@ environments: - 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_905.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_905.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 @@ -2873,13 +2873,13 @@ environments: - 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-ha3553a1_1.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_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/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 @@ -2887,7 +2887,9 @@ environments: - 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 @@ -2899,11 +2901,11 @@ environments: - 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/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-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 @@ -2914,12 +2916,10 @@ environments: - pypi: https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2f/c8/005d1de3af80f54411703d1263a0b9d31276411ec9f273d9432c59b17299/arviz_plots-1.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.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 @@ -2941,6 +2941,7 @@ environments: - 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 @@ -2949,11 +2950,9 @@ environments: - 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/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.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/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.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/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 @@ -2963,12 +2962,12 @@ environments: - 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/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl @@ -2980,11 +2979,9 @@ environments: - 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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.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/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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.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 @@ -2998,11 +2995,14 @@ environments: - 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 @@ -3011,17 +3011,18 @@ environments: - 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/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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 @@ -3049,14 +3050,13 @@ environments: - 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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-win_amd64.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/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 @@ -3188,7 +3188,7 @@ environments: - 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.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 @@ -3222,16 +3222,17 @@ environments: - 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/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.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 @@ -3240,6 +3241,7 @@ environments: - 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 @@ -3248,6 +3250,7 @@ environments: - 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 @@ -3266,9 +3269,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/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/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 @@ -3292,7 +3293,6 @@ environments: - 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/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/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 @@ -3399,7 +3399,7 @@ environments: - 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.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 @@ -3466,13 +3466,15 @@ environments: - 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/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.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 @@ -3488,6 +3490,7 @@ environments: - 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 @@ -3508,8 +3511,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/8f/5e/f1e1dd319e35e962a4e00b33150a8868b6329cc1d19fd533436ba5488f09/uncertainties-3.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/90/ad/cba91b3bcf04073e4d1655a5c1710ef3f457f56f7d1b79dcc3d72f4dd912/plotly-6.7.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-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/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 @@ -3535,7 +3536,6 @@ environments: - 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/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-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 @@ -3639,7 +3639,7 @@ environments: - 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.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 @@ -3692,9 +3692,9 @@ environments: - 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_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/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 @@ -3705,6 +3705,7 @@ environments: - 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 @@ -3727,12 +3728,12 @@ environments: - 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/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.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 @@ -3750,7 +3751,6 @@ environments: - 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/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/96/5d/0c59079aa7ef34980a5925a06a90ad2b7c94e486c194b3527d557cabb042/cryspy-0.11.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl @@ -3763,6 +3763,7 @@ environments: - 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 @@ -3773,10 +3774,10 @@ environments: - 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/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.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/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl @@ -3794,7 +3795,6 @@ environments: - 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/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/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 @@ -5628,7 +5628,7 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/matplotlib-inline?source=compressed-mapping + - 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 @@ -6013,9 +6013,9 @@ packages: - pkg:pypi/referencing?source=hash-mapping size: 51788 timestamp: 1760379115194 -- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda - sha256: 4487fdb341537e2df47159ed8e546add99080974c52d5b2dc2a710910619115a - md5: a5985537dab1ba7034b5ff4ea22e2fa9 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.2-pyhcf101f3_0.conda + sha256: 1715246b19c9f85ee022933b4845f2fc14ac9184981b7b7d9b728bec8e9588da + md5: 4a85203c1d80c1059086ae860836ffb9 depends: - python >=3.10 - certifi >=2023.5.7 @@ -6026,11 +6026,10 @@ packages: constrains: - chardet >=3.0.2,<8 license: Apache-2.0 - license_family: APACHE purls: - - pkg:pypi/requests?source=hash-mapping - size: 68658 - timestamp: 1778534036810 + - 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 @@ -7504,9 +7503,9 @@ packages: purls: [] size: 45831 timestamp: 1769456418774 -- conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.2-default_h4379cf1_1000.conda - sha256: 8cdf11333a81085468d9aa536ebb155abd74adc293576f6013fc0c85a7a90da3 - md5: 3b576f6860f838f950c570f4433b086e +- conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.13.0-default_h049141e_1000.conda + sha256: 2ee12e37223dfcd0acd050c80a91150c482b6e2899198521e1800dce66662467 + md5: 6a01c986e30292c715038d2788aa1385 depends: - libwinpthread >=12.0.0.r4.gg4f2fc60ca - libxml2 @@ -7517,8 +7516,8 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] - size: 2411241 - timestamp: 1765104337762 + size: 2396128 + timestamp: 1770954127918 - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda sha256: 0dcdb1a5f01863ac4e8ba006a8b0dc1a02d2221ec3319b5915a1863254d7efa7 md5: 64571d1dd6cdcfa25d0664a5950fdaa2 @@ -7688,12 +7687,12 @@ packages: - pkg:pypi/markupsafe?source=hash-mapping size: 30022 timestamp: 1772445159549 -- conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2026.0.0-hac47afa_905.conda - sha256: 76a43359adae10aef8de7ff8e4fab70647bda928146374298506afab2e4a7b4f - md5: 7741affec1b3d2275586397ed4c91639 +- 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.4 - - onemkl-license 2026.0.0 h57928b3_905 + - 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 @@ -7701,8 +7700,8 @@ packages: license: LicenseRef-IntelSimplifiedSoftwareOct2022 license_family: Proprietary purls: [] - size: 114620200 - timestamp: 1778111077072 + size: 114608976 + timestamp: 1778776186500 - conda: https://conda.anaconda.org/conda-forge/win-64/msgspec-0.21.1-py312he06e257_0.conda sha256: 003de3343b481937b5eb500ecdbfc882e87cea608be3741dc1fb13d22f8ed95e md5: 1f32f4f6aa595377a7e651e67ba53d30 @@ -7741,14 +7740,14 @@ packages: purls: [] size: 31271315 timestamp: 1774517904472 -- conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2026.0.0-h57928b3_905.conda - sha256: 848a7215e1ce227139074461664d01c00e7e1e8a367ccbd6581c0860d6ec4a19 - md5: fea22e21062046ba44336de37f4b6372 +- 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: 41103 - timestamp: 1778110756075 + size: 41154 + timestamp: 1778775952813 - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda sha256: feb5815125c60f2be4a411e532db1ed1cd2d7261a6a43c54cb6ae90724e2e154 md5: 05c7d624cff49dbd8db1ad5ba537a8a3 @@ -7987,19 +7986,18 @@ packages: - pkg:pypi/rpds-py?source=hash-mapping size: 235780 timestamp: 1764543046065 -- conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-ha3553a1_1.conda - sha256: 5ff149ba6832bf4ded4b43bf0a41cde7be814802a95070553176c087f65b2a01 - md5: 34aa94d586fe95fa121966c0d4e73cf4 +- conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-hd3d4ead_2.conda + sha256: 8a4053839b8e997a5965e2dff7d6cf3c77be62d82c0e48c8a04a5ed2d2e73035 + md5: 8ee01a693aecff5432069eaaf1183c45 depends: - - libhwloc >=2.12.2,<2.12.3.0a0 + - 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 - license_family: APACHE purls: [] - size: 156910 - timestamp: 1777976465531 + size: 156515 + timestamp: 1778673901757 - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda sha256: 0e79810fae28f3b69fe7391b0d43f5474d6bd91d451d5f2bde02f55ae481d5e3 md5: 0481bfd9814bf525bd4b3ee4b51494c4 @@ -8052,9 +8050,9 @@ packages: purls: [] size: 694692 timestamp: 1756385147981 -- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda - sha256: 9dc40c2610a6e6727d635c62cced5ef30b7b30123f5ef67d6139e23d21744b3a - md5: 1e610f2416b6acdd231c5f573d754a0f +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda + sha256: 7c86d8ed3ac473c3e4dde0dd05aeb1f3189a26ad66c0e250f6cf4018e73358f2 + md5: 3466ff4a8753003eeb173f508d3d5a49 depends: - vc14_runtime >=14.44.35208 track_features: @@ -8062,33 +8060,33 @@ packages: 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 + 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: - ucrt >=10.0.20348.0 - - vcomp14 14.44.35208 h818238b_34 + - vcomp14 14.44.35208 h818238b_36 constrains: - - vs2015_runtime 14.44.35208.* *_34 + - vs2015_runtime 14.44.35208.* *_36 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 + 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: - ucrt >=10.0.20348.0 constrains: - - vs2015_runtime 14.44.35208.* *_34 + - vs2015_runtime 14.44.35208.* *_36 license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime license_family: Proprietary purls: [] - size: 115235 - timestamp: 1767320173250 + size: 115995 + timestamp: 1778688058077 - conda: https://conda.anaconda.org/conda-forge/win-64/winpty-0.4.3-4.tar.bz2 sha256: 9df10c5b607dd30e05ba08cbd940009305c75db242476f4e845ea06008b0a283 md5: 1cee351bf20b830d991dbe0bc8cd7dfe @@ -8333,11 +8331,40 @@ packages: - sphinx ; extra == 'docs' - furo ; extra == 'docs' requires_python: '>=3.8' -- 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/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 @@ -8382,6 +8409,16 @@ packages: - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' requires_python: '>=3.11' +- 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: + - 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 @@ -8389,6 +8426,26 @@ packages: 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 @@ -8694,6 +8751,28 @@ packages: - 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 @@ -8773,19 +8852,6 @@ packages: requires_dist: - nbformat requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/20/5b/885f479093f6627669d39b57bc3d4e674da532e1a4b247d473a61d8d2118/virtualenv-21.3.2-py3-none-any.whl - name: virtualenv - version: 21.3.2 - sha256: c58ea748fa50bb2a4367da5ba3d30b02458ed40b4ea888faad94021f3309f764 - 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/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl name: gitpython version: 3.1.50 @@ -8871,24 +8937,21 @@ packages: - multidict>=4.0 - propcache>=0.2.1 requires_python: '>=3.10' +- 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/25/7d/cea3531f77df694ac7f169378250d85f19f69b09a5f4fa45f650837ae7cc/py3dmol-2.5.4-py2.py3-none-any.whl name: py3dmol version: 2.5.4 sha256: 32806726b5310524a2b5bfee320737f7feef635cafc945c991062806daa9e43a requires_dist: - ipython ; extra == 'ipython' -- pypi: https://files.pythonhosted.org/packages/28/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/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl name: fonttools - version: 4.62.1 - sha256: 8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae + version: 4.63.0 + sha256: fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745 requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -8919,6 +8982,14 @@ packages: - 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 @@ -8938,40 +9009,6 @@ packages: - pyupgrade ; extra == 'toolchain' - ruff ; extra == 'toolchain' requires_python: '>=3.9' -- 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/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl name: mkdocs-autorefs version: 1.4.4 @@ -9106,11 +9143,6 @@ packages: - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' - sqlcipher3-binary ; extra == 'sqlcipher' requires_python: '>=3.7' -- 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/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 @@ -9181,23 +9213,6 @@ packages: - h5netcdf[h5py] ; extra == 'test' - kaleido ; extra == 'test' requires_python: '>=3.12' -- pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl - name: python-discovery - version: 1.3.0 - sha256: 441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f - requires_dist: - - filelock>=3.15.4 - - platformdirs>=4.3.6,<5 - - furo>=2025.12.19 ; extra == 'docs' - - sphinx-autodoc-typehints>=3.6.3 ; extra == 'docs' - - sphinx>=9.1 ; extra == 'docs' - - sphinxcontrib-mermaid>=2 ; extra == 'docs' - - covdefaults>=2.3 ; extra == 'testing' - - coverage>=7.5.4 ; extra == 'testing' - - pytest-mock>=3.14 ; extra == 'testing' - - pytest>=8.3.5 ; extra == 'testing' - - setuptools>=75.1 ; extra == 'testing' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl name: mkdocstrings-python version: 2.0.3 @@ -9310,11 +9325,6 @@ packages: version: 6.2.0 sha256: a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44 requires_python: '>=3.9' -- 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/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-cp312-cp312-macosx_11_0_arm64.whl name: coverage version: 7.14.0 @@ -9334,10 +9344,10 @@ packages: requires_dist: - regex ; extra == 'extras' requires_python: '>=3.10' -- 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/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: fonttools - version: 4.62.1 - sha256: fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca + version: 4.63.0 + sha256: 308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -9522,6 +9532,11 @@ packages: requires_dist: - prompt-toolkit>=2.0,<4.0 requires_python: '>=3.9' +- 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 @@ -9622,6 +9637,11 @@ packages: 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 @@ -9764,40 +9784,11 @@ packages: - typing-extensions!=3.10.0.1 ; extra == 'aiosqlite' - sqlcipher3-binary ; extra == 'sqlcipher' requires_python: '>=3.7' -- 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/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 @@ -10234,6 +10225,14 @@ packages: 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 @@ -10632,40 +10631,6 @@ packages: version: 1.8.0 sha256: 494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383 requires_python: '>=3.9' -- 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/6c/25/4f103d1bedb3593718713b3f743df7b3ff3fc68d36d6666c30265ef59c8a/ase-3.28.0-py3-none-any.whl name: ase version: 3.28.0 @@ -10714,21 +10679,6 @@ packages: requires_dist: - diffpy-structure requires_python: '>=3.12,<3.15' -- pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl - name: mdit-py-plugins - version: 0.6.0 - sha256: f7e7a25d8b616fee99cb1e330da73451d11a8061baf39bb9663ab9ce0e005b90 - 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/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl name: partd version: 1.4.2 @@ -10771,6 +10721,40 @@ packages: - 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 @@ -10847,6 +10831,15 @@ packages: - pytest-xdist ; extra == 'test-no-images' - wurlitzer ; extra == 'test-no-images' requires_python: '>=3.11' +- 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: + - 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 @@ -10900,15 +10893,39 @@ packages: requires_dist: - nbformat requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - name: pydoclint - version: 0.8.3 - sha256: 5fc9b82d0d515afce0908cb70e8ff695a68b19042785c248c4f227ad66b4a164 +- 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: - - click>=8.1.0 - - docstring-parser-fork>=0.0.12 - - tomli>=2.0.1 ; python_full_version < '3.11' - - flake8>=4 ; extra == 'flake8' + - 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 @@ -11150,19 +11167,6 @@ packages: - sphinx-autodoc-typehints ; extra == 'docs' - sphinx-rtd-theme>=0.2.5 ; extra == 'docs' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/91/88/b55b3117287a8540b76dbdd87733808d4d01c8067a3b339408c250bb3600/typeguard-4.5.1-py3-none-any.whl - name: typeguard - version: 4.5.1 - sha256: 44d2bf329d49a244110a090b55f5f91aa82d9a9834ebfd30bcc73651e4a8cc40 - requires_dist: - - importlib-metadata>=3.6 ; python_full_version < '3.10' - - typing-extensions>=4.14.0 - requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/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/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl name: radon version: 6.0.1 @@ -11195,31 +11199,6 @@ packages: requires_dist: - numpy requires_python: '>=3.11,<3.15' -- 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/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/99/31/6cf181011dc738c33bf6ba7aea2e8e1d3c1f71b7dab1942f3054f66f6202/asteval-1.0.8-py3-none-any.whl name: asteval version: 1.0.8 @@ -11250,40 +11229,6 @@ packages: version: 1.5.0 sha256: ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819 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/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl name: aiohttp version: 3.13.5 @@ -11302,11 +11247,6 @@ packages: - 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/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/9c/2f/4c5af01fd1a7506a1d5375403d68925eac70289229492db5aa68b58103d8/chardet-7.4.3-cp312-cp312-macosx_11_0_arm64.whl name: chardet version: 7.4.3 @@ -11494,6 +11434,21 @@ packages: 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 @@ -11536,11 +11491,11 @@ packages: - sphinx ; extra == 'docs' - furo ; extra == 'docs' requires_python: '>=3.8' -- 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/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 @@ -11588,6 +11543,11 @@ packages: - 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/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl name: jupyterlab-widgets version: 3.0.16 @@ -11732,6 +11692,25 @@ packages: - 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' @@ -12057,6 +12036,40 @@ packages: 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: + - 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 @@ -12126,28 +12139,6 @@ packages: - sphinx-rtd-theme ; extra == 'dev' - trustregion>=1.1 ; extra == 'trustregion' requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/c7/e1/68c2256b69a314eba133673377ba9118c356f6342a0c02b61de449cf2bf2/narwhals-2.21.0-py3-none-any.whl - name: narwhals - version: 2.21.0 - sha256: 1e6617d0fca68ae1fda29e5397c4eaacd3ffc9fffe6bcd6ded0c690475e853be - requires_dist: - - cudf-cu12>=24.10.0 ; extra == 'cudf' - - dask[dataframe]>=2024.8 ; extra == 'dask' - - duckdb>=1.1 ; extra == 'duckdb' - - ibis-framework>=6.0.0 ; extra == 'ibis' - - packaging ; extra == 'ibis' - - pyarrow-hotfix ; extra == 'ibis' - - rich ; extra == 'ibis' - - modin ; extra == 'modin' - - pandas>=1.1.3 ; extra == 'pandas' - - polars>=0.20.4 ; extra == 'polars' - - pyarrow>=13.0.0 ; extra == 'pyarrow' - - pyspark>=3.5.0 ; extra == 'pyspark' - - pyspark[connect]>=3.5.0 ; extra == 'pyspark-connect' - - duckdb>=1.1 ; extra == 'sql' - - sqlparse ; extra == 'sql' - - sqlframe>=3.22.0,!=3.39.3 ; extra == 'sqlframe' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/c7/ea/7988934c8e3e3418aa043f70421817df28d06aef50bfd85f5ad3ec6e70f1/ncrystal_core-4.4.2-py3-none-macosx_11_0_arm64.whl name: ncrystal-core version: 4.4.2 @@ -12398,6 +12389,11 @@ packages: - 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 @@ -12412,6 +12408,11 @@ packages: - 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 @@ -12833,10 +12834,10 @@ packages: version: 1.5.0 sha256: 80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac requires_python: '>=3.10' -- 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/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: ruff - version: 0.15.12 - sha256: 83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0 + 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 @@ -12933,6 +12934,19 @@ packages: requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' +- 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: + - 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 @@ -12996,15 +13010,6 @@ packages: - 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/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl - name: pymdown-extensions - version: 10.21.2 - sha256: 5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638 - requires_dist: - - markdown>=3.6 - - pyyaml - - pygments>=2.19.1 ; extra == 'extra' - requires_python: '>=3.9' - pypi: https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl name: ghp-import version: 2.1.0 @@ -13022,11 +13027,6 @@ packages: requires_dist: - tomli ; python_full_version <= '3.11' and extra == 'toml' requires_python: '>=3.10' -- 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' - pypi: https://files.pythonhosted.org/packages/fa/bc/8b8ec5a4bfc5b9cf3ce27a118339e994f88410be5677c96493e0ea28e76d/dunamai-1.26.1-py3-none-any.whl name: dunamai version: 1.26.1 diff --git a/pyproject.toml b/pyproject.toml index 71bf2f71b..3a4b78a01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,7 +168,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/__main__.py b/src/easydiffraction/__main__.py index 18220904f..ca1d3f8c4 100644 --- a/src/easydiffraction/__main__.py +++ b/src/easydiffraction/__main__.py @@ -99,8 +99,9 @@ def fit( if dry: project.info._path = None project.analysis.fit() - project.display.fit.results() - project.display.fit.correlations() + if getattr(project.analysis, 'fitting_mode_type', None) != 'sequential': + project.display.fit.results() + project.display.fit.correlations() for expt in project.experiments: project.display.pattern(expt_name=expt.name) # project.summary.show_report() diff --git a/src/easydiffraction/analysis/__init__.py b/src/easydiffraction/analysis/__init__.py index 78150ea54..0fe4386c4 100644 --- a/src/easydiffraction/analysis/__init__.py +++ b/src/easydiffraction/analysis/__init__.py @@ -1,2 +1,16 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + +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 FitModeEnum diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index a2a4f7dee..863352413 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -4,17 +4,24 @@ from __future__ import annotations from contextlib import suppress +from pathlib import Path import numpy as np import pandas as pd from easydiffraction.analysis.categories.aliases.factory import AliasesFactory 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.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 FitModeEnum from easydiffraction.analysis.fitting import Fitter +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 @@ -363,10 +370,16 @@ def __init__(self, project: object) -> None: 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._fitting: Fitting = FittingFactory.create(FittingFactory.default_tag()) + self._fitting._parent = self + self._fitting_mode_type: FitModeEnum = FitModeEnum.default() + self._joint_fit: JointFitCollection = JointFitCollection() + self._sequential_fit: SequentialFit = SequentialFitFactory.create( + SequentialFitFactory.default_tag() + ) + self._sequential_fit._parent = self + self._sequential_fit_extract = SequentialFitExtractCollection() + self.fitter = Fitter(self._fitting.minimizer_type.value) self.fit_results = None self._parameter_snapshots: dict[str, dict[str, dict]] = {} self._display = AnalysisDisplay(self) @@ -378,7 +391,67 @@ def display(self) -> AnalysisDisplay: def help(self) -> None: """Print a summary of analysis properties and methods.""" - render_object_help(self) + cls = type(self) + console.paragraph(f"Help for '{cls.__name__}'") + + 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=filtered_property_rows, + ) + + if filtered_method_rows: + console.paragraph('Methods') + render_table( + columns_headers=['#', 'Name', 'Description'], + columns_alignment=['right', 'left', 'left'], + 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 # ------------------------------------------------------------------ # Parameter helpers @@ -434,100 +507,239 @@ 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 - def _run_fit( - self, - verbosity: str | None = None, - *, - use_physical_limits: bool = False, - random_seed: int | None = None, - ) -> None: - """ - Execute fitting for all experiments. - - 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. - - 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. + @property + def sequential_fit(self) -> SequentialFit: + """Persisted settings for sequential fitting.""" + return self._sequential_fit - Sets :attr:`fit_results` on success, which can be accessed - programmatically (e.g., - ``analysis.fit_results.reduced_chi_square``). + @property + def sequential_fit_extract(self) -> SequentialFitExtractCollection: + """Persisted extract rules for sequential fitting.""" + return self._sequential_fit_extract - 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. - random_seed : int | None, default=None - Optional random seed passed to stochastic minimizers. + def _resolve_sequential_data_dir(self) -> Path: """ - verb = VerbosityEnum(verbosity if verbosity is not None else self.project.verbosity) + 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) 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 user-constrained # parameters are marked and excluded from the free parameter # list built by the fitter. 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, - random_seed=random_seed, - ) - elif mode is FitModeEnum.SINGLE: - self._fit_single( - verb, - structures, - experiments, - use_physical_limits=use_physical_limits, - random_seed=random_seed, - ) - 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 - # After fitting, save the project + 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 + + 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() + + 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 + if self.project.info.path is not None: self.project.save() @@ -557,19 +769,17 @@ def _fit_joint( 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, @@ -742,79 +952,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, *, diff --git a/src/easydiffraction/analysis/categories/__init__.py b/src/easydiffraction/analysis/categories/__init__.py index 4e798e209..6c070cf1a 100644 --- a/src/easydiffraction/analysis/categories/__init__.py +++ b/src/easydiffraction/analysis/categories/__init__.py @@ -1,2 +1,15 @@ # 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.constraints import Constraint +from easydiffraction.analysis.categories.constraints import Constraints +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/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/default.py b/src/easydiffraction/analysis/categories/fit/default.py deleted file mode 100644 index 7a9ebcb12..000000000 --- a/src/easydiffraction/analysis/categories/fit/default.py +++ /dev/null @@ -1,212 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -""" -Fit category item. - -Stores the active minimizer and fitting mode as CIF-serializable -descriptors and provides the public entry-point for running fits. -""" - -from __future__ import annotations - -from easydiffraction.analysis.categories.fit.enums import FitModeEnum -from easydiffraction.analysis.categories.fit.factory import FitFactory -from easydiffraction.analysis.fitting import Fitter -from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum -from easydiffraction.analysis.minimizers.factory import MinimizerFactory -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.utils.logging import console -from easydiffraction.utils.logging import log -from easydiffraction.utils.utils import render_table - - -@FitFactory.register -class Fit(CategoryItem): - """ - Analysis fitting configuration and execution entry-point. - - Holds the active minimizer backend tag and fit mode value. - """ - - type_info = TypeInfo( - tag='default', - description='Fit configuration category', - ) - - def __init__(self) -> None: - super().__init__() - - self._minimizer_type: StringDescriptor = StringDescriptor( - name='minimizer_type', - description='Fitting minimizer backend type', - value_spec=AttributeSpec( - default=MinimizerTypeEnum.default().value, - validator=MembershipValidator( - 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']), - ) - - self._identity.category_code = 'fit' - - @property - def minimizer_type(self) -> StringDescriptor: - """Fitting minimizer backend type.""" - return self._minimizer_type - - @minimizer_type.setter - def minimizer_type(self, value: str) -> None: - new_fitter = Fitter(value) - self._minimizer_type.value = value - parent = getattr(self, '_parent', None) - if parent is None: - return - parent.fitter = new_fitter - console.paragraph('Current minimizer changed to') - console.print(self._minimizer_type.value) - - @property - def minimizer(self) -> object | None: - """Live minimizer backend instance, if attached to Analysis.""" - parent = getattr(self, '_parent', None) - if parent is None or getattr(parent, 'fitter', None) is 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 - supported = MinimizerFactory.supported_tags() - all_classes = MinimizerFactory._supported_map() - columns_data = [ - ['*' if tag == current else '', tag, cls.type_info.description] - for tag, cls in all_classes.items() - if tag in supported - ] - console.paragraph('Minimizer types') - render_table( - columns_headers=['', 'Type', 'Description'], - columns_alignment=['left', 'left', 'left'], - columns_data=columns_data, - ) - - @staticmethod - 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, - random_seed: int | None = None, - ) -> 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. - random_seed : int | None, default=None - Optional random seed passed to stochastic minimizers. - - 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, - random_seed=random_seed, - ) - - def __call__( - self, - verbosity: str | None = None, - *, - use_physical_limits: bool = False, - random_seed: int | None = None, - ) -> None: - """Execute :meth:`run` for convenience.""" - self.run( - verbosity=verbosity, - use_physical_limits=use_physical_limits, - random_seed=random_seed, - ) - - def from_cif(self, block: object, idx: int = 0) -> None: - """ - Populate this fit configuration from a CIF block. - - Parameters - ---------- - block : object - Parsed CIF block. - idx : int, default=0 - Row index for loop-like callers; unused for this category. - """ - super().from_cif(block, idx) - parent = getattr(self, '_parent', None) - if parent is None: - return - try: - parent.fitter = Fitter(self._minimizer_type.value) - except ValueError as error: - log.warning(str(error)) 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/fitting/default.py b/src/easydiffraction/analysis/categories/fitting/default.py new file mode 100644 index 000000000..925259cfc --- /dev/null +++ b/src/easydiffraction/analysis/categories/fitting/default.py @@ -0,0 +1,125 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +""" +Fitting category item. + +Stores the active minimizer as a CIF-serializable descriptor. +""" + +from __future__ import annotations + +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 +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.utils.logging import console +from easydiffraction.utils.logging import log +from easydiffraction.utils.utils import render_table + + +@FittingFactory.register +class Fitting(CategoryItem): + """ + Analysis fitting configuration category. + + Holds the active minimizer backend tag. + """ + + type_info = TypeInfo( + tag='default', + description='Fitting configuration category', + ) + + def __init__(self) -> None: + super().__init__() + + self._minimizer_type: StringDescriptor = StringDescriptor( + name='minimizer_type', + description='Fitting minimizer backend type', + value_spec=AttributeSpec( + default=MinimizerTypeEnum.default().value, + validator=MembershipValidator( + allowed=[member.value for member in MinimizerTypeEnum] + ), + ), + cif_handler=CifHandler(names=['_fitting.minimizer_type']), + ) + + self._identity.category_code = 'fitting' + + @property + def minimizer_type(self) -> StringDescriptor: + """Fitting minimizer backend type.""" + return self._minimizer_type + + @minimizer_type.setter + def minimizer_type(self, value: str) -> None: + new_fitter = Fitter(value) + self._minimizer_type.value = value + parent = getattr(self, '_parent', None) + if parent is None: + return + parent.fitter = new_fitter + console.paragraph('Current minimizer changed to') + console.print(self._minimizer_type.value) + + @property + def minimizer(self) -> object | None: + """Live minimizer backend instance, if attached to Analysis.""" + parent = getattr(self, '_parent', None) + if parent is None or getattr(parent, 'fitter', None) is None: + return None + return parent.fitter.minimizer + + def show_minimizer_types(self) -> None: + """Print supported minimizers and mark the current selection.""" + current = self.minimizer_type.value + supported = MinimizerFactory.supported_tags() + all_classes = MinimizerFactory._supported_map() + columns_data = [ + ['*' if tag == current else '', tag, cls.type_info.description] + for tag, cls in all_classes.items() + if tag in supported + ] + console.paragraph('Minimizer types') + render_table( + columns_headers=['', 'Type', 'Description'], + columns_alignment=['left', 'left', 'left'], + columns_data=columns_data, + ) + + @staticmethod + def show_available_minimizers() -> None: + """Print available minimizer drivers on this system.""" + MinimizerFactory.show_supported() + + @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 fitting configuration from a CIF block. + + Parameters + ---------- + block : object + Parsed CIF block. + idx : int, default=0 + Row index for loop-like callers; unused for this category. + """ + super().from_cif(block, idx) + parent = getattr(self, '_parent', None) + if parent is None: + return + try: + parent.fitter = Fitter(self._minimizer_type.value) + except ValueError as error: + log.warning(str(error)) diff --git a/src/easydiffraction/analysis/categories/joint_fit_experiments/factory.py b/src/easydiffraction/analysis/categories/fitting/factory.py similarity index 65% rename from src/easydiffraction/analysis/categories/joint_fit_experiments/factory.py rename to src/easydiffraction/analysis/categories/fitting/factory.py index 992af7270..b88bf9178 100644 --- a/src/easydiffraction/analysis/categories/joint_fit_experiments/factory.py +++ b/src/easydiffraction/analysis/categories/fitting/factory.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Joint-fit-experiments factory — delegates to ``FactoryBase``.""" +"""Fitting factory - delegates entirely to ``FactoryBase``.""" 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 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..62f9e7e36 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,20 @@ from easydiffraction.io.cif.handler import CifHandler -class JointFitExperiment(CategoryItem): +class JointFitItem(CategoryItem): """A single joint-fit entry.""" def __init__(self) -> None: super().__init__() - self._id: StringDescriptor = StringDescriptor( - name='id', # TODO: need new name instead of id + 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 +43,14 @@ 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 - # ------------------------------------------------------------------ + self._identity.category_code = 'joint_fit' + self._identity.category_entry_name = lambda: str(self.experiment_id.value) @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/fit/factory.py b/src/easydiffraction/analysis/categories/joint_fit/factory.py similarity index 70% rename from src/easydiffraction/analysis/categories/fit/factory.py rename to src/easydiffraction/analysis/categories/joint_fit/factory.py index 37bef2bbc..fa15edc00 100644 --- a/src/easydiffraction/analysis/categories/fit/factory.py +++ b/src/easydiffraction/analysis/categories/joint_fit/factory.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Fit factory — delegates entirely to ``FactoryBase``.""" +"""Joint-fit factory - delegates to ``FactoryBase``.""" from __future__ import annotations @@ -9,8 +9,8 @@ from easydiffraction.core.factory import FactoryBase -class FitFactory(FactoryBase): - """Create fit category items by tag.""" +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..aad826f18 --- /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.""" + + 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']), + ) + + self._identity.category_code = 'sequential_fit' + + @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..84bbf70c5 --- /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.""" + + 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']), + ) + + self._identity.category_code = 'sequential_fit_extract' + self._identity.category_entry_name = lambda: str(self.id.value) + + @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/categories/fit/enums.py b/src/easydiffraction/analysis/enums.py similarity index 73% rename from src/easydiffraction/analysis/categories/fit/enums.py rename to src/easydiffraction/analysis/enums.py index a5b87054e..c1ef93f7b 100644 --- a/src/easydiffraction/analysis/categories/fit/enums.py +++ b/src/easydiffraction/analysis/enums.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Enumeration for fit-mode values.""" +"""Enumeration types used by analysis components.""" from __future__ import annotations @@ -22,9 +22,9 @@ def default(cls) -> FitModeEnum: def description(self) -> str: """Return a human-readable description of this fit mode.""" if self is FitModeEnum.SINGLE: - return 'Independent fitting of each experiment' + return 'Fit one experiment at a time.' if self is FitModeEnum.JOINT: - return 'Simultaneous fitting of all experiments with weights' + return 'Fit several experiments together with shared parameters.' if self is FitModeEnum.SEQUENTIAL: - return 'Sequential fitting over data files in a directory' + return 'Fit one experiment against a series of data files.' return '' diff --git a/src/easydiffraction/analysis/fit_helpers/bayesian.py b/src/easydiffraction/analysis/fit_helpers/bayesian.py index e10d9c698..ca191c835 100644 --- a/src/easydiffraction/analysis/fit_helpers/bayesian.py +++ b/src/easydiffraction/analysis/fit_helpers/bayesian.py @@ -42,8 +42,8 @@ class PosteriorParameterSummary: Unique parameter name used across EasyDiffraction. display_name : str Human-readable label used in plots and tables. - map_value : float - Maximum-a-posteriori or best-sampled parameter value. + best_sample_value : float + Highest-posterior sampled parameter value. median : float Posterior median value. standard_deviation : float @@ -60,7 +60,7 @@ class PosteriorParameterSummary: unique_name: str display_name: str - map_value: float + best_sample_value: float median: float standard_deviation: float interval_68: tuple[float, float] @@ -82,7 +82,7 @@ class PosteriorPredictiveSummary: Name of the x-axis used for the predictive arrays. x : np.ndarray X-axis values for the predictive curves. - map_prediction : np.ndarray + 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. @@ -99,7 +99,7 @@ class PosteriorPredictiveSummary: experiment_name: str x_axis_name: str x: np.ndarray - map_prediction: 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 @@ -212,7 +212,7 @@ class BayesianFitResults(FitResults): Total fitting time in seconds. sampler_name : str, default='dream' Sampler identifier. - point_estimate_name : str, default='map' + 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. @@ -239,7 +239,7 @@ class BayesianFitResults(FitResults): starting_parameters: list[object] | None = None fitting_time: float | None = None sampler_name: str = 'dream' - point_estimate_name: str = 'map' + point_estimate_name: str = 'best_sample' posterior_samples: PosteriorSamples | None = None posterior_parameter_summaries: SummaryList = None posterior_predictive: PredictiveMap = None @@ -412,7 +412,7 @@ def compute_convergence_diagnostics(posterior_samples: PosteriorSamples) -> dict def summarize_posterior_parameters( parameter_names: list[str], posterior_samples: PosteriorSamples, - map_values: np.ndarray, + best_sample_values: np.ndarray, parameter_display_names: list[str] | None = None, convergence_diagnostics: dict[str, object] | None = None, ) -> list[PosteriorParameterSummary]: @@ -425,8 +425,8 @@ def summarize_posterior_parameters( Sampled parameter names in EasyDiffraction order. posterior_samples : PosteriorSamples Posterior sample container. - map_values : np.ndarray - MAP or best-sampled parameter values in the same order. + 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 @@ -473,7 +473,7 @@ def summarize_posterior_parameters( PosteriorParameterSummary( unique_name=parameter_name, display_name=display_name, - map_value=float(map_values[index]), + 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])), @@ -574,8 +574,8 @@ def _print_fit_quality_metrics(metrics: dict[str, float | None]) -> None: 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 == 'map': - return 'Max posterior' + if normalized_name in {'best sample', 'map'}: + return 'Best posterior sample' return point_estimate_name.replace('_', ' ').title() @@ -631,7 +631,7 @@ def _render_committed_parameter_table(parameters: list[object]) -> None: 'parameter', 'units', 'start', - 'max posterior', + 'best posterior sample', 'uncertainty', 'change', ] diff --git a/src/easydiffraction/analysis/minimizers/bumps_dream.py b/src/easydiffraction/analysis/minimizers/bumps_dream.py index 256ff9a55..1cb42fb51 100644 --- a/src/easydiffraction/analysis/minimizers/bumps_dream.py +++ b/src/easydiffraction/analysis/minimizers/bumps_dream.py @@ -874,7 +874,10 @@ def _build_success_result( 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] - map_values = np.array([best_by_name[uid] for uid in context.parameter_uids], dtype=float) + 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, @@ -885,7 +888,7 @@ def _build_success_result( posterior_parameter_summaries = summarize_posterior_parameters( parameter_names=context.parameter_names, posterior_samples=posterior_samples, - map_values=map_values, + best_sample_values=best_sample_values, parameter_display_names=context.parameter_display_names, convergence_diagnostics=convergence_diagnostics, ) @@ -897,7 +900,7 @@ def _build_success_result( log.warning('Convergence diagnostics indicate the posterior may be poorly mixed.') return OptimizeResult( - x=map_values, + x=best_sample_values, dx=posterior_standard_deviations, fun=float(best_nllf), success=True, @@ -921,7 +924,7 @@ def _sync_result_to_parameters( raw_result: object, ) -> None: """ - Commit MAP values on success and restore starts on failure. + Sync best posterior values or restore starts. Parameters ---------- @@ -985,7 +988,7 @@ def _build_fit_results( starting_parameters=parameters, fitting_time=self.tracker.fitting_time, sampler_name='dream', - point_estimate_name='map', + point_estimate_name='best_sample', posterior_samples=getattr(raw_result, 'posterior_samples', None), posterior_parameter_summaries=getattr(raw_result, 'posterior_parameter_summaries', []), posterior_predictive={}, diff --git a/src/easydiffraction/analysis/sequential.py b/src/easydiffraction/analysis/sequential.py index a52d38b1a..029486c79 100644 --- a/src/easydiffraction/analysis/sequential.py +++ b/src/easydiffraction/analysis/sequential.py @@ -9,12 +9,14 @@ 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 @@ -23,15 +25,23 @@ 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: """ @@ -50,6 +60,7 @@ class SequentialFitTemplate: constraints_enabled: bool minimizer_tag: str calculator_tag: str + diffrn_extract_rules: list[SequentialFitExtractRule] diffrn_field_names: list[str] @@ -108,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, @@ -122,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 + project.verbosity = 'silent' + try: + project.analysis.fit() + finally: + project.verbosity = original_verbosity - # 10. Collect results + # 11. Collect results result.update(_collect_results(project, template)) except ( @@ -234,6 +253,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, @@ -357,7 +452,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]: @@ -415,7 +550,7 @@ def _read_csv_for_recovery( for row in reader: file_path = row.get('file_path', '') if file_path: - fitted.add(file_path) + fitted.add(_resolve_csv_file_path(csv_path, file_path)) if row.get('fit_success', '').lower() == 'true': params = _extract_params_from_row(row) if params: @@ -443,7 +578,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())) @@ -472,12 +614,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, @@ -487,8 +648,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, ) @@ -498,11 +660,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_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 = '—' + + 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('reduced_chi_squared') + chi2_str = f'{reduced_chi2:.2f}' if reduced_chi2 is not None else '—' + iterations = str(result.get('n_iterations') or 0) + status = '✅' if result.get('fit_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: {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) + + _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. @@ -513,65 +979,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 '❌' - console.print( - f'{status} Chunk {chunk_idx}/{total_chunks}: {num_files} files, avg χ² = {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, ) - elif verbosity is VerbosityEnum.FULL: - console.print( - f'Chunk {chunk_idx}/{total_chunks}: ' - f'{num_files} files, {len(successful)} succeeded, ' - f'avg reduced χ² = {chi2_str}' - ) - 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 '—' - console.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), + ) # ------------------------------------------------------------------ @@ -757,9 +1204,7 @@ def _run_fit_loop( chunks: list[list[str]], template: SequentialFitTemplate, csv_info: tuple[Path, list[str]], - extract_diffrn: Callable | None, - verb: VerbosityEnum, - indicator: ActivityIndicator | None, + progress: SequentialProgressContext, ) -> None: """ Execute the chunk-based fitting loop. @@ -774,15 +1219,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. - indicator : ActivityIndicator | None - Shared sequential-fit activity indicator. + 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: @@ -791,13 +1235,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) - if indicator is not None: - indicator.update() + 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) @@ -819,7 +1271,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: @@ -840,8 +1291,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. @@ -850,62 +1299,40 @@ def fit_sequential( if mp.parent_process() is not None: return - verb = VerbosityEnum(analysis.project.verbosity) - - _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, - 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: - console.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 χ²):') - - indicator = None - if verb is not VerbosityEnum.SILENT: - indicator = ActivityIndicator(ACTIVITY_LABEL_FITTING, verbosity=verb) - indicator.start() + _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, - indicator, + _run_fit_loop_with_pool( + plan.max_workers, + plan.chunks, + plan.template, + (plan.csv_path, plan.header), + progress, ) finally: - if indicator is not None: - indicator.stop() - _restore_main_state(main_mod, main_file_bak, main_spec_bak) + _stop_progress_display(progress) - if verb is not VerbosityEnum.SILENT: - console.print( - f'✅ Sequential fitting complete: ' - f'{len(already_fitted) + len(remaining)} files processed.' - ) - console.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/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/variable.py b/src/easydiffraction/core/variable.py index 55af6cba9..76554bdd1 100644 --- a/src/easydiffraction/core/variable.py +++ b/src/easydiffraction/core/variable.py @@ -223,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.""" @@ -536,6 +558,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.""" diff --git a/src/easydiffraction/display/plotters/ascii.py b/src/easydiffraction/display/plotters/ascii.py index 467467001..625b2e2fd 100644 --- a/src/easydiffraction/display/plotters/ascii.py +++ b/src/easydiffraction/display/plotters/ascii.py @@ -32,6 +32,7 @@ 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): @@ -54,16 +55,24 @@ def _resample_series_for_chart( cls, y_series: object, ) -> list[list[float]]: - """Return y-series resampled to the available chart width.""" + """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 in y_series: - series_array = np.ravel(np.asarray(series, dtype=float)) - if ( - series_array.size <= target_point_count - or series_array.size < ASCII_CHART_MIN_POINT_COUNT - ): - resampled_series.append(series_array.tolist()) + 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) @@ -73,6 +82,30 @@ def _resample_series_for_chart( ) 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: """ @@ -193,8 +226,8 @@ def plot_powder_meas_vs_calc( 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, @@ -229,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))) @@ -272,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, @@ -282,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 6910321be..a080ba8e7 100644 --- a/src/easydiffraction/display/plotters/base.py +++ b/src/easydiffraction/display/plotters/base.py @@ -193,7 +193,7 @@ class XAxisType(StrEnum): }, 'posterior': { 'mode': 'lines', - 'name': 'Max posterior', + 'name': 'Best posterior sample', }, 'density': { 'mode': 'lines', diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index 3380141d3..85cb1545a 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -99,7 +99,7 @@ class PosteriorPairPlotStyleEnum(StrEnum): 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 = 'Max posterior' +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)' @@ -330,7 +330,12 @@ 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): + 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) @@ -723,7 +728,7 @@ def _plot_meas_vs_calc_request( 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. @@ -738,11 +743,11 @@ def plot_param_series( param : object Parameter descriptor whose ``unique_name`` identifies the values to plot. - versus : object | None, default=None - A diffrn descriptor (e.g. - ``expt.diffrn.ambient_temperature``) whose value is used as - the x-axis for each experiment. When ``None``, the - experiment sequence number is used instead. + 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 @@ -758,18 +763,133 @@ def plot_param_series( csv_path=csv_path, unique_name=unique_name, 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, + versus, self._project.experiments, self._project.analysis._parameter_snapshots, ) + 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, @@ -1205,7 +1325,7 @@ def _plot_single_crystal_posterior_predictive( 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.map_prediction).shape: + 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.' @@ -2729,7 +2849,7 @@ def _add_posterior_distribution_reference_traces( fig.add_trace( self._posterior_reference_line_trace( - x_value=summary.map_value, + 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, @@ -3259,7 +3379,7 @@ def _build_posterior_predictive_summary( if predictive_data is None: return None - map_prediction, x_values, predictive_draw_array = predictive_data + 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) @@ -3268,7 +3388,7 @@ def _build_posterior_predictive_summary( experiment_name=expt_name, x_axis_name=str(x_axis_name), x=np.asarray(x_values, dtype=float), - map_prediction=np.asarray(map_prediction, 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), @@ -3324,7 +3444,7 @@ def _evaluate_posterior_predictive_draws( expt_name: str, x_axis: object, ) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None: - """Return MAP and sampled predictive curves.""" + """Return best-sample and sampled predictive curves.""" original_values = np.array( [parameter.value for parameter in sampled_parameters], dtype=float, @@ -3334,14 +3454,14 @@ def _evaluate_posterior_predictive_draws( draw_indices = self._posterior_predictive_draw_indices(flattened_samples.shape[0]) try: - map_prediction, x_values = self._evaluate_posterior_predictive_state( + 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 map_prediction is None or x_values is None: + if best_sample_prediction is None or x_values is None: return None for index in draw_indices: @@ -3354,7 +3474,10 @@ def _evaluate_posterior_predictive_draws( ) if prediction is None or current_x is None: return None - if prediction.shape != map_prediction.shape or current_x.shape != x_values.shape: + 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) @@ -3367,7 +3490,7 @@ def _evaluate_posterior_predictive_draws( ) return ( - np.asarray(map_prediction, dtype=float), + np.asarray(best_sample_prediction, dtype=float), np.asarray(x_values, dtype=float), np.asarray(predictive_draws, dtype=float), ) @@ -3511,7 +3634,7 @@ def _plot_posterior_predictive_summary( expt_name=expt_name, x=np.asarray(summary.x, dtype=float), y_meas=np.asarray(y_meas, dtype=float), - y_calc=np.asarray(summary.map_prediction, dtype=float), + y_calc=np.asarray(summary.best_sample_prediction, dtype=float), axes_labels=axes_labels, excluded_ranges=excluded_ranges, ) @@ -3579,7 +3702,7 @@ def _plot_posterior_predictive_summary( fig.add_trace( go.Scatter( x=summary.x, - y=summary.map_prediction, + y=summary.best_sample_prediction, mode='lines', line={ 'color': POSTERIOR_POINT_ESTIMATE_LINE_COLOR, @@ -3673,23 +3796,26 @@ def _plot_single_crystal_posterior_predictive_summary( ) return - map_prediction = np.asarray(summary.map_prediction, dtype=float) + 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 != map_prediction.shape or upper_95.shape != map_prediction.shape: + 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=map_prediction, + 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 - map_prediction), - 'arrayminus': np.maximum(0.0, map_prediction - lower_95), + '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)) @@ -3734,7 +3860,12 @@ def _filtered_posterior_predictive_summary( experiment_name=summary.experiment_name, x_axis_name=summary.x_axis_name, x=x_filtered, - map_prediction=self._filtered_y_array(summary.map_prediction, summary.x, x_min, x_max), + 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 @@ -3808,7 +3939,10 @@ def _plot_posterior_predictive_data( 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.map_prediction, summary.x, ctx['x_min'], ctx['x_max'] + summary.best_sample_prediction, + summary.x, + ctx['x_min'], + ctx['x_max'], ) excluded_ranges = ( self._excluded_ranges( @@ -5405,20 +5539,20 @@ def _plot_param_series_from_csv( csv_path: str, unique_name: 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. + ``{unique_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 ---------- @@ -5428,9 +5562,9 @@ def _plot_param_series_from_csv( Unique name of the parameter to plot (CSV column key). 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) @@ -5446,14 +5580,12 @@ def _plot_param_series_from_csv( sy = df[uncert_col].astype(float).tolist() 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.' @@ -5476,7 +5608,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: @@ -5491,8 +5623,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]] @@ -5508,7 +5640,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 @@ -5522,7 +5657,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 index 1cde4d1c2..ef381c505 100644 --- a/src/easydiffraction/display/progress.py +++ b/src/easydiffraction/display/progress.py @@ -35,6 +35,7 @@ ACTIVITY_LABEL_SAMPLING = 'Sampling...' ACTIVITY_ACCENT_COLOR = '#d97706' ACTIVITY_TERMINAL_STYLE = ACTIVITY_ACCENT_COLOR +ACTIVITY_TERMINAL_FALLBACK_STYLE = 'bold yellow' SPINNER_FRAMES: tuple[str, ...] = ( '⠋', @@ -52,6 +53,27 @@ _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. @@ -60,13 +82,14 @@ class _TerminalLiveHandle: and notebook handles through a single update-oriented interface. """ - def __init__(self, *, console: object) -> None: + def __init__(self, *, console: object, auto_refresh: bool = True) -> None: self._renderable: object = Text('') self._live = Live( console=console, - auto_refresh=True, + auto_refresh=auto_refresh, refresh_per_second=1 / _SPINNER_FRAME_SECONDS, get_renderable=self._get_renderable, + vertical_overflow='visible', ) self._live.start() @@ -94,10 +117,15 @@ def close(self) -> None: self._live.stop() -def make_display_handle() -> object | None: +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 @@ -110,7 +138,7 @@ def make_display_handle() -> object | None: handle.display(HTML('')) return handle - return _TerminalLiveHandle(console=ConsoleManager.get()) + return _TerminalLiveHandle(console=ConsoleManager.get(), auto_refresh=auto_refresh) class ActivityIndicator: @@ -125,6 +153,12 @@ class ActivityIndicator: 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__( @@ -133,11 +167,17 @@ def __init__( *, 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 @@ -174,9 +214,10 @@ def start(self) -> None: live = Live( console=ConsoleManager.get(), - auto_refresh=True, - refresh_per_second=1 / _SPINNER_FRAME_SECONDS, + auto_refresh=self._animated, + refresh_per_second=self._refresh_per_second, get_renderable=self._terminal_renderable, + vertical_overflow='visible', ) live.start() self._live = live @@ -286,11 +327,14 @@ def _terminal_content(self) -> object | None: return Text(str(self._content)) def _terminal_indicator_line(self) -> Text | None: + style = resolve_activity_terminal_style(ConsoleManager.get()) if self._running: - frame = self._current_frame() - return Text(f'{frame} {self._label}', style=ACTIVITY_TERMINAL_STYLE) + 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=ACTIVITY_TERMINAL_STYLE) + return Text(self._label, style=style) return None def _current_frame(self) -> str: @@ -326,6 +370,12 @@ def _html_indicator(self) -> str: safe_label = html.escape(self._label) if self._running: + if not self._animated: + return ( + '
' + f'{safe_label}' + '
' + ) return ( '
' '' diff --git a/src/easydiffraction/io/ascii.py b/src/easydiffraction/io/ascii.py index 189926a16..faab76820 100644 --- a/src/easydiffraction/io/ascii.py +++ b/src/easydiffraction/io/ascii.py @@ -13,6 +13,20 @@ import numpy as np +def _resolve_extraction_destination(destination: str | Path | None) -> Path: + """Return an extraction directory for ZIP contents.""" + if destination is None: + return Path(tempfile.mkdtemp(prefix='ed_zip_')) + + extract_dir = 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, destination: str | Path | None = None, @@ -92,7 +106,8 @@ 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. Returns ------- @@ -111,12 +126,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 +173,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 0527ac690..ae5b59fa8 100644 --- a/src/easydiffraction/io/cif/serialize.py +++ b/src/easydiffraction/io/cif/serialize.py @@ -48,6 +48,9 @@ def format_value(value: object) -> str: # None → CIF unknown marker if value is None: value = '?' + # Booleans use CIF true/false tokens + elif isinstance(value, bool): + value = 'true' if value else 'false' # Convert ints to floats elif isinstance(value, int): value = float(value) @@ -70,6 +73,22 @@ 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 _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 ################## @@ -371,18 +390,34 @@ 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)}'] + + fitting_cif = analysis.fitting.as_cif + if fitting_cif: + parts.append(fitting_cif) + + aliases_cif = analysis.aliases.as_cif + if aliases_cif: + parts.append(aliases_cif) + + constraints_cif = analysis.constraints.as_cif + if constraints_cif: + parts.append(constraints_cif) + + if analysis.fitting_mode_type == 'joint': + joint_fit_cif = analysis.joint_fit.as_cif + if joint_fit_cif: + parts.append(joint_fit_cif) + elif analysis.fitting_mode_type == 'sequential': + sequential_fit_cif = analysis.sequential_fit.as_cif + if sequential_fit_cif: + parts.append(sequential_fit_cif) + + sequential_extract_cif = analysis.sequential_fit_extract.as_cif + if sequential_extract_cif: + parts.append(sequential_extract_cif) + + return '\n\n'.join(parts) def summary_to_cif(_summary: object) -> str: @@ -487,8 +522,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) @@ -498,8 +537,111 @@ 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) + +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: @@ -534,6 +676,20 @@ def _read(tag: str) -> str | None: 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: ###################### @@ -591,10 +747,10 @@ def param_from_cif( # 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 + self.value = _strip_optional_quotes(raw) + + elif self._value_type == DataTypes.BOOL: + self.value = _parse_bool_cif_value(raw) # Other types are not supported else: @@ -642,10 +798,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}') diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py index c248c9ad3..83a9e40f2 100644 --- a/src/easydiffraction/project/display.py +++ b/src/easydiffraction/project/display.py @@ -106,11 +106,22 @@ def correlations( def series( self, - param: object, - versus: object | None = None, + param: object | None = None, + versus: str | None = None, ) -> None: - """Plot one fitted parameter across sequential results.""" - self._project.rendering.plotter.plot_param_series(param=param, versus=versus) + """ + 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.""" diff --git a/src/easydiffraction/project/project.py b/src/easydiffraction/project/project.py index e7508201f..df8972de9 100644 --- a/src/easydiffraction/project/project.py +++ b/src/easydiffraction/project/project.py @@ -6,6 +6,7 @@ import pathlib import tempfile +from typing import ClassVar from typeguard import typechecked from varname import varname @@ -59,6 +60,51 @@ 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 + + class Project(GuardedBase): """ Central API for managing a diffraction data analysis project. @@ -71,6 +117,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, @@ -91,6 +138,15 @@ def __init__( 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 @@ -382,7 +438,13 @@ def save(self) -> None: with (analysis_dir / 'analysis.cif').open('w') as f: f.write(self.analysis.as_cif) console.print('├── 📁 analysis/') - console.print('│ └── 📄 analysis.cif') + + 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: @@ -458,13 +520,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/summary/summary.py b/src/easydiffraction/summary/summary.py index 27826dab7..25977bfa9 100644 --- a/src/easydiffraction/summary/summary.py +++ b/src/easydiffraction/summary/summary.py @@ -219,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/utils.py b/src/easydiffraction/utils/utils.py index 2fe66e739..cf461aeea 100644 --- a/src/easydiffraction/utils/utils.py +++ b/src/easydiffraction/utils/utils.py @@ -28,9 +28,9 @@ _DATA_REPO = 'easyscience/diffraction' _DATA_ROOT = 'data' # commit SHA preferred -_DATA_INDEX_REF = 'd5a1fddd0d3e3e919c7e4a19e83b94b4231b99b6' +_DATA_INDEX_REF = '39dad256ba1faedf4b26fad3e44a361c802fd8e4' # macOS: sha256sum index.json -_DATA_INDEX_HASH = 'sha256:8305fd55d5b0c7c63ffa2641c082c623a107c32af09343ba901e196f68fd9f73' +_DATA_INDEX_HASH = 'sha256:301aaca0f35927cd63715b858a1f03164e4d05d1d39234325a3798d2b4a5f4ea' def _build_data_url(path: str) -> str: 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_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 index 0ed65e4fc..5633cb6ad 100644 --- a/tests/integration/fitting/test_analysis_and_fit_category_support.py +++ b/tests/integration/fitting/test_analysis_and_fit_category_support.py @@ -78,7 +78,7 @@ class Project: def test_fit_mode_enum_members_default_and_descriptions(): - from easydiffraction.analysis.categories.fit.enums import FitModeEnum + from easydiffraction.analysis.enums import FitModeEnum assert FitModeEnum.SINGLE == 'single' assert FitModeEnum.JOINT == 'joint' @@ -87,112 +87,84 @@ def test_fit_mode_enum_members_default_and_descriptions(): assert all(member.description() for member in FitModeEnum) -def test_fit_instantiation_defaults_and_run_paths(): - from easydiffraction.analysis.categories.fit.default import Fit - import easydiffraction.analysis.categories.fit.default as fit_mod +def test_fitting_instantiation_defaults_and_helpers(): + from easydiffraction.analysis.categories.fitting.default import Fitting + import easydiffraction.analysis.categories.fitting.default as fitting_mod - fit = Fit() + fitting = Fitting() - assert fit._identity.category_code == 'fit' - assert fit.mode.value == 'single' - assert fit.minimizer_type.value == 'lmfit (leastsq)' - assert fit.minimizer is None - - with pytest.raises( - RuntimeError, - match=r'Fit category is not attached to an Analysis object\.', - ): - fit.run() - - calls: list[tuple[str | None, bool, int | None]] = [] - - class Parent: - fitter = object() - - @staticmethod - def _run_fit( - verbosity: str | None = None, - *, - use_physical_limits: bool = False, - random_seed: int | None = None, - ) -> None: - calls.append((verbosity, use_physical_limits, random_seed)) - - fit._parent = Parent() - fit(verbosity='silent', use_physical_limits=True, random_seed=7) - - assert calls == [('silent', True, 7)] + 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'})() - fit._parent = ParentWithMinimizer() - assert fit.minimizer == 'MIN' + fitting._parent = ParentWithMinimizer() + assert fitting.minimizer == 'MIN' shown: list[str] = [] monkeypatch = pytest.MonkeyPatch() - monkeypatch.setattr(fit_mod.MinimizerFactory, 'show_supported', lambda: shown.append('shown')) - Fit.show_available_minimizers() + 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.fit.default as fit_mod - from easydiffraction.analysis.categories.fit.default import Fit + import easydiffraction.analysis.categories.fitting.default as fitting_mod + from easydiffraction.analysis.categories.fitting.default import Fitting - fit = Fit() - fit._minimizer_type._value = 'bad-minimizer' + fitting = Fitting() + fitting._minimizer_type._value = 'bad-minimizer' class Parent: fitter = None warnings: list[str] = [] - fit._parent = Parent() - monkeypatch.setattr(fit_mod.CategoryItem, 'from_cif', lambda self, block, idx=0: None) + fitting._parent = Parent() + monkeypatch.setattr(fitting_mod.CategoryItem, 'from_cif', lambda self, block, idx=0: None) monkeypatch.setattr( - fit_mod, + fitting_mod, 'Fitter', lambda value: (_ for _ in ()).throw(ValueError('bad minimizer')), ) - monkeypatch.setattr(fit_mod.log, 'warning', lambda message: warnings.append(message)) + monkeypatch.setattr(fitting_mod.log, 'warning', lambda message: warnings.append(message)) - fit.from_cif(object()) + fitting.from_cif(object()) assert warnings == ['bad minimizer'] -def test_fit_fallback_paths_without_parent_or_project(capsys, monkeypatch): - import easydiffraction.analysis.categories.fit.default as fit_mod - from easydiffraction.analysis.categories.fit.default import Fit +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 - fit = Fit() + fitting = Fitting() - assert fit.minimizer is None - - fit.show_modes() - out = capsys.readouterr().out - assert 'single' in out - assert 'joint' in out - assert 'sequential' in out + assert fitting.minimizer is None - monkeypatch.setattr(fit_mod.CategoryItem, 'from_cif', lambda self, block, idx=0: None) - fit.from_cif(object()) + monkeypatch.setattr(fitting_mod.CategoryItem, 'from_cif', lambda self, block, idx=0: None) + fitting.from_cif(object()) -def test_fit_show_modes_for_single_and_multiple_experiments(capsys): +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.fit.show_modes() + single.show_fitting_mode_types() out_single = capsys.readouterr().out - assert 'Fit modes' in out_single + assert 'Fitting mode types' in out_single assert 'single' in out_single - assert 'joint' not in out_single + assert 'joint' in out_single multi = Analysis(project=_make_project_with_names(['e1', 'e2'])) - multi.fit.show_modes() + multi.show_fitting_mode_types() out_multi = capsys.readouterr().out assert 'joint' in out_multi assert 'sequential' in out_multi @@ -202,7 +174,7 @@ def test_show_minimizer_types_prints(capsys): from easydiffraction.analysis.analysis import Analysis analysis = Analysis(project=_make_project_with_names([])) - analysis.fit.show_minimizer_types() + analysis.fitting.show_minimizer_types() out = capsys.readouterr().out assert 'Minimizer types' in out assert 'lmfit (leastsq)' in out @@ -212,19 +184,19 @@ 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.fit.mode.value == 'single' - analysis.fit.mode = 'joint' - assert analysis.fit.mode.value == 'joint' - assert len(analysis.joint_fit_experiments) == 0 + 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 'fit' in out + assert 'fitting' in out assert 'display' in out assert 'Properties' in out assert 'Methods' in out - assert 'fit_sequential()' in out + assert 'fit()' in out def test_display_fit_results_warns_when_no_results(capsys): diff --git a/tests/integration/fitting/test_analysis_display.py b/tests/integration/fitting/test_analysis_display.py index bc549ee42..3a139be26 100644 --- a/tests/integration/fitting/test_analysis_display.py +++ b/tests/integration/fitting/test_analysis_display.py @@ -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 index 23e0de589..d6695973b 100644 --- a/tests/integration/fitting/test_bayesian_dream.py +++ b/tests/integration/fitting/test_bayesian_dream.py @@ -88,8 +88,8 @@ def _dream_parameters(project: Project) -> tuple[object, object, object]: def _configure_small_dream(project: Project) -> None: - project.analysis.fit.minimizer_type = 'bumps (dream)' - minimizer = project.analysis.fit.minimizer + project.analysis.fitting.minimizer_type = 'bumps (dream)' + minimizer = project.analysis.fitting.minimizer minimizer.steps = 20 minimizer.burn = 5 minimizer.thin = 1 @@ -97,6 +97,20 @@ def _configure_small_dream(project: Project) -> None: 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) @@ -111,7 +125,7 @@ def test_small_bounded_dream_refinement_produces_posterior_results(): offset.fit_max = 1.0 _configure_small_dream(project) - project.analysis.fit(verbosity='silent', random_seed=11) + _run_single_fit(project, random_seed=11) results = project.analysis.fit_results assert results.success is True @@ -130,8 +144,8 @@ def test_lm_prefit_followed_by_dream_uses_uncertainty_based_bounds(): for parameter in (length_a, scale, offset): parameter.free = True - project.analysis.fit.minimizer_type = 'bumps (lm)' - project.analysis.fit(verbosity='silent') + project.analysis.fitting.minimizer_type = 'bumps (lm)' + _run_single_fit(project) for parameter in (length_a, scale, offset): assert parameter.uncertainty is not None @@ -140,7 +154,7 @@ def test_lm_prefit_followed_by_dream_uses_uncertainty_based_bounds(): assert np.isfinite(parameter.fit_max) _configure_small_dream(project) - project.analysis.fit(verbosity='silent', random_seed=13) + _run_single_fit(project, random_seed=13) results = project.analysis.fit_results assert results.success is True @@ -163,7 +177,7 @@ def test_bayesian_fit_results_are_runtime_only_after_save_load(tmp_path): offset.fit_max = 1.0 _configure_small_dream(project) - project.analysis.fit(verbosity='silent', random_seed=17) + _run_single_fit(project, random_seed=17) assert project.analysis.fit_results.posterior_samples is not None diff --git a/tests/integration/fitting/test_bayesian_helper_support.py b/tests/integration/fitting/test_bayesian_helper_support.py index a4eef9007..b67d5db02 100644 --- a/tests/integration/fitting/test_bayesian_helper_support.py +++ b/tests/integration/fitting/test_bayesian_helper_support.py @@ -150,7 +150,7 @@ def test_summarize_posterior_parameters_preserves_order_and_display_names(): summaries = summarize_posterior_parameters( parameter_names=['beta', 'alpha'], posterior_samples=posterior_samples, - map_values=np.array([2.05, 1.05]), + 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}, @@ -182,7 +182,7 @@ def test_summarize_posterior_parameters_validates_display_name_length(): summarize_posterior_parameters( parameter_names=['alpha'], posterior_samples=posterior_samples, - map_values=np.array([1.0]), + best_sample_values=np.array([1.0]), parameter_display_names=['Alpha', 'Extra'], ) @@ -195,7 +195,7 @@ def test_standard_deviations_from_summaries_returns_float_array(): PosteriorParameterSummary( unique_name='a', display_name='A', - map_value=1.0, + best_sample_value=1.0, median=1.0, standard_deviation=0.2, interval_68=(0.9, 1.1), @@ -204,7 +204,7 @@ def test_standard_deviations_from_summaries_returns_float_array(): PosteriorParameterSummary( unique_name='b', display_name='B', - map_value=2.0, + best_sample_value=2.0, median=2.0, standard_deviation=0.3, interval_68=(1.9, 2.1), @@ -240,8 +240,8 @@ def test_bayesian_format_helpers_cover_edge_cases(): _format_sampler_settings({'steps': 10, 'burn': 2, 'samples': 40}) == 'steps=10, burn=2, samples=40' ) - assert _format_point_estimate_name('map') == 'Max posterior' - assert _format_point_estimate_name('best_sample') == 'Best Sample' + 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, @@ -322,7 +322,7 @@ def test_bayesian_fit_results_display_results_prints_sampler_and_convergence(cap PosteriorParameterSummary( unique_name='a', display_name='a', - map_value=1.2, + best_sample_value=1.2, median=1.15, standard_deviation=0.05, interval_68=(1.1, 1.2), @@ -377,7 +377,7 @@ def test_build_posterior_summary_row_restores_identifier_columns(): summary = PosteriorParameterSummary( unique_name='a', display_name='a', - map_value=1.2, + best_sample_value=1.2, median=1.15, standard_deviation=0.05, interval_68=(1.1, 1.2), @@ -424,7 +424,7 @@ def fake_render_table(*, columns_headers, columns_alignment, columns_data): 'parameter', 'units', 'start', - 'max posterior', + 'best posterior sample', 'uncertainty', 'change', ] @@ -473,7 +473,7 @@ def fake_render_table(*, columns_headers, columns_alignment, columns_data): PosteriorParameterSummary( unique_name='a', display_name='a', - map_value=1.2, + best_sample_value=1.2, median=1.15, standard_deviation=0.05, interval_68=(1.1, 1.2), @@ -529,7 +529,7 @@ def test_posterior_table_notes_split_failed_diagnostics(): PosteriorParameterSummary( unique_name='a', display_name='a', - map_value=1.0, + best_sample_value=1.0, median=1.0, standard_deviation=0.1, interval_68=(0.9, 1.1), @@ -554,7 +554,7 @@ def test_bayesian_helpers_cover_non_warning_and_default_display_paths(): summary = PosteriorParameterSummary( unique_name='missing', display_name='Missing', - map_value=1.0, + best_sample_value=1.0, median=1.0, standard_deviation=0.1, interval_68=(0.9, 1.1), diff --git a/tests/integration/fitting/test_bumps_dream_support.py b/tests/integration/fitting/test_bumps_dream_support.py index 4bcfed845..c09f0b92e 100644 --- a/tests/integration/fitting/test_bumps_dream_support.py +++ b/tests/integration/fitting/test_bumps_dream_support.py @@ -471,7 +471,7 @@ def best(self): PosteriorParameterSummary( unique_name='beta', display_name='Beta', - map_value=22.0, + best_sample_value=22.0, median=21.0, standard_deviation=0.4, interval_68=(20.5, 21.5), @@ -480,7 +480,7 @@ def best(self): PosteriorParameterSummary( unique_name='alpha', display_name='Alpha', - map_value=11.0, + best_sample_value=11.0, median=10.5, standard_deviation=0.3, interval_68=(10.0, 11.0), @@ -686,7 +686,7 @@ def best(): PosteriorParameterSummary( unique_name='alpha', display_name='Alpha', - map_value=1.0, + best_sample_value=1.0, median=1.0, standard_deviation=0.2, interval_68=(0.9, 1.1), 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_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..f1f81c801 100644 --- a/tests/integration/fitting/test_project_load.py +++ b/tests/integration/fitting/test_project_load.py @@ -209,8 +209,11 @@ def test_save_load_round_trip_preserves_parameters(tmp_path) -> None: 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 +230,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 +242,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..da4c28774 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' @@ -157,10 +184,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 +192,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 +209,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 +222,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 +247,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 +272,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 +281,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 +292,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 +304,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' @@ -320,19 +332,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 +363,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 +380,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/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_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_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 index 3b641e6d6..348f380f3 100644 --- a/tests/unit/easydiffraction/analysis/fit_helpers/test_bayesian.py +++ b/tests/unit/easydiffraction/analysis/fit_helpers/test_bayesian.py @@ -121,7 +121,7 @@ def test_summarize_posterior_parameters_preserves_order_and_display_names(): summaries = summarize_posterior_parameters( parameter_names=['beta', 'alpha'], posterior_samples=posterior_samples, - map_values=np.array([2.05, 1.05]), + 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}, @@ -171,7 +171,7 @@ def test_bayesian_fit_results_display_results_prints_sampler_and_convergence(cap PosteriorParameterSummary( unique_name='a', display_name='a', - map_value=1.2, + best_sample_value=1.2, median=1.15, standard_deviation=0.05, interval_68=(1.1, 1.2), @@ -218,7 +218,7 @@ def test_build_posterior_summary_row_restores_identifier_columns(): summary = PosteriorParameterSummary( unique_name='a', display_name='a', - map_value=1.2, + best_sample_value=1.2, median=1.15, standard_deviation=0.05, interval_68=(1.1, 1.2), @@ -265,7 +265,7 @@ def fake_render_table(*, columns_headers, columns_alignment, columns_data): 'parameter', 'units', 'start', - 'max posterior', + 'best posterior sample', 'uncertainty', 'change', ] @@ -314,7 +314,7 @@ def fake_render_table(*, columns_headers, columns_alignment, columns_data): PosteriorParameterSummary( unique_name='a', display_name='a', - map_value=1.2, + best_sample_value=1.2, median=1.15, standard_deviation=0.05, interval_68=(1.1, 1.2), @@ -370,7 +370,7 @@ def test_posterior_table_notes_split_failed_diagnostics(): PosteriorParameterSummary( unique_name='a', display_name='a', - map_value=1.0, + best_sample_value=1.0, median=1.0, standard_deviation=0.1, interval_68=(0.9, 1.1), diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_bumps_dream.py b/tests/unit/easydiffraction/analysis/minimizers/test_bumps_dream.py index eea9f0898..d17dce4fa 100644 --- a/tests/unit/easydiffraction/analysis/minimizers/test_bumps_dream.py +++ b/tests/unit/easydiffraction/analysis/minimizers/test_bumps_dream.py @@ -261,7 +261,7 @@ def best(self): PosteriorParameterSummary( unique_name='beta', display_name='Beta', - map_value=22.0, + best_sample_value=22.0, median=21.0, standard_deviation=0.4, interval_68=(20.5, 21.5), @@ -270,7 +270,7 @@ def best(self): PosteriorParameterSummary( unique_name='alpha', display_name='Alpha', - map_value=11.0, + best_sample_value=11.0, median=10.5, standard_deviation=0.3, interval_68=(10.0, 11.0), diff --git a/tests/unit/easydiffraction/analysis/test_analysis.py b/tests/unit/easydiffraction/analysis/test_analysis.py index 637facc02..2af863797 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis.py +++ b/tests/unit/easydiffraction/analysis/test_analysis.py @@ -33,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): @@ -66,7 +66,8 @@ 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): @@ -227,3 +228,74 @@ def fake_update_short_table( 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_coverage.py b/tests/unit/easydiffraction/analysis/test_analysis_coverage.py index 9ff2926ea..42ef01ace 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis_coverage.py +++ b/tests/unit/easydiffraction/analysis/test_analysis_coverage.py @@ -164,8 +164,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 diff --git a/tests/unit/easydiffraction/analysis/test_enums.py b/tests/unit/easydiffraction/analysis/test_enums.py new file mode 100644 index 000000000..fe854e536 --- /dev/null +++ b/tests/unit/easydiffraction/analysis/test_enums.py @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Tests for analysis/enums.py.""" + +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 diff --git a/tests/unit/easydiffraction/analysis/test_sequential.py b/tests/unit/easydiffraction/analysis/test_sequential.py index 5e01b586d..ab2327c94 100644 --- a/tests/unit/easydiffraction/analysis/test_sequential.py +++ b/tests/unit/easydiffraction/analysis/test_sequential.py @@ -14,11 +14,16 @@ 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' + # ------------------------------------------------------------------ # Fixture: a minimal template @@ -43,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=verbosity), + fitter=SimpleNamespace(selection='lmfit'), + ) + + sequential_mod.fit_sequential(analysis, data_dir=str(tmp_path)) + return events + + # ------------------------------------------------------------------ # _build_csv_header # ------------------------------------------------------------------ @@ -128,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'] @@ -160,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( @@ -168,7 +335,7 @@ def test_returns_fitted_file_paths(self, tmp_path): header, [ { - 'file_path': '/data/a.dat', + 'file_path': str(project_dir / 'experiments' / 'a.dat'), 'fit_success': 'True', 'chi_squared': '5.0', 'reduced_chi_squared': '2.5', @@ -177,7 +344,7 @@ def test_returns_fitted_file_paths(self, tmp_path): 'cell.a.uncertainty': '0.01', }, { - 'file_path': '/data/b.dat', + 'file_path': str(project_dir / 'experiments' / 'b.dat'), 'fit_success': 'False', 'chi_squared': '', 'reduced_chi_squared': '', @@ -189,7 +356,36 @@ 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_success': 'True', + 'chi_squared': '5.0', + 'reduced_chi_squared': '2.5', + 'n_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' @@ -306,75 +502,176 @@ def test_fields_accessible(self): assert template.calculator_tag == 'cryspy' -def test_fit_sequential_short_starts_and_stops_shared_indicator(monkeypatch, tmp_path): +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 - events: list[tuple[object, ...]] = [] - template = _minimal_template() + update_calls: list[object] = [] - class FakeIndicator: - def __init__(self, label, *, verbosity): - events.append(('init', label, verbosity)) + class RecordingIndicator: + def update(self, *, label=None, content=None): + del label + update_calls.append(content) - def start(self): - events.append(('start',)) + progress_state = sequential_mod.SequentialProgressState(chunk_rows=[], file_rows=[]) + progress = sequential_mod.SequentialProgressContext( + verbosity=verbosity, + state=progress_state, + indicator=RecordingIndicator(), + ) - def update(self): - events.append(('update',)) + sequential_mod._report_chunk_progress( + 1, + 3, + [_TEST_SCAN_001, _TEST_SCAN_002], + [ + { + 'file_path': _TEST_SCAN_001, + 'fit_success': True, + 'reduced_chi_squared': 4.0, + 'n_iterations': 11, + }, + { + 'file_path': _TEST_SCAN_002, + 'fit_success': False, + 'reduced_chi_squared': None, + 'n_iterations': 0, + }, + ], + progress, + sequential_mod._ChunkProgressMetrics( + completed_files_before=0, + total_files=3, + elapsed_time=19.76, + ), + ) - def stop(self): - events.append(('stop',)) + 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 - def fake_run_fit_loop( - pool_cm, chunks, template_arg, csv_info, extract_diffrn, verb, indicator - ): - del pool_cm, csv_info, extract_diffrn - assert chunks == [['scan_001.xye']] - assert template_arg == template - assert verb is VerbosityEnum.SHORT - assert indicator is not None - indicator.update() + assert len(update_calls) == 1 + assert update_calls[0] is not None - monkeypatch.setattr(sequential_mod, 'ActivityIndicator', FakeIndicator) - 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'], + +@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, ) - monkeypatch.setattr(sequential_mod, '_build_template', lambda project: template) + + 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: {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_success': True, + 'reduced_chi_squared': 1.0, + 'n_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, - '_setup_csv_and_recovery', - lambda project, template_arg, verb: ( - tmp_path / 'results.csv', - ['file_path'], - set(), - template_arg, - ), + '_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, '_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), + '_report_chunk_progress', + lambda *args: events.append(('report', [result['file_path'] for result in args[3]])), ) - 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='short'), - fitter=SimpleNamespace(selection='lmfit'), + sequential_mod._run_fit_loop( + FakePool(), + [[_TEST_SCAN_001, _TEST_SCAN_002]], + template, + (tmp_path / 'results.csv', header), + progress, ) - sequential_mod.fit_sequential(analysis, data_dir=str(tmp_path)) - assert events == [ - ('init', ACTIVITY_LABEL_FITTING, VerbosityEnum.SHORT), - ('start',), - ('update',), - ('stop',), + ('append', tmp_path / 'results.csv', header, [_TEST_SCAN_001, _TEST_SCAN_002]), + ('report', [_TEST_SCAN_001, _TEST_SCAN_002]), ] @@ -389,13 +686,18 @@ def __init__(self, *args, **kwargs): raise AssertionError(message) def fake_run_fit_loop( - pool_cm, chunks, template_arg, csv_info, extract_diffrn, verb, indicator + pool_cm, + chunks, + template_arg, + csv_info, + progress, ): - del pool_cm, csv_info, extract_diffrn + del pool_cm, csv_info assert chunks == [['scan_001.xye']] assert template_arg == template - assert verb is VerbosityEnum.SILENT - assert indicator is None + 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) diff --git a/tests/unit/easydiffraction/display/plotters/test_ascii.py b/tests/unit/easydiffraction/display/plotters/test_ascii.py index a0e84dc42..54e533ea0 100644 --- a/tests/unit/easydiffraction/display/plotters/test_ascii.py +++ b/tests/unit/easydiffraction/display/plotters/test_ascii.py @@ -44,7 +44,31 @@ def test_ascii_plotter_plot_supports_max_posterior_legend(capsys): out = capsys.readouterr().out assert 'Measured (Imeas)' in out - assert 'Max posterior' 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): @@ -111,7 +135,7 @@ def test_ascii_plotter_plot_powder_meas_vs_calc_announces_plotly_only_bragg_row( assert 'Bragg peak subplot rows are available with the Plotly engine only.' in out -def test_ascii_plotter_plot_resamples_to_detected_terminal_width(monkeypatch): +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 @@ -145,6 +169,46 @@ def fake_plot(series, config): 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 @@ -179,3 +243,66 @@ def fake_plot(series, config): 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 5b5836229..097d4321f 100644 --- a/tests/unit/easydiffraction/display/plotters/test_plotly.py +++ b/tests/unit/easydiffraction/display/plotters/test_plotly.py @@ -822,7 +822,7 @@ def fake_show_figure(self, fig): 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='Max posterior', + y_calc_name='Best posterior sample', y_calc_line_dash='dot', ), ) @@ -831,7 +831,9 @@ def fake_show_figure(self, 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 == 'Max posterior') + 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 diff --git a/tests/unit/easydiffraction/display/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py index c32424044..461e85ce2 100644 --- a/tests/unit/easydiffraction/display/test_plotting.py +++ b/tests/unit/easydiffraction/display/test_plotting.py @@ -313,7 +313,7 @@ def _make_bayesian_plotter_fixture(): PosteriorParameterSummary( unique_name=name, display_name=name, - map_value=float(samples[-1, -1, index]), + 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()), @@ -627,7 +627,7 @@ def test_posterior_pair_diagonal_matches_standalone_distribution_when_thinned(): PosteriorParameterSummary( unique_name=name, display_name=name, - map_value=float(samples[0, -1, index]), + 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()), @@ -693,12 +693,14 @@ def test_build_param_distribution_plot_returns_plotly_figure(): 'Marginal density', '95% credible interval', 'Median', - 'Max posterior', + '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 == 'Max posterior') + 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 @@ -765,7 +767,7 @@ def test_plot_posterior_predictive_summary_uses_consistent_labels_and_styles(mon 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]), - map_prediction=np.array([9.0, 10.0, 11.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)'], @@ -776,7 +778,9 @@ def test_plot_posterior_predictive_summary_uses_consistent_labels_and_styles(mon 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 == 'Max posterior') + 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 @@ -868,7 +872,7 @@ class Experiment: 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]), - map_prediction=np.array([9.0, 11.0, 10.5]), + best_sample_prediction=np.array([9.0, 11.0, 10.5]), draws=None, ), ) @@ -891,7 +895,7 @@ class Experiment: ) plot_spec = captured['plot_spec'] - assert plot_spec.y_calc_name == 'Max posterior' + assert plot_spec.y_calc_name == 'Best posterior sample' assert plot_spec.y_calc_line_dash == 'dot' @@ -963,7 +967,7 @@ def test_plot_posterior_predictive_summary_routes_ascii_to_measured_and_map(monk expt_name='pdf', summary=SimpleNamespace( x=np.array([1.0, 2.0, 3.0]), - map_prediction=np.array([9.0, 10.0, 11.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]]), @@ -1027,7 +1031,7 @@ class Experiment: 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]), - map_prediction=np.array([9.0, 11.0, 10.5]), + best_sample_prediction=np.array([9.0, 11.0, 10.5]), draws=None, ), ) @@ -1168,7 +1172,7 @@ def test_resolve_posterior_parameter_names_warns_on_ambiguous_label(monkeypatch) PosteriorParameterSummary( unique_name='phase_a.length_a', display_name='length_a', - map_value=1.0, + best_sample_value=1.0, median=1.0, standard_deviation=0.1, interval_68=(0.9, 1.1), @@ -1177,7 +1181,7 @@ def test_resolve_posterior_parameter_names_warns_on_ambiguous_label(monkeypatch) PosteriorParameterSummary( unique_name='phase_b.length_a', display_name='length_a', - map_value=2.0, + best_sample_value=2.0, median=2.0, standard_deviation=0.1, interval_68=(1.9, 2.1), @@ -1262,7 +1266,7 @@ def fake_evaluate(self, *, sampled_parameters, values, experiment, expt_name, x_ assert summary.experiment_name == 'hrpt' assert summary.x_axis_name == 'two_theta' assert summary.draws.shape == (4, 2) - np.testing.assert_allclose(summary.map_prediction, np.array([3.0, -1.0])) + 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] @@ -1465,7 +1469,7 @@ class Project: experiment_name='pdf', x_axis_name='two_theta', x=np.array([1.0, 2.0, 3.0]), - map_prediction=np.array([9.0, 19.0, 29.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]), ), @@ -1497,7 +1501,10 @@ def fake_plot_summary( assert captured['expt_name'] == 'pdf' np.testing.assert_allclose(captured['summary'].x, np.array([2.0])) - np.testing.assert_allclose(captured['summary'].map_prediction, np.array([19.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])) diff --git a/tests/unit/easydiffraction/display/test_plotting_coverage.py b/tests/unit/easydiffraction/display/test_plotting_coverage.py index 542dfa4f5..008881bcf 100644 --- a/tests/unit/easydiffraction/display/test_plotting_coverage.py +++ b/tests/unit/easydiffraction/display/test_plotting_coverage.py @@ -271,6 +271,40 @@ class Ptn: 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 @@ -331,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 = [] @@ -348,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] @@ -416,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 index f690159ed..b12147fa2 100644 --- a/tests/unit/easydiffraction/display/test_progress.py +++ b/tests/unit/easydiffraction/display/test_progress.py @@ -19,6 +19,7 @@ def __init__( auto_refresh, refresh_per_second, get_renderable=None, + vertical_overflow=None, ): self.renderable = renderable self.console = console @@ -60,6 +61,42 @@ def refresh(self): 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 @@ -95,6 +132,22 @@ def test_activity_indicator_terminal_line_uses_accent_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 @@ -107,6 +160,7 @@ def __init__( auto_refresh, refresh_per_second, get_renderable=None, + vertical_overflow=None, ): self.renderable = renderable self.console = console @@ -144,6 +198,56 @@ def refresh(self): 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 @@ -167,6 +271,7 @@ def __init__( auto_refresh, refresh_per_second, get_renderable=None, + vertical_overflow=None, ): self.renderable = renderable self.console = console diff --git a/tests/unit/easydiffraction/io/cif/test_serialize_more.py b/tests/unit/easydiffraction/io/cif/test_serialize_more.py index 1add7692d..f526f21a8 100644 --- a/tests/unit/easydiffraction/io/cif/test_serialize_more.py +++ b/tests/unit/easydiffraction/io/cif/test_serialize_more.py @@ -164,7 +164,6 @@ def as_cif(self): 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): @@ -175,16 +174,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/project/test_display.py b/tests/unit/easydiffraction/project/test_display.py index b56f8fce0..1a892bca0 100644 --- a/tests/unit/easydiffraction/project/test_display.py +++ b/tests/unit/easydiffraction/project/test_display.py @@ -38,6 +38,7 @@ def _recorder(*args, **kwargs): 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'), @@ -192,7 +193,8 @@ def test_fit_display_delegates_to_analysis_and_rendering(): max_parameters=4, show_diagonal=False, ) - display.fit.series(param='scale', versus='temperature') + display.fit.series(param='scale', versus='diffrn.ambient_temperature') + display.fit.series(versus='diffrn.ambient_temperature') assert calls[0] == ('fit_results', (), {}) assert calls[1] == ( @@ -208,7 +210,12 @@ def test_fit_display_delegates_to_analysis_and_rendering(): assert calls[2] == ( 'plot_param_series', (), - {'param': 'scale', 'versus': 'temperature'}, + {'param': 'scale', 'versus': 'diffrn.ambient_temperature'}, + ) + assert calls[3] == ( + 'plot_all_param_series', + (), + {'versus': 'diffrn.ambient_temperature'}, ) diff --git a/tests/unit/easydiffraction/project/test_project.py b/tests/unit/easydiffraction/project/test_project.py index e0ff677dd..e3dcf7a19 100644 --- a/tests/unit/easydiffraction/project/test_project.py +++ b/tests/unit/easydiffraction/project/test_project.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from collections import UserList +import csv from types import SimpleNamespace @@ -74,3 +76,48 @@ def test_project_exposes_rendering_and_display_facades(): 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_load.py b/tests/unit/easydiffraction/project/test_project_load.py index 448345777..4f32ffde4 100644 --- a/tests/unit/easydiffraction/project/test_project_load.py +++ b/tests/unit/easydiffraction/project/test_project_load.py @@ -70,16 +70,16 @@ 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_rendering_configuration(self, tmp_path): original = Project(name='d1') @@ -136,7 +136,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 +150,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 08292525d..27212251b 100644 --- a/tests/unit/easydiffraction/project/test_project_save.py +++ b/tests/unit/easydiffraction/project/test_project_save.py @@ -38,3 +38,27 @@ 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 diff --git a/tests/unit/easydiffraction/summary/test_summary.py b/tests/unit/easydiffraction/summary/test_summary.py index 6c34baf11..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 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 9edd2bc27..62ad8c68c 100644 --- a/tests/unit/easydiffraction/test___main__.py +++ b/tests/unit/easydiffraction/test___main__.py @@ -127,6 +127,63 @@ def pattern(expt_name, **kwargs): 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): import easydiffraction.__main__ as main_mod from easydiffraction.project.project import Project From aa44c46a0a08c554ab49984e8016db768816ba32 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Sun, 17 May 2026 15:47:24 +0200 Subject: [PATCH 08/10] Separate category-owning sections from real CIF datablocks (#176) * Add CategoryOwner ADR and migration plan * Add category-owner baseline tests * Add CategoryOwner base class * Make DatablockItem inherit CategoryOwner * Split category-owner CIF serialization * Make Analysis inherit CategoryOwner * Generalize dirty-flag lookup to CategoryOwner * Document CategoryOwner architecture changes * Move project info into ProjectConfig categories * Plan Project-to-Workspace rename * Finalize CategoryOwner refactor * Standardize project.save_as usage in tutorials * Ignore projects directory * Mark verification steps as complete * Accept CategoryOwner ADR and remove migration plan --- .gitignore | 1 + .../adr_workspace-root-project-category.md | 328 ++++++ docs/dev/ADRs/adr_category-owner-sections.md | 206 ++++ docs/dev/Issues/issues_closed.md | 13 + docs/dev/Issues/issues_open.md | 19 +- docs/dev/architecture.md | 70 +- docs/dev/package-structure-full.md | 11 +- docs/dev/package-structure-short.md | 6 + .../plan_workspace-root-project-category.md | 1041 +++++++++++++++++ docs/docs/tutorials/ed-17.py | 2 +- docs/docs/tutorials/ed-20.py | 2 +- src/easydiffraction/analysis/analysis.py | 77 +- src/easydiffraction/core/category_owner.py | 76 ++ src/easydiffraction/core/datablock.py | 80 +- src/easydiffraction/core/variable.py | 36 +- src/easydiffraction/io/cif/serialize.py | 121 +- .../project/categories/info/__init__.py | 8 + .../project/categories/info/default.py | 175 +++ .../project/categories/info/factory.py | 17 + .../project/categories/rendering/default.py | 11 +- src/easydiffraction/project/project.py | 35 +- src/easydiffraction/project/project_config.py | 47 + src/easydiffraction/project/project_info.py | 136 +-- .../core/test_category_owner.py | 134 +++ .../test_serialize_category_owner_baseline.py | 97 ++ .../project/categories/info/test_default.py | 63 + .../project/categories/info/test_factory.py | 32 + .../project/test_project_config.py | 68 ++ 28 files changed, 2556 insertions(+), 356 deletions(-) create mode 100644 docs/dev/ADR-suggestions/adr_workspace-root-project-category.md create mode 100644 docs/dev/ADRs/adr_category-owner-sections.md create mode 100644 docs/dev/plan_workspace-root-project-category.md create mode 100644 src/easydiffraction/core/category_owner.py create mode 100644 src/easydiffraction/project/categories/info/__init__.py create mode 100644 src/easydiffraction/project/categories/info/default.py create mode 100644 src/easydiffraction/project/categories/info/factory.py create mode 100644 src/easydiffraction/project/project_config.py create mode 100644 tests/unit/easydiffraction/core/test_category_owner.py create mode 100644 tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py create mode 100644 tests/unit/easydiffraction/project/categories/info/test_default.py create mode 100644 tests/unit/easydiffraction/project/categories/info/test_factory.py create mode 100644 tests/unit/easydiffraction/project/test_project_config.py 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/docs/dev/ADR-suggestions/adr_workspace-root-project-category.md b/docs/dev/ADR-suggestions/adr_workspace-root-project-category.md new file mode 100644 index 000000000..b53ead958 --- /dev/null +++ b/docs/dev/ADR-suggestions/adr_workspace-root-project-category.md @@ -0,0 +1,328 @@ +# ADR: Workspace Root and Project Information Category + +**Status:** Proposed +**Date:** 2026-05-17 + +## Context + +The current public root object is `Project`. It acts as the top-level +facade for an EasyDiffraction working session: + +```python +project = ed.Project(name='lbco_hrpt') +project.structures +project.experiments +project.analysis +project.display +project.summary +project.save() +``` + +The same word, "project", is also the natural CIF category name for +information about the scientific project: + +```cif +_project.id +_project.title +_project.description +_project.created +_project.last_modified +``` + +This creates a naming conflict. The root object is a broad runtime +facade, while the `_project.*` category is only information about the +scientific project. Using the same name for both makes category naming +awkward: + +```python +project.project_info.title +project.config.project_info.title +project.project.title +``` + +At the same time, replacing `_project.*` with a generic `_meta.*` +category would weaken the CIF model: + +```cif +_meta.project_id +_meta.project_title +``` + +The category name `meta` is too generic. It forces each item name to +repeat what the category should already communicate. The existing +`_project.id` and `_project.title` names are more semantic and better +aligned with the repository rule to follow CIF naming unless there is a +clearly better API. + +The design question is therefore: + +- should the top-level runtime object remain `Project`, and project + information move to a different category such as `meta`; +- or should the top-level runtime object be renamed to `Workspace`, so + `project` can be used cleanly for project information? + +## Decision + +Rename the top-level runtime facade from `Project` to `Workspace`. + +Use `project` as the public project-information category under the +workspace: + +```python +workspace = ed.Workspace(project_id='lbco_hrpt') +workspace.project.id +workspace.project.title = 'La0.5Ba0.5CoO3 at HRPT@PSI' +workspace.rendering.table_engine = 'rich' +workspace.structures +workspace.experiments +workspace.analysis +``` + +Persist workspace-level singleton categories in `workspace.cif`: + +```cif +_project.id +_project.title +_project.description +_project.created +_project.last_modified + +_rendering.chart_engine +_rendering.table_engine +``` + +Do not introduce `_meta.*` CIF tags. + +The intended naming split is: + +```text +Workspace +|-- project # information about the scientific project +|-- rendering # rendering preferences +|-- structures # real structure datablocks +|-- experiments # real experiment datablocks +|-- analysis # analysis section +|-- display # display facade +`-- summary # summary/report facade +``` + +## Rationale + +### `Workspace` better describes the top-level facade + +The top-level object is more than project metadata. It owns active +collections, analysis state, display helpers, save/load behavior, and +runtime orchestration. `Workspace` describes that broader role without +consuming the domain word `project`. + +The name is also familiar in scientific software. It commonly means an +active analysis environment, a data/model container, or a working area. +That is close to the role of the current EasyDiffraction root object. + +### `project` is the right category name for project information + +Project information is not generic metadata. It is specifically the +identity, title, description, and timestamps of the scientific project. + +This reads cleanly: + +```python +workspace.project.title +``` + +and maps directly to clean CIF: + +```cif +_project.title +``` + +### `_meta.project_title` is weaker than `_project.title` + +The `_meta` category would make the CIF less domain-oriented. It also +creates longer and more repetitive item names: + +```cif +_meta.project_id +_meta.project_title +_meta.project_description +``` + +The existing `_project.*` tags are clearer: + +```cif +_project.id +_project.title +_project.description +``` + +### This keeps layer-specific consistency + +After this decision, each layer has a clear rule: + +| Layer | Rule | Example | +| --------------- | ------------------------------ | ------------------- | +| Runtime root | working-session facade | `Workspace` | +| Public category | semantic category name | `workspace.project` | +| CIF category | semantic CIF category | `_project.*` | +| Config file | workspace singleton categories | `workspace.cif` | + +This avoids one-off aliases such as `project.info` while preserving +semantic CIF names. + +## Consequences + +### Positive + +- The root object and project-information category no longer share the + same conceptual name. +- Public category access becomes uniform: `workspace.project`, + `workspace.rendering`, `workspace.analysis`. +- CIF stays semantic and does not introduce `_meta.*`. +- Project information can use short item names such as `id`, `title`, + and `description`. +- The top-level facade name better reflects active runtime + orchestration. + +### Negative + +- This is a breaking public API change. +- Tutorials, scripts, tests, docs, type hints, and imports must be + updated from `Project` to `Workspace`. +- Existing saved directories using `project.cif` must be migrated to + `workspace.cif` if no compatibility loader is kept. +- Users familiar with `Project` must learn the new root name. +- `Workspace` can be confused with a filesystem workspace in some + ecosystems, so documentation must define it clearly as the active + EasyDiffraction working object. + +## Compatibility Policy + +EasyDiffraction is in beta, and repository instructions say not to keep +legacy shims by default. + +Therefore the target implementation should not add a permanent +`Project = Workspace` alias unless explicitly approved before +implementation. + +The migration plan still includes a review gate before removing the old +`Project` public symbol, because this is a user-facing breaking change. + +## Alternatives Considered + +### Keep `Project` root and rename project information to `meta` + +Rejected. + +Example: + +```python +project.meta.project_title +``` + +```cif +_meta.project_title +``` + +This keeps the root class stable, but it weakens the category model. +`meta` is too broad, and the CIF item names become repetitive. + +### Keep `Project` root and use `project.config.project` + +Rejected for now. + +Example: + +```python +project.config.project.title +``` + +This is technically consistent, but it still repeats `project` at +different semantic layers. It also adds depth to common user workflows. +It is a reasonable fallback if the public root rename is rejected. + +### Keep `Project` root and use `project.info` + +Rejected for the target design. + +Example: + +```python +project.info.title +``` + +This is readable, but it preserves a special-case category alias. The +current goal is stronger consistency between public categories and CIF +category concepts. + +### Rename only internal files and keep public API unchanged + +Rejected for the target design. + +This improves implementation clarity but does not solve the public +naming inconsistency. + +### Use `Study` instead of `Workspace` + +Rejected. + +`Study` is a plausible scientific term, but it is less established for +an active computational container. It also does not map as naturally to +save/load, display, and analysis orchestration. + +## Implementation Notes + +The implementation should follow: + +```text +docs/dev/plan_workspace-root-project-category.md +``` + +The high-level migration is: + +1. Rename the root facade `Project` to `Workspace`. +2. Rename the project package/module surface from `project` to + `workspace`. +3. Rename `ProjectConfig` to `WorkspaceConfig`. +4. Rename project-level category access from `info` to `project`. +5. Rename project-information `name` access to `id`, matching + `_project.id`. +6. Move the storage path to `Workspace.path`, because it describes the + saved workspace directory rather than project information. +7. Keep the information category class named `ProjectInfo` unless a + later decision chooses `ProjectMetadata`. +8. Keep CIF tags `_project.*` and `_rendering.*`. +9. Rename saved singleton config file from `project.cif` to + `workspace.cif`. +10. Update code, tests, scripts, tutorials, docs, and architecture + references. + +## Post-Implementation ADR Update + +This ADR must be updated after the migration plan is implemented. + +When implementation is complete: + +1. Change status from `Proposed` to `Accepted and implemented`. +2. Record the final public API and saved file layout. +3. Record whether a temporary or permanent `Project` compatibility alias + was approved. +4. Record any deviations from the migration plan. +5. Move this file from `docs/dev/ADR-suggestions/` to `docs/dev/ADRs/` + if that is the repository convention for accepted decisions. +6. Update `docs/dev/architecture.md`. +7. Update or close related items in `docs/dev/Issues/issues_open.md`. + +## Acceptance Criteria + +This ADR is satisfied when: + +- `ed.Workspace` is the public top-level facade. +- `ed.Project` is removed unless explicitly approved as an alias. +- the public project-information category is `workspace.project`. +- project identity is exposed as `workspace.project.id`. +- the saved directory path is exposed as `workspace.path`. +- the public rendering category is `workspace.rendering`. +- saved singleton configuration lives in `workspace.cif`. +- `workspace.cif` uses `_project.*` and `_rendering.*` tags. +- no `_meta.*` tags are introduced for project information. +- tutorials and architecture documentation use `Workspace`. diff --git a/docs/dev/ADRs/adr_category-owner-sections.md b/docs/dev/ADRs/adr_category-owner-sections.md new file mode 100644 index 000000000..46f9b3db0 --- /dev/null +++ b/docs/dev/ADRs/adr_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/Issues/issues_closed.md b/docs/dev/Issues/issues_closed.md index fee4f327b..0e6db4dc5 100644 --- a/docs/dev/Issues/issues_closed.md +++ b/docs/dev/Issues/issues_closed.md @@ -62,6 +62,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/issues_open.md b/docs/dev/Issues/issues_open.md index 3e961f1cc..2e6142e8e 100644 --- a/docs/dev/Issues/issues_open.md +++ b/docs/dev/Issues/issues_open.md @@ -27,23 +27,6 @@ match `project.experiments.names`. --- -## 5. 🟡 Make `Analysis` a `DatablockItem` - -**Type:** Consistency - -`Analysis` owns categories (`Aliases`, `Constraints`, -`JointFitCollection`) but does not extend `DatablockItem`. Its ad-hoc -`_update_categories()` iterates over a hard-coded list and does not -participate in standard category discovery, parameter enumeration, or -CIF serialisation. - -**Fix:** make `Analysis` extend `DatablockItem`, or extract a shared -`_update_categories()` protocol. - -**Depends on:** nothing. - ---- - ## 8. 🟡 Add Explicit `create()` Signatures on Collections **Type:** API safety @@ -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. --- diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index d6a63b6d5..768531e72 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -53,7 +53,8 @@ GuardedBase # Controlled attribute access, parent lin ├── 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) +└── CategoryOwner # Flat category owner (e.g. Analysis, DatablockItem) + └── DatablockItem # Real CIF data block (e.g. Structure, Experiment) ``` `CollectionBase` provides a unified dict-like API over an ordered item @@ -185,7 +186,18 @@ design constraint. Categories are never nested inside other categories **Update priority:** lower values run first. This ensures correct execution order within a datablock (e.g. background before data). -### 2.4 DatablockItem and DatablockCollection +### 2.4 CategoryOwner, DatablockItem, and DatablockCollection + +`CategoryOwner` is the shared base class for objects that own flat +category siblings. It provides category discovery, category sorting by +`_update_priority`, parameter aggregation, `_need_categories_update` +tracking, and category-body CIF serialization without a `data_` header. + +`DatablockItem` extends `CategoryOwner` for real CIF `data_` blocks. +`Structure` and `ExperimentBase` subclasses are real datablocks. +`Analysis` is also a `CategoryOwner`, but it serializes as a singleton +section body in `analysis/analysis.cif` and does not emit a fake +`data_analysis` header. | Aspect | `DatablockItem` | `DatablockCollection` | | ------------------ | ------------------------------------------- | -------------------------------------------------------- | @@ -199,7 +211,7 @@ execution order within a datablock (e.g. background before data). | Dirty flag | `_need_categories_update` | N/A | When any `Parameter.value` is set, it propagates -`_need_categories_update = True` up to the owning `DatablockItem`. +`_need_categories_update = True` up to the owning `CategoryOwner`. Serialisation (`as_cif`) and plotting trigger `_update_categories()` if the flag is set. @@ -847,14 +859,16 @@ workflow: `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). +- Singleton section: `Analysis` is a `CategoryOwner`, not a + `DatablockItem`. It owns sibling categories and serializes as the body + of `analysis/analysis.cif` without a `data_` header. +- Fit configuration: `fitting` (`CategoryItem` with `minimizer_type`). + `fitting.minimizer_type` selects the minimizer backend. The active + fitting mode lives on the owner as `analysis.fitting_mode_type`, not + as a nested child category field. `fitting.show_minimizer_types()` + lists supported minimizers. - Joint-fit weights: `joint_fit` (`CategoryCollection` of per-experiment - weight entries); sibling of `fit`, not a child. + weight entries); sibling of `fitting`, not a child. - Fit results: `analysis.fit_results` stores the latest runtime result object. This is `FitResults` for deterministic fits and `BayesianFitResults` for Bayesian DREAM runs. @@ -922,12 +936,18 @@ It owns and coordinates all components: | `project.summary` | `Summary` | Report generation | | `project.verbosity` | `str` | Console output level (full/short/silent) | +Internally, `Project` keeps project-scoped singleton categories under a +private `ProjectConfig(CategoryOwner)` object. That owner currently +holds `project.info` (`ProjectInfo`) and `project.rendering` +(`Rendering`), while the public access paths stay flat on `Project` for +user discoverability. + ### 7.1 Data Flow ``` Parameter.value set → AttributeSpec validation (type + value) - → _need_categories_update = True (on parent DatablockItem) + → _need_categories_update = True (on parent CategoryOwner) Plot / CIF export / fit objective evaluation → _update_categories() @@ -944,7 +964,7 @@ Projects are saved as a directory of CIF files: ```shell project_dir/ -├── project.cif # ProjectInfo + Display preferences +├── project.cif # ProjectConfig categories (info + rendering) ├── summary.cif # Summary report ├── structures/ │ └── lbco.cif # One file per structure @@ -954,14 +974,16 @@ project_dir/ └── analysis.cif # Analysis settings ``` -`project.cif` carries both the `_project.*` metadata and the -`_rendering.*` engine preferences (`chart_engine`, `table_engine`), 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 (`_fitting.minimizer_type`, -`_fitting.mode_type`) lives in `analysis/analysis.cif`. Runtime fit -outputs, including `analysis.fit_results`, posterior chains, posterior -predictive summaries, and convergence diagnostics, are not serialized. +`project.cif` serializes the private `ProjectConfig` owner without a +`data_` header. It carries both the `_project.*` metadata category and +the `_rendering.*` engine preferences (`chart_engine`, `table_engine`), +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 +(`_fitting.minimizer_type`, `_fitting.mode_type`) lives in +`analysis/analysis.cif`. Runtime fit outputs, including +`analysis.fit_results`, posterior chains, posterior predictive +summaries, and convergence diagnostics, are not serialized. ### 7.3 Verbosity @@ -1362,12 +1384,12 @@ if self._fitting_mode_type == '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 (`CategoryOwner`). 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) +Owner (CategoryOwner) ├── CategoryA (CategoryItem or CategoryCollection) ├── CategoryB (CategoryItem or CategoryCollection) └── CategoryC (CategoryItem or CategoryCollection) diff --git a/docs/dev/package-structure-full.md b/docs/dev/package-structure-full.md index c1728f1ba..720505baa 100644 --- a/docs/dev/package-structure-full.md +++ b/docs/dev/package-structure-full.md @@ -122,6 +122,8 @@ │ ├── 📄 category.py │ │ ├── 🏷️ class CategoryItem │ │ └── 🏷️ class CategoryCollection +│ ├── 📄 category_owner.py +│ │ └── 🏷️ class CategoryOwner │ ├── 📄 collection.py │ │ └── 🏷️ class CollectionBase │ ├── 📄 datablock.py @@ -425,6 +427,12 @@ │ └── 📄 ascii.py ├── 📁 project │ ├── 📁 categories +│ │ ├── 📁 info +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ └── 🏷️ class ProjectInfo +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class ProjectInfoFactory │ │ ├── 📁 rendering │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py @@ -441,8 +449,9 @@ │ │ └── 🏷️ 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/dev/package-structure-short.md b/docs/dev/package-structure-short.md index 4c1c62f8d..da42f7a57 100644 --- a/docs/dev/package-structure-short.md +++ b/docs/dev/package-structure-short.md @@ -64,6 +64,7 @@ ├── 📁 core │ ├── 📄 __init__.py │ ├── 📄 category.py +│ ├── 📄 category_owner.py │ ├── 📄 collection.py │ ├── 📄 datablock.py │ ├── 📄 diagnostic.py @@ -207,6 +208,10 @@ │ └── 📄 ascii.py ├── 📁 project │ ├── 📁 categories +│ │ ├── 📁 info +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py │ │ ├── 📁 rendering │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py @@ -215,6 +220,7 @@ │ ├── 📄 __init__.py │ ├── 📄 display.py │ ├── 📄 project.py +│ ├── 📄 project_config.py │ └── 📄 project_info.py ├── 📁 summary │ ├── 📄 __init__.py diff --git a/docs/dev/plan_workspace-root-project-category.md b/docs/dev/plan_workspace-root-project-category.md new file mode 100644 index 000000000..b57e318a6 --- /dev/null +++ b/docs/dev/plan_workspace-root-project-category.md @@ -0,0 +1,1041 @@ +# Workspace Root and Project Category Migration Plan + +## Status + +Branch: `feature/workspace-root-project-category` + +ADR suggestion: + +```text +docs/dev/ADR-suggestions/adr_workspace-root-project-category.md +``` + +Two-phase workflow from `.github/copilot-instructions.md`: + +- Phase 1 - Implementation. Code, docs, and architecture updates only. + Do not create or run tests unless the user explicitly asks. +- Phase 2 - Verification. Add/update tests, then run the verification + commands listed near the end of this plan. + +Stop after Phase 1 and request review before starting Phase 2. + +Status checklist. Mark `[x]` only while implementing: + +```text +Phase 1 - Implementation +[ ] Phase 0: Confirm breaking-change approval. +[ ] Phase 1: Rename root package and public facade to Workspace. +[ ] Phase 2: Rename project-info access from info to project. +[ ] Phase 3: Align project information fields with _project.* tags. +[ ] Phase 4: Rename project config file to workspace.cif. +[ ] Phase 5: Update root-object references across runtime code. +[ ] Phase 6: Update docs, tutorials, and ADR references. +[ ] Phase 7: Remove old public Project surface unless approved. +[ ] Phase 1 review gate: present diff for approval. + +Phase 2 - Verification +[ ] Move/update unit tests to workspace paths. +[ ] Add workspace naming and CIF layout tests. +[ ] pixi run test-structure-check +[ ] pixi run fix +[ ] pixi run check +[ ] pixi run unit-tests +[ ] pixi run integration-tests +[ ] pixi run script-tests +[ ] pixi run notebook-prepare +[ ] pixi run notebook-tests +``` + +## Commit Discipline + +When an AI agent follows this plan, every completed Phase 1 +implementation step must be staged with explicit paths and committed +locally before moving to the next implementation step or to the Phase 1 +review gate. + +Follow the **Commits** section of `.github/copilot-instructions.md`. + +Rules: + +- One commit per phase. +- Keep each commit atomic and single-purpose. +- Stage explicit paths only. Do not use `git add .`. +- Use `git mv` for file and directory moves. +- Do not stage unrelated user changes. +- Do not stage generated artifacts unless the user explicitly asks. +- If a serious uncovered design issue appears, stop and ask before + continuing. + +Suggested commit messages: + +```text +Rename Project facade to Workspace +Expose project information as workspace.project +Align project metadata fields with CIF names +Rename project config file to workspace.cif +Update runtime references to Workspace +Update docs for Workspace root API +Remove old Project public API surface +``` + +## Goal + +Split the name "project" into two distinct concepts: + +1. `Workspace` - the top-level runtime facade, currently named + `Project`. +2. `workspace.project` - the category that stores information about the + scientific project. + +Target public API: + +```python +import easydiffraction as ed + +workspace = ed.Workspace(project_id='lbco_hrpt') +workspace.project.id +workspace.project.title = 'La0.5Ba0.5CoO3 at HRPT@PSI' +workspace.rendering.table_engine = 'rich' +workspace.structures +workspace.experiments +workspace.analysis +workspace.display +workspace.summary +workspace.save_as('lbco_hrpt') +``` + +Target workspace-level config file: + +```text +workspace.cif +``` + +Target CIF tags inside `workspace.cif`: + +```cif +_project.id +_project.title +_project.description +_project.created +_project.last_modified + +_rendering.chart_engine +_rendering.table_engine +``` + +Do not introduce `_meta.*` tags. + +## Decisions Already Made + +Use these decisions unless the user explicitly changes the ADR before +implementation: + +- The public root class becomes `Workspace`. +- The public root import becomes + `from easydiffraction import Workspace`. +- The public project-information category becomes `workspace.project`. +- The public rendering category remains `workspace.rendering`. +- The project-information category keeps semantic CIF tags `_project.*`. +- The rendering category keeps semantic CIF tags `_rendering.*`. +- The saved singleton config file becomes `workspace.cif`. +- The storage directory path belongs to `Workspace.path`, not + `workspace.project.path`. +- The old `Project` public API is removed unless the user explicitly + approves an alias before implementation. + +## Current Shape + +The current implementation already has a category-owner based project +configuration layer: + +```text +src/easydiffraction/project/ +|-- project.py # class Project +|-- project_config.py # class ProjectConfig +|-- project_info.py # ProjectInfo export +|-- display.py # class ProjectDisplay +`-- categories/ + |-- info/ # ProjectInfo category + `-- rendering/ # Rendering category +``` + +Current public API: + +```python +project = ed.Project(name='my_project') +project.info.title +project.rendering.table_engine +``` + +Current saved config file: + +```text +project.cif +``` + +## Target Shape + +Target implementation: + +```text +src/easydiffraction/workspace/ +|-- workspace.py # class Workspace +|-- workspace_config.py # class WorkspaceConfig +|-- project_info.py # ProjectInfo export +|-- display.py # class WorkspaceDisplay +`-- categories/ + |-- project/ # ProjectInfo category + `-- rendering/ # Rendering category +``` + +Target public API: + +```python +workspace = ed.Workspace(project_id='my_project') +workspace.project.title +workspace.rendering.table_engine +``` + +## Out Of Scope + +Do not do these in this migration: + +- Do not add `_meta.*` CIF tags. +- Do not redesign structure or experiment datablocks. +- Do not change analysis fit-mode semantics. +- Do not change calculator behavior. +- Do not edit generated package-structure docs by hand. +- Do not edit generated notebooks directly. Edit tutorial `.py` sources + and regenerate notebooks during Phase 2. +- Do not keep a `Project = Workspace` compatibility alias unless the + user explicitly approves it. + +## Phase 0: Confirm Breaking-Change Approval + +This migration removes or replaces the public `Project` API unless the +user approves a compatibility alias. + +Before changing code, ask the user to confirm: + +```text +This migration removes ed.Project and replaces it with ed.Workspace. +Should implementation proceed without a Project compatibility alias? +``` + +If the user asks for a compatibility alias, record that decision in this +plan and in the ADR before implementation. + +Do not implement code before this approval gate. + +Commit: no commit required for this phase unless the plan or ADR is +updated. + +## Phase 1: Rename Root Package And Public Facade + +### Objective + +Rename the top-level runtime facade and package from `project` to +`workspace`. + +### Files Likely To Change + +- `src/easydiffraction/project/` +- `src/easydiffraction/__init__.py` +- `src/easydiffraction/__main__.py` +- `src/easydiffraction/analysis/analysis.py` +- `src/easydiffraction/analysis/sequential.py` +- `src/easydiffraction/display/plotting.py` +- `src/easydiffraction/summary/summary.py` +- any source file importing `easydiffraction.project.*` + +### Steps + +1. Move the source package: + + ```text + src/easydiffraction/project/ + -> src/easydiffraction/workspace/ + ``` + +2. Rename files: + + ```text + workspace/project.py + -> workspace/workspace.py + + workspace/project_config.py + -> workspace/workspace_config.py + ``` + +3. Rename classes: + + ```text + Project -> Workspace + ProjectConfig -> WorkspaceConfig + ProjectDisplay -> WorkspaceDisplay + ``` + +4. Update top-level import: + + ```python + from easydiffraction.workspace.workspace import Workspace + ``` + +5. Remove the old top-level `Project` import unless the user approved an + alias. + +6. Update type-checking imports: + + ```python + from easydiffraction.workspace.workspace import Workspace + ``` + +7. Update docstrings from "Project facade" to "Workspace facade" where + they describe the root object. + +8. Run a source-only grep. Do not run tests in Phase 1: + + ```shell + rg -n "easydiffraction\\.project|\\bProject\\b|ProjectDisplay|ProjectConfig" src + ``` + + For every match, decide whether it refers to: + - the old root object, which should become `Workspace`; + - the project-information category, which should remain project; + - historical text that should be updated in docs later. + +### Stop Conditions + +Stop and ask if: + +- another public class named `Workspace` already exists; +- package moves break imports in a way that would require compatibility + shims; +- a file has both root-object `project` and category `project` meanings + that cannot be separated clearly. + +### Commit + +Stage explicit moved and edited files, then commit: + +```text +Rename Project facade to Workspace +``` + +## Phase 2: Rename Project-Info Access From `info` To `project` + +### Objective + +Make the project-information category public as `workspace.project` +instead of `workspace.info`. + +### Files Likely To Change + +- `src/easydiffraction/workspace/workspace_config.py` +- `src/easydiffraction/workspace/workspace.py` +- `src/easydiffraction/workspace/categories/info/` +- `src/easydiffraction/workspace/project_info.py` +- `src/easydiffraction/io/cif/serialize.py` +- all code using `.info` for project information + +### Steps + +1. Rename category package: + + ```text + src/easydiffraction/workspace/categories/info/ + -> src/easydiffraction/workspace/categories/project/ + ``` + +2. Keep the category class name `ProjectInfo` for now. The class name is + explicit and avoids a confusing `Project` class after the root class + is renamed to `Workspace`. + +3. Rename imports: + + ```python + from easydiffraction.workspace.categories.project import ProjectInfo + from easydiffraction.workspace.categories.project import ProjectInfoFactory + ``` + +4. In `WorkspaceConfig`, rename: + + ```text + _info -> _project + info -> project + ``` + +5. In `Workspace`, rename: + + ```text + _info -> _project + info -> project + ``` + +6. Remove the public `.info` property unless the user approved a + compatibility alias. + +7. Update all runtime references: + + ```text + workspace.info.title -> workspace.project.title + workspace.info.description -> workspace.project.description + workspace.info.update_last_modified() -> workspace.project.update_last_modified() + ``` + +8. Run grep: + + ```shell + rg -n "\\.info\\b|categories/info|categories\\.info" src + ``` + +9. For every match, update it if it refers to project information. Leave + unrelated uses of the word "info" alone. + +### Stop Conditions + +Stop and ask if: + +- `info` appears as a different public concept unrelated to project + information; +- removing `.info` would break a user-requested compatibility alias. + +### Commit + +```text +Expose project information as workspace.project +``` + +## Phase 3: Align Project Information Fields With `_project.*` + +### Objective + +Expose project identity as `workspace.project.id`, matching CIF +`_project.id`. + +Move the saved directory path to `workspace.path`, because it describes +the workspace location and is not serialized project information. + +### Files Likely To Change + +- `src/easydiffraction/workspace/categories/project/default.py` +- `src/easydiffraction/workspace/workspace.py` +- `src/easydiffraction/io/cif/serialize.py` +- `src/easydiffraction/summary/summary.py` +- `src/easydiffraction/display/plotting.py` +- any code using `.name` for project identity or `.project.path` + +### Steps + +1. In `ProjectInfo`, rename the public identity property: + + ```text + name -> id + ``` + +2. Keep the underlying CIF tag unchanged: + + ```python + CifHandler(names=['_project.id']) + ``` + +3. Update `ProjectInfo.unique_name` to return `self.id`. + +4. Update `project_info_to_cif()` and CIF loading helpers to use + `info.id`. + +5. Rename constructor arguments: + + ```text + name -> project_id + ``` + + Apply this to: + - `Workspace.__init__` + - `WorkspaceConfig.__init__` + - `ProjectInfo.__init__` + - `ProjectInfoFactory.create(...)` call sites + +6. Add `Workspace.path` as the runtime storage path. + + Suggested shape: + + ```python + @property + def path(self) -> pathlib.Path | None: + """Saved workspace directory.""" + return self._path + + @path.setter + def path(self, value: object) -> None: + self._path = pathlib.Path(value) + ``` + +7. Remove `ProjectInfo.path` unless explicitly approved as a + compatibility alias. + +8. Update save/load logic: + + ```text + workspace.path + ``` + + should replace: + + ```text + workspace.project.path + ``` + +9. Update messages and string representations: + + ```text + Workspace '' (...) + Saving workspace '' to ... + ``` + +10. Run grep: + +```shell +rg -n "\\.name\\b|\\.path\\b|project_id|Project identifier" src/easydiffraction/workspace src/easydiffraction/io src/easydiffraction/display src/easydiffraction/summary +``` + +Inspect each match manually. Do not blindly replace every `.name`; +structures and experiments still use `.name`. + +### Stop Conditions + +Stop and ask if: + +- a caller depends on `workspace.name` as a root-object property; +- moving `path` out of `ProjectInfo` makes save/load unclear; +- external saved fixtures require an approved compatibility path. + +### Commit + +```text +Align project metadata fields with CIF names +``` + +## Phase 4: Rename Project Config File To `workspace.cif` + +### Objective + +Rename the saved singleton configuration file from `project.cif` to +`workspace.cif`. + +### Files Likely To Change + +- `src/easydiffraction/workspace/workspace.py` +- `src/easydiffraction/io/cif/serialize.py` +- CLI entry points in `src/easydiffraction/__main__.py` +- docs that describe saved project directories +- test fixtures in Phase 2 + +### Steps + +1. Rename serializer functions if they still use project-root naming: + + ```text + project_config_to_cif -> workspace_config_to_cif + project_config_from_cif -> workspace_config_from_cif + project_to_cif -> workspace_to_cif + ``` + + Do not rename `project_info_to_cif`; it serializes the `_project` + category and that name remains correct. + +2. Update `Workspace.save()` to write: + + ```text + workspace.cif + ``` + +3. Update `Workspace.load()` to read: + + ```text + workspace.cif + ``` + +4. Do not add `project.cif` fallback unless the user approved a + compatibility loader. + +5. Keep the contents semantic: + + ```cif + _project.id + _project.title + _rendering.table_engine + ``` + +6. Update logging and console output from `project.cif` to + `workspace.cif`. + +7. Run grep: + + ```shell + rg -n "project\\.cif|project_config_to_cif|project_config_from_cif|project_to_cif" src docs tests + ``` + + In Phase 1, update source and docs only. Test files are handled in + Phase 2 unless the user explicitly asks otherwise. + +### Stop Conditions + +Stop and ask if: + +- repository fixtures or tutorials contain saved directories that must + remain loadable without conversion; +- the user wants a one-release compatibility loader. + +### Commit + +```text +Rename project config file to workspace.cif +``` + +## Phase 5: Update Root-Object References Across Runtime Code + +### Objective + +Replace root-object variables and attributes named `project` with +`workspace` where they refer to the top-level facade. + +Keep the word `project` where it refers to the project-information +category or the scientific project itself. + +### Files Likely To Change + +- `src/easydiffraction/analysis/analysis.py` +- `src/easydiffraction/analysis/sequential.py` +- `src/easydiffraction/display/plotting.py` +- `src/easydiffraction/project/display.py` after it has moved to + `workspace/display.py` +- `src/easydiffraction/summary/summary.py` +- `src/easydiffraction/__main__.py` +- `src/easydiffraction/io/*` + +### Steps + +1. Rename root references in `Analysis`: + + ```text + self.project -> self.workspace + analysis.project -> analysis.workspace + ``` + +2. Rename display internals: + + ```text + self._project -> self._workspace + _set_project(...) -> _set_workspace(...) + ``` + + Only do this when the object is the top-level runtime facade. + +3. Rename local variables in runtime code: + + ```text + project = Workspace(...) + -> workspace = Workspace(...) + ``` + +4. Keep scientific-project wording where appropriate: + + ```text + workspace.project.title + project_id + _project.id + ``` + +5. Update user-facing messages carefully. Good examples: + + ```text + "Workspace directory not found" + "Saving workspace" + "Project title" + ``` + +6. Run grep: + + ```shell + rg -n "\\bproject\\b|\\bProject\\b|_project|ProjectDisplay" src/easydiffraction + ``` + +7. Inspect each match. Do not replace `_project` CIF tags. + +### Stop Conditions + +Stop and ask if: + +- a name has both root-workspace and project-information meanings in the + same function and cannot be made clear; +- renaming a method such as `_set_project` would require updating public + plugin or user code. + +### Commit + +```text +Update runtime references to Workspace +``` + +## Phase 6: Update Docs, Tutorials, And ADR References + +### Objective + +Update user-facing and developer-facing documentation to describe the +new root object and project-information category. + +### Files Likely To Change + +- `docs/dev/architecture.md` +- `docs/dev/Issues/issues_open.md` +- `docs/dev/ADRs/*.md` +- `docs/dev/ADR-suggestions/*.md` +- `docs/docs/tutorials/*.py` +- `README.md` +- `CONTRIBUTING.md` only if it contains API examples + +Do not edit these by hand: + +- `docs/dev/package-structure-full.md` +- `docs/dev/package-structure-short.md` +- generated tutorial notebooks +- generated `docs/site/` files + +### Steps + +1. Update architecture section 7: + + ```text + Project - The Top-Level Facade + -> Workspace - The Top-Level Facade + ``` + +2. Update the architecture table to use: + + ```text + workspace.project ProjectInfo + workspace.rendering Rendering + workspace.display WorkspaceDisplay + ``` + +3. Update saved layout examples: + + ```text + workspace.cif + structures/ + experiments/ + analysis/ + summary.cif + ``` + +4. Update public examples: + + ```python + workspace = ed.Workspace(project_id='lbco_hrpt') + workspace.project.title = '...' + ``` + +5. Update ADRs that describe current API. Historical reasoning can keep + old names only if it is clearly historical and not presented as + current usage. + +6. Update tutorial `.py` files, not notebooks. Phase 2 will run + `pixi run notebook-prepare`. + +7. Run grep: + + ```shell + rg -n "ed\\.Project|from easydiffraction import Project|project\\.info|project\\.rendering|ProjectDisplay|project\\.cif" docs README.md CONTRIBUTING.md + ``` + +8. Inspect each match manually. + +### Stop Conditions + +Stop and ask if: + +- a tutorial title uses "Project" as ordinary English rather than API + naming; +- historical ADRs would become misleading if edited mechanically. + +### Commit + +```text +Update docs for Workspace root API +``` + +## Phase 7: Remove Old Public `Project` Surface Unless Approved + +### Objective + +Finish the breaking rename by removing old public imports and module +paths unless the user approved compatibility. + +### Steps Without Compatibility Alias + +1. Ensure top-level `easydiffraction.__init__` exports `Workspace`, not + `Project`. + +2. Ensure no source imports from: + + ```text + easydiffraction.project + ``` + +3. Ensure no public source package remains at: + + ```text + src/easydiffraction/project + ``` + +4. Run grep: + + ```shell + rg -n "from easydiffraction import Project|ed\\.Project|easydiffraction\\.project|\\bProject\\(" src docs tests tools README.md CONTRIBUTING.md + ``` + +5. Any remaining match must be: + - historical text that intentionally names the old API; or + - a test that will be updated in Phase 2; or + - a generated artifact that should not be edited manually. + +### Steps With Approved Compatibility Alias + +Only do this if the user explicitly approved it. + +1. Add a temporary alias in `src/easydiffraction/__init__.py`: + + ```python + Project = Workspace + ``` + +2. Keep the alias undocumented unless the user asks for a migration + note. + +3. Add tests in Phase 2 proving both `Workspace` and `Project` construct + the same root object. + +### Commit + +Without alias: + +```text +Remove old Project public API surface +``` + +With alias: + +```text +Add Project alias for Workspace migration +``` + +## Phase 1 Review Gate + +After Phase 1 commits are complete: + +1. Run `git status --short`. +2. Confirm only intended files are changed. +3. Summarize: + - whether `Project` was removed or aliased; + - whether `workspace.cif` replaced `project.cif`; + - any files intentionally left for Phase 2 test updates; + - any unresolved questions. + +4. Stop and ask the user to review before starting Phase 2. + +Do not run the full verification suite until the user approves moving to +Phase 2. + +## Phase 2: Verification And Tests + +Only start this phase after the user approves the Phase 1 +implementation. + +### Test Updates + +Move or update tests to mirror the new source tree: + +```text +tests/unit/easydiffraction/project/ +-> tests/unit/easydiffraction/workspace/ +``` + +Update imports: + +```python +from easydiffraction.workspace.workspace import Workspace +from easydiffraction.workspace.display import WorkspaceDisplay +``` + +Update functional and integration tests: + +```python +from easydiffraction import Workspace +workspace = Workspace(project_id='...') +``` + +### New Tests To Add + +Add focused tests for: + +1. `from easydiffraction import Workspace`. +2. `Workspace(project_id='p1').project.id == 'p1'`. +3. `workspace.project.title` round-trips through `workspace.cif`. +4. `workspace.rendering.table_engine` round-trips through + `workspace.cif`. +5. `Workspace.save()` writes `workspace.cif`. +6. `Workspace.load()` reads `workspace.cif`. +7. `workspace.cif` contains `_project.id`, not `_meta.project_id`. +8. `workspace.cif` contains `_rendering.table_engine`. +9. `workspace.path` is set after `save_as()` and `load()`. +10. `workspace.project` has no serialized path field. +11. `project.cif` is not written unless compatibility was approved. +12. `ed.Project` is absent unless compatibility was approved. + +If compatibility alias was approved, add tests for: + +1. `from easydiffraction import Project`. +2. `Project is Workspace` or equivalent behavior. +3. Any approved `project.cif` fallback behavior. + +### Verification Commands + +Run in this order: + +```shell +pixi run test-structure-check +pixi run fix +pixi run check +pixi run unit-tests +pixi run integration-tests +pixi run script-tests +pixi run notebook-prepare +pixi run notebook-tests +``` + +If `pixi run fix` regenerates package-structure docs, accept those +generated changes and do not hand-edit them. + +### Phase 2 Commit Suggestions + +Use one or more commits, depending on size: + +```text +Update workspace unit tests +Update tutorials for Workspace API +Regenerate tutorial notebooks for Workspace API +``` + +## Grep Checklist + +Use this checklist before final review. + +Runtime root object should use `Workspace`: + +```shell +rg -n "\\bProject\\b|ed\\.Project|from easydiffraction import Project" src tests docs tools README.md CONTRIBUTING.md +``` + +Project-information category should use `workspace.project`: + +```shell +rg -n "\\.info\\b|workspace\\.project|project\\.info" src tests docs tools README.md CONTRIBUTING.md +``` + +CIF project category should stay `_project`: + +```shell +rg -n "_meta\\.|_project\\." src tests docs tools README.md CONTRIBUTING.md +``` + +Saved config file should be `workspace.cif`: + +```shell +rg -n "project\\.cif|workspace\\.cif" src tests docs tools README.md CONTRIBUTING.md +``` + +Generated docs should not be manually edited: + +```shell +git diff -- docs/site docs/dev/package-structure-full.md docs/dev/package-structure-short.md +``` + +If package-structure docs changed because of `pixi run fix`, that is +expected. If `docs/site` changed, ask before staging. + +## Common Mistakes + +### Mistake: Renaming `_project.*` To `_workspace.*` + +Do not do this. The CIF category describes scientific project +information, not the runtime facade. + +Correct: + +```cif +_project.id +_project.title +``` + +Incorrect: + +```cif +_workspace.project_id +_workspace.title +``` + +### Mistake: Introducing `_meta.*` + +Do not replace `_project.*` with `_meta.*`. + +Correct: + +```cif +_project.title +``` + +Incorrect: + +```cif +_meta.project_title +``` + +### Mistake: Blindly Replacing Every `project` + +Some uses of `project` should remain: + +- CIF tags such as `_project.id` +- `workspace.project` +- scientific project wording in prose +- `ProjectInfo` class name, unless a later ADR changes it + +Only root-facade uses should become `workspace` or `Workspace`. + +### Mistake: Leaving Path On Project Information + +The saved directory path belongs to the workspace runtime state. It +should be `workspace.path`, not `workspace.project.path`. + +### Mistake: Editing Generated Notebooks Directly + +Tutorial notebooks are generated artifacts. Edit tutorial `.py` files, +then run `pixi run notebook-prepare` in Phase 2. + +## Suggested Pull Request + +Title: + +```text +Rename Project root object to Workspace +``` + +Description: + +```text +This change separates the working EasyDiffraction workspace from the +scientific project information stored inside it. Users now create a +Workspace, while project title and description live under +workspace.project and continue to serialize with clear _project.* CIF +names. +``` diff --git a/docs/docs/tutorials/ed-17.py b/docs/docs/tutorials/ed-17.py index 2fd4b0f64..24e648c56 100644 --- a/docs/docs/tutorials/ed-17.py +++ b/docs/docs/tutorials/ed-17.py @@ -26,7 +26,7 @@ # results can be written to `analysis/results.csv`. # %% -project.save_as('projects/cosio', temporary=False) +project.save_as(dir_path='projects/cosio', temporary=False) # %% [markdown] # ## Step 2: Define Crystal Structure diff --git a/docs/docs/tutorials/ed-20.py b/docs/docs/tutorials/ed-20.py index 8c6a5b549..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 diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index 863352413..08303f65b 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -21,6 +21,7 @@ ) from easydiffraction.analysis.enums import FitModeEnum from easydiffraction.analysis.fitting import Fitter +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 @@ -346,7 +347,7 @@ def as_cif(self) -> None: self._analysis.show_as_cif() -class Analysis: +class Analysis(CategoryOwner): """ High-level orchestration of analysis tasks for a Project. @@ -364,31 +365,63 @@ 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._constraints = ConstraintsFactory.create(self._constraints_type) + self._constraints_handler = ConstraintsHandler.get() self._fitting: Fitting = FittingFactory.create(FittingFactory.default_tag()) - self._fitting._parent = self self._fitting_mode_type: FitModeEnum = FitModeEnum.default() self._joint_fit: JointFitCollection = JointFitCollection() self._sequential_fit: SequentialFit = SequentialFitFactory.create( SequentialFitFactory.default_tag() ) - self._sequential_fit._parent = self self._sequential_fit_extract = SequentialFitExtractCollection() - self.fitter = Fitter(self._fitting.minimizer_type.value) - self.fit_results = None + 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 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.""" + return self._fit_results + + @fit_results.setter + def fit_results(self, value: object | None) -> None: + self._fit_results = value + def help(self) -> None: """Print a summary of analysis properties and methods.""" cls = type(self) @@ -453,6 +486,24 @@ def _help_filter( 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, + ]) + + return categories + # ------------------------------------------------------------------ # Parameter helpers # ------------------------------------------------------------------ @@ -968,13 +1019,13 @@ 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: 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/datablock.py b/src/easydiffraction/core/datablock.py index 0ac201760..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, - ) - # ====================================================================== diff --git a/src/easydiffraction/core/variable.py b/src/easydiffraction/core/variable.py index 76554bdd1..257a5771f 100644 --- a/src/easydiffraction/core/variable.py +++ b/src/easydiffraction/core/variable.py @@ -128,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: @@ -153,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: @@ -175,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: @@ -355,17 +355,17 @@ 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 + 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._user_constrained = True - 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 free(self) -> bool: diff --git a/src/easydiffraction/io/cif/serialize.py b/src/easydiffraction/io/cif/serialize.py index ae5b59fa8..f7625330e 100644 --- a/src/easydiffraction/io/cif/serialize.py +++ b/src/easydiffraction/io/cif/serialize.py @@ -268,6 +268,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, @@ -290,32 +323,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( - cif_text - for cif_text in (v.as_cif for v in vars(datablock).values() if isinstance(v, CategoryItem)) - if cif_text - ) - - # Then collections - parts.extend( - cif_text - for cif_text in ( - category_collection_to_cif(v, max_display=max_loop_display) - for v in vars(datablock).values() - if isinstance(v, CategoryCollection) - ) - if cif_text - ) - - 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: @@ -340,8 +352,8 @@ def project_info_to_cif(info: object) -> str: else: 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 = f"'{info.created.strftime('%d %b %Y %H:%M:%S')}'" + last_modified = f"'{info.last_modified.strftime('%d %b %Y %H:%M:%S')}'" return ( f'_project.id {name}\n' @@ -360,6 +372,10 @@ 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)] rendering = getattr(project, 'rendering', None) if rendering is not None: @@ -392,30 +408,28 @@ def analysis_to_cif(analysis: object) -> str: """Render analysis metadata, aliases, and constraints to CIF.""" parts: list[str] = [f'_fitting.mode_type {format_value(analysis.fitting_mode_type)}'] - fitting_cif = analysis.fitting.as_cif - if fitting_cif: - parts.append(fitting_cif) - - aliases_cif = analysis.aliases.as_cif - if aliases_cif: - parts.append(aliases_cif) - - constraints_cif = analysis.constraints.as_cif - if constraints_cif: - parts.append(constraints_cif) - - if analysis.fitting_mode_type == 'joint': - joint_fit_cif = analysis.joint_fit.as_cif - if joint_fit_cif: - parts.append(joint_fit_cif) - elif analysis.fitting_mode_type == 'sequential': - sequential_fit_cif = analysis.sequential_fit.as_cif - if sequential_fit_cif: - parts.append(sequential_fit_cif) - - sequential_extract_cif = analysis.sequential_fit_extract.as_cif - if sequential_extract_cif: - parts.append(sequential_extract_cif) + 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) @@ -449,6 +463,11 @@ def _populate_project_info_from_block( 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') 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..1d7b8e318 --- /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.""" + + 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 + + self._identity.category_code = 'project' + + @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/default.py b/src/easydiffraction/project/categories/rendering/default.py index f8970cb55..b55a6f923 100644 --- a/src/easydiffraction/project/categories/rendering/default.py +++ b/src/easydiffraction/project/categories/rendering/default.py @@ -83,9 +83,14 @@ def table_engine(self, value: str) -> None: @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) + 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 diff --git a/src/easydiffraction/project/project.py b/src/easydiffraction/project/project.py index df8972de9..5bbe8acba 100644 --- a/src/easydiffraction/project/project.py +++ b/src/easydiffraction/project/project.py @@ -6,6 +6,7 @@ import pathlib import tempfile +from typing import TYPE_CHECKING from typing import ClassVar from typeguard import typechecked @@ -17,15 +18,17 @@ from easydiffraction.datablocks.structure.collection import Structures from easydiffraction.io.cif.serialize import project_config_to_cif from easydiffraction.io.cif.serialize import project_to_cif -from easydiffraction.project.categories.rendering import Rendering -from easydiffraction.project.categories.rendering import RenderingFactory from easydiffraction.project.display import ProjectDisplay -from easydiffraction.project.project_info import ProjectInfo +from easydiffraction.project.project_config import ProjectConfig from easydiffraction.summary.summary import Summary from easydiffraction.utils.enums import VerbosityEnum from easydiffraction.utils.logging import console from easydiffraction.utils.logging import log +if TYPE_CHECKING: + from easydiffraction.project.categories.rendering import Rendering + from easydiffraction.project.project_info import ProjectInfo + def _apply_csv_row_to_params( row: object, @@ -127,11 +130,11 @@ 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._rendering = RenderingFactory.create('default') - self._rendering._parent = self + object.__setattr__(self, '_rendering', self._config.rendering) self._display = ProjectDisplay(self) self._analysis = Analysis(self) self._summary = Summary(self) @@ -323,7 +326,7 @@ def load(cls, dir_path: str) -> Project: cif_text = project_cif_path.read_text() project_config_from_cif(project, cif_text) - project._info.path = project_path + project.info.path = project_path # 2. Load structures structures_dir = project_path / 'structures' @@ -390,7 +393,7 @@ def _resolve_alias_references(self) -> None: 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 @@ -403,15 +406,15 @@ def save(self) -> None: 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(): @@ -422,7 +425,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(): @@ -433,7 +436,7 @@ 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) @@ -447,11 +450,11 @@ def save(self) -> None: 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( @@ -464,7 +467,7 @@ def save_as( if temporary: tmp: str = tempfile.gettempdir() dir_path = pathlib.Path(tmp) / dir_path - self._info.path = dir_path + self.info.path = dir_path self.save() def apply_params_from_csv(self, row_index: int) -> None: diff --git a/src/easydiffraction/project/project_config.py b/src/easydiffraction/project/project_config.py new file mode 100644 index 000000000..32147b609 --- /dev/null +++ b/src/easydiffraction/project/project_config.py @@ -0,0 +1,47 @@ +# 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 + + +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()) + + @property + def info(self) -> ProjectInfo: + """Project metadata category.""" + return self._info + + @property + def rendering(self) -> Rendering: + """Rendering configuration category.""" + return self._rendering + + @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 9bdfac7b0..215e8ed54 100644 --- a/src/easydiffraction/project/project_info.py +++ b/src/easydiffraction/project/project_info.py @@ -1,137 +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.""" from __future__ import annotations -import datetime -import pathlib +from easydiffraction.project.categories.info.default import ProjectInfo as _ProjectInfo -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 - - -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 - @property - 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/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/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..badd72418 --- /dev/null +++ b/tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py @@ -0,0 +1,97 @@ +# 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.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/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/test_project_config.py b/tests/unit/easydiffraction/project/test_project_config.py new file mode 100644 index 000000000..3ba20eea1 --- /dev/null +++ b/tests/unit/easydiffraction/project/test_project_config.py @@ -0,0 +1,68 @@ +# 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.categories == [config.info, config.rendering] + assert config.parameters == config.info.parameters + config.rendering.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 + + +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 + + 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' From ab689796963c26dc4ac72aec0757ef97c4ae77e0 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Mon, 18 May 2026 14:04:21 +0200 Subject: [PATCH 09/10] Use declarative category identity metadata (#177) * Document verbosity category and workspace.cif layout * Reorganize development documentation structure * Tighten workspace migration plan for autonomous implementation * Consolidate ADR index into single table * Add loop category key identity ADR and plan * Add declarative category identity resolution * Declare category identities on current items * Persist explicit constraint identifiers * Update ADR for declarative category identity * Verify declarative category identity behavior --- .github/copilot-instructions.md | 57 +- .../accepted/category-owner-sections.md} | 0 .../accepted/category-parameter-access.md | 44 + .../accepted/development-docs-structure.md | 66 + .../accepted/display-ux.md} | 0 .../accepted/enum-backed-closed-values.md | 34 + docs/dev/adrs/accepted/factory-contracts.md | 44 + docs/dev/adrs/accepted/factory-tag-naming.md | 48 + .../accepted/fit-mode-categories.md} | 0 .../adrs/accepted/free-flag-cif-encoding.md | 39 + .../accepted/guarded-public-properties.md | 44 + .../accepted/help-discoverability.md} | 0 .../accepted/immutable-experiment-type.md | 35 + .../accepted/lint-complexity-thresholds.md | 33 + docs/dev/adrs/accepted/notebook-generation.md | 34 + .../project-facade-and-persistence.md | 47 + .../accepted/property-docstring-template.md | 39 + docs/dev/adrs/accepted/runtime-fit-results.md | 36 + docs/dev/adrs/accepted/selector-families.md | 40 + .../string-paths-and-live-descriptors.md | 45 + .../adrs/accepted/switchable-category-api.md | 45 + docs/dev/adrs/accepted/test-strategy.md | 41 + .../accepted/type-neutral-adp-parameters.md | 42 + docs/dev/adrs/index.md | 46 + .../suggestions/analysis-cif-fit-state.md} | 8 +- .../suggestions/loop-category-key-identity.md | 158 ++ .../parameter-correlation-persistence.md} | 0 .../parameter-posterior-summary.md} | 4 +- .../python-cif-category-correspondence.md | 314 ++++ .../suggestions/undo-fit.md} | 2 +- .../workspace-root-project-category.md} | 76 +- docs/dev/architecture.md | 1631 ----------------- docs/dev/index.md | 29 + .../issues_closed.md => issues/closed.md} | 3 +- .../{Issues/issues_open.md => issues/open.md} | 12 +- .../full.md} | 0 .../short.md} | 0 docs/dev/plans/loop-category-key-identity.md | 620 +++++++ .../workspace-root-project-category.md} | 324 +++- docs/dev/{ => roadmap}/ROADMAP.md | 0 .../analysis/categories/aliases/default.py | 6 +- .../categories/constraints/default.py | 42 +- .../analysis/categories/fitting/default.py | 4 +- .../analysis/categories/joint_fit/default.py | 6 +- .../categories/sequential_fit/default.py | 4 +- .../sequential_fit_extract/default.py | 6 +- src/easydiffraction/core/category.py | 18 + src/easydiffraction/core/identity.py | 7 + .../categories/background/chebyshev.py | 6 +- .../categories/background/line_segment.py | 6 +- .../categories/calculation/default.py | 4 +- .../experiment/categories/data/bragg_pd.py | 10 +- .../experiment/categories/data/total_pd.py | 6 +- .../experiment/categories/diffrn/default.py | 4 +- .../categories/excluded_regions/default.py | 5 +- .../categories/experiment_type/default.py | 4 +- .../categories/extinction/becker_coppens.py | 4 +- .../experiment/categories/instrument/base.py | 5 +- .../categories/linked_crystal/default.py | 4 +- .../categories/linked_phases/default.py | 6 +- .../experiment/categories/peak/base.py | 3 +- .../experiment/categories/refln/bragg_sc.py | 6 +- .../categories/atom_site_aniso/default.py | 6 +- .../categories/atom_sites/default.py | 6 +- .../structure/categories/cell/default.py | 4 +- .../categories/space_group/default.py | 4 +- src/easydiffraction/io/cif/serialize.py | 6 + .../project/categories/info/default.py | 4 +- .../project/categories/rendering/default.py | 4 +- .../test_analysis_and_fit_category_support.py | 7 +- .../integration/fitting/test_project_load.py | 3 + .../analysis/categories/test_constraints.py | 36 + .../analysis/test_analysis_coverage.py | 7 +- .../easydiffraction/core/test_category.py | 157 +- .../test_serialize_category_owner_baseline.py | 1 + .../project/test_project_load.py | 2 + tools/generate_package_docs.py | 12 +- tools/param_consistency.py | 4 +- 78 files changed, 2648 insertions(+), 1821 deletions(-) rename docs/dev/{ADRs/adr_category-owner-sections.md => adrs/accepted/category-owner-sections.md} (100%) create mode 100644 docs/dev/adrs/accepted/category-parameter-access.md create mode 100644 docs/dev/adrs/accepted/development-docs-structure.md rename docs/dev/{ADRs/adr_display-ux.md => adrs/accepted/display-ux.md} (100%) create mode 100644 docs/dev/adrs/accepted/enum-backed-closed-values.md create mode 100644 docs/dev/adrs/accepted/factory-contracts.md create mode 100644 docs/dev/adrs/accepted/factory-tag-naming.md rename docs/dev/{ADRs/adr_fit-mode-categories.md => adrs/accepted/fit-mode-categories.md} (100%) create mode 100644 docs/dev/adrs/accepted/free-flag-cif-encoding.md create mode 100644 docs/dev/adrs/accepted/guarded-public-properties.md rename docs/dev/{ADRs/adr_help-discoverability.md => adrs/accepted/help-discoverability.md} (100%) create mode 100644 docs/dev/adrs/accepted/immutable-experiment-type.md create mode 100644 docs/dev/adrs/accepted/lint-complexity-thresholds.md create mode 100644 docs/dev/adrs/accepted/notebook-generation.md create mode 100644 docs/dev/adrs/accepted/project-facade-and-persistence.md create mode 100644 docs/dev/adrs/accepted/property-docstring-template.md create mode 100644 docs/dev/adrs/accepted/runtime-fit-results.md create mode 100644 docs/dev/adrs/accepted/selector-families.md create mode 100644 docs/dev/adrs/accepted/string-paths-and-live-descriptors.md create mode 100644 docs/dev/adrs/accepted/switchable-category-api.md create mode 100644 docs/dev/adrs/accepted/test-strategy.md create mode 100644 docs/dev/adrs/accepted/type-neutral-adp-parameters.md create mode 100644 docs/dev/adrs/index.md rename docs/dev/{ADR-suggestions/adr_analysis-cif-fit-state.md => adrs/suggestions/analysis-cif-fit-state.md} (96%) create mode 100644 docs/dev/adrs/suggestions/loop-category-key-identity.md rename docs/dev/{ADR-suggestions/adr_parameter-correlation-persistence.md => adrs/suggestions/parameter-correlation-persistence.md} (100%) rename docs/dev/{ADR-suggestions/adr_parameter-posterior-summary.md => adrs/suggestions/parameter-posterior-summary.md} (99%) create mode 100644 docs/dev/adrs/suggestions/python-cif-category-correspondence.md rename docs/dev/{ADR-suggestions/adr_undo-fit.md => adrs/suggestions/undo-fit.md} (99%) rename docs/dev/{ADR-suggestions/adr_workspace-root-project-category.md => adrs/suggestions/workspace-root-project-category.md} (76%) delete mode 100644 docs/dev/architecture.md create mode 100644 docs/dev/index.md rename docs/dev/{Issues/issues_closed.md => issues/closed.md} (98%) rename docs/dev/{Issues/issues_open.md => issues/open.md} (99%) rename docs/dev/{package-structure-full.md => package-structure/full.md} (100%) rename docs/dev/{package-structure-short.md => package-structure/short.md} (100%) create mode 100644 docs/dev/plans/loop-category-key-identity.md rename docs/dev/{plan_workspace-root-project-category.md => plans/workspace-root-project-category.md} (67%) rename docs/dev/{ => roadmap}/ROADMAP.md (100%) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0a54cc941..d5f96553d 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/dev/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/dev/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,34 +146,45 @@ 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/dev/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. - Open issues / design questions / planned improvements live in - `docs/dev/issues_open.md` (priority-ordered). On resolution, move to - `docs/dev/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 @@ -184,6 +204,9 @@ 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. +- 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/docs/dev/ADRs/adr_category-owner-sections.md b/docs/dev/adrs/accepted/category-owner-sections.md similarity index 100% rename from docs/dev/ADRs/adr_category-owner-sections.md rename to docs/dev/adrs/accepted/category-owner-sections.md 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/adr_display-ux.md b/docs/dev/adrs/accepted/display-ux.md similarity index 100% rename from docs/dev/ADRs/adr_display-ux.md rename to docs/dev/adrs/accepted/display-ux.md 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/adr_fit-mode-categories.md b/docs/dev/adrs/accepted/fit-mode-categories.md similarity index 100% rename from docs/dev/ADRs/adr_fit-mode-categories.md rename to docs/dev/adrs/accepted/fit-mode-categories.md 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/adr_help-discoverability.md b/docs/dev/adrs/accepted/help-discoverability.md similarity index 100% rename from docs/dev/ADRs/adr_help-discoverability.md rename to docs/dev/adrs/accepted/help-discoverability.md 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/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/project-facade-and-persistence.md b/docs/dev/adrs/accepted/project-facade-and-persistence.md new file mode 100644 index 000000000..67492d8d4 --- /dev/null +++ b/docs/dev/adrs/accepted/project-facade-and-persistence.md @@ -0,0 +1,47 @@ +# ADR: Project Facade and Persistence Layout + +## Status + +Accepted current design. + +## Date + +2026-05-17 + +## Group + +Persistence. + +## Context + +`Project` is the current top-level user facade. It owns project +metadata, structures, experiments, rendering preferences, display +helpers, analysis, summaries, verbosity, and save/load behavior. + +The persisted project directory needs to separate real CIF datablocks +from singleton project sections. + +## Decision + +Use `Project` as the current 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. + +## Consequences + +The saved layout mirrors the current object graph while preserving the +semantic difference between real datablocks and singleton sections. A +proposed `Workspace` rename is tracked separately as an ADR suggestion. 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..ed0940bf3 --- /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 | Suggestion | Analysis CIF Fit State | Proposes a persisted scalar projection of fit state in `analysis.cif`. | [`analysis-cif-fit-state.md`](suggestions/analysis-cif-fit-state.md) | +| Analysis and fitting | Suggestion | Parameter Correlation Persistence | Proposes persisting deterministic and posterior correlation summaries. | [`parameter-correlation-persistence.md`](suggestions/parameter-correlation-persistence.md) | +| Analysis and fitting | Suggestion | Parameter-Level Posterior Projection and Bayesian Persistence | Proposes saved Bayesian summaries and canonical posterior storage. | [`parameter-posterior-summary.md`](suggestions/parameter-posterior-summary.md) | +| Analysis and fitting | Suggestion | Undo Fit | Proposes an analysis-owned rollback operation for the latest pre-fit scalar state. | [`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 | 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 | Loop Category Keys and Identity Naming | Documents current loop collection keys and proposes naming rules aligned with CIF category keys. | [`loop-category-key-identity.md`](suggestions/loop-category-key-identity.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) | +| Workspace model | Suggestion | Workspace Root and Project Information Category | Proposes renaming the top-level facade from `Project` to `Workspace` and reserving `project` for project metadata. | [`workspace-root-project-category.md`](suggestions/workspace-root-project-category.md) | diff --git a/docs/dev/ADR-suggestions/adr_analysis-cif-fit-state.md b/docs/dev/adrs/suggestions/analysis-cif-fit-state.md similarity index 96% rename from docs/dev/ADR-suggestions/adr_analysis-cif-fit-state.md rename to docs/dev/adrs/suggestions/analysis-cif-fit-state.md index c39fc192b..d471bb117 100644 --- a/docs/dev/ADR-suggestions/adr_analysis-cif-fit-state.md +++ b/docs/dev/adrs/suggestions/analysis-cif-fit-state.md @@ -33,7 +33,7 @@ This separation matters because: - `analysis.fit_results` already changes by fit type, but its persisted projection should have a stable analysis-owned home -The current architecture document still describes fit results as +The accepted runtime-fit-results ADR describes fit results as runtime-only. This ADR proposes a narrower persisted projection of the latest fit state, not a direct dump of backend runtime objects. @@ -137,7 +137,7 @@ and Bayesian fitting. Fit-type-specific extensions are layered on top: - Bayesian persistence extends this with `_bayesian_*` categories and an - HDF5 sidecar, as described in `adr_parameter-posterior-summary.md`. + HDF5 sidecar, as described in `parameter-posterior-summary.md`. - Future fit-specific summaries should follow the same pattern: generic shared fields in `_fit_result`, specialized fields in separate categories. @@ -204,7 +204,7 @@ _fit_result.reduced_chi_square 1.031 ### Trade-offs -- The architecture document must be updated because fit state is no +- The runtime fit-results ADR must be updated because fit state is no longer entirely runtime-only. - Analysis persistence becomes more stateful and must be kept in sync with live parameter objects. @@ -214,7 +214,7 @@ _fit_result.reduced_chi_square 1.031 ## Deferred Work - Bayesian-specific categories and HDF5 sidecar details remain in - `adr_parameter-posterior-summary.md`. + `parameter-posterior-summary.md`. - Undo semantics for `start_value` and `start_uncertainty` are defined in a separate ADR. - Correlation-matrix persistence is defined in a separate ADR. diff --git a/docs/dev/adrs/suggestions/loop-category-key-identity.md b/docs/dev/adrs/suggestions/loop-category-key-identity.md new file mode 100644 index 000000000..f55ef4a89 --- /dev/null +++ b/docs/dev/adrs/suggestions/loop-category-key-identity.md @@ -0,0 +1,158 @@ +# ADR: Loop Category Keys and Identity Naming + +**Status:** Proposed +**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 currently 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 is whether the current `category_entry_name` +approach is enough, and how closely Python-facing identity names should +follow CIF key tags. + +## Assessment + +The current 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. + +It is not explicit enough yet. The key field is encoded as a lambda on +each item, not as declarative metadata on the category or descriptor. +Nothing validates that the runtime key corresponds to a serialized CIF +field. The main visible example is `Constraint`: the current collection +key is derived from the left-hand side of `_constraint.expression`, but +no separate `_constraint.id` field is 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 should add an explicit `id` field and use it +as the collection key: + +```text +analysis.constraints[id].id -> _constraint.id +``` + +The existing `lhs_alias` and `rhs_expr` properties should 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. Current implementation derives 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 current `category_entry_name` mechanism can stay, but it should be +made easier to audit. The implementation now 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, add a descriptor-backed `id` property serialized as +`_constraint.id`, and change `category_entry_name` to resolve from that +descriptor. Keep `_constraint.expression` for the full equation. Keep +`lhs_alias` and `rhs_expr` as derived convenience properties. + +When reading older CIF files that only contain `_constraint.expression`, +derive a deterministic fallback `id` from the old `lhs_alias` key after +row values are loaded, then write `_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/ADR-suggestions/adr_parameter-correlation-persistence.md b/docs/dev/adrs/suggestions/parameter-correlation-persistence.md similarity index 100% rename from docs/dev/ADR-suggestions/adr_parameter-correlation-persistence.md rename to docs/dev/adrs/suggestions/parameter-correlation-persistence.md diff --git a/docs/dev/ADR-suggestions/adr_parameter-posterior-summary.md b/docs/dev/adrs/suggestions/parameter-posterior-summary.md similarity index 99% rename from docs/dev/ADR-suggestions/adr_parameter-posterior-summary.md rename to docs/dev/adrs/suggestions/parameter-posterior-summary.md index f81e64472..3de39bf37 100644 --- a/docs/dev/ADR-suggestions/adr_parameter-posterior-summary.md +++ b/docs/dev/adrs/suggestions/parameter-posterior-summary.md @@ -12,8 +12,8 @@ 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 current -architecture document describes this state as runtime-only and not +`posterior_predictive`, diagnostics, and sampler settings. The accepted +runtime-fit-results ADR describes this state as runtime-only and not serialized. `analysis.fit_results` already changes by analysis type: deterministic 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..6f970a0e2 --- /dev/null +++ b/docs/dev/adrs/suggestions/python-cif-category-correspondence.md @@ -0,0 +1,314 @@ +# 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.level -> project.cif: _verbosity.level +``` + +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. + +## 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` | not persisted | none | Runtime-only string property backed by `VerbosityEnum`; no `_verbosity` category. | +| `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` | none | No | Runtime-only string convenience property; current code has no `project.verbosity.level` category and no `_verbosity.level` tag. | + +### 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. + +Target project-level mappings if the current Python names are kept: + +| 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.level` | `_verbosity.level` | Currently no persisted verbosity category. | + +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.level`: the file +scope tells the reader this is project-level verbosity. + +### The Current `Project` Root Already Matches User Language + +The current public root object is already `Project`. Keeping it avoids a +user-facing `workspace.project.*` nesting and aligns with scientific +workflows where a project is the container for structures, experiments, +analysis, and saved files. + +### 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. +- If verbosity is persisted, `project.verbosity` would either need to + become a category object or remain as a convenience alias for a new + `project.verbosity.level` field. +- 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 metadata move from `_project.*` to `_info.*`, or is + `_project.*` clearer even inside `project.cif`? +- 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.*` be accepted as a read-only legacy fallback when + loading older saved projects? +- Should `project.verbosity = 'short'` remain as a convenience alias for + `project.verbosity.level = 'short'`, or should strict correspondence + remove the alias? diff --git a/docs/dev/ADR-suggestions/adr_undo-fit.md b/docs/dev/adrs/suggestions/undo-fit.md similarity index 99% rename from docs/dev/ADR-suggestions/adr_undo-fit.md rename to docs/dev/adrs/suggestions/undo-fit.md index 299741308..578d43a38 100644 --- a/docs/dev/ADR-suggestions/adr_undo-fit.md +++ b/docs/dev/adrs/suggestions/undo-fit.md @@ -82,7 +82,7 @@ and is deferred. The minimum persisted state required for clean cross-session undo is the pair of `_fit_parameter.start_value` and `_fit_parameter.start_uncertainty` defined in -`adr_analysis-cif-fit-state.md`. +`analysis-cif-fit-state.md`. If a parameter has no saved `start_value`, `undo_fit()` leaves that parameter unchanged. diff --git a/docs/dev/ADR-suggestions/adr_workspace-root-project-category.md b/docs/dev/adrs/suggestions/workspace-root-project-category.md similarity index 76% rename from docs/dev/ADR-suggestions/adr_workspace-root-project-category.md rename to docs/dev/adrs/suggestions/workspace-root-project-category.md index b53ead958..a6c94cba6 100644 --- a/docs/dev/ADR-suggestions/adr_workspace-root-project-category.md +++ b/docs/dev/adrs/suggestions/workspace-root-project-category.md @@ -73,6 +73,7 @@ workspace = ed.Workspace(project_id='lbco_hrpt') workspace.project.id workspace.project.title = 'La0.5Ba0.5CoO3 at HRPT@PSI' workspace.rendering.table_engine = 'rich' +workspace.verbosity = 'short' workspace.structures workspace.experiments workspace.analysis @@ -89,9 +90,29 @@ _project.last_modified _rendering.chart_engine _rendering.table_engine + +_verbosity.level +``` + +The saved directory is a workspace directory whose filesystem name is +chosen by the user. The canonical layout is: + +```text +/ +|-- workspace.cif +|-- structures/ +| `-- cosio.cif +|-- experiments/ +| `-- d20.cif +|-- analysis/ +| `-- analysis.cif +`-- summary/ + `-- summary.cif ``` -Do not introduce `_meta.*` CIF tags. +Do not introduce `_meta.*` CIF tags. Do not use `project.cif`, +`config.cif`, or `meta.cif` as the primary singleton configuration file +in the target layout. The intended naming split is: @@ -99,6 +120,7 @@ The intended naming split is: Workspace |-- project # information about the scientific project |-- rendering # rendering preferences +|-- verbosity # console/output verbosity preference |-- structures # real structure datablocks |-- experiments # real experiment datablocks |-- analysis # analysis section @@ -169,15 +191,25 @@ After this decision, each layer has a clear rule: This avoids one-off aliases such as `project.info` while preserving semantic CIF names. +### `workspace.cif` is clearer than `project.cif`, `config.cif`, or `meta.cif` + +The file stores singleton settings owned by the workspace: scientific +project information, rendering preferences, and verbosity. `project.cif` +overloads the project name again, while `config.cif` and `meta.cif` are +generic. `workspace.cif` names the owning layer and lets each category +inside the file keep its domain-specific name. + ## Consequences ### Positive - The root object and project-information category no longer share the same conceptual name. -- Public category access becomes uniform: `workspace.project`, - `workspace.rendering`, `workspace.analysis`. +- Public access becomes uniform: `workspace.project`, + `workspace.rendering`, `workspace.verbosity`, `workspace.analysis`. - CIF stays semantic and does not introduce `_meta.*`. +- Workspace-level preferences such as rendering and verbosity are saved + with the workspace instead of being hidden runtime-only state. - Project information can use short item names such as `id`, `title`, and `description`. - The top-level facade name better reflects active runtime @@ -190,6 +222,8 @@ semantic CIF names. updated from `Project` to `Workspace`. - Existing saved directories using `project.cif` must be migrated to `workspace.cif` if no compatibility loader is kept. +- Existing code that expected verbosity to be runtime-only must account + for it round-tripping through `workspace.cif`. - Users familiar with `Project` must learn the new root name. - `Workspace` can be confused with a filesystem workspace in some ecosystems, so documentation must define it clearly as the active @@ -254,6 +288,16 @@ This is readable, but it preserves a special-case category alias. The current goal is stronger consistency between public categories and CIF category concepts. +### Use `project.cif`, `config.cif`, or `meta.cif` for singleton settings + +Rejected for the target layout. + +`project.cif` repeats the overloaded term that this migration removes. +`config.cif` and `meta.cif` are too generic and do not say which layer +owns the settings. `workspace.cif` is more explicit while still allowing +semantic categories such as `_project`, `_rendering`, and `_verbosity` +inside the file. + ### Rename only internal files and keep public API unchanged Rejected for the target design. @@ -274,7 +318,7 @@ save/load, display, and analysis orchestration. The implementation should follow: ```text -docs/dev/plan_workspace-root-project-category.md +docs/dev/plans/workspace-root-project-category.md ``` The high-level migration is: @@ -293,8 +337,10 @@ The high-level migration is: 8. Keep CIF tags `_project.*` and `_rendering.*`. 9. Rename saved singleton config file from `project.cif` to `workspace.cif`. -10. Update code, tests, scripts, tutorials, docs, and architecture - references. +10. Persist workspace verbosity in `workspace.cif` as + `_verbosity.level`, owned by a first-class `Verbosity` category + under `WorkspaceConfig` (parallel to `Rendering`). +11. Update code, tests, scripts, tutorials, docs, and ADR references. ## Post-Implementation ADR Update @@ -307,10 +353,11 @@ When implementation is complete: 3. Record whether a temporary or permanent `Project` compatibility alias was approved. 4. Record any deviations from the migration plan. -5. Move this file from `docs/dev/ADR-suggestions/` to `docs/dev/ADRs/` - if that is the repository convention for accepted decisions. -6. Update `docs/dev/architecture.md`. -7. Update or close related items in `docs/dev/Issues/issues_open.md`. +5. Move this file from `docs/dev/adrs/suggestions/` to + `docs/dev/adrs/accepted/` if the decision is accepted. +6. Update `docs/dev/adrs/index.md` and related accepted ADRs if the ADR + map changes. +7. Update or close related items in `docs/dev/issues/open.md`. ## Acceptance Criteria @@ -323,6 +370,11 @@ This ADR is satisfied when: - the saved directory path is exposed as `workspace.path`. - the public rendering category is `workspace.rendering`. - saved singleton configuration lives in `workspace.cif`. -- `workspace.cif` uses `_project.*` and `_rendering.*` tags. +- `workspace.cif` uses `_project.*`, `_rendering.*`, and + `_verbosity.level` tags. +- workspace verbosity is owned by a registered `Verbosity` category + alongside `Rendering`. +- `ProjectInfo.path` is removed; the saved directory path is exposed + only as `workspace.path`. - no `_meta.*` tags are introduced for project information. -- tutorials and architecture documentation use `Workspace`. +- tutorials and accepted ADRs use `Workspace`. diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md deleted file mode 100644 index 768531e72..000000000 --- a/docs/dev/architecture.md +++ /dev/null @@ -1,1631 +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) -└── CategoryOwner # Flat category owner (e.g. Analysis, DatablockItem) - └── DatablockItem # Real 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 CategoryOwner, DatablockItem, and DatablockCollection - -`CategoryOwner` is the shared base class for objects that own flat -category siblings. It provides category discovery, category sorting by -`_update_priority`, parameter aggregation, `_need_categories_update` -tracking, and category-body CIF serialization without a `data_` header. - -`DatablockItem` extends `CategoryOwner` for real CIF `data_` blocks. -`Structure` and `ExperimentBase` subclasses are real datablocks. -`Analysis` is also a `CategoryOwner`, but it serializes as a singleton -section body in `analysis/analysis.cif` and does not emit a fake -`data_analysis` header. - -| 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 | `Parameter`s not blocked by user or symmetry constraints | -| 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 `CategoryOwner`. -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, user_constrained, symmetry_constrained -``` - -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 | - -### 2.7 String Paths vs Live Descriptors in Public APIs - -Public APIs reference parameters in one of two ways. The choice is not -stylistic — it follows the call site's role: - -- **Setup-time / schema-level APIs use string paths** (CIF-style - `'category.attribute'`). The targeted descriptor may not yet exist on - any concrete object (e.g. an extraction rule applies uniformly to - files about to be loaded), and the value must round-trip through CIF. - Examples: - `sequential_fit_extract.create(target='diffrn.ambient_temperature', ...)`, - alias/constraint definitions persisted in project CIF. -- **Cross-experiment selectors use the same string paths at runtime.** - `project.display.fit.series(..., versus=...)` selects a persisted - `diffrn.*` column in `analysis/results.csv` and the matching field - across experiments; it does not use one experiment's current live - descriptor value. Example: - `project.display.fit.series(param=structure.cell.length_a, versus='diffrn.ambient_temperature')`. -- **Concrete model parameters still use live descriptors.** The call - needs the parameter's `unique_name`, `description`, and `units`, and - it refers to one exact fitted quantity in the model. Example: - `param=structure.cell.length_a` in `project.display.fit.series(...)`. - -When adding a new public API, place it on one side of this rule rather -than accepting both. Use string paths when the value names a persisted -field or cross-experiment selector, and use live descriptors when the -value names one concrete model parameter. - ---- - -## 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_constrained = 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 symmetry constrained. Surface helpers -`cell_symmetry_constrained_flags(...)` and -`atom_site_symmetry_constrained_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`: parameter -objects stay stable while their values and CIF output names change. - -**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. - -```shell -Structure -├── cell (CategoryItem) -├── space_group (CategoryItem) -├── atom_sites (CategoryCollection of AtomSite) -└── atom_site_aniso (CategoryCollection of AtomSiteAniso) -``` - -**Type-neutral names.** `atom_site.adp_iso` is the 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. - -| Parameter | Location | CIF names | -| ---------- | --------------- | -------------------------------------------------------- | -| `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` | - -`adp_type` is a `StringDescriptor` validated against `AdpTypeEnum` -(`Biso`, `Uiso`, `Bani`, `Uani`). - -**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. - -For example, switching from `Biso` to `Uiso` makes -`_atom_site.U_iso_or_equiv` the first CIF name on `adp_iso`; switching -from `Bani` to `Uani` does the same for all six `_atom_site_aniso.U_*` -tensor tags. No dynamic CIF handler is needed. - -**Auto-conversion.** Setting `adp_type` triggers value conversion: B ↔ U -via `B = 8π²U`; iso → ani seeds the diagonal; ani → iso averages the -diagonal. - -| Transition | Conversion rule | -| ---------- | --------------------------------------------------------------------- | -| B ↔ U | `B = 8π²U` | -| Iso → Ani | `adp_11 = adp_22 = adp_33 = adp_iso`; off-diagonal terms become `0.0` | -| Ani → Iso | `adp_iso = (adp_11 + adp_22 + adp_33) / 3` | - -**Collection sync.** `Structure._update_categories()` reconciles the two -collections: adds missing aniso entries, removes stale ones, and rekeys -on label rename. Sync follows the datablock dirty-flag pattern: category -or parameter changes mark the structure as needing an update, and the -next serialisation, plot, or fit call performs the reconciliation. - -| Event | Sync action | -| ---------------------------------- | --------------------------------------------------------- | -| Atom added to `atom_sites` | Create matching `AtomSiteAniso` entry with `0.0` defaults | -| Atom removed from `atom_sites` | Remove matching `AtomSiteAniso` entry | -| Atom label renamed in `atom_sites` | Rekey the matching `AtomSiteAniso` entry | - -**User-facing access.** ADP parameters follow the same two-level access -pattern as other category parameters: - -```python -structure.atom_sites['Si'].adp_type = 'Biso' -structure.atom_sites['Si'].adp_iso = 0.47 -structure.atom_site_aniso['Si'].adp_11 = 0.05 -``` - -Creating an atom uses `adp_iso`; the matching `atom_site_aniso` entry is -created by `_update_categories()`: - -```python -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, -) -``` - -**Design rule.** ADP parameter names are type-neutral because -type-specific names (`b_iso`, `u_iso`, etc.) would require replacing -parameter objects when the ADP type changes, which would break -constraints, free flags, and existing references. The always-present -`atom_site_aniso` collection avoids conditional branches in -serialisation, calculators, parameter tables, constraint wiring, and UI. - ---- - -## 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` | -| `JointFitFactory` | Joint-fit weights | `JointFitCollection` | -| `CalculatorFactory` | Calculation engines | `CryspyCalculator`, `CrysfmlCalculator`, `PdffitCalculator` | -| `MinimizerFactory` | Minimisers | `LmfitMinimizer`, `LmfitLeastsqMinimizer`, `LmfitLeastSquaresMinimizer`, `DfolsMinimizer`, `BumpsMinimizer`, `BumpsLmMinimizer`, `BumpsDreamMinimizer`, `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 (dream)` | `BumpsDreamMinimizer` | -| `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` | -| `JointFitCollection` | `JointFitFactory` | - -#### 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` | -| `JointFitItem` | `JointFitCollection` | - -#### 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: - -- Singleton section: `Analysis` is a `CategoryOwner`, not a - `DatablockItem`. It owns sibling categories and serializes as the body - of `analysis/analysis.cif` without a `data_` header. -- Fit configuration: `fitting` (`CategoryItem` with `minimizer_type`). - `fitting.minimizer_type` selects the minimizer backend. The active - fitting mode lives on the owner as `analysis.fitting_mode_type`, not - as a nested child category field. `fitting.show_minimizer_types()` - lists supported minimizers. -- Joint-fit weights: `joint_fit` (`CategoryCollection` of per-experiment - weight entries); sibling of `fitting`, not a child. -- Fit results: `analysis.fit_results` stores the latest runtime result - object. This is `FitResults` for deterministic fits and - `BayesianFitResults` for Bayesian DREAM runs. -- Parameter tables: `show_all_params()`, `show_fittable_params()`, - `show_free_params()`, `how_to_access_parameters()` Compact - summary-style parameter displays intentionally hide the large - loop-backed experiment categories `pd_data`, `total_data`, and `refln` - in `all()`, `access()`, and `cif_uids()` so the output stays readable. -- Fitting: `fitting.minimizer_type` stores the shared minimizer - selection; `fitting_mode_type` stores the active mode on `Analysis` - itself; `fit()` dispatches to the current mode using the persisted - sibling categories `joint_fit`, `sequential_fit`, and - `sequential_fit_extract`. `display.fit_results()` dispatches through - the active runtime result object. -- Aliases and constraints (single-type categories; no public `_type` - getter or setter) - -#### 6.4.1 Bayesian DREAM Runtime Results - -Bayesian sampling is integrated as a normal minimizer selection with tag -`'bumps (dream)'`. It does not create a parallel `Analysis` stack or a -new persisted results category. - -- `BayesianFitResults` extends `FitResults` with runtime-only posterior - state such as `posterior_samples`, `posterior_parameter_summaries`, - `posterior_predictive`, `diagnostics`, and `sampler_settings`. -- Posterior arrays and predictive caches remain runtime-only; they are - not serialized into CIF or project directories. -- `sampler_settings` records the resolved stochastic settings actually - used for the run, including `random_seed`, `steps`, `burn`, `thin`, - `pop`, and `parallel`. -- The current user-facing DREAM controls live on the active minimizer - object, for example `project.analysis.fitting.minimizer.steps`, - `burn`, `thin`, `pop`, `parallel`, and `init`. -- `plot_param_correlations()` uses posterior samples when available and - otherwise falls back to deterministic covariance or engine-derived - correlations. -- Bayesian-only plotting methods are exposed explicitly rather than by - overloading deterministic plot calls: `plot_posterior_pairs()`, - `plot_param_distribution(param)`, and - `plot_posterior_predictive(expt_name, ...)`. - ---- - -## 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.rendering` | `Rendering` | Plot/table engine selection | -| `project.display` | `ProjectDisplay` | Pattern/report facade | -| `project.analysis` | `Analysis` | Minimiser, fitting, aliases, constraints | -| `project.summary` | `Summary` | Report generation | -| `project.verbosity` | `str` | Console output level (full/short/silent) | - -Internally, `Project` keeps project-scoped singleton categories under a -private `ProjectConfig(CategoryOwner)` object. That owner currently -holds `project.info` (`ProjectInfo`) and `project.rendering` -(`Rendering`), while the public access paths stay flat on `Project` for -user discoverability. - -### 7.1 Data Flow - -``` -Parameter.value set - → AttributeSpec validation (type + value) - → _need_categories_update = True (on parent CategoryOwner) - -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 # ProjectConfig categories (info + rendering) -├── 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` serializes the private `ProjectConfig` owner without a -`data_` header. It carries both the `_project.*` metadata category and -the `_rendering.*` engine preferences (`chart_engine`, `table_engine`), -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 -(`_fitting.minimizer_type`, `_fitting.mode_type`) lives in -`analysis/analysis.cif`. Runtime fit outputs, including -`analysis.fit_results`, posterior chains, posterior predictive -summaries, and convergence diagnostics, are not serialized. - -### 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()`) read -`project.verbosity`. - -```python -# Use project-level default for all operations -project.verbosity = 'short' -project.analysis.fit() # → short mode - -# Override for one fit, then restore the project default -original_verbosity = project.verbosity -project.verbosity = 'silent' -project.analysis.fit() # → silent -project.verbosity = original_verbosity -``` - -**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.fitting.minimizer_type = 'lmfit' - -# Plot before fitting -project.display.pattern(expt_name='hrpt') - -# 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.display.parameters.free() - -# Fit and show results -project.analysis.fit() -project.display.fit.results() - -# Plot after fitting -project.display.pattern(expt_name='hrpt') - -# Save -project.save() -``` - -### 8.4.1 Bayesian Refinement - -```python -# Deterministic pre-fit remains explicit -project.analysis.fitting.minimizer_type = 'bumps (lm)' -project.analysis.fit() - -# Switch to Bayesian sampling using the same entry point -project.analysis.fitting.minimizer_type = 'bumps (dream)' -project.analysis.fitting.minimizer.steps = 1000 -project.analysis.fitting.minimizer.parallel = 0 -project.analysis.fit() - -# Runtime-only Bayesian summaries and plots -project.display.fit.results() -project.display.fit.correlations() -project.display.posterior.pairs() -project.display.posterior.distribution(param) -project.display.posterior.predictive(expt_name='hrpt') -``` - -### 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`, `fitting`, `sequential_fit`. - -`fitting` is a dedicated analysis configuration category, but the fit -mode selector lives on the owner as `analysis.fitting_mode_type`. This -is the project's active-sibling selector pattern: the owner stores the -authoritative mode and decides which sibling categories are active, -shown in help, and serialized. `joint_fit`, `sequential_fit`, and -`sequential_fit_extract` remain direct `Analysis` siblings even when -inactive. See the fit-mode ADR for the full contract: -[`adr_fit-mode-categories.md`](ADRs/adr_fit-mode-categories.md). -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 `chart_engine` and -`table_engine` (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 | `fitting.minimizer_type`, `calculation.calculator_type`, `rendering.chart_engine` | `_fitting.minimizer_type`, `_calculation.calculator_type`, `_rendering.chart_engine` | -| Switchable-category impl. selector | Swap a category implementation | `experiment.background_type`, `experiment.peak_profile_type` | category-owned type tag such as `_peak.profile_type` | -| Active-sibling selector | Pick the active sibling surface | `analysis.fitting_mode_type` | owner-owned tag such as `_fitting.mode_type` | - -Backend selectors live on a dedicated configuration category (`fitting`, -`calculation`, `rendering`). Switchable-category implementation -selectors are owned by the host (typically the experiment) because -switching them replaces the category instance, as described in §9.3. -Active-sibling selectors are also owner-level, but they do not swap one -category implementation for another. Instead, they select which sibling -category family is authoritative while the shared configuration category -keeps a stable shape. - -### 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.fitting.show_minimizer_types() -project.analysis.show_fitting_mode_types() -project.rendering.show_chart_engines() -project.rendering.show_table_engines() -``` - -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._fitting_mode_type is FitModeEnum.JOINT: - -# ❌ Wrong — compare with raw string -if self._fitting_mode_type == 'joint': -``` - -### 9.7 Flat Category Structure — No Nested Categories - -Following CIF conventions, categories are **flat siblings** within their -owner (`CategoryOwner`). 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 (CategoryOwner) -├── 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`:** `fit` is a `CategoryItem` holding -the active minimizer and fitting mode. `joint_fit` is a separate -`CategoryCollection` holding per-experiment weights. Both are direct -children of `Analysis`, not nested: - -```python -# ✅ Correct — sibling categories on Analysis -project.analysis.fitting_mode_type = 'joint' -project.analysis.joint_fit['npd'].weight = 0.7 - -# ❌ Wrong — joint_fit as a child of fit -project.analysis.fitting.joint_fit['npd'].weight = 0.7 -``` - -In CIF output, sibling categories appear as independent blocks: - -``` -_fitting.mode_type joint -_fitting.minimizer_type lmfit - -loop_ -_joint_fit.experiment_id -_joint_fit.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/issues_open.md) — prioritised - backlog. -- **Closed:** [`issues_closed.md`](Issues/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/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/issues_closed.md b/docs/dev/issues/closed.md similarity index 98% rename from docs/dev/Issues/issues_closed.md rename to docs/dev/issues/closed.md index 0e6db4dc5..c2e60653a 100644 --- a/docs/dev/Issues/issues_closed.md +++ b/docs/dev/issues/closed.md @@ -12,7 +12,8 @@ that do not inherit the guarded object hierarchy: `project.display`, `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 `docs/dev/ADRs/adr_help-discoverability.md`. +convention in +[`help-discoverability.md`](../adrs/accepted/help-discoverability.md). --- diff --git a/docs/dev/Issues/issues_open.md b/docs/dev/issues/open.md similarity index 99% rename from docs/dev/Issues/issues_open.md rename to docs/dev/issues/open.md index 2e6142e8e..6c96f87ea 100644 --- a/docs/dev/Issues/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 @@ -200,7 +200,7 @@ 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 architecture and validation rules. +in an ADR and validation rules. **Depends on:** nothing. @@ -1452,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. @@ -1600,7 +1600,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. diff --git a/docs/dev/package-structure-full.md b/docs/dev/package-structure/full.md similarity index 100% rename from docs/dev/package-structure-full.md rename to docs/dev/package-structure/full.md diff --git a/docs/dev/package-structure-short.md b/docs/dev/package-structure/short.md similarity index 100% rename from docs/dev/package-structure-short.md rename to docs/dev/package-structure/short.md diff --git a/docs/dev/plans/loop-category-key-identity.md b/docs/dev/plans/loop-category-key-identity.md new file mode 100644 index 000000000..9917d1bae --- /dev/null +++ b/docs/dev/plans/loop-category-key-identity.md @@ -0,0 +1,620 @@ +# Loop Category Key Identity Implementation Plan + +## Status + +Workflow instructions: + +```text +.github/copilot-instructions.md +``` + +Related ADR suggestion: + +```text +docs/dev/adrs/suggestions/loop-category-key-identity.md +``` + +This plan implements two related changes: + +1. Move category identity declarations from per-instance assignments to + class-level declarations. +2. Add an explicit persisted `id` field to the constraints loop. + +Status checklist. Mark `[x]` only while implementing: + +```text +Phase 1 - Implementation +[x] Add class-level identity declarations to CategoryItem. +[x] Teach Identity to resolve declared category entry names. +[x] Rebuild collection indexes after CIF loop loading. +[x] Add _category_code to all current CategoryItem subclasses. +[x] Add _category_entry_name to all current loop CategoryItem subclasses. +[x] Remove direct self._identity.category_code assignments. +[x] Remove direct self._identity.category_entry_name lambda assignments. +[x] Add Constraint.id descriptor serialized as _constraint.id. +[x] Change constraints collection keys from lhs_alias to id. +[x] Preserve default constraints.create(expression=...) behavior by using lhs_alias as the default id. +[x] Add backward-compatible loading for old CIF loops without _constraint.id. +[x] Update constraints display. +[x] Update loop-category-key-identity.md if implementation details differ from the ADR. +[ ] Phase 1 review gate: present diff for approval. + +Phase 2 - Verification +[x] Add tests for the base declarative identity behavior. +[x] Add parametrized tests for current loop category identity declarations. +[x] Update constraints tests. +[x] Update existing round-trip tests that compare constraints CIF. +[x] Run formatting. +[x] Run targeted unit tests. +[x] Run broader checks. +``` + +## Commit Discipline + +When an AI agent follows this plan, every completed Phase 1 +implementation step must be staged with explicit paths and committed +locally before moving to the next implementation step or to the Phase 1 +review gate. + +Follow the **Commits** section of `.github/copilot-instructions.md`. + +Rules: + +- One commit per implementation step. +- Keep each commit atomic and single-purpose. +- Stage explicit paths only. Do not use `git add .`. +- Do not stage unrelated user changes. +- Do not stage generated artifacts unless the user explicitly asks. +- If a serious uncovered design issue appears, stop and ask before + continuing. + +Suggested branch: + +```text +feature/loop-category-key-identity +``` + +Suggested commit messages: + +```text +Add declarative category identity resolution +Declare category identities on current items +Persist explicit constraint identifiers +Update ADR for declarative category identity +Add declarative category identity tests +``` + +## Goal + +Replace repeated constructor code like this: + +```python +self._identity.category_code = 'atom_site' +self._identity.category_entry_name = lambda: str(self.label.value) +``` + +with class-level declarations: + +```python +class AtomSite(CategoryItem): + _category_code = 'atom_site' + _category_entry_name = 'label' +``` + +The name `_category_entry_name` is intentionally kept because this is +the preferred project terminology. In this plan it means "the name of +the item attribute used to resolve the entry name". The resolved entry +value is still obtained through: + +```python +item._identity.category_entry_name +``` + +For example, `AtomSite._category_entry_name == 'label'`, while +`atom_site._identity.category_entry_name == 'Ba1'`. + +## Non-Goals + +Do not change these in this migration: + +- Do not rename `Identity.category_entry_name`. +- Do not rename `CategoryCollection`. +- Do not change public collection access syntax. +- Do not change CIF tags except adding `_constraint.id`. +- Do not rename `label` to `id` for atom sites or aliases. +- Do not make `phase_id` the row key for powder reflection loops. + +## Design + +This plan intentionally uses a narrow metadata lookup based on +`_category_entry_name`. That is an allowed exception to the general "no +string-based dispatch" rule because the attribute name is a class-level +declaration, not user input, and resolution is centralized in +`CategoryItem`. + +### CategoryItem Declarations + +Add these class attributes to `CategoryItem` in +`src/easydiffraction/core/category.py`: + +```python +class CategoryItem(GuardedBase): + _category_code: str | None = None + _category_entry_name: str | None = None +``` + +Update `CategoryItem.__init__()` so it assigns `_category_code` once: + +```python +def __init__(self) -> None: + super().__init__() + if self._category_code is not None: + self._identity.category_code = self._category_code +``` + +Do not try to resolve `_category_entry_name` in `CategoryItem.__init__`. +Many item classes create descriptors after `super().__init__()` returns, +and some mixin-based classes run `CategoryItem.__init__()` before their +descriptors are created. + +Add a resolver method to `CategoryItem`: + +```python +def _resolve_category_entry_name(self) -> str | None: + 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) +``` + +Import `GenericDescriptorBase` from `easydiffraction.core.variable` in +`category.py` if it is not already in scope. + +### Identity Resolution + +Update `Identity._resolve_up()` in +`src/easydiffraction/core/identity.py` so that category entries can be +resolved from the owning object before walking to the parent. + +Add this logic after checking direct callable/string values and before +climbing to the parent: + +```python +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 +``` + +Keep the existing `category_entry_name` setter. It remains useful as an +escape hatch and keeps old code compatible during migration. + +### Collection Index Rebuild After CIF Loading + +Update `category_collection_from_cif()` in +`src/easydiffraction/io/cif/serialize.py`. + +Currently `_adopt_items()` rebuilds the index before loop values are +loaded into each item. After declarative keys are resolved from +descriptor values, the index must be rebuilt after parameters are +loaded. + +After the row population loop, run any collection hook and rebuild: + +```python +after_from_cif = getattr(self, '_after_from_cif', None) +if callable(after_from_cif): + after_from_cif() + +self._rebuild_index() +``` + +The hook is needed for constraints to backfill missing ids from old CIF +files. + +## Category Migration Table + +Add `_category_code` to each listed class. Add `_category_entry_name` +only when the class is a loop item and currently has a collection key. +Then remove matching constructor assignments. + +| File | Class | `_category_code` | `_category_entry_name` | Notes | +| ----------------------------------------------------------------------------------- | -------------------------- | ------------------------ | ---------------------- | -------------------------------------------- | +| `src/easydiffraction/project/categories/info/default.py` | `ProjectInfo` | `project` | none | Singleton category. | +| `src/easydiffraction/project/categories/rendering/default.py` | `Rendering` | `rendering` | none | Singleton category. | +| `src/easydiffraction/analysis/categories/fitting/default.py` | `Fitting` | `fitting` | none | Singleton category. | +| `src/easydiffraction/analysis/categories/sequential_fit/default.py` | `SequentialFit` | `sequential_fit` | none | Singleton category. | +| `src/easydiffraction/analysis/categories/aliases/default.py` | `Alias` | `alias` | `label` | Loop key stays `_alias.label`. | +| `src/easydiffraction/analysis/categories/constraints/default.py` | `Constraint` | `constraint` | `id` | Add the id descriptor first. | +| `src/easydiffraction/analysis/categories/joint_fit/default.py` | `JointFitItem` | `joint_fit` | `experiment_id` | Loop key stays `_joint_fit.experiment_id`. | +| `src/easydiffraction/analysis/categories/sequential_fit_extract/default.py` | `SequentialFitExtractItem` | `sequential_fit_extract` | `id` | Loop key stays `_sequential_fit_extract.id`. | +| `src/easydiffraction/datablocks/structure/categories/cell/default.py` | `Cell` | `cell` | none | Singleton category. | +| `src/easydiffraction/datablocks/structure/categories/space_group/default.py` | `SpaceGroup` | `space_group` | none | Singleton category. | +| `src/easydiffraction/datablocks/structure/categories/atom_sites/default.py` | `AtomSite` | `atom_site` | `label` | Loop key stays `_atom_site.label`. | +| `src/easydiffraction/datablocks/structure/categories/atom_site_aniso/default.py` | `AtomSiteAniso` | `atom_site_aniso` | `label` | Loop key stays `_atom_site_aniso.label`. | +| `src/easydiffraction/datablocks/experiment/categories/experiment_type/default.py` | `ExperimentType` | `expt_type` | none | Singleton category. | +| `src/easydiffraction/datablocks/experiment/categories/calculation/default.py` | `Calculation` | `calculation` | none | Singleton category. | +| `src/easydiffraction/datablocks/experiment/categories/diffrn/default.py` | `DefaultDiffrn` | `diffrn` | none | Singleton category. | +| `src/easydiffraction/datablocks/experiment/categories/instrument/base.py` | `InstrumentBase` | `instrument` | none | Subclasses inherit this code. | +| `src/easydiffraction/datablocks/experiment/categories/peak/base.py` | `PeakBase` | `peak` | none | Subclasses inherit this code. | +| `src/easydiffraction/datablocks/experiment/categories/extinction/becker_coppens.py` | `BeckerCoppensExtinction` | `extinction` | none | Singleton category. | +| `src/easydiffraction/datablocks/experiment/categories/linked_crystal/default.py` | `LinkedCrystal` | `linked_crystal` | none | Singleton category. | +| `src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py` | `LinkedPhase` | `linked_phases` | `id` | Loop key stays `_pd_phase_block.id`. | +| `src/easydiffraction/datablocks/experiment/categories/background/line_segment.py` | `LineSegment` | `background` | `id` | Loop key stays `_pd_background.id`. | +| `src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py` | `PolynomialTerm` | `background` | `id` | Loop key stays `_pd_background.id`. | +| `src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py` | `ExcludedRegion` | `excluded_regions` | `id` | Loop key stays `_excluded_region.id`. | +| `src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py` | `Refln` | `refln` | `id` | Powder reflection rows inherit this key. | +| `src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py` | `PdCwlDataPoint` | `pd_data` | `point_id` | CWL powder data row. | +| `src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py` | `PdTofDataPoint` | `pd_data` | `point_id` | TOF powder data row. | +| `src/easydiffraction/datablocks/experiment/categories/data/total_pd.py` | `TotalDataPoint` | `total_data` | `point_id` | Total-scattering data row. | + +Do not add `_category_code` or `_category_entry_name` to +`PowderReflnBase`, `PowderCwlRefln`, or `PowderTofRefln`; they inherit +the `refln` identity declarations from `Refln`. + +## Constraints Migration + +### Target Shape + +`Constraint` should become: + +```python +class Constraint(CategoryItem): + _category_code = 'constraint' + _category_entry_name = 'id' + + def __init__(self) -> None: + super().__init__() + + self._id = StringDescriptor( + name='id', + description='Identifier for this constraint.', + value_spec=AttributeSpec( + default='_', + validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), + ), + cif_handler=CifHandler(names=['_constraint.id']), + ) + self._expression = StringDescriptor(...) +``` + +Define the public property: + +```python +@property +def id(self) -> StringDescriptor: + """Identifier for this constraint.""" + return self._id + +@id.setter +def id(self, value: str) -> None: + self._id.value = value +``` + +Keep `lhs_alias` and `rhs_expr` as derived read-only properties. + +### Create API + +Change `Constraints.create()` from: + +```python +def create(self, *, expression: str) -> None: +``` + +to: + +```python +def create(self, *, expression: str, id: str | None = None) -> None: +``` + +Implementation order: + +```python +item = Constraint() +item.expression = expression +item.id = id if id is not None else item.lhs_alias +self.add(item) +self._enabled = True +``` + +This preserves the current default user experience: + +```python +analysis.constraints.create(expression='biso_Ba = biso_La') +analysis.constraints['biso_Ba'] +``` + +It also allows explicit ids: + +```python +analysis.constraints.create( + id='constraint_1', + expression='biso_Ba = biso_La', +) +analysis.constraints['constraint_1'] +``` + +### Backward-Compatible CIF Loading + +Old CIF files only contain: + +```cif +loop_ +_constraint.expression +biso_Ba = biso_La +``` + +New CIF files should contain: + +```cif +loop_ +_constraint.id +_constraint.expression +biso_Ba "biso_Ba = biso_La" +``` + +Add a hook on `Constraints`: + +```python +def _after_from_cif(self) -> None: + for index, item in enumerate(self._items, start=1): + if item.id.value in {'', '_'}: + fallback = item.lhs_alias or f'constraint_{index}' + item.id = fallback +``` + +The generic `category_collection_from_cif()` hook described above must +call this before rebuilding the index. + +### Constraints Display + +Update `Constraints.show()` to include the id: + +```text +id | expression +``` + +Keep the existing empty warning behavior. + +## Required Code Searches + +After migration, these searches should return no category-item +constructor assignments: + +```shell +git grep -n -E "self\\._identity\\.category_code =" -- src/easydiffraction +git grep -n -E "self\\._identity\\.category_entry_name =" -- src/easydiffraction +git grep -n -E "category_entry_name = lambda" -- src/easydiffraction +``` + +The following reads are expected to remain: + +```shell +git grep -n "_identity\\.category_entry_name" -- src/easydiffraction +``` + +Those reads are used by collections, display, reporting, and parameter +unique names. + +Do not use `rg` in this plan; it is not available in every contributor +environment. Use `git grep` for repository searches. + +## Tests To Add Or Update + +### Base Identity Tests + +Add or update tests under `tests/unit/easydiffraction/core/`. + +Test a fake category item: + +```python +class FakeItem(CategoryItem): + _category_code = 'fake' + _category_entry_name = 'id' + + def __init__(self): + super().__init__() + self._id = StringDescriptor(...) + + @property + def id(self): + return self._id +``` + +Assert: + +```python +item._identity.category_code == 'fake' +item._identity.category_entry_name == item.id.value +item.id.unique_name.endswith('.fake..id') +``` + +Also test that a singleton-like item with `_category_code` and no +`_category_entry_name` resolves category code but returns no entry name. + +### Current Category Parametrized Tests + +Add a parametrized test that instantiates representative loop item +classes and verifies category code plus entry: + +```text +Alias -> alias, label +JointFitItem -> joint_fit, experiment_id +SequentialFitExtractItem -> sequential_fit_extract, id +AtomSite -> atom_site, label +AtomSiteAniso -> atom_site_aniso, label +LinkedPhase -> linked_phases, id +LineSegment -> background, id +PolynomialTerm -> background, id +ExcludedRegion -> excluded_regions, id +Refln -> refln, id +PdCwlDataPoint -> pd_data, point_id +PdTofDataPoint -> pd_data, point_id +TotalDataPoint -> total_data, point_id +``` + +Do not instantiate heavy calculator-backed owner objects for this test; +instantiate item classes directly. + +### Constraints Tests + +Update +`tests/unit/easydiffraction/analysis/categories/test_constraints.py`. + +Required assertions: + +```python +c = Constraint() +c.expression = 'a = b + c' +c.id = 'constraint_a' +assert c.id.value == 'constraint_a' +assert c.lhs_alias == 'a' +assert c.rhs_expr == 'b + c' +assert c._identity.category_entry_name == 'constraint_a' +``` + +Default id behavior: + +```python +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' +``` + +Explicit id behavior: + +```python +coll = Constraints() +coll.create(id='c1', expression='a = b + c') +assert 'c1' in coll.names +assert coll['c1'].lhs_alias == 'a' +``` + +CIF serialization: + +```python +cif = coll.as_cif +assert '_constraint.id' in cif +assert '_constraint.expression' in cif +``` + +Backward-compatible CIF loading: + +```python +cif = ''' +data_analysis + +loop_ +_constraint.expression +"a = b + c" +''' +``` + +Load through the existing analysis/constraints loader and assert the +loaded collection has key `a`. + +### Existing Round-Trip Tests + +Update tests that compare constraint CIF or constraint collection keys: + +- `tests/unit/easydiffraction/project/test_project_load.py` +- `tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py` +- `tests/integration/fitting/test_project_load.py` + +Expected changes: + +- New saved analysis CIF includes `_constraint.id`. +- Old expression-only fixtures, if any, still load. +- Existing constraint workflows still work when no explicit id is + supplied. + +## Verification Commands + +Run the smallest useful checks first: + +```shell +pixi run python -m pytest tests/unit/easydiffraction/analysis/categories/test_constraints.py +pixi run python -m pytest tests/unit/easydiffraction/core/ +pixi run python -m pytest tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py +``` + +Then run broader checks: + +```shell +pixi run unit-tests +pixi run integration-tests +pixi run check +``` + +If the modified-file Prettier helper is still missing, format changed +Markdown directly: + +```shell +npx prettier --write --config=prettierrc.toml docs/dev/plans/loop-category-key-identity.md docs/dev/adrs/suggestions/loop-category-key-identity.md +``` + +## Local Availability Check + +The plan was checked against the current repository state: + +- `.github/copilot-instructions.md` exists. +- `src/easydiffraction/core/category.py`, + `src/easydiffraction/core/identity.py`, and + `src/easydiffraction/io/cif/serialize.py` exist. +- `tests/unit/easydiffraction/analysis/categories/test_constraints.py` + exists. +- `tests/unit/easydiffraction/core/` exists. +- `tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py` + exists. +- `pixi.toml` defines `fix`, `check`, `unit-tests`, `integration-tests`, + and `test-structure-check`. +- `prettierrc.toml` and local Prettier are available. +- `tools/nonpy_prettier_modified.py` is not present, so use the direct + `npx prettier --write --config=prettierrc.toml ...` fallback for + touched Markdown files. + +## Acceptance Criteria + +The implementation is done when all of these are true: + +- All current category codes are declared as `_category_code` on item + classes. +- All current loop collection keys are declared as + `_category_entry_name` on item classes. +- No current category item sets `self._identity.category_code` in its + constructor. +- No current category item sets `self._identity.category_entry_name` in + its constructor. +- `item._identity.category_entry_name` still works for all loop rows. +- Descriptor `unique_name` values still include datablock, category, + entry, and descriptor name. +- Constraints persist `_constraint.id`. +- Constraints created without an explicit id still default to the left + hand alias. +- Old constraints CIF without `_constraint.id` still loads and receives + deterministic ids. +- Collection indexes are correct after CIF loading. + +## Suggested Pull Request + +Title: + +```text +Use declarative category identity metadata +``` + +Description: + +```text +This change makes category and loop-row identities easier to audit and +keeps saved CIF identifiers explicit. Constraint rows gain a stable +identifier while existing constraint expressions continue to work. +``` diff --git a/docs/dev/plan_workspace-root-project-category.md b/docs/dev/plans/workspace-root-project-category.md similarity index 67% rename from docs/dev/plan_workspace-root-project-category.md rename to docs/dev/plans/workspace-root-project-category.md index b57e318a6..778fd7584 100644 --- a/docs/dev/plan_workspace-root-project-category.md +++ b/docs/dev/plans/workspace-root-project-category.md @@ -7,13 +7,13 @@ Branch: `feature/workspace-root-project-category` ADR suggestion: ```text -docs/dev/ADR-suggestions/adr_workspace-root-project-category.md +docs/dev/adrs/suggestions/workspace-root-project-category.md ``` Two-phase workflow from `.github/copilot-instructions.md`: -- Phase 1 - Implementation. Code, docs, and architecture updates only. - Do not create or run tests unless the user explicitly asks. +- Phase 1 - Implementation. Code and docs updates only. Do not create or + run tests unless the user explicitly asks. - Phase 2 - Verification. Add/update tests, then run the verification commands listed near the end of this plan. @@ -96,6 +96,7 @@ workspace = ed.Workspace(project_id='lbco_hrpt') workspace.project.id workspace.project.title = 'La0.5Ba0.5CoO3 at HRPT@PSI' workspace.rendering.table_engine = 'rich' +workspace.verbosity = 'short' workspace.structures workspace.experiments workspace.analysis @@ -121,10 +122,27 @@ _project.last_modified _rendering.chart_engine _rendering.table_engine + +_verbosity.level ``` Do not introduce `_meta.*` tags. +Target saved layout: + +```text +/ +|-- workspace.cif +|-- structures/ +| `-- cosio.cif +|-- experiments/ +| `-- d20.cif +|-- analysis/ +| `-- analysis.cif +`-- summary/ + `-- summary.cif +``` + ## Decisions Already Made Use these decisions unless the user explicitly changes the ADR before @@ -137,11 +155,28 @@ implementation: - The public rendering category remains `workspace.rendering`. - The project-information category keeps semantic CIF tags `_project.*`. - The rendering category keeps semantic CIF tags `_rendering.*`. +- The verbosity preference is serialized as `_verbosity.level`. - The saved singleton config file becomes `workspace.cif`. +- The saved root is a workspace directory with a user-chosen filesystem + name; do not use `project` as the conceptual root name in new docs. +- Do not use `project.cif`, `config.cif`, or `meta.cif` as the primary + singleton config file in the target layout. - The storage directory path belongs to `Workspace.path`, not `workspace.project.path`. - The old `Project` public API is removed unless the user explicitly approves an alias before implementation. +- The category class name `ProjectInfo` is kept; only the public + attribute name (`info` → `project`) changes. +- `ProjectInfo.path` is removed; the saved directory path lives on + `Workspace.path` only. +- A first-class `Verbosity` category under `WorkspaceConfig` is required + (not optional) so that `_verbosity.level` round-trips through + `workspace.cif` like other singleton categories. +- Source-tree imports may be temporarily inconsistent between phases + (for example, Phase 1 leaves `project_config_to_cif` imports until + Phase 4 renames the serializer functions). This is acceptable because + tests are not run in Phase 1. Each phase must still leave the source + importable at the end of the phase. ## Current Shape @@ -185,7 +220,8 @@ src/easydiffraction/workspace/ |-- display.py # class WorkspaceDisplay `-- categories/ |-- project/ # ProjectInfo category - `-- rendering/ # Rendering category + |-- rendering/ # Rendering category + `-- verbosity/ # Verbosity category ``` Target public API: @@ -194,6 +230,7 @@ Target public API: workspace = ed.Workspace(project_id='my_project') workspace.project.title workspace.rendering.table_engine +workspace.verbosity = 'short' ``` ## Out Of Scope @@ -201,6 +238,8 @@ workspace.rendering.table_engine Do not do these in this migration: - Do not add `_meta.*` CIF tags. +- Do not use `project.cif`, `config.cif`, or `meta.cif` as the target + singleton settings file. - Do not redesign structure or experiment datablocks. - Do not change analysis fit-mode semantics. - Do not change calculator behavior. @@ -250,7 +289,7 @@ Rename the top-level runtime facade and package from `project` to ### Steps -1. Move the source package: +1. Move the source package with `git mv` so history is preserved: ```text src/easydiffraction/project/ @@ -275,34 +314,67 @@ Rename the top-level runtime facade and package from `project` to ProjectDisplay -> WorkspaceDisplay ``` -4. Update top-level import: +4. Rename class-level state inside the facade: + + ```text + Project._current_project -> Workspace._current_workspace + Project._loading -> Workspace._loading # name kept + Project.current_project_path() -> Workspace.current_workspace_path() + ``` + + Update the `ClassVar` annotation accordingly and update all internal + references (`type(self)._current_project = self`, + `cls._current_project`). + +5. Update the `varname()` fallback inside `__init__` so the default + variable name becomes `'workspace'` instead of `'project'`: + + ```python + self._varname = 'workspace' if type(self)._loading else varname() + ``` + +6. Update top-level import: ```python from easydiffraction.workspace.workspace import Workspace ``` -5. Remove the old top-level `Project` import unless the user approved an +7. Remove the old top-level `Project` import unless the user approved an alias. -6. Update type-checking imports: +8. Update type-checking imports: ```python from easydiffraction.workspace.workspace import Workspace ``` -7. Update docstrings from "Project facade" to "Workspace facade" where - they describe the root object. +9. Update docstrings from "Project facade" to "Workspace facade" where + they describe the root object. Keep wording that talks about the + scientific project (titles, descriptions, identity) unchanged. -8. Run a source-only grep. Do not run tests in Phase 1: +10. Rename `io/ascii.py::extract_project_from_zip` to + `extract_workspace_from_zip` and update its re-exports in + `src/easydiffraction/io/__init__.py` and + `src/easydiffraction/__init__.py`. The function extracts a saved + workspace directory, not scientific project information. - ```shell - rg -n "easydiffraction\\.project|\\bProject\\b|ProjectDisplay|ProjectConfig" src - ``` +11. Note: `src/easydiffraction/io/cif/serialize.py` still defines + `project_config_to_cif`, `project_config_from_cif`, and + `project_to_cif` at this point. Leave those imports as-is in + `workspace.py`; they are renamed in Phase 4. The function + `project_info_to_cif` keeps its name. - For every match, decide whether it refers to: - - the old root object, which should become `Workspace`; - - the project-information category, which should remain project; - - historical text that should be updated in docs later. +12. Run a source-only grep. Do not run tests in Phase 1: + +```shell +rg -n "easydiffraction\\.project|\\bProject\\b|ProjectDisplay|ProjectConfig" src +``` + +For every match, decide whether it refers to: + +- the old root object, which should become `Workspace`; +- the project-information category, which should remain project; +- historical text that should be updated in docs later. ### Stop Conditions @@ -427,12 +499,16 @@ the workspace location and is not serialized project information. ### Steps -1. In `ProjectInfo`, rename the public identity property: +1. In `ProjectInfo`, rename the public identity property and its setter: ```text - name -> id + name (getter) -> id (getter) + name (setter) -> id (setter) ``` + Do not also rename the internal descriptor attribute + `self._project_id`; it already matches the new public name. + 2. Keep the underlying CIF tag unchanged: ```python @@ -441,10 +517,12 @@ the workspace location and is not serialized project information. 3. Update `ProjectInfo.unique_name` to return `self.id`. -4. Update `project_info_to_cif()` and CIF loading helpers to use +4. Keep `ProjectInfo._identity.category_code = 'project'` as-is. + +5. Update `project_info_to_cif()` and CIF loading helpers to use `info.id`. -5. Rename constructor arguments: +6. Rename constructor arguments: ```text name -> project_id @@ -455,10 +533,15 @@ the workspace location and is not serialized project information. - `WorkspaceConfig.__init__` - `ProjectInfo.__init__` - `ProjectInfoFactory.create(...)` call sites + - Default value: `'untitled_project'` (unchanged value, just the + parameter name changes) -6. Add `Workspace.path` as the runtime storage path. +7. Add `Workspace.path` as the runtime storage path. Initialize + `self._path: pathlib.Path | None = None` in `Workspace.__init__`. - Suggested shape: + Suggested shape (match the surrounding `GuardedBase` pattern; do not + add `@typechecked` here because the setter accepts both `str` and + `pathlib.Path`): ```python @property @@ -468,13 +551,14 @@ the workspace location and is not serialized project information. @path.setter def path(self, value: object) -> None: - self._path = pathlib.Path(value) + self._path = pathlib.Path(value) if value is not None else None ``` -7. Remove `ProjectInfo.path` unless explicitly approved as a - compatibility alias. +8. Remove `ProjectInfo.path` (property, setter, and the `self._path` + attribute inside `ProjectInfo.__init__`) unless explicitly approved + as a compatibility alias. -8. Update save/load logic: +9. Update save/load logic across the codebase: ```text workspace.path @@ -483,17 +567,22 @@ the workspace location and is not serialized project information. should replace: ```text - workspace.project.path + project.info.path # old + workspace.project.path # never used; do not introduce ``` -9. Update messages and string representations: + Concrete call sites include `Workspace.save`, `Workspace.load`, + `Workspace.current_workspace_path`, and any consumers in `analysis/`, + `display/`, `summary/`, and `io/`. - ```text - Workspace '' (...) - Saving workspace '' to ... - ``` +10. Update messages and string representations: + +```text +Workspace '' (...) +Saving workspace '' to ... +``` -10. Run grep: +11. Run grep: ```shell rg -n "\\.name\\b|\\.path\\b|project_id|Project identifier" src/easydiffraction/workspace src/easydiffraction/io src/easydiffraction/display src/easydiffraction/summary @@ -527,20 +616,29 @@ Rename the saved singleton configuration file from `project.cif` to - `src/easydiffraction/workspace/workspace.py` - `src/easydiffraction/io/cif/serialize.py` +- `src/easydiffraction/workspace/workspace_config.py` +- `src/easydiffraction/workspace/categories/verbosity/` - CLI entry points in `src/easydiffraction/__main__.py` - docs that describe saved project directories - test fixtures in Phase 2 ### Steps -1. Rename serializer functions if they still use project-root naming: +1. Rename serializer functions in + `src/easydiffraction/io/cif/serialize.py` and every call site: ```text - project_config_to_cif -> workspace_config_to_cif + project_config_to_cif -> workspace_config_to_cif project_config_from_cif -> workspace_config_from_cif - project_to_cif -> workspace_to_cif + project_to_cif -> workspace_to_cif ``` + Call sites include `workspace.py` (formerly `project.py`), + `workspace_config.py`, and the serializer itself (the + `project_to_cif` body calls `project_config_to_cif`). After this step + the imports that were intentionally left stale in Phase 1 must + compile cleanly. + Do not rename `project_info_to_cif`; it serializes the `_project` category and that name remains correct. @@ -556,28 +654,68 @@ Rename the saved singleton configuration file from `project.cif` to workspace.cif ``` -4. Do not add `project.cif` fallback unless the user approved a - compatibility loader. +4. Do not add `project.cif`, `config.cif`, or `meta.cif` fallbacks + unless the user approved a compatibility loader. + +5. Add a `Verbosity` category under + `src/easydiffraction/workspace/categories/verbosity/` following the + same shape as `rendering/` (a `default.py` with a `Verbosity` + `CategoryItem`, a `factory.py` with `VerbosityFactory`, and an + `__init__.py` that imports both to trigger registration). + + The category owns one descriptor: + + ```python + CifHandler(names=['_verbosity.level']) + ``` + + Bind it in `WorkspaceConfig.__init__` next to `_rendering`, and + expose it from `Workspace` so that the public access path remains: + + ```python + workspace.verbosity = 'short' + workspace.verbosity # -> 'short' + ``` + + The public `verbosity` getter/setter on `Workspace` reads and writes + the category's `level` descriptor (validated against `VerbosityEnum`) + and replaces the current `self._verbosity: VerbosityEnum` + runtime-only attribute. Remove that attribute. -5. Keep the contents semantic: + Serialize it as: + + ```cif + _verbosity.level short + ``` + +6. Keep the contents semantic: ```cif _project.id _project.title _rendering.table_engine + _verbosity.level ``` -6. Update logging and console output from `project.cif` to +7. Update logging and console output from `project.cif` to `workspace.cif`. -7. Run grep: +8. Run grep: ```shell - rg -n "project\\.cif|project_config_to_cif|project_config_from_cif|project_to_cif" src docs tests + rg -n "project\\.cif|config\\.cif|meta\\.cif|project_config_to_cif|project_config_from_cif|project_to_cif|verbosity" src docs tests ``` - In Phase 1, update source and docs only. Test files are handled in - Phase 2 unless the user explicitly asks otherwise. + Update source and docs only. Test files are handled in Phase 2 unless + the user explicitly asks otherwise. + +9. Saved on-disk fixtures under `data/` and `projects/` still contain + `project.cif` files (for example `data/lbco_project/project.cif`, + `projects/cosio/project.cif`). Do **not** edit or regenerate them in + Phase 1. They are inputs to integration/script tests and will either + be regenerated in Phase 2 or the relevant tests will be updated to + write fresh workspace directories. Flag any that block Phase 2 in the + review gate. ### Stop Conditions @@ -586,6 +724,8 @@ Stop and ask if: - repository fixtures or tutorials contain saved directories that must remain loadable without conversion; - the user wants a one-release compatibility loader. +- the verbosity setting cannot be represented as a category without + weakening the public `workspace.verbosity` API. ### Commit @@ -623,14 +763,21 @@ category or the scientific project itself. analysis.project -> analysis.workspace ``` -2. Rename display internals: + This includes the constructor argument, the stored attribute, and any + public read-only property exposing the parent workspace. + +2. Rename display internals in `WorkspaceDisplay` (formerly + `ProjectDisplay`) and in any other class that stores a back- + reference to the root facade: ```text self._project -> self._workspace _set_project(...) -> _set_workspace(...) ``` - Only do this when the object is the top-level runtime facade. + Only do this when the stored object is the top-level runtime facade. + Do not touch `self._project_id` inside `ProjectInfo` or `_project.*` + CIF tags. 3. Rename local variables in runtime code: @@ -687,35 +834,36 @@ new root object and project-information category. ### Files Likely To Change -- `docs/dev/architecture.md` -- `docs/dev/Issues/issues_open.md` -- `docs/dev/ADRs/*.md` -- `docs/dev/ADR-suggestions/*.md` +- `docs/dev/adrs/index.md` +- `docs/dev/issues/open.md` +- `docs/dev/adrs/accepted/*.md` +- `docs/dev/adrs/suggestions/*.md` - `docs/docs/tutorials/*.py` - `README.md` - `CONTRIBUTING.md` only if it contains API examples Do not edit these by hand: -- `docs/dev/package-structure-full.md` -- `docs/dev/package-structure-short.md` +- `docs/dev/package-structure/full.md` +- `docs/dev/package-structure/short.md` - generated tutorial notebooks - generated `docs/site/` files ### Steps -1. Update architecture section 7: +1. Update the relevant accepted ADRs: ```text - Project - The Top-Level Facade - -> Workspace - The Top-Level Facade + Project Facade and Persistence Layout + -> Workspace Facade and Persistence Layout ``` -2. Update the architecture table to use: +2. Update the affected ADR examples to use: ```text workspace.project ProjectInfo workspace.rendering Rendering + workspace.verbosity str workspace.display WorkspaceDisplay ``` @@ -884,14 +1032,22 @@ Add focused tests for: 3. `workspace.project.title` round-trips through `workspace.cif`. 4. `workspace.rendering.table_engine` round-trips through `workspace.cif`. -5. `Workspace.save()` writes `workspace.cif`. -6. `Workspace.load()` reads `workspace.cif`. -7. `workspace.cif` contains `_project.id`, not `_meta.project_id`. -8. `workspace.cif` contains `_rendering.table_engine`. -9. `workspace.path` is set after `save_as()` and `load()`. -10. `workspace.project` has no serialized path field. -11. `project.cif` is not written unless compatibility was approved. -12. `ed.Project` is absent unless compatibility was approved. +5. `workspace.verbosity` round-trips through `workspace.cif`. +6. `Workspace.save()` writes `workspace.cif`. +7. `Workspace.load()` reads `workspace.cif`. +8. `workspace.cif` contains `_project.id`, not `_meta.project_id`. +9. `workspace.cif` contains `_rendering.table_engine`. +10. `workspace.cif` contains `_verbosity.level`. +11. `workspace.path` is set after `save_as()` and `load()`. +12. `workspace.project` has no serialized path field. +13. `project.cif`, `config.cif`, and `meta.cif` are not written unless + compatibility was approved. +14. `ed.Project` is absent unless compatibility was approved. +15. `Verbosity` category is registered via its factory and reachable + through `WorkspaceConfig` (parallel to `Rendering`). +16. `ed.extract_workspace_from_zip` is importable and + `ed.extract_project_from_zip` is not (unless compatibility + approved). If compatibility alias was approved, add tests for: @@ -952,13 +1108,19 @@ rg -n "_meta\\.|_project\\." src tests docs tools README.md CONTRIBUTING.md Saved config file should be `workspace.cif`: ```shell -rg -n "project\\.cif|workspace\\.cif" src tests docs tools README.md CONTRIBUTING.md +rg -n "project\\.cif|config\\.cif|meta\\.cif|workspace\\.cif" src tests docs tools README.md CONTRIBUTING.md +``` + +Workspace verbosity should serialize as a workspace-level category: + +```shell +rg -n "_verbosity|verbosity" src tests docs tools README.md CONTRIBUTING.md ``` Generated docs should not be manually edited: ```shell -git diff -- docs/site docs/dev/package-structure-full.md docs/dev/package-structure-short.md +git diff -- docs/site docs/dev/package-structure/full.md docs/dev/package-structure/short.md ``` If package-structure docs changed because of `pixi run fix`, that is @@ -1001,6 +1163,17 @@ Incorrect: _meta.project_title ``` +### Mistake: Keeping `project.cif` Or Switching To Generic File Names + +Do not use `project.cif`, `config.cif`, or `meta.cif` as the target +singleton settings file. The file belongs to the workspace layer. + +Correct: + +```text +workspace.cif +``` + ### Mistake: Blindly Replacing Every `project` Some uses of `project` should remain: @@ -1017,6 +1190,21 @@ Only root-facade uses should become `workspace` or `Workspace`. The saved directory path belongs to the workspace runtime state. It should be `workspace.path`, not `workspace.project.path`. +### Mistake: Forgetting Facade Class-Level State + +When renaming `Project` to `Workspace`, the `ClassVar` +`_current_project`, the `current_project_path()` classmethod, and the +`varname()` fallback string `'project'` all live on the class itself and +are easy to miss with a single search-and-replace. Rename them to +`_current_workspace`, `current_workspace_path()`, and `'workspace'` +respectively. + +### Mistake: Renaming `ProjectInfo._project_id` + +The internal descriptor attribute `self._project_id` inside +`ProjectInfo` already matches the new public name `id` and stays as is. +Only the public `name` property/setter becomes `id`. + ### Mistake: Editing Generated Notebooks Directly Tutorial notebooks are generated artifacts. Edit tutorial `.py` files, 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/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/constraints/default.py b/src/easydiffraction/analysis/categories/constraints/default.py index fc1d74fde..d6f026221 100644 --- a/src/easydiffraction/analysis/categories/constraints/default.py +++ b/src/easydiffraction/analysis/categories/constraints/default.py @@ -26,9 +26,21 @@ 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".', @@ -39,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: """ @@ -133,21 +151,33 @@ def create(self, *, expression: str) -> None: """ item = Constraint() item.expression = expression + if 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.expression.value] for constraint in self] + rows = [[constraint.id.value, constraint.expression.value] for constraint in self] console.paragraph('User defined constraints') render_table( - columns_headers=['expression'], - columns_alignment=['left'], + 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/fitting/default.py b/src/easydiffraction/analysis/categories/fitting/default.py index 925259cfc..141e22365 100644 --- a/src/easydiffraction/analysis/categories/fitting/default.py +++ b/src/easydiffraction/analysis/categories/fitting/default.py @@ -31,6 +31,8 @@ class Fitting(CategoryItem): Holds the active minimizer backend tag. """ + _category_code = 'fitting' + type_info = TypeInfo( tag='default', description='Fitting configuration category', @@ -51,8 +53,6 @@ def __init__(self) -> None: cif_handler=CifHandler(names=['_fitting.minimizer_type']), ) - self._identity.category_code = 'fitting' - @property def minimizer_type(self) -> StringDescriptor: """Fitting minimizer backend type.""" diff --git a/src/easydiffraction/analysis/categories/joint_fit/default.py b/src/easydiffraction/analysis/categories/joint_fit/default.py index 62f9e7e36..2821f2e8f 100644 --- a/src/easydiffraction/analysis/categories/joint_fit/default.py +++ b/src/easydiffraction/analysis/categories/joint_fit/default.py @@ -24,6 +24,9 @@ class JointFitItem(CategoryItem): """A single joint-fit entry.""" + _category_code = 'joint_fit' + _category_entry_name = 'experiment_id' + def __init__(self) -> None: super().__init__() @@ -46,9 +49,6 @@ def __init__(self) -> None: cif_handler=CifHandler(names=['_joint_fit.weight']), ) - self._identity.category_code = 'joint_fit' - self._identity.category_entry_name = lambda: str(self.experiment_id.value) - @property def experiment_id(self) -> StringDescriptor: """ diff --git a/src/easydiffraction/analysis/categories/sequential_fit/default.py b/src/easydiffraction/analysis/categories/sequential_fit/default.py index aad826f18..52519f56d 100644 --- a/src/easydiffraction/analysis/categories/sequential_fit/default.py +++ b/src/easydiffraction/analysis/categories/sequential_fit/default.py @@ -22,6 +22,8 @@ class SequentialFit(CategoryItem): """Persisted settings for sequential fitting.""" + _category_code = 'sequential_fit' + type_info = TypeInfo( tag='default', description='Sequential fitting settings', @@ -67,8 +69,6 @@ def __init__(self) -> None: cif_handler=CifHandler(names=['_sequential_fit.reverse']), ) - self._identity.category_code = 'sequential_fit' - @property def data_dir(self) -> StringDescriptor: """Directory containing sequential-fit data files.""" diff --git a/src/easydiffraction/analysis/categories/sequential_fit_extract/default.py b/src/easydiffraction/analysis/categories/sequential_fit_extract/default.py index 84bbf70c5..a724692c3 100644 --- a/src/easydiffraction/analysis/categories/sequential_fit_extract/default.py +++ b/src/easydiffraction/analysis/categories/sequential_fit_extract/default.py @@ -58,6 +58,9 @@ def _validate_extract_pattern(value: str) -> None: class SequentialFitExtractItem(CategoryItem): """A single sequential-fit extract rule.""" + _category_code = 'sequential_fit_extract' + _category_entry_name = 'id' + def __init__(self) -> None: super().__init__() @@ -89,9 +92,6 @@ def __init__(self) -> None: cif_handler=CifHandler(names=['_sequential_fit_extract.required']), ) - self._identity.category_code = 'sequential_fit_extract' - self._identity.category_entry_name = lambda: str(self.id.value) - @property def id(self) -> StringDescriptor: """Identifier for this extract rule.""" 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/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/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 b1e30aa0a..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): diff --git a/src/easydiffraction/datablocks/experiment/categories/data/total_pd.py b/src/easydiffraction/datablocks/experiment/categories/data/total_pd.py index 31ac92e76..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 # ------------------------------------------------------------------ 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 ada32c70f..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 # ------------------------------------------------------------------ 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 6c91a8561..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 # ------------------------------------------------------------------ diff --git a/src/easydiffraction/datablocks/structure/categories/cell/default.py b/src/easydiffraction/datablocks/structure/categories/cell/default.py index 712629501..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 # ------------------------------------------------------------------ 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/io/cif/serialize.py b/src/easydiffraction/io/cif/serialize.py index f7625330e..5c3a29c14 100644 --- a/src/easydiffraction/io/cif/serialize.py +++ b/src/easydiffraction/io/cif/serialize.py @@ -912,3 +912,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/project/categories/info/default.py b/src/easydiffraction/project/categories/info/default.py index 1d7b8e318..ff730dcee 100644 --- a/src/easydiffraction/project/categories/info/default.py +++ b/src/easydiffraction/project/categories/info/default.py @@ -24,6 +24,8 @@ class ProjectInfo(CategoryItem): """Project metadata category.""" + _category_code = 'project' + type_info = TypeInfo( tag='default', description='Project metadata category', @@ -72,8 +74,6 @@ def __init__( ) self._path: pathlib.Path | None = None - self._identity.category_code = 'project' - @staticmethod def _parse_timestamp(value: str) -> datetime.datetime: """Parse project timestamp text from CIF storage format.""" diff --git a/src/easydiffraction/project/categories/rendering/default.py b/src/easydiffraction/project/categories/rendering/default.py index b55a6f923..d3e2fad3e 100644 --- a/src/easydiffraction/project/categories/rendering/default.py +++ b/src/easydiffraction/project/categories/rendering/default.py @@ -24,6 +24,8 @@ class Rendering(CategoryItem): """Chart and table engine selection for a project.""" + _category_code = 'rendering' + type_info = TypeInfo( tag='default', description='Project rendering category', @@ -58,8 +60,6 @@ def __init__(self) -> None: cif_handler=CifHandler(names=['_rendering.table_engine']), ) - self._identity.category_code = 'rendering' - @property def chart_engine(self) -> StringDescriptor: """Chart renderer backend type.""" diff --git a/tests/integration/fitting/test_analysis_and_fit_category_support.py b/tests/integration/fitting/test_analysis_and_fit_category_support.py index 5633cb6ad..89c0fd931 100644 --- a/tests/integration/fitting/test_analysis_and_fit_category_support.py +++ b/tests/integration/fitting/test_analysis_and_fit_category_support.py @@ -267,10 +267,14 @@ def test_analysis_display_as_cif_and_constraints(monkeypatch, capsys): 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()] @@ -279,7 +283,8 @@ class FakeConstraint: analysis.display.constraints() out = capsys.readouterr().out assert 'User defined constraints' in out - assert captured['columns_data'][0][0] == 'x = y + 1' + assert captured['columns_headers'] == ['id', 'expression'] + assert captured['columns_data'][0] == ['constraint_1', 'x = y + 1'] def test_discover_helpers_and_snapshot_params(): diff --git a/tests/integration/fitting/test_project_load.py b/tests/integration/fitting/test_project_load.py index f1f81c801..f908789a6 100644 --- a/tests/integration/fitting/test_project_load.py +++ b/tests/integration/fitting/test_project_load.py @@ -205,6 +205,9 @@ 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 diff --git a/tests/unit/easydiffraction/analysis/categories/test_constraints.py b/tests/unit/easydiffraction/analysis/categories/test_constraints.py index 15dddc4f2..b887408ea 100644 --- a/tests/unit/easydiffraction/analysis/categories/test_constraints.py +++ b/tests/unit/easydiffraction/analysis/categories/test_constraints.py @@ -1,16 +1,52 @@ # 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_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/test_analysis_coverage.py b/tests/unit/easydiffraction/analysis/test_analysis_coverage.py index 42ef01ace..13072544a 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis_coverage.py +++ b/tests/unit/easydiffraction/analysis/test_analysis_coverage.py @@ -78,10 +78,14 @@ def test_constraints_with_items(self, capsys, monkeypatch): 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()] @@ -95,8 +99,9 @@ def fake_render_table(**kwargs): 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'] # ------------------------------------------------------------------ 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/io/cif/test_serialize_category_owner_baseline.py b/tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py index badd72418..7d45dfbd3 100644 --- a/tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py +++ b/tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py @@ -52,6 +52,7 @@ def test_real_analysis_as_cif_includes_aliases_and_constraints_when_present() -> 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 diff --git a/tests/unit/easydiffraction/project/test_project_load.py b/tests/unit/easydiffraction/project/test_project_load.py index 4f32ffde4..e44c12608 100644 --- a/tests/unit/easydiffraction/project/test_project_load.py +++ b/tests/unit/easydiffraction/project/test_project_load.py @@ -120,6 +120,8 @@ 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 diff --git a/tools/generate_package_docs.py b/tools/generate_package_docs.py index 1d860557e..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/dev/: - - 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' / 'dev' +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``: From 57f00e1cb92565c699262ef0932f19ccd80d04c6 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Tue, 19 May 2026 15:09:45 +0200 Subject: [PATCH 10/10] Add persistent fit results and project-centric workflows (#178) * Split persistence ADR and add fit output ADR * Add artifact root for reproducible tutorials * Finalize Project facade decision * Store auto rendering defaults in project config * Accept loop category key identity ADR * Add project save to ed-2 tutorial * Remove extraneous blank line * Add project fit verbosity category * Add analysis fit state implementation plan * Reformat documentation tables and line breaks * Add project fit verbosity verification * Clarify analysis fit-state ADR schema * Add common analysis fit-state categories * Add deterministic fit-result categories * Add Bayesian fit-result metadata categories * Add Bayesian fit-cache manifest categories * Add project save step to tutorial * Wire analysis fit-state CIF restore * Linting and formatting * Add h5py dependency * Capture persisted fit-state projections * Persist Bayesian fit arrays in results sidecar * Restore fit results from saved analysis state * Align persisted fit-state schema with review feedback * Persist Bayesian plot caches in sidecar * Add integer type and skip redundant processing * Update tutorial notebooks with consistent IDs and saves * Add pre/post-processing tracking to Bayesian fits * Add project-first CLI commands for saved projects * Fix missing pre-processing row in DREAM progress table * Add CLI commands for example data downloads * Remove pattern display from tutorial * Group saved-project tutorials under Load Project * Consolidate download and extract in tutorials * Refactor Bayesian fit-state manifest create APIs * Remove deterministic parameter-result persistence * Update data index reference and hash * Consolidate saved project tutorials to download_data * Remove deterministic_parameter_results from docs * Revise docs for CLI syntax and project loading * Update undo command to show it's a placeholder * Harden fit-state sidecar paths and finalize ADR * Reorder CLI command definitions * Restore saved DREAM sampler settings on project load * Restore legacy DREAM sampler settings before CLI fit * Clarify DREAM multiprocessing issue for direct scripts * Update data index ref and hash * Add DREAM multiprocessing issue #95 * Remove obsolete package references from docs * Update CIF serializer test for long descriptions * Update ADR suggestions for implemented fit-state work * Accept parameter correlation persistence ADR * Rename sequential fit result CSV columns * Add overwrite option to Project.save_as * Improve code formatting and docstrings * Update data index reference and hash * Simplify tutorial with API shortcuts * Improve tutorial index descriptions * Accept str or list for column_names * Fix fit tracking finalization cleanup * Add tutorial benchmark runner and pixi task * Write benchmark results incrementally * Reduce tutorial computation time and fix data ID * Simplify tutorial titles for consistency * Honor max_iterations in BUMPS fits * Add tutorial benchmark results * Refactor BumpsMinimizer evaluation limit handling * Plot fit quality vs temperature * Improve output formatting and add logging * Optimize tutorial execution time * Add newline before saved path * Remove notebook timeout, add CI artifact root --- .github/copilot-instructions.md | 9 + .github/workflows/docs.yml | 2 +- .../adrs/accepted/analysis-cif-fit-state.md | 193 +++ .../loop-category-key-identity.md | 95 +- .../parameter-correlation-persistence.md | 104 ++ .../project-facade-and-persistence.md | 45 +- docs/dev/adrs/index.md | 62 +- .../suggestions/analysis-cif-fit-state.md | 220 --- .../fit-output-files-and-data-exports.md | 172 +++ .../parameter-correlation-persistence.md | 114 -- .../parameter-posterior-summary.md | 437 +----- .../python-cif-category-correspondence.md | 98 +- docs/dev/adrs/suggestions/undo-fit.md | 146 +- .../workspace-root-project-category.md | 380 ----- ...darwin-arm64_py314_tutorial-benchmarks.csv | 24 + ...darwin-arm64_py314_tutorial-benchmarks.csv | 24 + ...darwin-arm64_py314_tutorial-benchmarks.csv | 24 + docs/dev/issues/open.md | 205 +-- docs/dev/package-structure/full.md | 94 +- docs/dev/package-structure/short.md | 52 +- docs/dev/plans/loop-category-key-identity.md | 620 --------- .../plans/workspace-root-project-category.md | 1229 ----------------- docs/docs/cli/index.md | 73 +- docs/docs/quick-reference/index.md | 22 +- docs/docs/tutorials/ed-13.ipynb | 2 +- docs/docs/tutorials/ed-15.ipynb | 39 +- docs/docs/tutorials/ed-15.py | 4 + docs/docs/tutorials/ed-17.ipynb | 243 ++-- docs/docs/tutorials/ed-17.py | 176 ++- docs/docs/tutorials/ed-18.ipynb | 39 +- docs/docs/tutorials/ed-18.py | 11 +- docs/docs/tutorials/ed-2.ipynb | 18 + docs/docs/tutorials/ed-2.py | 6 + docs/docs/tutorials/ed-20.ipynb | 2 +- docs/docs/tutorials/ed-21.ipynb | 126 +- docs/docs/tutorials/ed-21.py | 9 +- docs/docs/tutorials/ed-22.ipynb | 3 +- docs/docs/tutorials/ed-22.py | 3 +- docs/docs/tutorials/ed-23.ipynb | 85 +- docs/docs/tutorials/ed-23.py | 41 +- docs/docs/tutorials/ed-24.ipynb | 223 +++ docs/docs/tutorials/ed-24.py | 82 ++ docs/docs/tutorials/index.md | 20 +- .../user-guide/analysis-workflow/project.md | 40 +- docs/mkdocs.yml | 6 +- pixi.lock | 1 + pixi.toml | 12 +- pyproject.toml | 1 + src/easydiffraction/__init__.py | 1 + src/easydiffraction/__main__.py | 152 +- src/easydiffraction/analysis/__init__.py | 52 + src/easydiffraction/analysis/analysis.py | 1134 ++++++++++++++- .../analysis/categories/__init__.py | 31 + .../bayesian_convergence/__init__.py | 7 + .../bayesian_convergence/default.py | 121 ++ .../bayesian_convergence/factory.py | 17 + .../bayesian_distribution_caches/__init__.py | 12 + .../bayesian_distribution_caches/default.py | 151 ++ .../bayesian_distribution_caches/factory.py | 17 + .../bayesian_pair_caches/__init__.py | 8 + .../bayesian_pair_caches/default.py | 267 ++++ .../bayesian_pair_caches/factory.py | 17 + .../bayesian_parameter_posteriors/__init__.py | 12 + .../bayesian_parameter_posteriors/default.py | 242 ++++ .../bayesian_parameter_posteriors/factory.py | 17 + .../bayesian_predictive_datasets/__init__.py | 12 + .../bayesian_predictive_datasets/default.py | 260 ++++ .../bayesian_predictive_datasets/factory.py | 17 + .../categories/bayesian_result/__init__.py | 5 + .../categories/bayesian_result/default.py | 215 +++ .../categories/bayesian_result/factory.py | 17 + .../categories/bayesian_sampler/__init__.py | 5 + .../categories/bayesian_sampler/default.py | 133 ++ .../categories/bayesian_sampler/factory.py | 17 + .../categories/constraints/default.py | 9 +- .../deterministic_result/__init__.py | 7 + .../deterministic_result/default.py | 187 +++ .../deterministic_result/factory.py | 17 + .../fit_parameter_correlations/__init__.py | 12 + .../fit_parameter_correlations/default.py | 184 +++ .../fit_parameter_correlations/factory.py | 17 + .../categories/fit_parameters/__init__.py | 6 + .../categories/fit_parameters/default.py | 177 +++ .../categories/fit_parameters/factory.py | 17 + .../categories/fit_result/__init__.py | 5 + .../analysis/categories/fit_result/default.py | 135 ++ .../analysis/categories/fit_result/factory.py | 17 + src/easydiffraction/analysis/enums.py | 24 + .../analysis/fit_helpers/tracking.py | 107 +- src/easydiffraction/analysis/fitting.py | 181 ++- .../analysis/minimizers/base.py | 50 +- .../analysis/minimizers/bumps.py | 271 +++- .../analysis/minimizers/bumps_dream.py | 37 +- src/easydiffraction/analysis/sequential.py | 61 +- src/easydiffraction/core/validation.py | 2 + src/easydiffraction/core/variable.py | 40 + .../datablocks/experiment/collection.py | 2 +- src/easydiffraction/display/plotting.py | 337 ++++- src/easydiffraction/display/progress.py | 2 + src/easydiffraction/io/ascii.py | 21 +- src/easydiffraction/io/cif/serialize.py | 206 ++- src/easydiffraction/io/results_sidecar.py | 652 +++++++++ .../project/categories/rendering/default.py | 61 +- .../project/categories/verbosity/__init__.py | 8 + .../project/categories/verbosity/default.py | 55 + .../project/categories/verbosity/factory.py | 17 + src/easydiffraction/project/display.py | 137 +- src/easydiffraction/project/project.py | 190 ++- src/easydiffraction/project/project_config.py | 8 + src/easydiffraction/utils/environment.py | 94 ++ src/easydiffraction/utils/utils.py | 124 +- tests/functional/test_project_lifecycle.py | 6 +- .../fitting/test_bayesian_dream.py | 11 +- .../fitting/test_bumps_dream_support.py | 5 +- tests/integration/fitting/test_sequential.py | 18 +- .../categories/test_bayesian_convergence.py | 17 + .../test_bayesian_distribution_caches.py | 17 + .../categories/test_bayesian_pair_caches.py | 15 + .../test_bayesian_parameter_posteriors.py | 17 + .../test_bayesian_predictive_datasets.py | 17 + .../categories/test_bayesian_result.py | 13 + .../categories/test_bayesian_sampler.py | 13 + .../analysis/categories/test_constraints.py | 11 + .../categories/test_deterministic_result.py | 17 + .../test_fit_parameter_correlations.py | 17 + .../categories/test_fit_parameters.py | 13 + .../analysis/categories/test_fit_result.py | 13 + .../analysis/categories/test_fit_state.py | 207 +++ .../analysis/fit_helpers/test_tracking.py | 47 + .../analysis/minimizers/test_base.py | 90 +- .../analysis/minimizers/test_bumps.py | 121 ++ .../analysis/minimizers/test_bumps_dream.py | 22 + .../analysis/minimizers/test_dfols.py | 4 + .../analysis/minimizers/test_lmfit.py | 35 + .../analysis/test_analysis_coverage.py | 105 ++ .../easydiffraction/analysis/test_enums.py | 14 + .../easydiffraction/analysis/test_fitting.py | 184 ++- .../analysis/test_sequential.py | 77 +- .../easydiffraction/display/test_plotting.py | 73 + .../easydiffraction/io/cif/test_serialize.py | 2 +- .../io/cif/test_serialize_more.py | 29 +- .../io/test_results_sidecar.py | 138 ++ .../categories/rendering/test_default.py | 18 +- .../categories/verbosity/test_default.py | 44 + .../categories/verbosity/test_factory.py | 25 + .../easydiffraction/project/test_display.py | 78 +- .../easydiffraction/project/test_project.py | 8 +- .../project/test_project_config.py | 65 +- .../project/test_project_load.py | 163 +++ .../project/test_project_save.py | 59 + tests/unit/easydiffraction/test___main__.py | 27 +- .../easydiffraction/utils/test_environment.py | 41 + .../utils/test_utils_coverage.py | 36 + tests/unit/test_benchmark_tutorials.py | 133 ++ tools/benchmark_tutorials.py | 216 +++ 155 files changed, 10177 insertions(+), 4156 deletions(-) create mode 100644 docs/dev/adrs/accepted/analysis-cif-fit-state.md rename docs/dev/adrs/{suggestions => accepted}/loop-category-key-identity.md (78%) create mode 100644 docs/dev/adrs/accepted/parameter-correlation-persistence.md delete mode 100644 docs/dev/adrs/suggestions/analysis-cif-fit-state.md create mode 100644 docs/dev/adrs/suggestions/fit-output-files-and-data-exports.md delete mode 100644 docs/dev/adrs/suggestions/parameter-correlation-persistence.md delete mode 100644 docs/dev/adrs/suggestions/workspace-root-project-category.md create mode 100644 docs/dev/benchmarking/20260519-103251_darwin-arm64_py314_tutorial-benchmarks.csv create mode 100644 docs/dev/benchmarking/20260519-103500_darwin-arm64_py314_tutorial-benchmarks.csv create mode 100644 docs/dev/benchmarking/20260519-121524_darwin-arm64_py314_tutorial-benchmarks.csv delete mode 100644 docs/dev/plans/loop-category-key-identity.md delete mode 100644 docs/dev/plans/workspace-root-project-category.md create mode 100644 docs/docs/tutorials/ed-24.ipynb create mode 100644 docs/docs/tutorials/ed-24.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_convergence/__init__.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_convergence/default.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_convergence/factory.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_distribution_caches/__init__.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_distribution_caches/default.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_distribution_caches/factory.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_pair_caches/__init__.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_pair_caches/default.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_pair_caches/factory.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_parameter_posteriors/__init__.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_parameter_posteriors/default.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_parameter_posteriors/factory.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_predictive_datasets/__init__.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_predictive_datasets/default.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_predictive_datasets/factory.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_result/__init__.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_result/default.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_result/factory.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_sampler/__init__.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_sampler/default.py create mode 100644 src/easydiffraction/analysis/categories/bayesian_sampler/factory.py create mode 100644 src/easydiffraction/analysis/categories/deterministic_result/__init__.py create mode 100644 src/easydiffraction/analysis/categories/deterministic_result/default.py create mode 100644 src/easydiffraction/analysis/categories/deterministic_result/factory.py create mode 100644 src/easydiffraction/analysis/categories/fit_parameter_correlations/__init__.py create mode 100644 src/easydiffraction/analysis/categories/fit_parameter_correlations/default.py create mode 100644 src/easydiffraction/analysis/categories/fit_parameter_correlations/factory.py create mode 100644 src/easydiffraction/analysis/categories/fit_parameters/__init__.py create mode 100644 src/easydiffraction/analysis/categories/fit_parameters/default.py create mode 100644 src/easydiffraction/analysis/categories/fit_parameters/factory.py create mode 100644 src/easydiffraction/analysis/categories/fit_result/__init__.py create mode 100644 src/easydiffraction/analysis/categories/fit_result/default.py create mode 100644 src/easydiffraction/analysis/categories/fit_result/factory.py create mode 100644 src/easydiffraction/io/results_sidecar.py create mode 100644 src/easydiffraction/project/categories/verbosity/__init__.py create mode 100644 src/easydiffraction/project/categories/verbosity/default.py create mode 100644 src/easydiffraction/project/categories/verbosity/factory.py create mode 100644 tests/unit/easydiffraction/analysis/categories/test_bayesian_convergence.py create mode 100644 tests/unit/easydiffraction/analysis/categories/test_bayesian_distribution_caches.py create mode 100644 tests/unit/easydiffraction/analysis/categories/test_bayesian_pair_caches.py create mode 100644 tests/unit/easydiffraction/analysis/categories/test_bayesian_parameter_posteriors.py create mode 100644 tests/unit/easydiffraction/analysis/categories/test_bayesian_predictive_datasets.py create mode 100644 tests/unit/easydiffraction/analysis/categories/test_bayesian_result.py create mode 100644 tests/unit/easydiffraction/analysis/categories/test_bayesian_sampler.py create mode 100644 tests/unit/easydiffraction/analysis/categories/test_deterministic_result.py create mode 100644 tests/unit/easydiffraction/analysis/categories/test_fit_parameter_correlations.py create mode 100644 tests/unit/easydiffraction/analysis/categories/test_fit_parameters.py create mode 100644 tests/unit/easydiffraction/analysis/categories/test_fit_result.py create mode 100644 tests/unit/easydiffraction/analysis/categories/test_fit_state.py create mode 100644 tests/unit/easydiffraction/io/test_results_sidecar.py create mode 100644 tests/unit/easydiffraction/project/categories/verbosity/test_default.py create mode 100644 tests/unit/easydiffraction/project/categories/verbosity/test_factory.py create mode 100644 tests/unit/test_benchmark_tutorials.py create mode 100644 tools/benchmark_tutorials.py diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d5f96553d..46a066d2b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -160,6 +160,12 @@ Notes: `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/dev/issues/open.md` (priority-ordered). On resolution, move to `docs/dev/issues/closed.md` and update the relevant ADR or @@ -204,6 +210,9 @@ 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. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index def30827c..ea6bdad11 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -119,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/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/suggestions/loop-category-key-identity.md b/docs/dev/adrs/accepted/loop-category-key-identity.md similarity index 78% rename from docs/dev/adrs/suggestions/loop-category-key-identity.md rename to docs/dev/adrs/accepted/loop-category-key-identity.md index f55ef4a89..4334b4e13 100644 --- a/docs/dev/adrs/suggestions/loop-category-key-identity.md +++ b/docs/dev/adrs/accepted/loop-category-key-identity.md @@ -1,6 +1,6 @@ # ADR: Loop Category Keys and Identity Naming -**Status:** Proposed +**Status:** Accepted **Date:** 2026-05-18 ## Context @@ -10,7 +10,7 @@ CIF dictionaries can declare the key column for a loop category through use domain-specific identity tags such as `_atom_site.label`, while other loop categories may use an explicit `id` tag. -EasyDiffraction currently models the same runtime concept with +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: @@ -19,23 +19,24 @@ value as the collection key, and category items use it in their .. ``` -The design question is whether the current `category_entry_name` -approach is enough, and how closely Python-facing identity names should -follow CIF key tags. +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 current approach is directionally good. It gives every loop item a -stable collection key without hard-coding the key field into +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. -It is not explicit enough yet. The key field is encoded as a lambda on -each item, not as declarative metadata on the category or descriptor. -Nothing validates that the runtime key corresponds to a serialized CIF -field. The main visible example is `Constraint`: the current collection -key is derived from the left-hand side of `_constraint.expression`, but -no separate `_constraint.id` field is persisted. +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 @@ -52,14 +53,19 @@ Keep `category_entry_name` as the runtime analogue of CIF identity. Use `label` or `*_id` when the value has clearer domain meaning. -The `constraint` category should add an explicit `id` field and use it -as the collection key: +The `constraint` category has an explicit `id` field and uses it as the +collection key: ```text analysis.constraints[id].id -> _constraint.id ``` -The existing `lhs_alias` and `rhs_expr` properties should remain derived +`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. @@ -74,22 +80,22 @@ Rows are sorted by the chosen Python key style: `id`, then `*_id`, then 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. Current implementation derives 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. | +| 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 @@ -123,22 +129,21 @@ Jupyter, CLI output, and saved CIF files. ## Implementation Notes -The current `category_entry_name` mechanism can stay, but it should be -made easier to audit. The implementation now 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`. +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, add a descriptor-backed `id` property serialized as -`_constraint.id`, and change `category_entry_name` to resolve from that -descriptor. Keep `_constraint.expression` for the full equation. Keep -`lhs_alias` and `rhs_expr` as derived convenience properties. +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`, -derive a deterministic fallback `id` from the old `lhs_alias` key after -row values are loaded, then write `_constraint.id` on the next save. +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 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 index 67492d8d4..3b922f423 100644 --- a/docs/dev/adrs/accepted/project-facade-and-persistence.md +++ b/docs/dev/adrs/accepted/project-facade-and-persistence.md @@ -14,16 +14,22 @@ Persistence. ## Context -`Project` is the current top-level user facade. It owns project -metadata, structures, experiments, rendering preferences, display -helpers, analysis, summaries, verbosity, and save/load behavior. +`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 current top-level facade and persist projects as a +Use `Project` as the top-level facade and persist projects as a directory of CIF files: ```text @@ -40,8 +46,35 @@ 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. A -proposed `Workspace` rename is tracked separately as an ADR suggestion. +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/index.md b/docs/dev/adrs/index.md index ed0940bf3..50a287db9 100644 --- a/docs/dev/adrs/index.md +++ b/docs/dev/adrs/index.md @@ -13,34 +13,34 @@ 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 | Suggestion | Analysis CIF Fit State | Proposes a persisted scalar projection of fit state in `analysis.cif`. | [`analysis-cif-fit-state.md`](suggestions/analysis-cif-fit-state.md) | -| Analysis and fitting | Suggestion | Parameter Correlation Persistence | Proposes persisting deterministic and posterior correlation summaries. | [`parameter-correlation-persistence.md`](suggestions/parameter-correlation-persistence.md) | -| Analysis and fitting | Suggestion | Parameter-Level Posterior Projection and Bayesian Persistence | Proposes saved Bayesian summaries and canonical posterior storage. | [`parameter-posterior-summary.md`](suggestions/parameter-posterior-summary.md) | -| Analysis and fitting | Suggestion | Undo Fit | Proposes an analysis-owned rollback operation for the latest pre-fit scalar state. | [`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 | 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 | Loop Category Keys and Identity Naming | Documents current loop collection keys and proposes naming rules aligned with CIF category keys. | [`loop-category-key-identity.md`](suggestions/loop-category-key-identity.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) | -| Workspace model | Suggestion | Workspace Root and Project Information Category | Proposes renaming the top-level facade from `Project` to `Workspace` and reserving `project` for project metadata. | [`workspace-root-project-category.md`](suggestions/workspace-root-project-category.md) | +| 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/analysis-cif-fit-state.md b/docs/dev/adrs/suggestions/analysis-cif-fit-state.md deleted file mode 100644 index d471bb117..000000000 --- a/docs/dev/adrs/suggestions/analysis-cif-fit-state.md +++ /dev/null @@ -1,220 +0,0 @@ -# ADR: Analysis CIF Fit State - -**Status:** Proposed -**Date:** 2026-05-13 - -## Context - -`analysis/analysis.cif` currently persists analysis configuration such -as `_fit.minimizer_type`, `_fit.mode`, aliases, constraints, and -joint-fit weights. It does not persist analysis-owned fit state such as -fit bounds, bound provenance, pre-fit scalar snapshots, or latest -fit-status metadata. - -At the same time, parameter CIF serialization already carries the -committed parameter `value`, the current `free` state, and the current -`uncertainty` via CIF bracket notation. That data belongs to the model -and should remain in structure or experiment CIF files. - -The missing piece is analysis-owned fit state: - -- fit controls that apply to parameters during fitting but are not part - of the model itself -- latest fit-status metadata shown by `display.fit_results()` -- deterministic and Bayesian fit metadata that should survive project - reloads and command-line workflows - -This separation matters because: - -- Bayesian plotting after reload needs `fit_min`, `fit_max`, and bound - provenance even when raw posterior arrays are absent -- command-line users need a saved pre-fit starting state to recover from - a poor minimization run -- `analysis.fit_results` already changes by fit type, but its persisted - projection should have a stable analysis-owned home - -The accepted runtime-fit-results ADR describes fit results as -runtime-only. This ADR proposes a narrower persisted projection of the -latest fit state, not a direct dump of backend runtime objects. - -## Decision - -### 1. `analysis/analysis.cif` becomes the home of analysis-owned fit state - -The analysis CIF file will persist: - -- fit configuration -- aliases and constraints -- joint-fit weights -- per-parameter fit controls owned by analysis -- latest fit-status metadata common to deterministic and Bayesian fits -- fit-type-specific extensions defined in separate ADRs - -Committed parameter values remain in structure or experiment CIF files. -They are not duplicated into `analysis/analysis.cif`. - -### 2. Add a real `_fit_parameter` loop - -Introduce a new analysis-owned loop category: - -```cif -loop_ -_fit_parameter.param_unique_name -_fit_parameter.fit_min -_fit_parameter.fit_max -_fit_parameter.fit_bounds_uncertainty_multiplier -_fit_parameter.start_value -_fit_parameter.start_uncertainty -cosio.atom_site.Co1.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 -cosio.atom_site.Co2.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 -``` - -Fields: - -- `param_unique_name` -- `fit_min` -- `fit_max` -- `fit_bounds_uncertainty_multiplier` -- `start_value` -- `start_uncertainty` - -Rationale: - -- `fit_min` and `fit_max` are required to restore deterministic and - Bayesian fit controls faithfully. -- `fit_bounds_uncertainty_multiplier` preserves the provenance of - uncertainty-derived bounds for restored Bayesian plot annotations. -- `start_value` and `start_uncertainty` capture the last committed - pre-fit scalar state and enable fit recovery workflows, especially in - command-line usage. -- `start_uncertainty` preserves a user-visible pre-fit uncertainty - instead of treating it as disposable fit residue. - -### 3. Add a generic `_fit_result` single-item category - -Persist the latest fit-status metadata shared across fit types in a -single analysis-owned category: - -- `result_kind` -- `success` -- `message` -- `iterations` -- `fitting_time` -- `reduced_chi_square` - -Suggested CIF fragment: - -```cif -_fit_result.result_kind deterministic -_fit_result.success yes -_fit_result.message "Fit converged" -_fit_result.iterations 37 -_fit_result.fitting_time 1.82 -_fit_result.reduced_chi_square 1.031 -``` - -`result_kind` distinguishes the latest persisted fit projection, for -example `deterministic` or `bayesian`. - -### 4. Persist only stable fit-status fields here - -The `_fit_result` category is for generic status fields that are stable -across engines and already belong to the result model. - -It should not persist backend runtime objects or arbitrary engine -payloads. - -Metrics such as R-factors shown by `display.fit_results()` are derived -from observed and calculated data and can be recomputed after load when -needed. They do not need to be part of the first persisted fit-state -schema. - -### 5. Fit-type-specific extensions build on this ADR - -This ADR defines the common `analysis.cif` contract for deterministic -and Bayesian fitting. - -Fit-type-specific extensions are layered on top: - -- Bayesian persistence extends this with `_bayesian_*` categories and an - HDF5 sidecar, as described in `parameter-posterior-summary.md`. -- Future fit-specific summaries should follow the same pattern: generic - shared fields in `_fit_result`, specialized fields in separate - categories. - -### 6. Restore order is analysis-first, fit-type-second - -Load order should be: - -1. standard analysis configuration -2. aliases and constraints -3. joint-fit weights -4. `_fit_parameter` -5. `_fit_result` -6. fit-type-specific extensions such as `_bayesian_*` - -This ensures that generic fit controls are available before restoring -specialized fit summaries. - -### 7. Suggested full `analysis.cif` example - -```cif -_fit.minimizer_type "bumps (dream)" -_fit.mode single - -loop_ -_alias.label -_alias.param_unique_name -biso_Co1 cosio.atom_site.Co1.adp_iso -biso_Co2 cosio.atom_site.Co2.adp_iso - -loop_ -_constraint.expression -"biso_Co2 = biso_Co1" - -loop_ -_fit_parameter.param_unique_name -_fit_parameter.fit_min -_fit_parameter.fit_max -_fit_parameter.fit_bounds_uncertainty_multiplier -_fit_parameter.start_value -_fit_parameter.start_uncertainty -cosio.atom_site.Co1.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 -cosio.atom_site.Co2.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 - -_fit_result.result_kind bayesian -_fit_result.success yes -_fit_result.message "Sampler converged" -_fit_result.iterations 3000 -_fit_result.fitting_time 82.4 -_fit_result.reduced_chi_square 1.031 - -# Bayesian-specific extension categories follow here. -``` - -## Consequences - -### Positive - -- `analysis.cif` becomes the single analysis-owned source of fit state. -- Deterministic and Bayesian persistence share one common base schema. -- Fit bounds, bound provenance, and start values survive project - reloads. -- Command-line workflows gain a persisted pre-fit starting state. - -### Trade-offs - -- The runtime fit-results ADR must be updated because fit state is no - longer entirely runtime-only. -- Analysis persistence becomes more stateful and must be kept in sync - with live parameter objects. -- Some existing serializer assumptions will need refactoring so that - `analysis.cif` owns fit metadata rather than individual parameters. - -## Deferred Work - -- Bayesian-specific categories and HDF5 sidecar details remain in - `parameter-posterior-summary.md`. -- Undo semantics for `start_value` and `start_uncertainty` are defined - in a separate ADR. -- Correlation-matrix persistence is defined in a separate ADR. 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-correlation-persistence.md b/docs/dev/adrs/suggestions/parameter-correlation-persistence.md deleted file mode 100644 index 9c494908b..000000000 --- a/docs/dev/adrs/suggestions/parameter-correlation-persistence.md +++ /dev/null @@ -1,114 +0,0 @@ -# ADR: Parameter Correlation Persistence - -**Status:** Proposed -**Date:** 2026-05-13 - -## Context - -`plot_param_correlations()` can currently visualize either: - -- deterministic parameter correlations derived from engine covariance -- Bayesian correlations derived from posterior samples - -After project reload, this correlation information is not available -unless the underlying runtime objects are rebuilt. For Bayesian fits, -full posterior samples may not always be restored. For deterministic -fits, engine covariance is typically not persisted at all. - -The correlation matrix is an analysis-owned summary, not model state. It -therefore belongs in `analysis/analysis.cif`, not in structure or -experiment CIF files. - -## Decision - -### 1. Add a `_fit_parameter_correlation` loop category - -Persist pairwise parameter correlations in a new analysis-owned loop: - -- `source_kind` -- `param_unique_name_i` -- `param_unique_name_j` -- `correlation` - -Suggested example: - -```cif -loop_ -_fit_parameter_correlation.source_kind -_fit_parameter_correlation.param_unique_name_i -_fit_parameter_correlation.param_unique_name_j -_fit_parameter_correlation.correlation -posterior cosio.atom_site.Co1.adp_iso cosio.atom_site.Co2.adp_iso 0.87 -``` - -`source_kind` records how the correlation was obtained, for example: - -- `deterministic` -- `posterior` - -### 2. Store only the upper triangle excluding the diagonal - -Each row stores one unordered parameter pair with -`param_unique_name_i < param_unique_name_j` in a stable ordering. - -The diagonal is omitted because it is always 1.0 and can be rebuilt on -load. - -This keeps the CIF loop compact while remaining lossless for the -correlation matrix. - -### 3. Treat the loop as a summary, not a replacement for raw samples - -For Bayesian fits, `_fit_parameter_correlation` is a persisted summary. -It does not replace posterior samples or posterior pair data. - -This means: - -- correlation heatmaps can be restored from the loop alone -- posterior pair plots still require posterior samples - -### 4. Deterministic and Bayesian fits share the same loop schema - -The same loop category is used for both deterministic and Bayesian fit -results. The distinction is carried by `source_kind`, not by separate -loop names. - -### 5. Suggested restore behavior - -On load: - -- if `_fit_parameter_correlation` is present, correlation summaries are - restored into a lightweight analysis-owned correlation structure -- if it is absent, correlation plots fall back to whatever live runtime - information is available - -### 6. Suggested user-facing behavior - -```python -# Restored from analysis.cif when available -project.display.plotter.plot_param_correlations() -``` - -If only the correlation loop is restored, the user still gets the -correlation heatmap without needing raw posterior samples. - -## Consequences - -### Positive - -- Deterministic and Bayesian correlation summaries survive reload. -- Correlation heatmaps no longer depend entirely on runtime-only data. -- The schema is compact and fit-type-agnostic. - -### Trade-offs - -- The correlation loop is a derived summary, so it must be kept in sync - with the latest fit result. -- Restored correlation data is not enough for posterior pair plots or - predictive summaries. - -## 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/suggestions/parameter-posterior-summary.md b/docs/dev/adrs/suggestions/parameter-posterior-summary.md index 3de39bf37..36c0b979b 100644 --- a/docs/dev/adrs/suggestions/parameter-posterior-summary.md +++ b/docs/dev/adrs/suggestions/parameter-posterior-summary.md @@ -1,7 +1,17 @@ -# ADR: Parameter-Level Posterior Projection and Bayesian Persistence +# ADR: Parameter-Level Posterior Projection -**Status:** Proposed -**Date:** 2026-05-13 +**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 @@ -13,8 +23,9 @@ 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 describes this state as runtime-only and not -serialized. +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 @@ -59,7 +70,7 @@ Do not add separate flat parameter attributes such as `median`, Instead, the parameter-level projection reuses the existing `PosteriorParameterSummary` object already produced for `BayesianFitResults`. This keeps one grouped summary shape for display, -inspection, and later persistence. +inspection, and restore from persisted analysis state. The summary object currently provides the right level of detail: @@ -192,6 +203,8 @@ while internal fit application installs fresh metadata atomically. 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 @@ -211,358 +224,28 @@ Asymmetric interval information is not squeezed into `parameter.uncertainty`; it remains available only via `parameter.posterior`. -### 10. Persist canonical Bayesian state at analysis level +### 10. Rebuild posterior from analysis-level state Canonical Bayesian state is owned by `analysis.fit_results`, not by -individual parameters. +individual parameters. The saved fit-state format and restore order are +defined in `analysis-cif-fit-state.md`. -When the active result is a `BayesianFitResults` instance, persistence -must save enough data to restore two distinct capability levels: - -- summary-only restore for parameter inspection and tables -- full restore for posterior plots and predictive plots +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 always rebuilt from the canonical analysis-level persisted data. - -### 11. Persist fit-control and Bayesian metadata in `analysis/analysis.cif` - -The existing `analysis/analysis.cif` file remains the text metadata -entry point for analysis persistence. - -The persisted fit-control and Bayesian metadata is split into explicit -CIF categories. - -#### 11.1 `_fit_parameter` loop - -Stores analysis-owned per-parameter fit metadata that is not currently -covered by parameter CIF serialization. - -This loop exists because the committed parameter CIF representation -already carries the active `value`, current `free` state, and current -`uncertainty`, but it does not carry fit bounds, bound provenance, or -the pre-fit uncertainty snapshot needed by undo. Those fields are -required for Bayesian plot ranges, pair-plot bound annotations, and -clean fit rollback after project reload. - -Fields: - -- `param_unique_name` -- `fit_min` -- `fit_max` -- `fit_bounds_uncertainty_multiplier` -- `start_value` -- `start_uncertainty` - -`fit_min` and `fit_max` are required for restored Bayesian plotting. -`fit_bounds_uncertainty_multiplier` is required if restored plots should -preserve the uncertainty-derived bound annotation exactly. `start_value` -and `start_uncertainty` are required for clean cross-session undo. If -omitted, restored fit reports may show `N/A` for `start` and `change`, -and undo may need to clear uncertainty as a compatibility fallback. - -#### 11.2 `_bayesian_result` single item - -Stores one saved Bayesian result header with these fields: - -- `schema_version` -- `sampler_name` -- `point_estimate_name` -- `success` -- `sampler_completed` -- `reduced_chi_square` -- `fitting_time` -- `best_log_posterior` -- `credible_interval_inner` -- `credible_interval_outer` -- `has_posterior_samples` -- `has_posterior_predictive` -- `sidecar_file` - -For the current design, `point_estimate_name` is always `best_sample`. - -#### 11.3 `_bayesian_sampler` single item - -Stores resolved sampler settings actually used for the run: - -- `steps` -- `burn` -- `thin` -- `pop` -- `parallel` -- `init` -- `random_seed` - -This persists the existing runtime `sampler_settings` in an explicit, -schema-driven form rather than as an open-ended key/value mapping. +It is rebuilt from the analysis-level saved result projection when that +projection is available. -#### 11.4 `_bayesian_convergence` single item +Two restore levels matter for the parameter API: -Stores top-level convergence and shape metadata: +- summary-only restore can populate `parameter.posterior` and fit-result + tables +- full restore can also support posterior plots and predictive plots -- `converged` -- `max_r_hat` -- `min_ess_bulk` -- `n_draws` -- `n_chains` -- `n_parameters` - -Per-parameter `r_hat` and `ess_bulk` remain in the parameter summary -loop described below. - -#### 11.5 `_bayesian_parameter_posterior` loop - -Stores one canonical posterior summary row per sampled parameter. These -rows are the source used to rebuild `parameter.posterior` on load. - -Fields: - -- `order_index` -- `unique_name` -- `display_name` -- `best_sample_value` -- `median` -- `uncertainty` -- `interval_68_lower` -- `interval_68_upper` -- `interval_95_lower` -- `interval_95_upper` -- `ess_bulk` -- `r_hat` - -`order_index` defines the parameter order used by posterior sample -columns in the sidecar arrays. - -#### 11.6 `_bayesian_predictive_dataset` loop - -Stores one manifest row per persisted posterior predictive summary. - -Fields: - -- `experiment_name` -- `x_axis_name` -- `x_path` -- `best_sample_prediction_path` -- `lower_95_path` -- `upper_95_path` -- `lower_68_path` -- `upper_68_path` -- `draws_path` -- `n_x` -- `n_draws_cached` - -This loop tells the loader which arrays to read from the sidecar file -for each experiment-level predictive summary. - -#### 11.7 Suggested CIF fragments - -The active parameter value remains in the structure or experiment CIF as -it does today, for example: - -```cif -_atom_site_U_iso_or_equiv 0.0319(21) -``` - -Analysis-owned fit-control and Bayesian metadata then lives in -`analysis/analysis.cif`, for example: - -```cif -_fit.minimizer_type "bumps (dream)" -_fit.mode single - -loop_ -_fit_parameter.param_unique_name -_fit_parameter.fit_min -_fit_parameter.fit_max -_fit_parameter.fit_bounds_uncertainty_multiplier -_fit_parameter.start_value -_fit_parameter.start_uncertainty -cosio.atom_site.Co1.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 -cosio.atom_site.Co2.adp_iso 0.0000 0.1200 4.0 0.0312 0.0021 - -_bayesian_result.schema_version 1 -_bayesian_result.sampler_name dream -_bayesian_result.point_estimate_name best_sample -_bayesian_result.success yes -_bayesian_result.sampler_completed yes -_bayesian_result.reduced_chi_square 1.031 -_bayesian_result.fitting_time 82.4 -_bayesian_result.best_log_posterior -1542.77 -_bayesian_result.credible_interval_inner 0.68 -_bayesian_result.credible_interval_outer 0.95 -_bayesian_result.has_posterior_samples yes -_bayesian_result.has_posterior_predictive yes -_bayesian_result.sidecar_file "bayesian_data.h5" - -_bayesian_sampler.steps 3000 -_bayesian_sampler.burn 600 -_bayesian_sampler.thin 1 -_bayesian_sampler.pop 20 -_bayesian_sampler.parallel 0 -_bayesian_sampler.init lhs -_bayesian_sampler.random_seed 12345 - -_bayesian_convergence.converged yes -_bayesian_convergence.max_r_hat 1.01 -_bayesian_convergence.min_ess_bulk 812.4 -_bayesian_convergence.n_draws 2400 -_bayesian_convergence.n_chains 20 -_bayesian_convergence.n_parameters 2 - -loop_ -_bayesian_parameter_posterior.order_index -_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 -0 cosio.atom_site.Co1.adp_iso "Co1 ADP" 0.0319 0.0317 0.0021 0.0298 0.0339 0.0278 0.0361 812.4 1.01 -1 cosio.atom_site.Co2.adp_iso "Co2 ADP" 0.0320 0.0318 0.0020 0.0300 0.0338 0.0281 0.0359 830.7 1.00 - -loop_ -_bayesian_predictive_dataset.experiment_name -_bayesian_predictive_dataset.x_axis_name -_bayesian_predictive_dataset.x_path -_bayesian_predictive_dataset.best_sample_prediction_path -_bayesian_predictive_dataset.lower_95_path -_bayesian_predictive_dataset.upper_95_path -_bayesian_predictive_dataset.lower_68_path -_bayesian_predictive_dataset.upper_68_path -_bayesian_predictive_dataset.draws_path -_bayesian_predictive_dataset.n_x -_bayesian_predictive_dataset.n_draws_cached -hrpt ttheta /predictive/hrpt/x /predictive/hrpt/best_sample_prediction /predictive/hrpt/lower_95 /predictive/hrpt/upper_95 /predictive/hrpt/lower_68 /predictive/hrpt/upper_68 /predictive/hrpt/draws 2500 200 -``` - -### 12. Persist bulk arrays in `analysis/bayesian_data.h5` - -Large numerical arrays are stored outside CIF text in a single sidecar -file: - -- `analysis/bayesian_data.h5` - -The sidecar is optional. Summary-only restore remains valid without it. - -HDF5 is selected instead of NPZ because the persisted Bayesian payload -is a structured collection of named datasets with heterogeneous shapes, -optional groups, and potentially large predictive arrays. HDF5 provides -explicit hierarchical storage, dataset metadata, selective reads, and a -better long-term path for compression or chunking. NPZ is simpler, but -it is flatter and less suitable once the saved Bayesian state grows -beyond a small set of arrays. - -#### 12.1 Required core HDF5 dataset paths when posterior samples are saved - -- `posterior_parameter_samples` -- `posterior_log_posterior` -- `posterior_draw_index` - -Expected array shapes: - -- `posterior_parameter_samples`: `(n_draws, n_chains, n_parameters)` -- `posterior_log_posterior`: `(n_draws, n_chains)` when available -- `posterior_draw_index`: `(n_draws,)` when available - -If `posterior_log_posterior` or `posterior_draw_index` are unavailable, -their corresponding header flags remain false and the arrays may be -omitted. - -#### 12.2 Predictive dataset keys - -Posterior predictive arrays are addressed through the -`_bayesian_predictive_dataset` manifest rather than inferred from file -ordering. - -Recommended HDF5 dataset naming is: - -- `predictive____x` -- `predictive____best_sample_prediction` -- `predictive____lower_95` -- `predictive____upper_95` -- `predictive____lower_68` -- `predictive____upper_68` -- `predictive____draws` - -The manifest, not the naming convention, is the source of truth. - -Recommended HDF5 group layout is: - -- `/posterior/parameter_samples` -- `/posterior/log_posterior` -- `/posterior/draw_index` -- `/predictive//x` -- `/predictive//best_sample_prediction` -- `/predictive//lower_95` -- `/predictive//upper_95` -- `/predictive//lower_68` -- `/predictive//upper_68` -- `/predictive//draws` - -The manifest remains the canonical mapping used by the loader, so this -layout is recommended rather than mandatory. - -#### 12.3 What is not persisted in the sidecar - -Do not persist backend-specific runtime objects such as `engine_result`, -the DREAM driver, or ArviZ `InferenceData`. - -Those objects can be rebuilt from canonical saved arrays when needed, or -left unavailable after load. - -### 13. Restore flow and partial-availability policy - -Persistence must support both full and partial restore. - -#### 13.1 Save flow - -When `Project.save()` sees `analysis.fit_results` as a -`BayesianFitResults` instance: - -1. It writes standard analysis configuration to `analysis/analysis.cif`. -2. It appends `_fit_parameter`, `_bayesian_result`, `_bayesian_sampler`, - `_bayesian_convergence`, and `_bayesian_parameter_posterior`. -3. If posterior predictive summaries are available, it also writes the - `_bayesian_predictive_dataset` manifest. -4. If posterior sample arrays or predictive arrays are available, it - writes `analysis/bayesian_data.h5`. - -#### 13.2 Load flow - -When `Project.load()` restores `analysis/analysis.cif`: - -1. Standard analysis configuration is restored first. -2. If `_fit_parameter` is present, fit bounds, bound provenance, and - optional pre-fit scalar snapshots are restored by matching - `param_unique_name` to live parameters. -3. If Bayesian categories are present, a lightweight - `BayesianFitResults` instance is rebuilt from persisted metadata and - parameter summary rows. -4. `parameter.posterior` is rebuilt by matching summary rows to live - parameters via `unique_name`. -5. `parameter.value` and `parameter.uncertainty` continue to come from - the normal project serialization path; Bayesian restore does not - overwrite them. -6. If `analysis/bayesian_data.h5` exists and matches the manifest, - `posterior_samples` and `posterior_predictive` are restored. -7. If the sidecar is missing or incomplete, the restore degrades to - summary-only mode without failing the whole project load. - -#### 13.3 Partial restore behavior - -The chosen partial-restore policy is: - -- `parameter.posterior` and `display.fit_results()` remain available - from saved metadata and summaries. -- Bayesian-only plots requiring canonical posterior arrays remain - unavailable unless those arrays were restored successfully. -- Missing sidecar data should produce a clear warning, not a hard load - failure. +If the saved project has no analysis-level posterior summary for a +parameter, `parameter.posterior` remains `None`. Example user access after load: @@ -574,28 +257,18 @@ posterior = param.posterior if posterior is not None: print(posterior.best_sample_value) - print(posterior.uncertainty) - print(posterior.interval_68) + print(posterior.uncertainty) + print(posterior.interval_68) project.analysis.display.fit_results() ``` -Example plotting behavior after load: - -```python -# Works with summary-only restore -project.analysis.display.fit_results() +Posterior plot availability after load follows the fit-state restore +level defined in `analysis-cif-fit-state.md`. -# Requires canonical posterior arrays from analysis/bayesian_data.h5 -project.display.plotter.plot_posterior_pairs() -project.display.plotter.plot_param_distribution(param) -project.display.plotter.plot_posterior_predictive(expt_name='hrpt') -``` +### 11. Keep parameter posterior data rebuilt, not duplicated -### 14. Keep parameter posterior data rebuilt, not duplicated - -`parameter.posterior` is always rebuilt from the canonical -`_bayesian_parameter_posterior` loop in `analysis/analysis.cif`. +`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 @@ -610,15 +283,10 @@ structure and experiment files would create multiple sources of truth. 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 source for full Bayesian - state. -- `analysis/analysis.cif` becomes the home for fit-control metadata that - does not belong in structure or experiment CIF files. -- Bayesian save/load gains a clear split between text metadata in - `analysis/analysis.cif` and bulk numerical arrays in - `analysis/bayesian_data.h5`. -- Partial restore works for summaries even when full posterior arrays - are absent. +- `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. @@ -632,8 +300,6 @@ structure and experiment files would create multiple sources of truth. application code must be updated to use dedicated internal helpers rather than a mix of `_set_value_from_minimizer(...)` and public uncertainty assignment. -- Bayesian persistence now spans both CIF metadata and a binary sidecar, - so save/load code must validate consistency between the two. ## Layering and Ownership @@ -647,14 +313,8 @@ type-only import is acceptable. ## Deferred Work -This ADR now defines persistence for one canonical saved Bayesian run. - -It still defers: +This ADR defines the parameter-level posterior projection. It defers: -- support for multiple saved Bayesian runs per project -- plot-ready cache layers beyond canonical posterior and predictive data -- persistence-time compression or chunking strategies beyond the first - HDF5 sidecar implementation - persistence for future posterior-capable minimizers beyond DREAM - enabling currently unsupported single-crystal predictive draw plots @@ -665,18 +325,13 @@ It still defers: - 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 restored canonical +- `parameter.posterior` should always be rebuilt from analysis-level Bayesian data rather than serialized redundantly at parameter level. -- The sidecar reader and writer should be isolated behind explicit - serializer helpers instead of being implemented inline in - `Project.save()` and `Project.load()`. ## Chosen Defaults - `parameter.value` remains committed to the best posterior sample after posterior fits. -- If a project is loaded without full posterior arrays, restoring only +- If a project is loaded with only posterior summaries, restoring `parameter.posterior` is acceptable for table display and parameter inspection. -- Posterior plotting remains unavailable unless the canonical Bayesian - containers needed by those plots are also restored. diff --git a/docs/dev/adrs/suggestions/python-cif-category-correspondence.md b/docs/dev/adrs/suggestions/python-cif-category-correspondence.md index 6f970a0e2..191f18e59 100644 --- a/docs/dev/adrs/suggestions/python-cif-category-correspondence.md +++ b/docs/dev/adrs/suggestions/python-cif-category-correspondence.md @@ -24,13 +24,20 @@ 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.level -> project.cif: _verbosity.level +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 @@ -48,7 +55,7 @@ to objects reached from the current `Project` root, for example | 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` | not persisted | none | Runtime-only string property backed by `VerbosityEnum`; no `_verbosity` category. | +| `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`. | @@ -58,17 +65,17 @@ to objects reached from the current `Project` root, for example ### 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` | none | No | Runtime-only string convenience property; current code has no `project.verbosity.level` category and no `_verbosity.level` tag. | +| 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 @@ -218,18 +225,30 @@ 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. -Target project-level mappings if the current Python names are kept: +The accepted baseline is: + +```text +project.info. -> project.cif: _project. +``` -| 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.level` | `_verbosity.level` | Currently no persisted verbosity category. | +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`: @@ -253,15 +272,24 @@ 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.level`: the file -scope tells the reader this is project-level verbosity. +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 -user-facing `workspace.project.*` nesting and aligns with scientific -workflows where a project is the container for structures, experiments, -analysis, and saved files. +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 @@ -291,9 +319,9 @@ unless a separate ADR changes the underlying API pattern. - `_info.*` is less self-describing if copied out of `project.cif`. - Existing `_project.*` project files would need migration or a deliberate compatibility decision. -- If verbosity is persisted, `project.verbosity` would either need to - become a category object or remain as a convenience alias for a new - `project.verbosity.level` field. +- 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. @@ -302,13 +330,9 @@ unless a separate ADR changes the underlying API pattern. - Should the project identity remain `project.info.name`, or should it become `project.info.id` to mirror the saved identifier field? -- Should project metadata move from `_project.*` to `_info.*`, or is - `_project.*` clearer even inside `project.cif`? - 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.*` be accepted as a read-only legacy fallback when - loading older saved projects? - Should `project.verbosity = 'short'` remain as a convenience alias for - `project.verbosity.level = 'short'`, or should strict correspondence + `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 index 578d43a38..cd658aba9 100644 --- a/docs/dev/adrs/suggestions/undo-fit.md +++ b/docs/dev/adrs/suggestions/undo-fit.md @@ -1,57 +1,61 @@ # ADR: Undo Fit **Status:** Proposed -**Date:** 2026-05-13 +**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 new `_fit_parameter.start_value` and -`_fit_parameter.start_uncertainty` fields in `analysis/analysis.cif` -capture the last committed pre-fit scalar state for each fitted -parameter. This is useful when a minimization run produces a poor result -and the user wants to return to the state from immediately before the -fit. +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 need is especially important in command-line workflows, where the -user may save a project after a bad fit and reopen it later expecting a -simple way to roll back to the pre-fit state. +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. -However, these snapshots alone do not define undo semantics. The API -owner, rollback scope, and interaction with fit-derived metadata must be -explicit. +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`, for example: +The rollback operation belongs on `Analysis`: ```python project.analysis.undo_fit() ``` -`Analysis` owns the fit lifecycle, fit metadata, and persisted -`analysis.cif` state, so it is the correct owner for this operation. +`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 posterior clear +### 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 state that belongs only to the -discarded fit. - -It does not attempt to restore every possible runtime detail of the -previous fit result. +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` -- `parameter.posterior` is cleared - `analysis.fit_results` is cleared +- persisted fit-state summaries and Bayesian caches for the discarded + fit are cleared -This gives the user a safe, predictable return to the pre-fit visible -parameter state without pretending to restore a full historical result. +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. @@ -67,51 +71,20 @@ The initial undo operation does not revert: - fit mode - joint-fit weights -These are analysis configuration, not fit output. +These belong to analysis configuration, not fit output. ### 4. Undo is single-level for now -Only the latest saved pre-fit state is addressable. - -The initial API does not create a stack of historical fits. Supporting -multiple undo levels would require a dedicated snapshot history design -and is deferred. - -### 5. Persisted scalar snapshots are the rollback anchors - -The minimum persisted state required for clean cross-session undo is the -pair of `_fit_parameter.start_value` and -`_fit_parameter.start_uncertainty` defined in -`analysis-cif-fit-state.md`. - -If a parameter has no saved `start_value`, `undo_fit()` leaves that -parameter unchanged. - -If a parameter has no saved `start_uncertainty`, `undo_fit()` may clear -that parameter's uncertainty as a compatibility fallback for older saved -projects. - -### 6. Suggested user flow - -```python -project.analysis.fit() - -# Decide that the latest fit should be discarded. -project.analysis.undo_fit() - -# Save the recovered state if desired. -project.save() -``` +Only the latest saved pre-fit snapshot is addressable. Multi-level undo +and redo require a dedicated snapshot-history design and remain +deferred. -### 7. Add a top-level `undo-fit` CLI command +### 5. CLI exposure follows the project-first command style -Because command-line recovery is one of the main motivations for this -feature, undo must also be exposed through the existing top-level CLI. - -Suggested command: +The command-line surface should follow the current CLI style: ```bash -python -m easydiffraction undo-fit PROJECT_DIR +python -m easydiffraction PROJECT_DIR undo ``` This command should: @@ -119,44 +92,35 @@ 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 perform the rollback in memory without overwriting - project files -- emit a clear message describing whether the latest fit snapshot was - successfully discarded - -Suggested dry-run form: - -```bash -python -m easydiffraction undo-fit PROJECT_DIR --dry -``` +- support `--dry` to preview the rollback without overwriting files +- fail with a clear non-zero exit status when no usable undo snapshot is + available -If the project does not contain a usable undo snapshot, the command -should fail with a clear non-zero exit status instead of silently doing -nothing. +Compatibility aliases may remain if the CLI supports them, but the +project-first form is the canonical user-facing syntax. ## Consequences ### Positive -- Users gain a simple recovery path after a poor fit. -- The feature works naturally with saved projects and both Python and - command-line workflows. -- The initial scope stays small and does not require full historical fit - snapshots. +- 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 pre-fit scalar parameter state, not a full historical - `fit_results` object. -- Older saved projects that do not carry `start_uncertainty` may still - fall back to clearing uncertainty. +- 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 projections beyond the - scalar parameter snapshot -- multi-level undo / redo -- user-facing confirmation or preview APIs -- rollback of fit-type-specific persisted summaries beyond parameter - values +- 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/adrs/suggestions/workspace-root-project-category.md b/docs/dev/adrs/suggestions/workspace-root-project-category.md deleted file mode 100644 index a6c94cba6..000000000 --- a/docs/dev/adrs/suggestions/workspace-root-project-category.md +++ /dev/null @@ -1,380 +0,0 @@ -# ADR: Workspace Root and Project Information Category - -**Status:** Proposed -**Date:** 2026-05-17 - -## Context - -The current public root object is `Project`. It acts as the top-level -facade for an EasyDiffraction working session: - -```python -project = ed.Project(name='lbco_hrpt') -project.structures -project.experiments -project.analysis -project.display -project.summary -project.save() -``` - -The same word, "project", is also the natural CIF category name for -information about the scientific project: - -```cif -_project.id -_project.title -_project.description -_project.created -_project.last_modified -``` - -This creates a naming conflict. The root object is a broad runtime -facade, while the `_project.*` category is only information about the -scientific project. Using the same name for both makes category naming -awkward: - -```python -project.project_info.title -project.config.project_info.title -project.project.title -``` - -At the same time, replacing `_project.*` with a generic `_meta.*` -category would weaken the CIF model: - -```cif -_meta.project_id -_meta.project_title -``` - -The category name `meta` is too generic. It forces each item name to -repeat what the category should already communicate. The existing -`_project.id` and `_project.title` names are more semantic and better -aligned with the repository rule to follow CIF naming unless there is a -clearly better API. - -The design question is therefore: - -- should the top-level runtime object remain `Project`, and project - information move to a different category such as `meta`; -- or should the top-level runtime object be renamed to `Workspace`, so - `project` can be used cleanly for project information? - -## Decision - -Rename the top-level runtime facade from `Project` to `Workspace`. - -Use `project` as the public project-information category under the -workspace: - -```python -workspace = ed.Workspace(project_id='lbco_hrpt') -workspace.project.id -workspace.project.title = 'La0.5Ba0.5CoO3 at HRPT@PSI' -workspace.rendering.table_engine = 'rich' -workspace.verbosity = 'short' -workspace.structures -workspace.experiments -workspace.analysis -``` - -Persist workspace-level singleton categories in `workspace.cif`: - -```cif -_project.id -_project.title -_project.description -_project.created -_project.last_modified - -_rendering.chart_engine -_rendering.table_engine - -_verbosity.level -``` - -The saved directory is a workspace directory whose filesystem name is -chosen by the user. The canonical layout is: - -```text -/ -|-- workspace.cif -|-- structures/ -| `-- cosio.cif -|-- experiments/ -| `-- d20.cif -|-- analysis/ -| `-- analysis.cif -`-- summary/ - `-- summary.cif -``` - -Do not introduce `_meta.*` CIF tags. Do not use `project.cif`, -`config.cif`, or `meta.cif` as the primary singleton configuration file -in the target layout. - -The intended naming split is: - -```text -Workspace -|-- project # information about the scientific project -|-- rendering # rendering preferences -|-- verbosity # console/output verbosity preference -|-- structures # real structure datablocks -|-- experiments # real experiment datablocks -|-- analysis # analysis section -|-- display # display facade -`-- summary # summary/report facade -``` - -## Rationale - -### `Workspace` better describes the top-level facade - -The top-level object is more than project metadata. It owns active -collections, analysis state, display helpers, save/load behavior, and -runtime orchestration. `Workspace` describes that broader role without -consuming the domain word `project`. - -The name is also familiar in scientific software. It commonly means an -active analysis environment, a data/model container, or a working area. -That is close to the role of the current EasyDiffraction root object. - -### `project` is the right category name for project information - -Project information is not generic metadata. It is specifically the -identity, title, description, and timestamps of the scientific project. - -This reads cleanly: - -```python -workspace.project.title -``` - -and maps directly to clean CIF: - -```cif -_project.title -``` - -### `_meta.project_title` is weaker than `_project.title` - -The `_meta` category would make the CIF less domain-oriented. It also -creates longer and more repetitive item names: - -```cif -_meta.project_id -_meta.project_title -_meta.project_description -``` - -The existing `_project.*` tags are clearer: - -```cif -_project.id -_project.title -_project.description -``` - -### This keeps layer-specific consistency - -After this decision, each layer has a clear rule: - -| Layer | Rule | Example | -| --------------- | ------------------------------ | ------------------- | -| Runtime root | working-session facade | `Workspace` | -| Public category | semantic category name | `workspace.project` | -| CIF category | semantic CIF category | `_project.*` | -| Config file | workspace singleton categories | `workspace.cif` | - -This avoids one-off aliases such as `project.info` while preserving -semantic CIF names. - -### `workspace.cif` is clearer than `project.cif`, `config.cif`, or `meta.cif` - -The file stores singleton settings owned by the workspace: scientific -project information, rendering preferences, and verbosity. `project.cif` -overloads the project name again, while `config.cif` and `meta.cif` are -generic. `workspace.cif` names the owning layer and lets each category -inside the file keep its domain-specific name. - -## Consequences - -### Positive - -- The root object and project-information category no longer share the - same conceptual name. -- Public access becomes uniform: `workspace.project`, - `workspace.rendering`, `workspace.verbosity`, `workspace.analysis`. -- CIF stays semantic and does not introduce `_meta.*`. -- Workspace-level preferences such as rendering and verbosity are saved - with the workspace instead of being hidden runtime-only state. -- Project information can use short item names such as `id`, `title`, - and `description`. -- The top-level facade name better reflects active runtime - orchestration. - -### Negative - -- This is a breaking public API change. -- Tutorials, scripts, tests, docs, type hints, and imports must be - updated from `Project` to `Workspace`. -- Existing saved directories using `project.cif` must be migrated to - `workspace.cif` if no compatibility loader is kept. -- Existing code that expected verbosity to be runtime-only must account - for it round-tripping through `workspace.cif`. -- Users familiar with `Project` must learn the new root name. -- `Workspace` can be confused with a filesystem workspace in some - ecosystems, so documentation must define it clearly as the active - EasyDiffraction working object. - -## Compatibility Policy - -EasyDiffraction is in beta, and repository instructions say not to keep -legacy shims by default. - -Therefore the target implementation should not add a permanent -`Project = Workspace` alias unless explicitly approved before -implementation. - -The migration plan still includes a review gate before removing the old -`Project` public symbol, because this is a user-facing breaking change. - -## Alternatives Considered - -### Keep `Project` root and rename project information to `meta` - -Rejected. - -Example: - -```python -project.meta.project_title -``` - -```cif -_meta.project_title -``` - -This keeps the root class stable, but it weakens the category model. -`meta` is too broad, and the CIF item names become repetitive. - -### Keep `Project` root and use `project.config.project` - -Rejected for now. - -Example: - -```python -project.config.project.title -``` - -This is technically consistent, but it still repeats `project` at -different semantic layers. It also adds depth to common user workflows. -It is a reasonable fallback if the public root rename is rejected. - -### Keep `Project` root and use `project.info` - -Rejected for the target design. - -Example: - -```python -project.info.title -``` - -This is readable, but it preserves a special-case category alias. The -current goal is stronger consistency between public categories and CIF -category concepts. - -### Use `project.cif`, `config.cif`, or `meta.cif` for singleton settings - -Rejected for the target layout. - -`project.cif` repeats the overloaded term that this migration removes. -`config.cif` and `meta.cif` are too generic and do not say which layer -owns the settings. `workspace.cif` is more explicit while still allowing -semantic categories such as `_project`, `_rendering`, and `_verbosity` -inside the file. - -### Rename only internal files and keep public API unchanged - -Rejected for the target design. - -This improves implementation clarity but does not solve the public -naming inconsistency. - -### Use `Study` instead of `Workspace` - -Rejected. - -`Study` is a plausible scientific term, but it is less established for -an active computational container. It also does not map as naturally to -save/load, display, and analysis orchestration. - -## Implementation Notes - -The implementation should follow: - -```text -docs/dev/plans/workspace-root-project-category.md -``` - -The high-level migration is: - -1. Rename the root facade `Project` to `Workspace`. -2. Rename the project package/module surface from `project` to - `workspace`. -3. Rename `ProjectConfig` to `WorkspaceConfig`. -4. Rename project-level category access from `info` to `project`. -5. Rename project-information `name` access to `id`, matching - `_project.id`. -6. Move the storage path to `Workspace.path`, because it describes the - saved workspace directory rather than project information. -7. Keep the information category class named `ProjectInfo` unless a - later decision chooses `ProjectMetadata`. -8. Keep CIF tags `_project.*` and `_rendering.*`. -9. Rename saved singleton config file from `project.cif` to - `workspace.cif`. -10. Persist workspace verbosity in `workspace.cif` as - `_verbosity.level`, owned by a first-class `Verbosity` category - under `WorkspaceConfig` (parallel to `Rendering`). -11. Update code, tests, scripts, tutorials, docs, and ADR references. - -## Post-Implementation ADR Update - -This ADR must be updated after the migration plan is implemented. - -When implementation is complete: - -1. Change status from `Proposed` to `Accepted and implemented`. -2. Record the final public API and saved file layout. -3. Record whether a temporary or permanent `Project` compatibility alias - was approved. -4. Record any deviations from the migration plan. -5. Move this file from `docs/dev/adrs/suggestions/` to - `docs/dev/adrs/accepted/` if the decision is accepted. -6. Update `docs/dev/adrs/index.md` and related accepted ADRs if the ADR - map changes. -7. Update or close related items in `docs/dev/issues/open.md`. - -## Acceptance Criteria - -This ADR is satisfied when: - -- `ed.Workspace` is the public top-level facade. -- `ed.Project` is removed unless explicitly approved as an alias. -- the public project-information category is `workspace.project`. -- project identity is exposed as `workspace.project.id`. -- the saved directory path is exposed as `workspace.path`. -- the public rendering category is `workspace.rendering`. -- saved singleton configuration lives in `workspace.cif`. -- `workspace.cif` uses `_project.*`, `_rendering.*`, and - `_verbosity.level` tags. -- workspace verbosity is owned by a registered `Verbosity` category - alongside `Rendering`. -- `ProjectInfo.path` is removed; the saved directory path is exposed - only as `workspace.path`. -- no `_meta.*` tags are introduced for project information. -- tutorials and accepted ADRs use `Workspace`. 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/issues/open.md b/docs/dev/issues/open.md index 6c96f87ea..04d97ab85 100644 --- a/docs/dev/issues/open.md +++ b/docs/dev/issues/open.md @@ -1557,26 +1557,35 @@ threaded because each step's output is the next step's input. --- -## 95. 🟡 Re-Enable DREAM Multiprocessing in CLI Workflows +## 95. 🟡 Re-Enable DREAM Multiprocessing in Direct Python Scripts -**Type:** Performance / CLI robustness +**Type:** Performance / Script runtime -On macOS and other spawn-based platforms, running Bayesian scripts via -direct CLI entry points such as `python script.py` can fail during BUMPS +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 in terminal -workflows. +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 CLI 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 and for platforms where -`fork` is unavailable. Document the tradeoff clearly because `fork` on -macOS is less conservative than `spawn`. +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. @@ -1688,90 +1697,90 @@ sampler progress displays — any fix should keep their visuals consistent ## 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 | 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 CLI workflows | 🟡 Med | Performance / CLI robustness | -| 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 | +| # | 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 index 720505baa..677f5851d 100644 --- a/docs/dev/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,6 +79,33 @@ │ │ │ │ └── 🏷️ class Constraints │ │ │ └── 📄 factory.py │ │ │ └── 🏷️ class ConstraintsFactory +│ │ ├── 📁 deterministic_result +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ └── 🏷️ class DeterministicResult +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class DeterministicResultFactory +│ │ ├── 📁 fit_parameter_correlations +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ ├── 🏷️ class FitParameterCorrelationItem +│ │ │ │ └── 🏷️ class FitParameterCorrelations +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ 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 @@ -76,7 +151,9 @@ │ │ ├── 📄 base.py │ │ │ └── 🏷️ class MinimizerBase │ │ ├── 📄 bumps.py +│ │ │ ├── 🏷️ class _BumpsEvaluationLimitError │ │ │ ├── 🏷️ class _EasyDiffractionFitness +│ │ │ ├── 🏷️ class _BumpsProgressMonitor │ │ │ └── 🏷️ class BumpsMinimizer │ │ ├── 📄 bumps_amoeba.py │ │ │ └── 🏷️ class BumpsAmoebaMinimizer @@ -105,9 +182,13 @@ │ ├── 📄 __init__.py │ ├── 📄 analysis.py │ │ ├── 🏷️ class AnalysisDisplay +│ │ ├── 🏷️ class _AnalysisOwnerAccessorsMixin +│ │ ├── 🏷️ class _AnalysisPersistedCategoryAccessorsMixin │ │ └── 🏷️ class Analysis │ ├── 📄 enums.py -│ │ └── 🏷️ class FitModeEnum +│ │ ├── 🏷️ class FitModeEnum +│ │ ├── 🏷️ class FitResultKindEnum +│ │ └── 🏷️ class FitCorrelationSourceEnum │ ├── 📄 fitting.py │ │ └── 🏷️ class Fitter │ └── 📄 sequential.py @@ -159,10 +240,12 @@ │ ├── 🏷️ class GenericStringDescriptor │ ├── 🏷️ class GenericBoolDescriptor │ ├── 🏷️ class GenericNumericDescriptor +│ ├── 🏷️ class GenericIntegerDescriptor │ ├── 🏷️ class GenericParameter │ ├── 🏷️ class StringDescriptor │ ├── 🏷️ class BoolDescriptor │ ├── 🏷️ class NumericDescriptor +│ ├── 🏷️ class IntegerDescriptor │ └── 🏷️ class Parameter ├── 📁 crystallography │ ├── 📄 __init__.py @@ -424,7 +507,8 @@ │ │ ├── 📄 parse.py │ │ └── 📄 serialize.py │ ├── 📄 __init__.py -│ └── 📄 ascii.py +│ ├── 📄 ascii.py +│ └── 📄 results_sidecar.py ├── 📁 project │ ├── 📁 categories │ │ ├── 📁 info @@ -439,6 +523,12 @@ │ │ │ │ └── 🏷️ class Rendering │ │ │ └── 📄 factory.py │ │ │ └── 🏷️ class RenderingFactory +│ │ ├── 📁 verbosity +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ │ └── 🏷️ class Verbosity +│ │ │ └── 📄 factory.py +│ │ │ └── 🏷️ class VerbosityFactory │ │ └── 📄 __init__.py │ ├── 📄 __init__.py │ ├── 📄 display.py diff --git a/docs/dev/package-structure/short.md b/docs/dev/package-structure/short.md index da42f7a57..367a36a51 100644 --- a/docs/dev/package-structure/short.md +++ b/docs/dev/package-structure/short.md @@ -15,10 +15,55 @@ │ │ │ ├── 📄 __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 +│ │ ├── 📁 deterministic_result +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py +│ │ ├── 📁 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 @@ -205,7 +250,8 @@ │ │ ├── 📄 parse.py │ │ └── 📄 serialize.py │ ├── 📄 __init__.py -│ └── 📄 ascii.py +│ ├── 📄 ascii.py +│ └── 📄 results_sidecar.py ├── 📁 project │ ├── 📁 categories │ │ ├── 📁 info @@ -216,6 +262,10 @@ │ │ │ ├── 📄 __init__.py │ │ │ ├── 📄 default.py │ │ │ └── 📄 factory.py +│ │ ├── 📁 verbosity +│ │ │ ├── 📄 __init__.py +│ │ │ ├── 📄 default.py +│ │ │ └── 📄 factory.py │ │ └── 📄 __init__.py │ ├── 📄 __init__.py │ ├── 📄 display.py diff --git a/docs/dev/plans/loop-category-key-identity.md b/docs/dev/plans/loop-category-key-identity.md deleted file mode 100644 index 9917d1bae..000000000 --- a/docs/dev/plans/loop-category-key-identity.md +++ /dev/null @@ -1,620 +0,0 @@ -# Loop Category Key Identity Implementation Plan - -## Status - -Workflow instructions: - -```text -.github/copilot-instructions.md -``` - -Related ADR suggestion: - -```text -docs/dev/adrs/suggestions/loop-category-key-identity.md -``` - -This plan implements two related changes: - -1. Move category identity declarations from per-instance assignments to - class-level declarations. -2. Add an explicit persisted `id` field to the constraints loop. - -Status checklist. Mark `[x]` only while implementing: - -```text -Phase 1 - Implementation -[x] Add class-level identity declarations to CategoryItem. -[x] Teach Identity to resolve declared category entry names. -[x] Rebuild collection indexes after CIF loop loading. -[x] Add _category_code to all current CategoryItem subclasses. -[x] Add _category_entry_name to all current loop CategoryItem subclasses. -[x] Remove direct self._identity.category_code assignments. -[x] Remove direct self._identity.category_entry_name lambda assignments. -[x] Add Constraint.id descriptor serialized as _constraint.id. -[x] Change constraints collection keys from lhs_alias to id. -[x] Preserve default constraints.create(expression=...) behavior by using lhs_alias as the default id. -[x] Add backward-compatible loading for old CIF loops without _constraint.id. -[x] Update constraints display. -[x] Update loop-category-key-identity.md if implementation details differ from the ADR. -[ ] Phase 1 review gate: present diff for approval. - -Phase 2 - Verification -[x] Add tests for the base declarative identity behavior. -[x] Add parametrized tests for current loop category identity declarations. -[x] Update constraints tests. -[x] Update existing round-trip tests that compare constraints CIF. -[x] Run formatting. -[x] Run targeted unit tests. -[x] Run broader checks. -``` - -## Commit Discipline - -When an AI agent follows this plan, every completed Phase 1 -implementation step must be staged with explicit paths and committed -locally before moving to the next implementation step or to the Phase 1 -review gate. - -Follow the **Commits** section of `.github/copilot-instructions.md`. - -Rules: - -- One commit per implementation step. -- Keep each commit atomic and single-purpose. -- Stage explicit paths only. Do not use `git add .`. -- Do not stage unrelated user changes. -- Do not stage generated artifacts unless the user explicitly asks. -- If a serious uncovered design issue appears, stop and ask before - continuing. - -Suggested branch: - -```text -feature/loop-category-key-identity -``` - -Suggested commit messages: - -```text -Add declarative category identity resolution -Declare category identities on current items -Persist explicit constraint identifiers -Update ADR for declarative category identity -Add declarative category identity tests -``` - -## Goal - -Replace repeated constructor code like this: - -```python -self._identity.category_code = 'atom_site' -self._identity.category_entry_name = lambda: str(self.label.value) -``` - -with class-level declarations: - -```python -class AtomSite(CategoryItem): - _category_code = 'atom_site' - _category_entry_name = 'label' -``` - -The name `_category_entry_name` is intentionally kept because this is -the preferred project terminology. In this plan it means "the name of -the item attribute used to resolve the entry name". The resolved entry -value is still obtained through: - -```python -item._identity.category_entry_name -``` - -For example, `AtomSite._category_entry_name == 'label'`, while -`atom_site._identity.category_entry_name == 'Ba1'`. - -## Non-Goals - -Do not change these in this migration: - -- Do not rename `Identity.category_entry_name`. -- Do not rename `CategoryCollection`. -- Do not change public collection access syntax. -- Do not change CIF tags except adding `_constraint.id`. -- Do not rename `label` to `id` for atom sites or aliases. -- Do not make `phase_id` the row key for powder reflection loops. - -## Design - -This plan intentionally uses a narrow metadata lookup based on -`_category_entry_name`. That is an allowed exception to the general "no -string-based dispatch" rule because the attribute name is a class-level -declaration, not user input, and resolution is centralized in -`CategoryItem`. - -### CategoryItem Declarations - -Add these class attributes to `CategoryItem` in -`src/easydiffraction/core/category.py`: - -```python -class CategoryItem(GuardedBase): - _category_code: str | None = None - _category_entry_name: str | None = None -``` - -Update `CategoryItem.__init__()` so it assigns `_category_code` once: - -```python -def __init__(self) -> None: - super().__init__() - if self._category_code is not None: - self._identity.category_code = self._category_code -``` - -Do not try to resolve `_category_entry_name` in `CategoryItem.__init__`. -Many item classes create descriptors after `super().__init__()` returns, -and some mixin-based classes run `CategoryItem.__init__()` before their -descriptors are created. - -Add a resolver method to `CategoryItem`: - -```python -def _resolve_category_entry_name(self) -> str | None: - 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) -``` - -Import `GenericDescriptorBase` from `easydiffraction.core.variable` in -`category.py` if it is not already in scope. - -### Identity Resolution - -Update `Identity._resolve_up()` in -`src/easydiffraction/core/identity.py` so that category entries can be -resolved from the owning object before walking to the parent. - -Add this logic after checking direct callable/string values and before -climbing to the parent: - -```python -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 -``` - -Keep the existing `category_entry_name` setter. It remains useful as an -escape hatch and keeps old code compatible during migration. - -### Collection Index Rebuild After CIF Loading - -Update `category_collection_from_cif()` in -`src/easydiffraction/io/cif/serialize.py`. - -Currently `_adopt_items()` rebuilds the index before loop values are -loaded into each item. After declarative keys are resolved from -descriptor values, the index must be rebuilt after parameters are -loaded. - -After the row population loop, run any collection hook and rebuild: - -```python -after_from_cif = getattr(self, '_after_from_cif', None) -if callable(after_from_cif): - after_from_cif() - -self._rebuild_index() -``` - -The hook is needed for constraints to backfill missing ids from old CIF -files. - -## Category Migration Table - -Add `_category_code` to each listed class. Add `_category_entry_name` -only when the class is a loop item and currently has a collection key. -Then remove matching constructor assignments. - -| File | Class | `_category_code` | `_category_entry_name` | Notes | -| ----------------------------------------------------------------------------------- | -------------------------- | ------------------------ | ---------------------- | -------------------------------------------- | -| `src/easydiffraction/project/categories/info/default.py` | `ProjectInfo` | `project` | none | Singleton category. | -| `src/easydiffraction/project/categories/rendering/default.py` | `Rendering` | `rendering` | none | Singleton category. | -| `src/easydiffraction/analysis/categories/fitting/default.py` | `Fitting` | `fitting` | none | Singleton category. | -| `src/easydiffraction/analysis/categories/sequential_fit/default.py` | `SequentialFit` | `sequential_fit` | none | Singleton category. | -| `src/easydiffraction/analysis/categories/aliases/default.py` | `Alias` | `alias` | `label` | Loop key stays `_alias.label`. | -| `src/easydiffraction/analysis/categories/constraints/default.py` | `Constraint` | `constraint` | `id` | Add the id descriptor first. | -| `src/easydiffraction/analysis/categories/joint_fit/default.py` | `JointFitItem` | `joint_fit` | `experiment_id` | Loop key stays `_joint_fit.experiment_id`. | -| `src/easydiffraction/analysis/categories/sequential_fit_extract/default.py` | `SequentialFitExtractItem` | `sequential_fit_extract` | `id` | Loop key stays `_sequential_fit_extract.id`. | -| `src/easydiffraction/datablocks/structure/categories/cell/default.py` | `Cell` | `cell` | none | Singleton category. | -| `src/easydiffraction/datablocks/structure/categories/space_group/default.py` | `SpaceGroup` | `space_group` | none | Singleton category. | -| `src/easydiffraction/datablocks/structure/categories/atom_sites/default.py` | `AtomSite` | `atom_site` | `label` | Loop key stays `_atom_site.label`. | -| `src/easydiffraction/datablocks/structure/categories/atom_site_aniso/default.py` | `AtomSiteAniso` | `atom_site_aniso` | `label` | Loop key stays `_atom_site_aniso.label`. | -| `src/easydiffraction/datablocks/experiment/categories/experiment_type/default.py` | `ExperimentType` | `expt_type` | none | Singleton category. | -| `src/easydiffraction/datablocks/experiment/categories/calculation/default.py` | `Calculation` | `calculation` | none | Singleton category. | -| `src/easydiffraction/datablocks/experiment/categories/diffrn/default.py` | `DefaultDiffrn` | `diffrn` | none | Singleton category. | -| `src/easydiffraction/datablocks/experiment/categories/instrument/base.py` | `InstrumentBase` | `instrument` | none | Subclasses inherit this code. | -| `src/easydiffraction/datablocks/experiment/categories/peak/base.py` | `PeakBase` | `peak` | none | Subclasses inherit this code. | -| `src/easydiffraction/datablocks/experiment/categories/extinction/becker_coppens.py` | `BeckerCoppensExtinction` | `extinction` | none | Singleton category. | -| `src/easydiffraction/datablocks/experiment/categories/linked_crystal/default.py` | `LinkedCrystal` | `linked_crystal` | none | Singleton category. | -| `src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py` | `LinkedPhase` | `linked_phases` | `id` | Loop key stays `_pd_phase_block.id`. | -| `src/easydiffraction/datablocks/experiment/categories/background/line_segment.py` | `LineSegment` | `background` | `id` | Loop key stays `_pd_background.id`. | -| `src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py` | `PolynomialTerm` | `background` | `id` | Loop key stays `_pd_background.id`. | -| `src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py` | `ExcludedRegion` | `excluded_regions` | `id` | Loop key stays `_excluded_region.id`. | -| `src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py` | `Refln` | `refln` | `id` | Powder reflection rows inherit this key. | -| `src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py` | `PdCwlDataPoint` | `pd_data` | `point_id` | CWL powder data row. | -| `src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py` | `PdTofDataPoint` | `pd_data` | `point_id` | TOF powder data row. | -| `src/easydiffraction/datablocks/experiment/categories/data/total_pd.py` | `TotalDataPoint` | `total_data` | `point_id` | Total-scattering data row. | - -Do not add `_category_code` or `_category_entry_name` to -`PowderReflnBase`, `PowderCwlRefln`, or `PowderTofRefln`; they inherit -the `refln` identity declarations from `Refln`. - -## Constraints Migration - -### Target Shape - -`Constraint` should become: - -```python -class Constraint(CategoryItem): - _category_code = 'constraint' - _category_entry_name = 'id' - - def __init__(self) -> None: - super().__init__() - - self._id = StringDescriptor( - name='id', - description='Identifier for this constraint.', - value_spec=AttributeSpec( - default='_', - validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), - ), - cif_handler=CifHandler(names=['_constraint.id']), - ) - self._expression = StringDescriptor(...) -``` - -Define the public property: - -```python -@property -def id(self) -> StringDescriptor: - """Identifier for this constraint.""" - return self._id - -@id.setter -def id(self, value: str) -> None: - self._id.value = value -``` - -Keep `lhs_alias` and `rhs_expr` as derived read-only properties. - -### Create API - -Change `Constraints.create()` from: - -```python -def create(self, *, expression: str) -> None: -``` - -to: - -```python -def create(self, *, expression: str, id: str | None = None) -> None: -``` - -Implementation order: - -```python -item = Constraint() -item.expression = expression -item.id = id if id is not None else item.lhs_alias -self.add(item) -self._enabled = True -``` - -This preserves the current default user experience: - -```python -analysis.constraints.create(expression='biso_Ba = biso_La') -analysis.constraints['biso_Ba'] -``` - -It also allows explicit ids: - -```python -analysis.constraints.create( - id='constraint_1', - expression='biso_Ba = biso_La', -) -analysis.constraints['constraint_1'] -``` - -### Backward-Compatible CIF Loading - -Old CIF files only contain: - -```cif -loop_ -_constraint.expression -biso_Ba = biso_La -``` - -New CIF files should contain: - -```cif -loop_ -_constraint.id -_constraint.expression -biso_Ba "biso_Ba = biso_La" -``` - -Add a hook on `Constraints`: - -```python -def _after_from_cif(self) -> None: - for index, item in enumerate(self._items, start=1): - if item.id.value in {'', '_'}: - fallback = item.lhs_alias or f'constraint_{index}' - item.id = fallback -``` - -The generic `category_collection_from_cif()` hook described above must -call this before rebuilding the index. - -### Constraints Display - -Update `Constraints.show()` to include the id: - -```text -id | expression -``` - -Keep the existing empty warning behavior. - -## Required Code Searches - -After migration, these searches should return no category-item -constructor assignments: - -```shell -git grep -n -E "self\\._identity\\.category_code =" -- src/easydiffraction -git grep -n -E "self\\._identity\\.category_entry_name =" -- src/easydiffraction -git grep -n -E "category_entry_name = lambda" -- src/easydiffraction -``` - -The following reads are expected to remain: - -```shell -git grep -n "_identity\\.category_entry_name" -- src/easydiffraction -``` - -Those reads are used by collections, display, reporting, and parameter -unique names. - -Do not use `rg` in this plan; it is not available in every contributor -environment. Use `git grep` for repository searches. - -## Tests To Add Or Update - -### Base Identity Tests - -Add or update tests under `tests/unit/easydiffraction/core/`. - -Test a fake category item: - -```python -class FakeItem(CategoryItem): - _category_code = 'fake' - _category_entry_name = 'id' - - def __init__(self): - super().__init__() - self._id = StringDescriptor(...) - - @property - def id(self): - return self._id -``` - -Assert: - -```python -item._identity.category_code == 'fake' -item._identity.category_entry_name == item.id.value -item.id.unique_name.endswith('.fake..id') -``` - -Also test that a singleton-like item with `_category_code` and no -`_category_entry_name` resolves category code but returns no entry name. - -### Current Category Parametrized Tests - -Add a parametrized test that instantiates representative loop item -classes and verifies category code plus entry: - -```text -Alias -> alias, label -JointFitItem -> joint_fit, experiment_id -SequentialFitExtractItem -> sequential_fit_extract, id -AtomSite -> atom_site, label -AtomSiteAniso -> atom_site_aniso, label -LinkedPhase -> linked_phases, id -LineSegment -> background, id -PolynomialTerm -> background, id -ExcludedRegion -> excluded_regions, id -Refln -> refln, id -PdCwlDataPoint -> pd_data, point_id -PdTofDataPoint -> pd_data, point_id -TotalDataPoint -> total_data, point_id -``` - -Do not instantiate heavy calculator-backed owner objects for this test; -instantiate item classes directly. - -### Constraints Tests - -Update -`tests/unit/easydiffraction/analysis/categories/test_constraints.py`. - -Required assertions: - -```python -c = Constraint() -c.expression = 'a = b + c' -c.id = 'constraint_a' -assert c.id.value == 'constraint_a' -assert c.lhs_alias == 'a' -assert c.rhs_expr == 'b + c' -assert c._identity.category_entry_name == 'constraint_a' -``` - -Default id behavior: - -```python -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' -``` - -Explicit id behavior: - -```python -coll = Constraints() -coll.create(id='c1', expression='a = b + c') -assert 'c1' in coll.names -assert coll['c1'].lhs_alias == 'a' -``` - -CIF serialization: - -```python -cif = coll.as_cif -assert '_constraint.id' in cif -assert '_constraint.expression' in cif -``` - -Backward-compatible CIF loading: - -```python -cif = ''' -data_analysis - -loop_ -_constraint.expression -"a = b + c" -''' -``` - -Load through the existing analysis/constraints loader and assert the -loaded collection has key `a`. - -### Existing Round-Trip Tests - -Update tests that compare constraint CIF or constraint collection keys: - -- `tests/unit/easydiffraction/project/test_project_load.py` -- `tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py` -- `tests/integration/fitting/test_project_load.py` - -Expected changes: - -- New saved analysis CIF includes `_constraint.id`. -- Old expression-only fixtures, if any, still load. -- Existing constraint workflows still work when no explicit id is - supplied. - -## Verification Commands - -Run the smallest useful checks first: - -```shell -pixi run python -m pytest tests/unit/easydiffraction/analysis/categories/test_constraints.py -pixi run python -m pytest tests/unit/easydiffraction/core/ -pixi run python -m pytest tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py -``` - -Then run broader checks: - -```shell -pixi run unit-tests -pixi run integration-tests -pixi run check -``` - -If the modified-file Prettier helper is still missing, format changed -Markdown directly: - -```shell -npx prettier --write --config=prettierrc.toml docs/dev/plans/loop-category-key-identity.md docs/dev/adrs/suggestions/loop-category-key-identity.md -``` - -## Local Availability Check - -The plan was checked against the current repository state: - -- `.github/copilot-instructions.md` exists. -- `src/easydiffraction/core/category.py`, - `src/easydiffraction/core/identity.py`, and - `src/easydiffraction/io/cif/serialize.py` exist. -- `tests/unit/easydiffraction/analysis/categories/test_constraints.py` - exists. -- `tests/unit/easydiffraction/core/` exists. -- `tests/unit/easydiffraction/io/cif/test_serialize_category_owner_baseline.py` - exists. -- `pixi.toml` defines `fix`, `check`, `unit-tests`, `integration-tests`, - and `test-structure-check`. -- `prettierrc.toml` and local Prettier are available. -- `tools/nonpy_prettier_modified.py` is not present, so use the direct - `npx prettier --write --config=prettierrc.toml ...` fallback for - touched Markdown files. - -## Acceptance Criteria - -The implementation is done when all of these are true: - -- All current category codes are declared as `_category_code` on item - classes. -- All current loop collection keys are declared as - `_category_entry_name` on item classes. -- No current category item sets `self._identity.category_code` in its - constructor. -- No current category item sets `self._identity.category_entry_name` in - its constructor. -- `item._identity.category_entry_name` still works for all loop rows. -- Descriptor `unique_name` values still include datablock, category, - entry, and descriptor name. -- Constraints persist `_constraint.id`. -- Constraints created without an explicit id still default to the left - hand alias. -- Old constraints CIF without `_constraint.id` still loads and receives - deterministic ids. -- Collection indexes are correct after CIF loading. - -## Suggested Pull Request - -Title: - -```text -Use declarative category identity metadata -``` - -Description: - -```text -This change makes category and loop-row identities easier to audit and -keeps saved CIF identifiers explicit. Constraint rows gain a stable -identifier while existing constraint expressions continue to work. -``` diff --git a/docs/dev/plans/workspace-root-project-category.md b/docs/dev/plans/workspace-root-project-category.md deleted file mode 100644 index 778fd7584..000000000 --- a/docs/dev/plans/workspace-root-project-category.md +++ /dev/null @@ -1,1229 +0,0 @@ -# Workspace Root and Project Category Migration Plan - -## Status - -Branch: `feature/workspace-root-project-category` - -ADR suggestion: - -```text -docs/dev/adrs/suggestions/workspace-root-project-category.md -``` - -Two-phase workflow from `.github/copilot-instructions.md`: - -- Phase 1 - Implementation. Code and docs updates only. Do not create or - run tests unless the user explicitly asks. -- Phase 2 - Verification. Add/update tests, then run the verification - commands listed near the end of this plan. - -Stop after Phase 1 and request review before starting Phase 2. - -Status checklist. Mark `[x]` only while implementing: - -```text -Phase 1 - Implementation -[ ] Phase 0: Confirm breaking-change approval. -[ ] Phase 1: Rename root package and public facade to Workspace. -[ ] Phase 2: Rename project-info access from info to project. -[ ] Phase 3: Align project information fields with _project.* tags. -[ ] Phase 4: Rename project config file to workspace.cif. -[ ] Phase 5: Update root-object references across runtime code. -[ ] Phase 6: Update docs, tutorials, and ADR references. -[ ] Phase 7: Remove old public Project surface unless approved. -[ ] Phase 1 review gate: present diff for approval. - -Phase 2 - Verification -[ ] Move/update unit tests to workspace paths. -[ ] Add workspace naming and CIF layout tests. -[ ] pixi run test-structure-check -[ ] pixi run fix -[ ] pixi run check -[ ] pixi run unit-tests -[ ] pixi run integration-tests -[ ] pixi run script-tests -[ ] pixi run notebook-prepare -[ ] pixi run notebook-tests -``` - -## Commit Discipline - -When an AI agent follows this plan, every completed Phase 1 -implementation step must be staged with explicit paths and committed -locally before moving to the next implementation step or to the Phase 1 -review gate. - -Follow the **Commits** section of `.github/copilot-instructions.md`. - -Rules: - -- One commit per phase. -- Keep each commit atomic and single-purpose. -- Stage explicit paths only. Do not use `git add .`. -- Use `git mv` for file and directory moves. -- Do not stage unrelated user changes. -- Do not stage generated artifacts unless the user explicitly asks. -- If a serious uncovered design issue appears, stop and ask before - continuing. - -Suggested commit messages: - -```text -Rename Project facade to Workspace -Expose project information as workspace.project -Align project metadata fields with CIF names -Rename project config file to workspace.cif -Update runtime references to Workspace -Update docs for Workspace root API -Remove old Project public API surface -``` - -## Goal - -Split the name "project" into two distinct concepts: - -1. `Workspace` - the top-level runtime facade, currently named - `Project`. -2. `workspace.project` - the category that stores information about the - scientific project. - -Target public API: - -```python -import easydiffraction as ed - -workspace = ed.Workspace(project_id='lbco_hrpt') -workspace.project.id -workspace.project.title = 'La0.5Ba0.5CoO3 at HRPT@PSI' -workspace.rendering.table_engine = 'rich' -workspace.verbosity = 'short' -workspace.structures -workspace.experiments -workspace.analysis -workspace.display -workspace.summary -workspace.save_as('lbco_hrpt') -``` - -Target workspace-level config file: - -```text -workspace.cif -``` - -Target CIF tags inside `workspace.cif`: - -```cif -_project.id -_project.title -_project.description -_project.created -_project.last_modified - -_rendering.chart_engine -_rendering.table_engine - -_verbosity.level -``` - -Do not introduce `_meta.*` tags. - -Target saved layout: - -```text -/ -|-- workspace.cif -|-- structures/ -| `-- cosio.cif -|-- experiments/ -| `-- d20.cif -|-- analysis/ -| `-- analysis.cif -`-- summary/ - `-- summary.cif -``` - -## Decisions Already Made - -Use these decisions unless the user explicitly changes the ADR before -implementation: - -- The public root class becomes `Workspace`. -- The public root import becomes - `from easydiffraction import Workspace`. -- The public project-information category becomes `workspace.project`. -- The public rendering category remains `workspace.rendering`. -- The project-information category keeps semantic CIF tags `_project.*`. -- The rendering category keeps semantic CIF tags `_rendering.*`. -- The verbosity preference is serialized as `_verbosity.level`. -- The saved singleton config file becomes `workspace.cif`. -- The saved root is a workspace directory with a user-chosen filesystem - name; do not use `project` as the conceptual root name in new docs. -- Do not use `project.cif`, `config.cif`, or `meta.cif` as the primary - singleton config file in the target layout. -- The storage directory path belongs to `Workspace.path`, not - `workspace.project.path`. -- The old `Project` public API is removed unless the user explicitly - approves an alias before implementation. -- The category class name `ProjectInfo` is kept; only the public - attribute name (`info` → `project`) changes. -- `ProjectInfo.path` is removed; the saved directory path lives on - `Workspace.path` only. -- A first-class `Verbosity` category under `WorkspaceConfig` is required - (not optional) so that `_verbosity.level` round-trips through - `workspace.cif` like other singleton categories. -- Source-tree imports may be temporarily inconsistent between phases - (for example, Phase 1 leaves `project_config_to_cif` imports until - Phase 4 renames the serializer functions). This is acceptable because - tests are not run in Phase 1. Each phase must still leave the source - importable at the end of the phase. - -## Current Shape - -The current implementation already has a category-owner based project -configuration layer: - -```text -src/easydiffraction/project/ -|-- project.py # class Project -|-- project_config.py # class ProjectConfig -|-- project_info.py # ProjectInfo export -|-- display.py # class ProjectDisplay -`-- categories/ - |-- info/ # ProjectInfo category - `-- rendering/ # Rendering category -``` - -Current public API: - -```python -project = ed.Project(name='my_project') -project.info.title -project.rendering.table_engine -``` - -Current saved config file: - -```text -project.cif -``` - -## Target Shape - -Target implementation: - -```text -src/easydiffraction/workspace/ -|-- workspace.py # class Workspace -|-- workspace_config.py # class WorkspaceConfig -|-- project_info.py # ProjectInfo export -|-- display.py # class WorkspaceDisplay -`-- categories/ - |-- project/ # ProjectInfo category - |-- rendering/ # Rendering category - `-- verbosity/ # Verbosity category -``` - -Target public API: - -```python -workspace = ed.Workspace(project_id='my_project') -workspace.project.title -workspace.rendering.table_engine -workspace.verbosity = 'short' -``` - -## Out Of Scope - -Do not do these in this migration: - -- Do not add `_meta.*` CIF tags. -- Do not use `project.cif`, `config.cif`, or `meta.cif` as the target - singleton settings file. -- Do not redesign structure or experiment datablocks. -- Do not change analysis fit-mode semantics. -- Do not change calculator behavior. -- Do not edit generated package-structure docs by hand. -- Do not edit generated notebooks directly. Edit tutorial `.py` sources - and regenerate notebooks during Phase 2. -- Do not keep a `Project = Workspace` compatibility alias unless the - user explicitly approves it. - -## Phase 0: Confirm Breaking-Change Approval - -This migration removes or replaces the public `Project` API unless the -user approves a compatibility alias. - -Before changing code, ask the user to confirm: - -```text -This migration removes ed.Project and replaces it with ed.Workspace. -Should implementation proceed without a Project compatibility alias? -``` - -If the user asks for a compatibility alias, record that decision in this -plan and in the ADR before implementation. - -Do not implement code before this approval gate. - -Commit: no commit required for this phase unless the plan or ADR is -updated. - -## Phase 1: Rename Root Package And Public Facade - -### Objective - -Rename the top-level runtime facade and package from `project` to -`workspace`. - -### Files Likely To Change - -- `src/easydiffraction/project/` -- `src/easydiffraction/__init__.py` -- `src/easydiffraction/__main__.py` -- `src/easydiffraction/analysis/analysis.py` -- `src/easydiffraction/analysis/sequential.py` -- `src/easydiffraction/display/plotting.py` -- `src/easydiffraction/summary/summary.py` -- any source file importing `easydiffraction.project.*` - -### Steps - -1. Move the source package with `git mv` so history is preserved: - - ```text - src/easydiffraction/project/ - -> src/easydiffraction/workspace/ - ``` - -2. Rename files: - - ```text - workspace/project.py - -> workspace/workspace.py - - workspace/project_config.py - -> workspace/workspace_config.py - ``` - -3. Rename classes: - - ```text - Project -> Workspace - ProjectConfig -> WorkspaceConfig - ProjectDisplay -> WorkspaceDisplay - ``` - -4. Rename class-level state inside the facade: - - ```text - Project._current_project -> Workspace._current_workspace - Project._loading -> Workspace._loading # name kept - Project.current_project_path() -> Workspace.current_workspace_path() - ``` - - Update the `ClassVar` annotation accordingly and update all internal - references (`type(self)._current_project = self`, - `cls._current_project`). - -5. Update the `varname()` fallback inside `__init__` so the default - variable name becomes `'workspace'` instead of `'project'`: - - ```python - self._varname = 'workspace' if type(self)._loading else varname() - ``` - -6. Update top-level import: - - ```python - from easydiffraction.workspace.workspace import Workspace - ``` - -7. Remove the old top-level `Project` import unless the user approved an - alias. - -8. Update type-checking imports: - - ```python - from easydiffraction.workspace.workspace import Workspace - ``` - -9. Update docstrings from "Project facade" to "Workspace facade" where - they describe the root object. Keep wording that talks about the - scientific project (titles, descriptions, identity) unchanged. - -10. Rename `io/ascii.py::extract_project_from_zip` to - `extract_workspace_from_zip` and update its re-exports in - `src/easydiffraction/io/__init__.py` and - `src/easydiffraction/__init__.py`. The function extracts a saved - workspace directory, not scientific project information. - -11. Note: `src/easydiffraction/io/cif/serialize.py` still defines - `project_config_to_cif`, `project_config_from_cif`, and - `project_to_cif` at this point. Leave those imports as-is in - `workspace.py`; they are renamed in Phase 4. The function - `project_info_to_cif` keeps its name. - -12. Run a source-only grep. Do not run tests in Phase 1: - -```shell -rg -n "easydiffraction\\.project|\\bProject\\b|ProjectDisplay|ProjectConfig" src -``` - -For every match, decide whether it refers to: - -- the old root object, which should become `Workspace`; -- the project-information category, which should remain project; -- historical text that should be updated in docs later. - -### Stop Conditions - -Stop and ask if: - -- another public class named `Workspace` already exists; -- package moves break imports in a way that would require compatibility - shims; -- a file has both root-object `project` and category `project` meanings - that cannot be separated clearly. - -### Commit - -Stage explicit moved and edited files, then commit: - -```text -Rename Project facade to Workspace -``` - -## Phase 2: Rename Project-Info Access From `info` To `project` - -### Objective - -Make the project-information category public as `workspace.project` -instead of `workspace.info`. - -### Files Likely To Change - -- `src/easydiffraction/workspace/workspace_config.py` -- `src/easydiffraction/workspace/workspace.py` -- `src/easydiffraction/workspace/categories/info/` -- `src/easydiffraction/workspace/project_info.py` -- `src/easydiffraction/io/cif/serialize.py` -- all code using `.info` for project information - -### Steps - -1. Rename category package: - - ```text - src/easydiffraction/workspace/categories/info/ - -> src/easydiffraction/workspace/categories/project/ - ``` - -2. Keep the category class name `ProjectInfo` for now. The class name is - explicit and avoids a confusing `Project` class after the root class - is renamed to `Workspace`. - -3. Rename imports: - - ```python - from easydiffraction.workspace.categories.project import ProjectInfo - from easydiffraction.workspace.categories.project import ProjectInfoFactory - ``` - -4. In `WorkspaceConfig`, rename: - - ```text - _info -> _project - info -> project - ``` - -5. In `Workspace`, rename: - - ```text - _info -> _project - info -> project - ``` - -6. Remove the public `.info` property unless the user approved a - compatibility alias. - -7. Update all runtime references: - - ```text - workspace.info.title -> workspace.project.title - workspace.info.description -> workspace.project.description - workspace.info.update_last_modified() -> workspace.project.update_last_modified() - ``` - -8. Run grep: - - ```shell - rg -n "\\.info\\b|categories/info|categories\\.info" src - ``` - -9. For every match, update it if it refers to project information. Leave - unrelated uses of the word "info" alone. - -### Stop Conditions - -Stop and ask if: - -- `info` appears as a different public concept unrelated to project - information; -- removing `.info` would break a user-requested compatibility alias. - -### Commit - -```text -Expose project information as workspace.project -``` - -## Phase 3: Align Project Information Fields With `_project.*` - -### Objective - -Expose project identity as `workspace.project.id`, matching CIF -`_project.id`. - -Move the saved directory path to `workspace.path`, because it describes -the workspace location and is not serialized project information. - -### Files Likely To Change - -- `src/easydiffraction/workspace/categories/project/default.py` -- `src/easydiffraction/workspace/workspace.py` -- `src/easydiffraction/io/cif/serialize.py` -- `src/easydiffraction/summary/summary.py` -- `src/easydiffraction/display/plotting.py` -- any code using `.name` for project identity or `.project.path` - -### Steps - -1. In `ProjectInfo`, rename the public identity property and its setter: - - ```text - name (getter) -> id (getter) - name (setter) -> id (setter) - ``` - - Do not also rename the internal descriptor attribute - `self._project_id`; it already matches the new public name. - -2. Keep the underlying CIF tag unchanged: - - ```python - CifHandler(names=['_project.id']) - ``` - -3. Update `ProjectInfo.unique_name` to return `self.id`. - -4. Keep `ProjectInfo._identity.category_code = 'project'` as-is. - -5. Update `project_info_to_cif()` and CIF loading helpers to use - `info.id`. - -6. Rename constructor arguments: - - ```text - name -> project_id - ``` - - Apply this to: - - `Workspace.__init__` - - `WorkspaceConfig.__init__` - - `ProjectInfo.__init__` - - `ProjectInfoFactory.create(...)` call sites - - Default value: `'untitled_project'` (unchanged value, just the - parameter name changes) - -7. Add `Workspace.path` as the runtime storage path. Initialize - `self._path: pathlib.Path | None = None` in `Workspace.__init__`. - - Suggested shape (match the surrounding `GuardedBase` pattern; do not - add `@typechecked` here because the setter accepts both `str` and - `pathlib.Path`): - - ```python - @property - def path(self) -> pathlib.Path | None: - """Saved workspace directory.""" - return self._path - - @path.setter - def path(self, value: object) -> None: - self._path = pathlib.Path(value) if value is not None else None - ``` - -8. Remove `ProjectInfo.path` (property, setter, and the `self._path` - attribute inside `ProjectInfo.__init__`) unless explicitly approved - as a compatibility alias. - -9. Update save/load logic across the codebase: - - ```text - workspace.path - ``` - - should replace: - - ```text - project.info.path # old - workspace.project.path # never used; do not introduce - ``` - - Concrete call sites include `Workspace.save`, `Workspace.load`, - `Workspace.current_workspace_path`, and any consumers in `analysis/`, - `display/`, `summary/`, and `io/`. - -10. Update messages and string representations: - -```text -Workspace '' (...) -Saving workspace '' to ... -``` - -11. Run grep: - -```shell -rg -n "\\.name\\b|\\.path\\b|project_id|Project identifier" src/easydiffraction/workspace src/easydiffraction/io src/easydiffraction/display src/easydiffraction/summary -``` - -Inspect each match manually. Do not blindly replace every `.name`; -structures and experiments still use `.name`. - -### Stop Conditions - -Stop and ask if: - -- a caller depends on `workspace.name` as a root-object property; -- moving `path` out of `ProjectInfo` makes save/load unclear; -- external saved fixtures require an approved compatibility path. - -### Commit - -```text -Align project metadata fields with CIF names -``` - -## Phase 4: Rename Project Config File To `workspace.cif` - -### Objective - -Rename the saved singleton configuration file from `project.cif` to -`workspace.cif`. - -### Files Likely To Change - -- `src/easydiffraction/workspace/workspace.py` -- `src/easydiffraction/io/cif/serialize.py` -- `src/easydiffraction/workspace/workspace_config.py` -- `src/easydiffraction/workspace/categories/verbosity/` -- CLI entry points in `src/easydiffraction/__main__.py` -- docs that describe saved project directories -- test fixtures in Phase 2 - -### Steps - -1. Rename serializer functions in - `src/easydiffraction/io/cif/serialize.py` and every call site: - - ```text - project_config_to_cif -> workspace_config_to_cif - project_config_from_cif -> workspace_config_from_cif - project_to_cif -> workspace_to_cif - ``` - - Call sites include `workspace.py` (formerly `project.py`), - `workspace_config.py`, and the serializer itself (the - `project_to_cif` body calls `project_config_to_cif`). After this step - the imports that were intentionally left stale in Phase 1 must - compile cleanly. - - Do not rename `project_info_to_cif`; it serializes the `_project` - category and that name remains correct. - -2. Update `Workspace.save()` to write: - - ```text - workspace.cif - ``` - -3. Update `Workspace.load()` to read: - - ```text - workspace.cif - ``` - -4. Do not add `project.cif`, `config.cif`, or `meta.cif` fallbacks - unless the user approved a compatibility loader. - -5. Add a `Verbosity` category under - `src/easydiffraction/workspace/categories/verbosity/` following the - same shape as `rendering/` (a `default.py` with a `Verbosity` - `CategoryItem`, a `factory.py` with `VerbosityFactory`, and an - `__init__.py` that imports both to trigger registration). - - The category owns one descriptor: - - ```python - CifHandler(names=['_verbosity.level']) - ``` - - Bind it in `WorkspaceConfig.__init__` next to `_rendering`, and - expose it from `Workspace` so that the public access path remains: - - ```python - workspace.verbosity = 'short' - workspace.verbosity # -> 'short' - ``` - - The public `verbosity` getter/setter on `Workspace` reads and writes - the category's `level` descriptor (validated against `VerbosityEnum`) - and replaces the current `self._verbosity: VerbosityEnum` - runtime-only attribute. Remove that attribute. - - Serialize it as: - - ```cif - _verbosity.level short - ``` - -6. Keep the contents semantic: - - ```cif - _project.id - _project.title - _rendering.table_engine - _verbosity.level - ``` - -7. Update logging and console output from `project.cif` to - `workspace.cif`. - -8. Run grep: - - ```shell - rg -n "project\\.cif|config\\.cif|meta\\.cif|project_config_to_cif|project_config_from_cif|project_to_cif|verbosity" src docs tests - ``` - - Update source and docs only. Test files are handled in Phase 2 unless - the user explicitly asks otherwise. - -9. Saved on-disk fixtures under `data/` and `projects/` still contain - `project.cif` files (for example `data/lbco_project/project.cif`, - `projects/cosio/project.cif`). Do **not** edit or regenerate them in - Phase 1. They are inputs to integration/script tests and will either - be regenerated in Phase 2 or the relevant tests will be updated to - write fresh workspace directories. Flag any that block Phase 2 in the - review gate. - -### Stop Conditions - -Stop and ask if: - -- repository fixtures or tutorials contain saved directories that must - remain loadable without conversion; -- the user wants a one-release compatibility loader. -- the verbosity setting cannot be represented as a category without - weakening the public `workspace.verbosity` API. - -### Commit - -```text -Rename project config file to workspace.cif -``` - -## Phase 5: Update Root-Object References Across Runtime Code - -### Objective - -Replace root-object variables and attributes named `project` with -`workspace` where they refer to the top-level facade. - -Keep the word `project` where it refers to the project-information -category or the scientific project itself. - -### Files Likely To Change - -- `src/easydiffraction/analysis/analysis.py` -- `src/easydiffraction/analysis/sequential.py` -- `src/easydiffraction/display/plotting.py` -- `src/easydiffraction/project/display.py` after it has moved to - `workspace/display.py` -- `src/easydiffraction/summary/summary.py` -- `src/easydiffraction/__main__.py` -- `src/easydiffraction/io/*` - -### Steps - -1. Rename root references in `Analysis`: - - ```text - self.project -> self.workspace - analysis.project -> analysis.workspace - ``` - - This includes the constructor argument, the stored attribute, and any - public read-only property exposing the parent workspace. - -2. Rename display internals in `WorkspaceDisplay` (formerly - `ProjectDisplay`) and in any other class that stores a back- - reference to the root facade: - - ```text - self._project -> self._workspace - _set_project(...) -> _set_workspace(...) - ``` - - Only do this when the stored object is the top-level runtime facade. - Do not touch `self._project_id` inside `ProjectInfo` or `_project.*` - CIF tags. - -3. Rename local variables in runtime code: - - ```text - project = Workspace(...) - -> workspace = Workspace(...) - ``` - -4. Keep scientific-project wording where appropriate: - - ```text - workspace.project.title - project_id - _project.id - ``` - -5. Update user-facing messages carefully. Good examples: - - ```text - "Workspace directory not found" - "Saving workspace" - "Project title" - ``` - -6. Run grep: - - ```shell - rg -n "\\bproject\\b|\\bProject\\b|_project|ProjectDisplay" src/easydiffraction - ``` - -7. Inspect each match. Do not replace `_project` CIF tags. - -### Stop Conditions - -Stop and ask if: - -- a name has both root-workspace and project-information meanings in the - same function and cannot be made clear; -- renaming a method such as `_set_project` would require updating public - plugin or user code. - -### Commit - -```text -Update runtime references to Workspace -``` - -## Phase 6: Update Docs, Tutorials, And ADR References - -### Objective - -Update user-facing and developer-facing documentation to describe the -new root object and project-information category. - -### Files Likely To Change - -- `docs/dev/adrs/index.md` -- `docs/dev/issues/open.md` -- `docs/dev/adrs/accepted/*.md` -- `docs/dev/adrs/suggestions/*.md` -- `docs/docs/tutorials/*.py` -- `README.md` -- `CONTRIBUTING.md` only if it contains API examples - -Do not edit these by hand: - -- `docs/dev/package-structure/full.md` -- `docs/dev/package-structure/short.md` -- generated tutorial notebooks -- generated `docs/site/` files - -### Steps - -1. Update the relevant accepted ADRs: - - ```text - Project Facade and Persistence Layout - -> Workspace Facade and Persistence Layout - ``` - -2. Update the affected ADR examples to use: - - ```text - workspace.project ProjectInfo - workspace.rendering Rendering - workspace.verbosity str - workspace.display WorkspaceDisplay - ``` - -3. Update saved layout examples: - - ```text - workspace.cif - structures/ - experiments/ - analysis/ - summary.cif - ``` - -4. Update public examples: - - ```python - workspace = ed.Workspace(project_id='lbco_hrpt') - workspace.project.title = '...' - ``` - -5. Update ADRs that describe current API. Historical reasoning can keep - old names only if it is clearly historical and not presented as - current usage. - -6. Update tutorial `.py` files, not notebooks. Phase 2 will run - `pixi run notebook-prepare`. - -7. Run grep: - - ```shell - rg -n "ed\\.Project|from easydiffraction import Project|project\\.info|project\\.rendering|ProjectDisplay|project\\.cif" docs README.md CONTRIBUTING.md - ``` - -8. Inspect each match manually. - -### Stop Conditions - -Stop and ask if: - -- a tutorial title uses "Project" as ordinary English rather than API - naming; -- historical ADRs would become misleading if edited mechanically. - -### Commit - -```text -Update docs for Workspace root API -``` - -## Phase 7: Remove Old Public `Project` Surface Unless Approved - -### Objective - -Finish the breaking rename by removing old public imports and module -paths unless the user approved compatibility. - -### Steps Without Compatibility Alias - -1. Ensure top-level `easydiffraction.__init__` exports `Workspace`, not - `Project`. - -2. Ensure no source imports from: - - ```text - easydiffraction.project - ``` - -3. Ensure no public source package remains at: - - ```text - src/easydiffraction/project - ``` - -4. Run grep: - - ```shell - rg -n "from easydiffraction import Project|ed\\.Project|easydiffraction\\.project|\\bProject\\(" src docs tests tools README.md CONTRIBUTING.md - ``` - -5. Any remaining match must be: - - historical text that intentionally names the old API; or - - a test that will be updated in Phase 2; or - - a generated artifact that should not be edited manually. - -### Steps With Approved Compatibility Alias - -Only do this if the user explicitly approved it. - -1. Add a temporary alias in `src/easydiffraction/__init__.py`: - - ```python - Project = Workspace - ``` - -2. Keep the alias undocumented unless the user asks for a migration - note. - -3. Add tests in Phase 2 proving both `Workspace` and `Project` construct - the same root object. - -### Commit - -Without alias: - -```text -Remove old Project public API surface -``` - -With alias: - -```text -Add Project alias for Workspace migration -``` - -## Phase 1 Review Gate - -After Phase 1 commits are complete: - -1. Run `git status --short`. -2. Confirm only intended files are changed. -3. Summarize: - - whether `Project` was removed or aliased; - - whether `workspace.cif` replaced `project.cif`; - - any files intentionally left for Phase 2 test updates; - - any unresolved questions. - -4. Stop and ask the user to review before starting Phase 2. - -Do not run the full verification suite until the user approves moving to -Phase 2. - -## Phase 2: Verification And Tests - -Only start this phase after the user approves the Phase 1 -implementation. - -### Test Updates - -Move or update tests to mirror the new source tree: - -```text -tests/unit/easydiffraction/project/ --> tests/unit/easydiffraction/workspace/ -``` - -Update imports: - -```python -from easydiffraction.workspace.workspace import Workspace -from easydiffraction.workspace.display import WorkspaceDisplay -``` - -Update functional and integration tests: - -```python -from easydiffraction import Workspace -workspace = Workspace(project_id='...') -``` - -### New Tests To Add - -Add focused tests for: - -1. `from easydiffraction import Workspace`. -2. `Workspace(project_id='p1').project.id == 'p1'`. -3. `workspace.project.title` round-trips through `workspace.cif`. -4. `workspace.rendering.table_engine` round-trips through - `workspace.cif`. -5. `workspace.verbosity` round-trips through `workspace.cif`. -6. `Workspace.save()` writes `workspace.cif`. -7. `Workspace.load()` reads `workspace.cif`. -8. `workspace.cif` contains `_project.id`, not `_meta.project_id`. -9. `workspace.cif` contains `_rendering.table_engine`. -10. `workspace.cif` contains `_verbosity.level`. -11. `workspace.path` is set after `save_as()` and `load()`. -12. `workspace.project` has no serialized path field. -13. `project.cif`, `config.cif`, and `meta.cif` are not written unless - compatibility was approved. -14. `ed.Project` is absent unless compatibility was approved. -15. `Verbosity` category is registered via its factory and reachable - through `WorkspaceConfig` (parallel to `Rendering`). -16. `ed.extract_workspace_from_zip` is importable and - `ed.extract_project_from_zip` is not (unless compatibility - approved). - -If compatibility alias was approved, add tests for: - -1. `from easydiffraction import Project`. -2. `Project is Workspace` or equivalent behavior. -3. Any approved `project.cif` fallback behavior. - -### Verification Commands - -Run in this order: - -```shell -pixi run test-structure-check -pixi run fix -pixi run check -pixi run unit-tests -pixi run integration-tests -pixi run script-tests -pixi run notebook-prepare -pixi run notebook-tests -``` - -If `pixi run fix` regenerates package-structure docs, accept those -generated changes and do not hand-edit them. - -### Phase 2 Commit Suggestions - -Use one or more commits, depending on size: - -```text -Update workspace unit tests -Update tutorials for Workspace API -Regenerate tutorial notebooks for Workspace API -``` - -## Grep Checklist - -Use this checklist before final review. - -Runtime root object should use `Workspace`: - -```shell -rg -n "\\bProject\\b|ed\\.Project|from easydiffraction import Project" src tests docs tools README.md CONTRIBUTING.md -``` - -Project-information category should use `workspace.project`: - -```shell -rg -n "\\.info\\b|workspace\\.project|project\\.info" src tests docs tools README.md CONTRIBUTING.md -``` - -CIF project category should stay `_project`: - -```shell -rg -n "_meta\\.|_project\\." src tests docs tools README.md CONTRIBUTING.md -``` - -Saved config file should be `workspace.cif`: - -```shell -rg -n "project\\.cif|config\\.cif|meta\\.cif|workspace\\.cif" src tests docs tools README.md CONTRIBUTING.md -``` - -Workspace verbosity should serialize as a workspace-level category: - -```shell -rg -n "_verbosity|verbosity" src tests docs tools README.md CONTRIBUTING.md -``` - -Generated docs should not be manually edited: - -```shell -git diff -- docs/site docs/dev/package-structure/full.md docs/dev/package-structure/short.md -``` - -If package-structure docs changed because of `pixi run fix`, that is -expected. If `docs/site` changed, ask before staging. - -## Common Mistakes - -### Mistake: Renaming `_project.*` To `_workspace.*` - -Do not do this. The CIF category describes scientific project -information, not the runtime facade. - -Correct: - -```cif -_project.id -_project.title -``` - -Incorrect: - -```cif -_workspace.project_id -_workspace.title -``` - -### Mistake: Introducing `_meta.*` - -Do not replace `_project.*` with `_meta.*`. - -Correct: - -```cif -_project.title -``` - -Incorrect: - -```cif -_meta.project_title -``` - -### Mistake: Keeping `project.cif` Or Switching To Generic File Names - -Do not use `project.cif`, `config.cif`, or `meta.cif` as the target -singleton settings file. The file belongs to the workspace layer. - -Correct: - -```text -workspace.cif -``` - -### Mistake: Blindly Replacing Every `project` - -Some uses of `project` should remain: - -- CIF tags such as `_project.id` -- `workspace.project` -- scientific project wording in prose -- `ProjectInfo` class name, unless a later ADR changes it - -Only root-facade uses should become `workspace` or `Workspace`. - -### Mistake: Leaving Path On Project Information - -The saved directory path belongs to the workspace runtime state. It -should be `workspace.path`, not `workspace.project.path`. - -### Mistake: Forgetting Facade Class-Level State - -When renaming `Project` to `Workspace`, the `ClassVar` -`_current_project`, the `current_project_path()` classmethod, and the -`varname()` fallback string `'project'` all live on the class itself and -are easy to miss with a single search-and-replace. Rename them to -`_current_workspace`, `current_workspace_path()`, and `'workspace'` -respectively. - -### Mistake: Renaming `ProjectInfo._project_id` - -The internal descriptor attribute `self._project_id` inside -`ProjectInfo` already matches the new public name `id` and stays as is. -Only the public `name` property/setter becomes `id`. - -### Mistake: Editing Generated Notebooks Directly - -Tutorial notebooks are generated artifacts. Edit tutorial `.py` files, -then run `pixi run notebook-prepare` in Phase 2. - -## Suggested Pull Request - -Title: - -```text -Rename Project root object to Workspace -``` - -Description: - -```text -This change separates the working EasyDiffraction workspace from the -scientific project information stored inside it. Users now create a -Workspace, while project title and description live under -workspace.project and continue to serialize with clear _project.* CIF -names. -``` 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/quick-reference/index.md b/docs/docs/quick-reference/index.md index c035d3fe3..9c4d715bd 100644 --- a/docs/docs/quick-reference/index.md +++ b/docs/docs/quick-reference/index.md @@ -39,10 +39,15 @@ ed.show_version() 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 @@ -398,8 +403,16 @@ project = ed.Project.load('lbco_hrpt') Run a saved project from the command line: ```bash -python -m easydiffraction fit lbco_hrpt -python -m easydiffraction fit lbco_hrpt --dry +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 @@ -407,8 +420,11 @@ python -m easydiffraction fit lbco_hrpt --dry ```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 fit PROJECT_DIR +python -m easydiffraction PROJECT_DIR fit +python -m easydiffraction PROJECT_DIR display ``` diff --git a/docs/docs/tutorials/ed-13.ipynb b/docs/docs/tutorials/ed-13.ipynb index 616c536c5..ca636d82b 100644 --- a/docs/docs/tutorials/ed-13.ipynb +++ b/docs/docs/tutorials/ed-13.ipynb @@ -2657,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-15.ipynb b/docs/docs/tutorials/ed-15.ipynb index b6d82a319..e79bdd876 100644 --- a/docs/docs/tutorials/ed-15.ipynb +++ b/docs/docs/tutorials/ed-15.ipynb @@ -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,7 +268,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -268,7 +279,7 @@ { "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,7 +299,7 @@ { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "28", "metadata": {}, "outputs": [], "source": [ @@ -297,7 +308,7 @@ }, { "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,7 +351,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -350,7 +361,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -360,7 +371,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -370,7 +381,7 @@ { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "36", "metadata": {}, "outputs": [], "source": [ @@ -380,7 +391,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -390,7 +401,7 @@ { "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 7359b712a..03dc35fa7 100644 --- a/docs/docs/tutorials/ed-15.py +++ b/docs/docs/tutorials/ed-15.py @@ -79,6 +79,10 @@ # %% 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 # in the input CIF files, are refined by default. diff --git a/docs/docs/tutorials/ed-17.ipynb b/docs/docs/tutorials/ed-17.ipynb index f32b0371f..e471d99a7 100644 --- a/docs/docs/tutorials/ed-17.ipynb +++ b/docs/docs/tutorials/ed-17.ipynb @@ -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('projects/cosio', 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')" ] }, { @@ -427,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" ] }, { @@ -488,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", ")" ] }, @@ -513,7 +516,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.constraints.create(expression='biso_Co2 = biso_Co1')" + "analysis.constraints.create(expression='biso_Co2 = biso_Co1')" ] }, { @@ -531,7 +534,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fitting.minimizer_type = 'bumps (lm)'" + "analysis.fitting.minimizer_type = 'bumps (lm)'" ] }, { @@ -554,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" ] @@ -568,16 +581,16 @@ { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "46", "metadata": {}, "outputs": [], "source": [ - "project.display.fit.correlations()" + "display.fit.correlations()" ] }, { "cell_type": "markdown", - "id": "46", + "id": "47", "metadata": {}, "source": [ "#### Compare measured and calculated patterns for the first fit." @@ -586,16 +599,16 @@ { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "48", "metadata": {}, "outputs": [], "source": [ - "project.display.pattern(expt_name='d20')" + "display.pattern(expt_name='d20')" ] }, { "cell_type": "markdown", - "id": "48", + "id": "49", "metadata": {}, "source": [ "#### Run Sequential Fitting\n", @@ -607,7 +620,7 @@ { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "50", "metadata": {}, "outputs": [], "source": [ @@ -616,7 +629,7 @@ }, { "cell_type": "markdown", - "id": "50", + "id": "51", "metadata": { "lines_to_next_cell": 2 }, @@ -629,7 +642,7 @@ { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "52", "metadata": {}, "outputs": [], "source": [ @@ -639,11 +652,11 @@ { "cell_type": "code", "execution_count": null, - "id": "52", + "id": "53", "metadata": {}, "outputs": [], "source": [ - "project.analysis.sequential_fit_extract.create(\n", + "analysis.sequential_fit_extract.create(\n", " id='temperature',\n", " target=temperature,\n", " pattern=r'^TEMP\\s+([0-9.]+)',\n", @@ -653,7 +666,7 @@ }, { "cell_type": "markdown", - "id": "53", + "id": "54", "metadata": {}, "source": [ "Set the sequential fitting parameters." @@ -662,19 +675,19 @@ { "cell_type": "code", "execution_count": null, - "id": "54", + "id": "55", "metadata": {}, "outputs": [], "source": [ - "project.analysis.fitting_mode_type = 'sequential'\n", - "project.analysis.sequential_fit.data_dir = scan_data_dir\n", - "project.analysis.sequential_fit.max_workers = 'auto'\n", - "project.analysis.sequential_fit.reverse = True" + "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": "55", + "id": "56", "metadata": {}, "source": [ "Run the sequential fit over all data files in the scan directory." @@ -683,16 +696,16 @@ { "cell_type": "code", "execution_count": null, - "id": "56", + "id": "57", "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit()" + "analysis.fit()" ] }, { "cell_type": "markdown", - "id": "57", + "id": "58", "metadata": {}, "source": [ "#### Replay a Dataset\n", @@ -703,17 +716,17 @@ { "cell_type": "code", "execution_count": null, - "id": "58", + "id": "59", "metadata": {}, "outputs": [], "source": [ "project.apply_params_from_csv(row_index=0)\n", - "project.display.pattern(expt_name='d20')" + "display.pattern(expt_name='d20')" ] }, { "cell_type": "markdown", - "id": "59", + "id": "60", "metadata": {}, "source": [ "\n", @@ -723,17 +736,17 @@ { "cell_type": "code", "execution_count": null, - "id": "60", + "id": "61", "metadata": {}, "outputs": [], "source": [ "project.apply_params_from_csv(row_index=-1)\n", - "project.display.pattern(expt_name='d20')" + "display.pattern(expt_name='d20')" ] }, { "cell_type": "markdown", - "id": "61", + "id": "62", "metadata": {}, "source": [ "#### Plot Parameter Evolution\n", @@ -743,7 +756,27 @@ }, { "cell_type": "markdown", - "id": "62", + "id": "63", + "metadata": {}, + "source": [ + "Plot fit quality metrics vs. temperature." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64", + "metadata": {}, + "outputs": [], + "source": [ + "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": "65", "metadata": {}, "source": [ "Plot unit cell parameters vs. temperature." @@ -752,18 +785,18 @@ { "cell_type": "code", "execution_count": null, - "id": "63", + "id": "66", "metadata": {}, "outputs": [], "source": [ - "project.display.fit.series(structure.cell.length_a, versus=temperature)\n", - "project.display.fit.series(structure.cell.length_b, versus=temperature)\n", - "project.display.fit.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": "64", + "id": "67", "metadata": {}, "source": [ "Plot isotropic displacement parameters vs. temperature." @@ -772,35 +805,20 @@ { "cell_type": "code", "execution_count": null, - "id": "65", + "id": "68", "metadata": {}, "outputs": [], "source": [ - "project.display.fit.series(\n", - " structure.atom_sites['Co1'].adp_iso,\n", - " versus=temperature,\n", - ")\n", - "project.display.fit.series(\n", - " structure.atom_sites['Si'].adp_iso,\n", - " versus=temperature,\n", - ")\n", - "project.display.fit.series(\n", - " structure.atom_sites['O1'].adp_iso,\n", - " versus=temperature,\n", - ")\n", - "project.display.fit.series(\n", - " structure.atom_sites['O2'].adp_iso,\n", - " versus=temperature,\n", - ")\n", - "project.display.fit.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": "66", + "id": "69", "metadata": {}, "source": [ "Plot selected fractional coordinates vs. temperature." @@ -809,30 +827,15 @@ { "cell_type": "code", "execution_count": null, - "id": "67", + "id": "70", "metadata": {}, "outputs": [], "source": [ - "project.display.fit.series(\n", - " structure.atom_sites['Co2'].fract_x,\n", - " versus=temperature,\n", - ")\n", - "project.display.fit.series(\n", - " structure.atom_sites['Co2'].fract_z,\n", - " versus=temperature,\n", - ")\n", - "project.display.fit.series(\n", - " structure.atom_sites['O1'].fract_z,\n", - " versus=temperature,\n", - ")\n", - "project.display.fit.series(\n", - " structure.atom_sites['O2'].fract_z,\n", - " versus=temperature,\n", - ")\n", - "project.display.fit.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 24e648c56..93f691fb7 100644 --- a/docs/docs/tutorials/ed-17.py +++ b/docs/docs/tutorials/ed-17.py @@ -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(dir_path='projects/cosio', 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,7 +128,7 @@ # #### 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 @@ -205,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 @@ -247,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.fitting.minimizer_type = 'bumps (lm)' +analysis.fitting.minimizer_type = 'bumps (lm)' # %% [markdown] # #### Run Single Fitting @@ -277,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.fit.correlations() +display.fit.correlations() # %% [markdown] # #### Compare measured and calculated patterns for the first fit. # %% -project.display.pattern(expt_name='d20') +display.pattern(expt_name='d20') # %% [markdown] # #### Run Sequential Fitting @@ -310,7 +316,7 @@ temperature = 'diffrn.ambient_temperature' # %% -project.analysis.sequential_fit_extract.create( +analysis.sequential_fit_extract.create( id='temperature', target=temperature, pattern=r'^TEMP\s+([0-9.]+)', @@ -321,16 +327,16 @@ # Set the sequential fitting parameters. # %% -project.analysis.fitting_mode_type = 'sequential' -project.analysis.sequential_fit.data_dir = scan_data_dir -project.analysis.sequential_fit.max_workers = 'auto' -project.analysis.sequential_fit.reverse = True +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() +analysis.fit() # %% [markdown] # #### Replay a Dataset @@ -339,7 +345,7 @@ # %% project.apply_params_from_csv(row_index=0) -project.display.pattern(expt_name='d20') +display.pattern(expt_name='d20') # %% [markdown] # @@ -347,67 +353,45 @@ # %% project.apply_params_from_csv(row_index=-1) -project.display.pattern(expt_name='d20') +display.pattern(expt_name='d20') # %% [markdown] # #### Plot Parameter Evolution # # Reuse the extracted diffrn path as the x-axis in the following plots. +# %% [markdown] +# Plot fit quality metrics vs. 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.fit.series(structure.cell.length_a, versus=temperature) -project.display.fit.series(structure.cell.length_b, versus=temperature) -project.display.fit.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.fit.series( - structure.atom_sites['Co1'].adp_iso, - versus=temperature, -) -project.display.fit.series( - structure.atom_sites['Si'].adp_iso, - versus=temperature, -) -project.display.fit.series( - structure.atom_sites['O1'].adp_iso, - versus=temperature, -) -project.display.fit.series( - structure.atom_sites['O2'].adp_iso, - versus=temperature, -) -project.display.fit.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.fit.series( - structure.atom_sites['Co2'].fract_x, - versus=temperature, -) -project.display.fit.series( - structure.atom_sites['Co2'].fract_z, - versus=temperature, -) -project.display.fit.series( - structure.atom_sites['O1'].fract_z, - versus=temperature, -) -project.display.fit.series( - structure.atom_sites['O2'].fract_z, - versus=temperature, -) -project.display.fit.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 c1200010a..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,7 +121,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -150,7 +131,7 @@ { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "12", "metadata": {}, "outputs": [], "source": [ @@ -160,7 +141,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "13", "metadata": {}, "outputs": [], "source": [ diff --git a/docs/docs/tutorials/ed-18.py b/docs/docs/tutorials/ed-18.py index 7c4fff9fc..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 diff --git a/docs/docs/tutorials/ed-2.ipynb b/docs/docs/tutorials/ed-2.ipynb index e3338eec8..1212cbcc7 100644 --- a/docs/docs/tutorials/ed-2.ipynb +++ b/docs/docs/tutorials/ed-2.ipynb @@ -519,6 +519,24 @@ "source": [ "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')" + ] } ], "metadata": { diff --git a/docs/docs/tutorials/ed-2.py b/docs/docs/tutorials/ed-2.py index a8e9e3d22..220ebae9c 100644 --- a/docs/docs/tutorials/ed-2.py +++ b/docs/docs/tutorials/ed-2.py @@ -227,3 +227,9 @@ # %% 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 688fc593a..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')" ] }, { diff --git a/docs/docs/tutorials/ed-21.ipynb b/docs/docs/tutorials/ed-21.ipynb index 4905c6f09..689c3d57a 100644 --- a/docs/docs/tutorials/ed-21.ipynb +++ b/docs/docs/tutorials/ed-21.ipynb @@ -74,7 +74,10 @@ "\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." + "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." ] }, { @@ -88,9 +91,19 @@ ] }, { - "cell_type": "markdown", + "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", @@ -102,7 +115,7 @@ { "cell_type": "code", "execution_count": null, - "id": "7", + "id": "8", "metadata": {}, "outputs": [], "source": [ @@ -112,7 +125,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -122,7 +135,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "10", "metadata": {}, "outputs": [], "source": [ @@ -133,7 +146,7 @@ { "cell_type": "code", "execution_count": null, - "id": "10", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -142,7 +155,7 @@ }, { "cell_type": "markdown", - "id": "11", + "id": "12", "metadata": {}, "source": [ "The atom-site definitions below form the starting structural model. The\n", @@ -153,7 +166,7 @@ { "cell_type": "code", "execution_count": null, - "id": "12", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -203,7 +216,7 @@ }, { "cell_type": "markdown", - "id": "13", + "id": "14", "metadata": {}, "source": [ "## Step 3: Define the Diffraction Experiment\n", @@ -215,7 +228,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "15", "metadata": {}, "source": [ "Download the measured data from the repository. Alternatively, you\n", @@ -226,7 +239,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "16", "metadata": {}, "outputs": [], "source": [ @@ -235,7 +248,7 @@ }, { "cell_type": "markdown", - "id": "16", + "id": "17", "metadata": {}, "source": [ "Create the experiment object and specify the sample form, beam mode,\n", @@ -245,7 +258,7 @@ { "cell_type": "code", "execution_count": null, - "id": "17", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -261,7 +274,7 @@ { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "19", "metadata": {}, "outputs": [], "source": [ @@ -270,7 +283,7 @@ }, { "cell_type": "markdown", - "id": "19", + "id": "20", "metadata": {}, "source": [ "Link the structural phase to the experiment." @@ -279,7 +292,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -288,7 +301,7 @@ }, { "cell_type": "markdown", - "id": "21", + "id": "22", "metadata": {}, "source": [ "Set instrument and peak profile parameters.\n", @@ -300,7 +313,7 @@ { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -311,7 +324,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "24", "metadata": {}, "outputs": [], "source": [ @@ -323,7 +336,7 @@ }, { "cell_type": "markdown", - "id": "24", + "id": "25", "metadata": {}, "source": [ "Add background points and excluded regions.\n", @@ -335,7 +348,7 @@ { "cell_type": "code", "execution_count": null, - "id": "25", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -348,7 +361,7 @@ { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "27", "metadata": {}, "outputs": [], "source": [ @@ -358,7 +371,7 @@ }, { "cell_type": "markdown", - "id": "27", + "id": "28", "metadata": {}, "source": [ "## Step 4: Run an Initial Local Refinement\n", @@ -378,7 +391,7 @@ { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "29", "metadata": {}, "outputs": [], "source": [ @@ -388,7 +401,7 @@ { "cell_type": "code", "execution_count": null, - "id": "29", + "id": "30", "metadata": {}, "outputs": [], "source": [ @@ -400,7 +413,7 @@ }, { "cell_type": "markdown", - "id": "30", + "id": "31", "metadata": {}, "source": [ "We choose the BUMPS Levenberg-Marquardt minimizer as a fast local\n", @@ -411,7 +424,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "32", "metadata": {}, "outputs": [], "source": [ @@ -421,7 +434,7 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -431,7 +444,7 @@ { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "34", "metadata": {}, "outputs": [], "source": [ @@ -441,7 +454,7 @@ { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "35", "metadata": {}, "outputs": [], "source": [ @@ -450,7 +463,7 @@ }, { "cell_type": "markdown", - "id": "35", + "id": "36", "metadata": {}, "source": [ "The correlation plot shows how strongly the fitted parameters move\n", @@ -462,7 +475,7 @@ { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -472,7 +485,7 @@ { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -481,7 +494,7 @@ }, { "cell_type": "markdown", - "id": "38", + "id": "39", "metadata": {}, "source": [ "## Step 5: Prepare for Bayesian Sampling\n", @@ -503,7 +516,7 @@ { "cell_type": "code", "execution_count": null, - "id": "39", + "id": "40", "metadata": {}, "outputs": [], "source": [ @@ -512,7 +525,7 @@ }, { "cell_type": "markdown", - "id": "40", + "id": "41", "metadata": {}, "source": [ "Set fit bounds for all free parameters using the default multiplier of\n", @@ -524,7 +537,7 @@ { "cell_type": "code", "execution_count": null, - "id": "41", + "id": "42", "metadata": {}, "outputs": [], "source": [ @@ -534,7 +547,7 @@ }, { "cell_type": "markdown", - "id": "42", + "id": "43", "metadata": {}, "source": [ "Displaying the free parameters again is a convenient way to confirm\n", @@ -545,7 +558,7 @@ { "cell_type": "code", "execution_count": null, - "id": "43", + "id": "44", "metadata": {}, "outputs": [], "source": [ @@ -554,7 +567,7 @@ }, { "cell_type": "markdown", - "id": "44", + "id": "45", "metadata": {}, "source": [ "## Step 6: Configure and Run DREAM\n", @@ -580,7 +593,7 @@ { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -590,7 +603,7 @@ { "cell_type": "code", "execution_count": null, - "id": "46", + "id": "47", "metadata": {}, "outputs": [], "source": [ @@ -600,17 +613,18 @@ { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "48", "metadata": {}, "outputs": [], "source": [ - "project.analysis.fitting.minimizer.steps = 300 # lower than the default 3000" + "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": "48", + "id": "49", "metadata": {}, "outputs": [], "source": [ @@ -619,7 +633,7 @@ }, { "cell_type": "markdown", - "id": "49", + "id": "50", "metadata": {}, "source": [ "## Step 7: Inspect Bayesian Results\n", @@ -632,7 +646,7 @@ { "cell_type": "code", "execution_count": null, - "id": "50", + "id": "51", "metadata": {}, "outputs": [], "source": [ @@ -641,7 +655,7 @@ }, { "cell_type": "markdown", - "id": "51", + "id": "52", "metadata": {}, "source": [ "The correlation and posterior-pair plots are complementary:\n", @@ -658,7 +672,7 @@ { "cell_type": "code", "execution_count": null, - "id": "52", + "id": "53", "metadata": {}, "outputs": [], "source": [ @@ -668,7 +682,7 @@ { "cell_type": "code", "execution_count": null, - "id": "53", + "id": "54", "metadata": {}, "outputs": [], "source": [ @@ -677,7 +691,7 @@ }, { "cell_type": "markdown", - "id": "54", + "id": "55", "metadata": {}, "source": [ "The one-dimensional posterior distributions below make it easier to\n", @@ -688,7 +702,7 @@ { "cell_type": "code", "execution_count": null, - "id": "55", + "id": "56", "metadata": {}, "outputs": [], "source": [ @@ -697,7 +711,7 @@ }, { "cell_type": "markdown", - "id": "56", + "id": "57", "metadata": {}, "source": [ "Finally, the posterior predictive plot propagates the sampled parameter\n", @@ -709,7 +723,7 @@ { "cell_type": "code", "execution_count": null, - "id": "57", + "id": "58", "metadata": {}, "outputs": [], "source": [ @@ -718,7 +732,7 @@ }, { "cell_type": "markdown", - "id": "58", + "id": "59", "metadata": {}, "source": [ "A final zoomed measured-vs-calculated plot is useful for checking how\n", @@ -729,7 +743,7 @@ { "cell_type": "code", "execution_count": null, - "id": "59", + "id": "60", "metadata": {}, "outputs": [], "source": [ diff --git a/docs/docs/tutorials/ed-21.py b/docs/docs/tutorials/ed-21.py index 87f003e46..df32bcabe 100644 --- a/docs/docs/tutorials/ed-21.py +++ b/docs/docs/tutorials/ed-21.py @@ -33,10 +33,16 @@ # 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 # @@ -291,7 +297,8 @@ project.analysis.fitting.minimizer_type = 'bumps (dream)' # %% -project.analysis.fitting.minimizer.steps = 300 # lower than the default 3000 +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() diff --git a/docs/docs/tutorials/ed-22.ipynb b/docs/docs/tutorials/ed-22.ipynb index bacb7c777..a8d511132 100644 --- a/docs/docs/tutorials/ed-22.ipynb +++ b/docs/docs/tutorials/ed-22.ipynb @@ -482,7 +482,8 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.fitting.minimizer.steps = 500 # lower than the default 3000" + "project.analysis.fitting.minimizer.steps = 100 # lower than the default 3000\n", + "project.analysis.fitting.minimizer.burn = 20 # lower than the default 600" ] }, { diff --git a/docs/docs/tutorials/ed-22.py b/docs/docs/tutorials/ed-22.py index da43c6d52..cbc08f74e 100644 --- a/docs/docs/tutorials/ed-22.py +++ b/docs/docs/tutorials/ed-22.py @@ -219,7 +219,8 @@ project.analysis.fitting.minimizer_type = 'bumps (dream)' # %% -project.analysis.fitting.minimizer.steps = 500 # lower than the default 3000 +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() diff --git a/docs/docs/tutorials/ed-23.ipynb b/docs/docs/tutorials/ed-23.ipynb index d20045c6c..11a28c791 100644 --- a/docs/docs/tutorials/ed-23.ipynb +++ b/docs/docs/tutorials/ed-23.ipynb @@ -56,10 +56,11 @@ "id": "4", "metadata": {}, "source": [ - "## Download Saved Project Archive\n", + "## Download Saved Project\n", "\n", - "The archive should contain a saved project directory with a partially\n", - "completed sequential fit, including `analysis/results.csv`." + "The returned path points directly to the saved project directory with\n", + "a partially completed sequential fit, including\n", + "`analysis/results.csv`." ] }, { @@ -69,34 +70,13 @@ "metadata": {}, "outputs": [], "source": [ - "zip_path = ed.download_data(id=34, destination='data')" + "project_dir = ed.download_data(id=37, destination='projects')" ] }, { "cell_type": "markdown", "id": "6", "metadata": {}, - "source": [ - "## Extract Project\n", - "\n", - "Extract the saved project directory locally. For a project you\n", - "already have on disk, set `project_dir` directly instead." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7", - "metadata": {}, - "outputs": [], - "source": [ - "project_dir = ed.extract_project_from_zip(zip_path, destination='projects')" - ] - }, - { - "cell_type": "markdown", - "id": "8", - "metadata": {}, "source": [ "## Load Saved Project" ] @@ -104,7 +84,7 @@ { "cell_type": "code", "execution_count": null, - "id": "9", + "id": "7", "metadata": {}, "outputs": [], "source": [ @@ -113,7 +93,7 @@ }, { "cell_type": "markdown", - "id": "10", + "id": "8", "metadata": {}, "source": [ "## Resume Sequential Analysis\n", @@ -127,7 +107,7 @@ { "cell_type": "code", "execution_count": null, - "id": "11", + "id": "9", "metadata": {}, "outputs": [], "source": [ @@ -136,7 +116,7 @@ }, { "cell_type": "markdown", - "id": "12", + "id": "10", "metadata": {}, "source": [ "## Replay Fitted Datasets\n", @@ -147,7 +127,7 @@ { "cell_type": "code", "execution_count": null, - "id": "13", + "id": "11", "metadata": {}, "outputs": [], "source": [ @@ -157,7 +137,7 @@ }, { "cell_type": "markdown", - "id": "14", + "id": "12", "metadata": {}, "source": [ "\n", @@ -167,7 +147,7 @@ { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "13", "metadata": {}, "outputs": [], "source": [ @@ -177,55 +157,70 @@ }, { "cell_type": "markdown", - "id": "16", + "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. Omitting `param` plots every fitted parameter one\n", - "after another." + "for the x-axis." ] }, { "cell_type": "code", "execution_count": null, - "id": "17", + "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": "18", + "id": "17", "metadata": {}, "outputs": [], "source": [ - "project.display.fit.series(versus=temperature)" + "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": "19", + "id": "18", "metadata": {}, "source": [ - "## Save Project\n", - "\n", - "Save the updated project so the appended `analysis/results.csv` and\n", - "refreshed summary files remain on disk." + "Omitting `param` plots every fitted parameter one after another." ] }, { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "19", "metadata": {}, "outputs": [], "source": [ - "project.save()" + "project.display.fit.series(versus=temperature)" ] } ], diff --git a/docs/docs/tutorials/ed-23.py b/docs/docs/tutorials/ed-23.py index 659cbcbb1..a4dbc8ed7 100644 --- a/docs/docs/tutorials/ed-23.py +++ b/docs/docs/tutorials/ed-23.py @@ -14,22 +14,14 @@ import easydiffraction as ed # %% [markdown] -# ## Download Saved Project Archive +# ## Download Saved Project # -# The archive should contain a saved project directory with a partially -# completed sequential fit, including `analysis/results.csv`. +# The returned path points directly to the saved project directory with +# a partially completed sequential fit, including +# `analysis/results.csv`. # %% -zip_path = ed.download_data(id=34, destination='data') - -# %% [markdown] -# ## Extract Project -# -# Extract the saved project directory locally. For a project you -# already have on disk, set `project_dir` directly instead. - -# %% -project_dir = ed.extract_project_from_zip(zip_path, destination='projects') +project_dir = ed.download_data(id=37, destination='projects') # %% [markdown] # ## Load Saved Project @@ -69,11 +61,30 @@ # ## Plot Parameter Evolution # # Use the same persisted diffrn path stored in `analysis/results.csv` -# for the x-axis. Omitting `param` plots every fitted parameter one -# after another. +# 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/index.md b/docs/docs/tutorials/index.md index a89601095..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 @@ -89,9 +98,6 @@ The tutorials are organized into the following categories: - [Co2SiO4 Temperature scan](ed-17.ipynb) – Sequential Rietveld refinement of Co2SiO4 using constant wavelength neutron powder diffraction data from D20 at ILL across a temperature scan. -- [Co2SiO4 Temperature scan, resumed](ed-23.ipynb) – Continue a saved - sequential refinement of Co2SiO4 from an existing - `analysis/results.csv` after an incomplete previous run. ## Simulated Data diff --git a/docs/docs/user-guide/analysis-workflow/project.md b/docs/docs/user-guide/analysis-workflow/project.md index 5e8e8b417..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.chart_engine  asciichartpy
-_display.table_engine   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/mkdocs.yml b/docs/mkdocs.yml index c54ff9353..02f862023 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -193,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 @@ -213,7 +216,6 @@ nav: - PbSO4 NPD+XRD: tutorials/ed-4.ipynb - Si Bragg+PDF: tutorials/ed-16.ipynb - Co2SiO4 T-scan: tutorials/ed-17.ipynb - - Co2SiO4 T-scan resumed: tutorials/ed-23.ipynb - Simulated Data: - LBCO+Si McStas: tutorials/ed-9.ipynb - BEER McStas: tutorials/ed-20.ipynb diff --git a/pixi.lock b/pixi.lock index 927dd5686..974cbe77e 100644 --- a/pixi.lock +++ b/pixi.lock @@ -8150,6 +8150,7 @@ packages: - diffpy-pdffit2 - diffpy-utils - gemmi + - h5py - lmfit - numpy - pandas diff --git a/pixi.toml b/pixi.toml index 958851cf2..70a8a310e 100644 --- a/pixi.toml +++ b/pixi.toml @@ -102,8 +102,8 @@ user = { features = ['py-max', 'user'] } 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 = [ @@ -184,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 3a4b78a01..507a069d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ 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 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 ca1d3f8c4..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,18 +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() - if getattr(project.analysis, 'fitting_mode_type', None) != 'sequential': - project.display.fit.results() - project.display.fit.correlations() - for expt in project.experiments: - project.display.pattern(expt_name=expt.name) - # 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 0fe4386c4..f9d5e94de 100644 --- a/src/easydiffraction/analysis/__init__.py +++ b/src/easydiffraction/analysis/__init__.py @@ -1,6 +1,56 @@ # 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 @@ -13,4 +63,6 @@ ) 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 08303f65b..eccbf28d8 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -4,13 +4,35 @@ 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.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 @@ -19,14 +41,23 @@ 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.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 @@ -40,6 +71,9 @@ 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]]: @@ -347,7 +381,111 @@ def as_cif(self) -> None: self._analysis.show_as_cif() -class Analysis(CategoryOwner): +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 + + @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. @@ -379,48 +517,321 @@ def __init__(self, project: object) -> None: 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 project(self) -> object: - """Project that owns this analysis section.""" - return self._project + @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') + } - @property - def aliases(self) -> object: - """Alias mappings used by symbolic constraints and displays.""" - return self._aliases + 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 - @property - def constraints(self) -> object: - """Symbolic constraints owned by this analysis section.""" - return self._constraints + 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 - @property - def display(self) -> AnalysisDisplay: - """Display helper for parameter tables, CIF, and fit results.""" - return self._display + if self.fit_result.result_kind.value != FitResultKindEnum.BAYESIAN.value: + return - @property - def fitter(self) -> Fitter: - """Fitting engine used by this analysis object.""" - return self._fitter + if self.fitting.minimizer_type.value != MinimizerTypeEnum.BUMPS_DREAM.value: + return - @fitter.setter - def fitter(self, value: Fitter) -> None: - self._fitter = value + minimizer = self.fitting.minimizer + if minimizer is None: + return - @property - def fit_results(self) -> object | None: - """Results from the most recent fit, if any.""" - return self._fit_results + steps = int(self.bayesian_sampler.steps.value) + if steps <= 0: + return - @fit_results.setter - def fit_results(self, value: object | None) -> None: - self._fit_results = value + 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.""" @@ -502,6 +913,9 @@ def _serializable_categories(self) -> list: self.sequential_fit_extract, ]) + if self._has_persisted_fit_state(): + categories.extend(self._fit_state_categories()) + return categories # ------------------------------------------------------------------ @@ -684,6 +1098,659 @@ 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 _selected_parameters_for_fit(self, experiments: list[object]) -> list[Parameter]: + """ + Return unique live parameters involved in the current fit slice. + """ + selected_parameters: list[Parameter] = [] + seen_unique_names: set[str] = set() + + 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) + + 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) + + return selected_parameters + + @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. @@ -704,7 +1771,7 @@ def _resolve_sequential_data_dir(self) -> Path: def _prepare_fit_run(self) -> tuple[VerbosityEnum, object, object] | None: """Resolve common inputs for single and joint fitting.""" - verb = VerbosityEnum(self.project.verbosity) + 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.') @@ -718,6 +1785,7 @@ def _prepare_fit_run(self) -> tuple[VerbosityEnum, object, object] | None: # 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() return verb, structures, experiments @@ -768,6 +1836,7 @@ def _run_sequential(self) -> None: 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) @@ -790,6 +1859,7 @@ def _run_sequential(self) -> None: 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() diff --git a/src/easydiffraction/analysis/categories/__init__.py b/src/easydiffraction/analysis/categories/__init__.py index 6c070cf1a..743e0ff02 100644 --- a/src/easydiffraction/analysis/categories/__init__.py +++ b/src/easydiffraction/analysis/categories/__init__.py @@ -3,8 +3,39 @@ 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 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/bayesian_convergence/factory.py b/src/easydiffraction/analysis/categories/bayesian_convergence/factory.py new file mode 100644 index 000000000..fbe5da383 --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_convergence/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian-convergence factory.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +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/analysis/categories/bayesian_result/factory.py b/src/easydiffraction/analysis/categories/bayesian_result/factory.py new file mode 100644 index 000000000..3d437a0df --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_result/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian-result factory.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +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/bayesian_sampler/factory.py b/src/easydiffraction/analysis/categories/bayesian_sampler/factory.py new file mode 100644 index 000000000..6f5d1033c --- /dev/null +++ b/src/easydiffraction/analysis/categories/bayesian_sampler/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Bayesian-sampler factory.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +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 d6f026221..0b622e183 100644 --- a/src/easydiffraction/analysis/categories/constraints/default.py +++ b/src/easydiffraction/analysis/categories/constraints/default.py @@ -137,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. @@ -148,10 +148,15 @@ 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 item.lhs_alias: + if id is not None: + item.id = id + elif item.lhs_alias: item.id = item.lhs_alias self.add(item) self._enabled = True 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_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/enums.py b/src/easydiffraction/analysis/enums.py index c1ef93f7b..94d78cc64 100644 --- a/src/easydiffraction/analysis/enums.py +++ b/src/easydiffraction/analysis/enums.py @@ -28,3 +28,27 @@ def description(self) -> str: 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/tracking.py b/src/easydiffraction/analysis/fit_helpers/tracking.py index 0f8e4c970..c24f94359 100644 --- a/src/easydiffraction/analysis/fit_helpers/tracking.py +++ b/src/easydiffraction/analysis/fit_helpers/tracking.py @@ -11,6 +11,8 @@ 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 @@ -28,6 +30,8 @@ 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'] @@ -85,6 +89,7 @@ def __init__(self) -> 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._activity_indicator: ActivityIndicator | None = None @@ -111,6 +116,7 @@ def reset(self) -> 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 @@ -148,8 +154,25 @@ def track( 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] = [] - elapsed_time = self._current_elapsed_time() if self._previous_chi2 is None: self._previous_chi2 = reduced_chi2 @@ -197,8 +220,6 @@ def track( 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. @@ -240,6 +261,37 @@ def track_sampler_progress(self, update: SamplerProgressUpdate) -> None: 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: """Best recorded reduced chi-square value or None.""" @@ -273,6 +325,16 @@ def stop_timer(self) -> None: self._end_time = time.perf_counter() self._fitting_time = self._end_time - self._start_time + 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. @@ -347,13 +409,21 @@ def _initial_sampler_progress_row( clamped_iteration: int, clamped_progress: float, ) -> list[str]: - if self._previous_chi2 is not None and self._best_chi2 is not None: + 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, @@ -431,6 +501,25 @@ def _sampler_progress_row( 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: @@ -453,6 +542,12 @@ def _final_sampler_tracking_row(self) -> list[str] | 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 @@ -602,8 +697,12 @@ def _default_activity_label(self) -> str: @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: diff --git a/src/easydiffraction/analysis/fitting.py b/src/easydiffraction/analysis/fitting.py index 343c15742..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, @@ -76,53 +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.user_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, - random_seed=random_seed, - ) + finally: + self.minimizer._stop_tracking() def _process_fit_results( self, @@ -243,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/base.py b/src/easydiffraction/analysis/minimizers/base.py index bbc639f10..2d8bc83f9 100644 --- a/src/easydiffraction/analysis/minimizers/base.py +++ b/src/easydiffraction/analysis/minimizers/base.py @@ -34,7 +34,7 @@ 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 @@ -42,8 +42,19 @@ def __init__( 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, @@ -61,19 +72,47 @@ def _start_tracking( """ self.tracker.reset() self.tracker._verbosity = verbosity + 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]: """ @@ -300,6 +339,7 @@ def fit( objective_function: Callable[..., object], verbosity: VerbosityEnum = VerbosityEnum.FULL, *, + finalize_tracking: bool = True, use_physical_limits: bool = False, random_seed: int | None = None, ) -> FitResults: @@ -315,6 +355,8 @@ 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 @@ -343,10 +385,10 @@ def fit( 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: - self._stop_tracking() - - return self._finalize_fit(parameters, raw_result) + 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 index 1cb42fb51..225c7ea0d 100644 --- a/src/easydiffraction/analysis/minimizers/bumps_dream.py +++ b/src/easydiffraction/analysis/minimizers/bumps_dream.py @@ -32,6 +32,8 @@ 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 @@ -304,14 +306,28 @@ def __init__( 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) + 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) + self._max_iterations = self._validated_positive_integer('steps', value) @property def burn(self) -> int | None: @@ -601,6 +617,8 @@ def _run_solver( 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, @@ -621,6 +639,8 @@ def _run_solver( sampler_completed=False, ) + self.tracker.start_sampler_post_processing() + return self._build_success_result( context=context, raw_state=driver_result.raw_state, @@ -729,7 +749,7 @@ def _build_mapper(self, problem: FitProblem) -> object | None: return None if self._requires_serial_mapper_for_spawn_main_module(): - log.warning( + self._warn_after_tracking( 'DREAM parallel evaluation requires an import-safe main ' 'module on spawn-based multiprocessing; falling back to ' 'serial execution.' @@ -745,7 +765,7 @@ def _build_mapper(self, problem: FitProblem) -> object | None: try: if not can_pickle(problem): - log.warning( + self._warn_after_tracking( 'DREAM parallel evaluation requires a picklable ' 'problem; falling back to serial execution.' ) @@ -756,7 +776,7 @@ def _build_mapper(self, problem: FitProblem) -> object | None: message = str(error) if 'bootstrapping phase' not in message: raise - log.warning( + self._warn_after_tracking( 'DREAM parallel evaluation requires an import-safe main ' 'module on spawn-based multiprocessing; falling back to ' 'serial execution.' @@ -885,6 +905,10 @@ def _build_success_result( 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, @@ -896,9 +920,6 @@ def _build_success_result( posterior_parameter_summaries ) - if not convergence_diagnostics.get('converged', True): - log.warning('Convergence diagnostics indicate the posterior may be poorly mixed.') - return OptimizeResult( x=best_sample_values, dx=posterior_standard_deviations, diff --git a/src/easydiffraction/analysis/sequential.py b/src/easydiffraction/analysis/sequential.py index 029486c79..a43b537a3 100644 --- a/src/easydiffraction/analysis/sequential.py +++ b/src/easydiffraction/analysis/sequential.py @@ -90,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. @@ -144,12 +144,12 @@ def _fit_worker( project.analysis.fitter = Fitter(template.minimizer_tag) # 10. Fit - original_verbosity = project.verbosity - project.verbosity = 'silent' + original_verbosity = project.verbosity.fit.value + project.verbosity.fit = 'silent' try: project.analysis.fit() finally: - project.verbosity = original_verbosity + project.verbosity.fit = original_verbosity # 11. Collect results result.update(_collect_results(project, template)) @@ -163,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 @@ -352,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 @@ -383,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', ] @@ -551,7 +549,8 @@ def _read_csv_for_recovery( file_path = row.get('file_path', '') if file_path: fitted.add(_resolve_csv_file_path(csv_path, file_path)) - if row.get('fit_success', '').lower() == 'true': + 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 @@ -725,9 +724,9 @@ class SequentialRunPlan: 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_success')] + successful = [r for r in results if r.get('fit_result.success')] if successful: - avg_chi2 = sum(r['reduced_chi_squared'] for r in successful) / len(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 = '—' @@ -798,10 +797,10 @@ def _build_file_progress_rows( rows: list[list[str]] = [] time_str = _format_elapsed_seconds(elapsed_time) for index, result in enumerate(results, start=1): - reduced_chi2 = result.get('reduced_chi_squared') + 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('n_iterations') or 0) - status = '✅' if result.get('fit_success') 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), @@ -897,7 +896,7 @@ def _print_sequential_completion( return console.print(f'✅ Sequential fitting complete: {processed_count} files processed.') - console.print(f'📄 Results saved to: {csv_path}') + console.print(f'📄 Results saved to:\n{csv_path}') def _prepare_sequential_run( @@ -910,7 +909,7 @@ def _prepare_sequential_run( reverse: bool, ) -> SequentialRunPlan | None: """Resolve inputs and bookkeeping for one sequential-fit run.""" - verbosity = VerbosityEnum(analysis.project.verbosity) + verbosity = VerbosityEnum(analysis.project.verbosity.fit.value) _check_seq_preconditions(analysis.project) @@ -1260,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 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 257a5771f..074afb714 100644 --- a/src/easydiffraction/core/variable.py +++ b/src/easydiffraction/core/variable.py @@ -276,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. @@ -461,6 +470,10 @@ def fit_bounds_uncertainty_multiplier(self) -> float | None: """ 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, @@ -612,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/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/display/plotting.py b/src/easydiffraction/display/plotting.py index 85cb1545a..3bfbd5ec5 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -16,6 +16,8 @@ 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 @@ -89,6 +91,7 @@ class PosteriorPairPlotStyleEnum(StrEnum): 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)' @@ -741,7 +744,7 @@ 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 : str | None, default=None Persisted diffrn path (e.g. @@ -749,7 +752,10 @@ def plot_param_series( 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 @@ -761,19 +767,49 @@ 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_path=versus, ) else: # Fallback: in-memory snapshots from fit() single mode self.plot_param_series_from_snapshots( - unique_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, @@ -1573,13 +1609,15 @@ def _get_param_correlation_dataframe(self) -> pd.DataFrame | None: return corr_df raw_result = self._raw_fit_result_for_correlation(fit_results) - if raw_result is None: - return None + 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_engine_result( - raw_result=raw_result, - parameters=fit_results.parameters, - ) + corr_df = self._correlation_dataframe_from_persisted_projection(fit_results) if corr_df is not None: return corr_df @@ -1606,15 +1644,71 @@ def _raw_fit_result_for_correlation(fit_results: object) -> object | None: if raw_result is None: 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.') 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.') 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, *, @@ -2431,7 +2525,7 @@ def _plot_legend_background_color(self) -> str: return legend_background_color() return PlotlyPlotter._legend_background_color() - def _posterior_contour_traces( + def _resolved_posterior_contour_surface( self, *, fit_results: object, @@ -2440,11 +2534,14 @@ def _posterior_contour_traces( 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']) + ) -> 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, @@ -2464,13 +2561,44 @@ def _posterior_contour_traces( 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 = float(np.max(density) * 0.20) - contour_end = float(np.max(density) * 0.95) - contour_size = float(np.max(density) * 0.15) + 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( @@ -2516,6 +2644,80 @@ def _posterior_contour_traces( ) 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, @@ -2608,11 +2810,13 @@ def _plot_ascii_param_distribution( fit_results=context.fit_results, parameter_name=context.parameter_name, ) - density_curve = self._posterior_density_curve( - context.values, - lower_bound=lower_bound, - upper_bound=upper_bound, - ) + 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}.' @@ -2629,6 +2833,30 @@ def _plot_ascii_param_distribution( 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, @@ -2974,11 +3202,13 @@ def _posterior_density_trace( fit_results=fit_results, parameter_name=parameter_name, ) - density_curve = self._posterior_density_curve( - values, - lower_bound=lower_bound, - upper_bound=upper_bound, - ) + 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 @@ -3312,8 +3542,7 @@ def _get_or_build_posterior_predictive_summary( return None posterior_predictive = getattr(fit_results, 'posterior_predictive', None) - posterior_samples = getattr(fit_results, 'posterior_samples', None) - if posterior_predictive is None or posterior_samples is None: + if posterior_predictive is None: return None x_axis_name = getattr(x_axis, 'value', x_axis) @@ -3331,9 +3560,18 @@ def _get_or_build_posterior_predictive_summary( 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, @@ -5537,7 +5775,7 @@ 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_path: str | None = None, ) -> None: @@ -5545,10 +5783,10 @@ def _plot_param_series_from_csv( 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_path* is provided, - the x-axis uses the corresponding ``diffrn.*`` CSV 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 use the live parameter descriptor and, when available, a template diffrn descriptor resolved from @@ -5558,8 +5796,8 @@ def _plot_param_series_from_csv( ---------- 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_path : str | None, default=None @@ -5568,16 +5806,23 @@ def _plot_param_series_from_csv( """ 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 diffrn_col = versus_path @@ -5594,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, diff --git a/src/easydiffraction/display/progress.py b/src/easydiffraction/display/progress.py index ef381c505..6123e626e 100644 --- a/src/easydiffraction/display/progress.py +++ b/src/easydiffraction/display/progress.py @@ -31,6 +31,8 @@ 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' diff --git a/src/easydiffraction/io/ascii.py b/src/easydiffraction/io/ascii.py index faab76820..dcbea91b3 100644 --- a/src/easydiffraction/io/ascii.py +++ b/src/easydiffraction/io/ascii.py @@ -5,20 +5,22 @@ 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 Path(tempfile.mkdtemp(prefix='ed_zip_')) + return create_artifact_temp_dir(prefix='ed_zip_') - extract_dir = Path(destination) + extract_dir = resolve_artifact_path(destination) if not extract_dir.is_absolute(): extract_dir = Path.cwd() / extract_dir @@ -45,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 ------- @@ -65,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 @@ -107,7 +107,8 @@ def extract_data_paths_from_zip( destination : str | Path | None, default=None Directory to extract files into. When ``None``, a temporary directory is created. Relative destinations are resolved against - the current working directory. + the current working directory, or against the configured + artifact root when ``EASYDIFFRACTION_ARTIFACT_ROOT`` is set. Returns ------- diff --git a/src/easydiffraction/io/cif/serialize.py b/src/easydiffraction/io/cif/serialize.py index 5c3a29c14..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: """ @@ -51,9 +52,9 @@ def format_value(value: object) -> str: # Booleans use CIF true/false tokens elif isinstance(value, bool): value = 'true' if value else 'false' - # Convert ints to floats - elif isinstance(value, int): - value = float(value) + # 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 = '?' @@ -79,6 +80,13 @@ def _strip_optional_quotes(raw: str) -> str: 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() @@ -335,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' @@ -458,6 +479,13 @@ 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, @@ -487,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 ---------- @@ -498,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) @@ -510,10 +533,7 @@ 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) @@ -521,6 +541,10 @@ def project_config_from_cif(project: object, cif_text: str) -> 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: """ @@ -556,6 +580,82 @@ def analysis_from_cif(analysis: object, cif_text: str) -> None: if analysis.constraints._items: analysis.constraints.enable() + 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.""" @@ -687,10 +787,8 @@ 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 @@ -747,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: - self.value = _strip_optional_quotes(raw) - - elif self._value_type == DataTypes.BOOL: - self.value = _parse_bool_cif_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( @@ -803,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 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/rendering/default.py b/src/easydiffraction/project/categories/rendering/default.py index d3e2fad3e..179162a08 100644 --- a/src/easydiffraction/project/categories/rendering/default.py +++ b/src/easydiffraction/project/categories/rendering/default.py @@ -19,6 +19,10 @@ 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): @@ -37,13 +41,14 @@ def __init__(self) -> None: 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=self._plotter.engine, + default=AUTO_ENGINE, validator=MembershipValidator( - allowed=[member.value for member in PlotterEngineEnum], + allowed=CHART_ENGINE_OPTIONS, ), ), cif_handler=CifHandler(names=['_rendering.chart_engine']), @@ -52,14 +57,46 @@ def __init__(self) -> None: name='table_engine', description='Table renderer backend type', value_spec=AttributeSpec( - default=self._tabler.engine, + default=AUTO_ENGINE, validator=MembershipValidator( - allowed=[member.value for member in TableEngineEnum], + 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.""" @@ -67,8 +104,7 @@ def chart_engine(self) -> StringDescriptor: @chart_engine.setter def chart_engine(self, value: str) -> None: - self._plotter.engine = value - self._chart_engine.value = self._plotter.engine + self._set_chart_engine(value) @property def table_engine(self) -> StringDescriptor: @@ -77,8 +113,7 @@ def table_engine(self) -> StringDescriptor: @table_engine.setter def table_engine(self, value: str) -> None: - self._tabler.engine = value - self._table_engine.value = self._tabler.engine + self._set_table_engine(value) @property def plotter(self) -> Plotter: @@ -123,14 +158,8 @@ def from_cif(self, block: object, idx: int = 0) -> None: del idx chart_engine = read_cif_str(block, '_rendering.chart_engine') if chart_engine is not None: - if chart_engine == self._plotter.engine: - self._chart_engine.value = chart_engine - else: - self.chart_engine = chart_engine + self._set_chart_engine(chart_engine) table_engine = read_cif_str(block, '_rendering.table_engine') if table_engine is not None: - if table_engine == self._tabler.engine: - self._table_engine.value = table_engine - else: - self.table_engine = table_engine + self._set_table_engine(table_engine) 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 index 83a9e40f2..5b760d997 100644 --- a/src/easydiffraction/project/display.py +++ b/src/easydiffraction/project/display.py @@ -4,6 +4,7 @@ from __future__ import annotations +from contextlib import nullcontext from dataclasses import dataclass from typing import TYPE_CHECKING @@ -134,6 +135,68 @@ class PosteriorDisplay: 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, @@ -143,10 +206,15 @@ def pairs( max_parameters: int = 6, ) -> None: """Plot posterior pair relationships for sampled parameters.""" - with activity_indicator( - ACTIVITY_LABEL_PROCESSING, - verbosity=VerbosityEnum(self._project.verbosity), - ): + 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, @@ -182,10 +250,19 @@ def predictive( x: object | None = None, ) -> None: """Plot posterior predictive summaries for one experiment.""" - with activity_indicator( - ACTIVITY_LABEL_PROCESSING, - verbosity=VerbosityEnum(self._project.verbosity), - ): + 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, @@ -249,10 +326,19 @@ def pattern( msg = self._status_by_name(statuses, 'auto').reason raise ValueError(msg) if 'uncertainty' in auto_include: - with activity_indicator( - ACTIVITY_LABEL_PROCESSING, - verbosity=VerbosityEnum(self._project.verbosity), - ): + 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', @@ -283,10 +369,19 @@ def pattern( raise ValueError(msg) if 'uncertainty' in normalized_include: - with activity_indicator( - ACTIVITY_LABEL_PROCESSING, - verbosity=VerbosityEnum(self._project.verbosity), - ): + 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', @@ -739,12 +834,16 @@ def _uncertainty_status( if fit_results is None: return False, 'No fit results are available.' - posterior_samples = getattr(fit_results, 'posterior_samples', None) posterior_predictive = getattr(fit_results, 'posterior_predictive', None) - if posterior_samples is None or posterior_predictive is None: + if not posterior_predictive: return False, 'Posterior predictive data is unavailable.' - if self._project.rendering.chart_engine.value != PlotterEngineEnum.PLOTLY.value: + 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 5bbe8acba..9b89eb500 100644 --- a/src/easydiffraction/project/project.py +++ b/src/easydiffraction/project/project.py @@ -5,6 +5,7 @@ from __future__ import annotations import pathlib +import shutil import tempfile from typing import TYPE_CHECKING from typing import ClassVar @@ -16,17 +17,25 @@ 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.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 @@ -108,6 +117,63 @@ def _resolve_data_path_from_results_csv( 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. @@ -135,12 +201,12 @@ def __init__( self._structures = Structures() self._experiments = Experiments() 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 @@ -249,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 ---------- @@ -271,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 @@ -303,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) - + _load_project_info(project, project_path) 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_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() @@ -373,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 @@ -391,6 +411,22 @@ 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: @@ -441,6 +477,10 @@ def save(self) -> None: with (analysis_dir / 'analysis.cif').open('w') as f: f.write(self.analysis.as_cif) console.print('├── 📁 analysis/') + 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() @@ -462,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: diff --git a/src/easydiffraction/project/project_config.py b/src/easydiffraction/project/project_config.py index 32147b609..dd92137ab 100644 --- a/src/easydiffraction/project/project_config.py +++ b/src/easydiffraction/project/project_config.py @@ -9,6 +9,8 @@ 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): @@ -28,6 +30,7 @@ def __init__( description=description, ) self._rendering = RenderingFactory.create(RenderingFactory.default_tag()) + self._verbosity = VerbosityFactory.create(VerbosityFactory.default_tag()) @property def info(self) -> ProjectInfo: @@ -39,6 +42,11 @@ 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.""" 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/utils.py b/src/easydiffraction/utils/utils.py index cf461aeea..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 = '39dad256ba1faedf4b26fad3e44a361c802fd8e4' +_DATA_INDEX_REF = 'dbe92a87e0106c4742eee0ff9a8e32bdb8b483cb' # macOS: sha256sum index.json -_DATA_INDEX_HASH = 'sha256:301aaca0f35927cd63715b858a1f03164e4d05d1d39234325a3798d2b4a5f4ea' +_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) 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/integration/fitting/test_bayesian_dream.py b/tests/integration/fitting/test_bayesian_dream.py index d6695973b..ef52c1f1a 100644 --- a/tests/integration/fitting/test_bayesian_dream.py +++ b/tests/integration/fitting/test_bayesian_dream.py @@ -163,7 +163,7 @@ def test_lm_prefit_followed_by_dream_uses_uncertainty_based_bounds(): assert len(results.posterior_parameter_summaries) == 3 -def test_bayesian_fit_results_are_runtime_only_after_save_load(tmp_path): +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): @@ -185,8 +185,13 @@ def test_bayesian_fit_results_are_runtime_only_after_save_load(tmp_path): 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 'posterior' not in analysis_cif.read_text().lower() + assert results_sidecar.is_file() loaded = Project.load(str(proj_dir)) - assert loaded.analysis.fit_results is None + 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_bumps_dream_support.py b/tests/integration/fitting/test_bumps_dream_support.py index c09f0b92e..a824347e1 100644 --- a/tests/integration/fitting/test_bumps_dream_support.py +++ b/tests/integration/fitting/test_bumps_dream_support.py @@ -277,10 +277,7 @@ def test_build_mapper_falls_back_for_serial_and_unpicklable(monkeypatch): monkeypatch.setattr( 'easydiffraction.analysis.minimizers.bumps_dream.can_pickle', lambda problem: False ) - monkeypatch.setattr( - 'easydiffraction.analysis.minimizers.bumps_dream.log.warning', - lambda message: warnings.append(message), - ) + monkeypatch.setattr(minimizer, '_warn_after_tracking', warnings.append) assert minimizer._build_mapper('problem') is None assert warnings == [ diff --git a/tests/integration/fitting/test_sequential.py b/tests/integration/fitting/test_sequential.py index da4c28774..73c068b3b 100644 --- a/tests/integration/fitting/test_sequential.py +++ b/tests/integration/fitting/test_sequential.py @@ -165,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] @@ -316,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] 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 b887408ea..07016b298 100644 --- a/tests/unit/easydiffraction/analysis/categories/test_constraints.py +++ b/tests/unit/easydiffraction/analysis/categories/test_constraints.py @@ -22,6 +22,17 @@ def test_constraint_creation_and_collection(): 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' 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_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/fit_helpers/test_tracking.py b/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py index 877a46b1a..ff9df3288 100644 --- a/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py +++ b/tests/unit/easydiffraction/analysis/fit_helpers/test_tracking.py @@ -77,3 +77,50 @@ def test_tracker_fit_adds_timed_rows_and_resets_counter(monkeypatch): ] 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 3b18f5354..4ee99f1d1 100644 --- a/tests/unit/easydiffraction/analysis/minimizers/test_base.py +++ b/tests/unit/easydiffraction/analysis/minimizers/test_base.py @@ -87,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 @@ -122,6 +123,37 @@ def _compute_residuals( 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 @@ -201,3 +233,59 @@ def _check_success(self, raw_result): 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 1a73fdbcd..830a4f64f 100644 --- a/tests/unit/easydiffraction/analysis/minimizers/test_bumps.py +++ b/tests/unit/easydiffraction/analysis/minimizers/test_bumps.py @@ -189,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 @@ -232,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]) @@ -265,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 index d17dce4fa..3a23f5577 100644 --- a/tests/unit/easydiffraction/analysis/minimizers/test_bumps_dream.py +++ b/tests/unit/easydiffraction/analysis/minimizers/test_bumps_dream.py @@ -53,6 +53,28 @@ def test_type_info_and_default_init(): 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 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_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_coverage.py b/tests/unit/easydiffraction/analysis/test_analysis_coverage.py index 13072544a..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: @@ -199,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 index fe854e536..75e1ca3ec 100644 --- a/tests/unit/easydiffraction/analysis/test_enums.py +++ b/tests/unit/easydiffraction/analysis/test_enums.py @@ -2,6 +2,8 @@ # 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 @@ -20,3 +22,15 @@ def test_fit_mode_enum_descriptions(): 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 ab2327c94..2be4c19f3 100644 --- a/tests/unit/easydiffraction/analysis/test_sequential.py +++ b/tests/unit/easydiffraction/analysis/test_sequential.py @@ -137,7 +137,7 @@ def _run_non_silent_fit(monkeypatch, tmp_path, *, verbosity, is_jupyter): del is_jupyter # legacy parameter, no longer affects behavior analysis = SimpleNamespace( - project=SimpleNamespace(verbosity=verbosity), + project=SimpleNamespace(verbosity=SimpleNamespace(fit=SimpleNamespace(value=verbosity))), fitter=SimpleNamespace(selection='lmfit'), ) @@ -198,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: @@ -336,19 +336,17 @@ def test_returns_fitted_file_paths(self, tmp_path): [ { 'file_path': str(project_dir / 'experiments' / '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': str(project_dir / 'experiments' / 'b.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': '', }, @@ -374,10 +372,9 @@ def test_resolves_legacy_repo_relative_paths(self, tmp_path, monkeypatch): writer.writeheader() writer.writerow({ 'file_path': 'projects/cosio/experiments/d20_scan/scan_001.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', }) @@ -397,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', }, @@ -436,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', @@ -452,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 @@ -466,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': '', }, @@ -537,15 +530,15 @@ def update(self, *, label=None, content=None): [ { 'file_path': _TEST_SCAN_001, - 'fit_success': True, - 'reduced_chi_squared': 4.0, - 'n_iterations': 11, + 'fit_result.success': True, + 'fit_result.reduced_chi_square': 4.0, + 'fit_result.iterations': 11, }, { 'file_path': _TEST_SCAN_002, - 'fit_success': False, - 'reduced_chi_squared': None, - 'n_iterations': 0, + 'fit_result.success': False, + 'fit_result.reduced_chi_square': None, + 'fit_result.iterations': 0, }, ], progress, @@ -608,7 +601,7 @@ def test_fit_sequential_non_silent_starts_indicator_with_progress_table( assert events[8] == ('stop',) assert events[9:] == [ ('console_print', ('✅ Sequential fitting complete: 1 files processed.',), {}), - ('console_print', (f'📄 Results saved to: {tmp_path / "results.csv"}',), {}), + ('console_print', (f'📄 Results saved to:\n{tmp_path / "results.csv"}',), {}), ] @@ -631,9 +624,9 @@ def map(self, func, templates, paths): for path in paths: yield { 'file_path': path, - 'fit_success': True, - 'reduced_chi_squared': 1.0, - 'n_iterations': 5, + 'fit_result.success': True, + 'fit_result.reduced_chi_square': 1.0, + 'fit_result.iterations': 5, 'params': {'cell.a': 4.0}, } @@ -728,7 +721,7 @@ def fake_run_fit_loop( monkeypatch.setattr(sequential_mod, '_restore_main_state', lambda *args: None) analysis = SimpleNamespace( - project=SimpleNamespace(verbosity='silent'), + project=SimpleNamespace(verbosity=SimpleNamespace(fit=SimpleNamespace(value='silent'))), fitter=SimpleNamespace(selection='lmfit'), ) diff --git a/tests/unit/easydiffraction/display/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py index 461e85ce2..d5b6ae76c 100644 --- a/tests/unit/easydiffraction/display/test_plotting.py +++ b/tests/unit/easydiffraction/display/test_plotting.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +import csv import re from types import MethodType from types import SimpleNamespace @@ -66,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 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_more.py b/tests/unit/easydiffraction/io/cif/test_serialize_more.py index f526f21a8..3c9db1bc1 100644 --- a/tests/unit/easydiffraction/io/cif/test_serialize_more.py +++ b/tests/unit/easydiffraction/io/cif/test_serialize_more.py @@ -34,7 +34,7 @@ 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 @@ -80,7 +80,7 @@ def __init__(self): 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 out == 'data_block1\n\n_aa 42\n\nloop_\n_aa\n7' assert '\n\n\n' not in out @@ -107,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(): @@ -159,7 +172,7 @@ 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(): 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/rendering/test_default.py b/tests/unit/easydiffraction/project/categories/rendering/test_default.py index edb884272..07a168ae2 100644 --- a/tests/unit/easydiffraction/project/categories/rendering/test_default.py +++ b/tests/unit/easydiffraction/project/categories/rendering/test_default.py @@ -5,14 +5,18 @@ 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 == rendering.plotter.engine - assert rendering.table_engine.value == rendering.tabler.engine + 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(): @@ -28,6 +32,8 @@ def test_rendering_plotter_binds_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() @@ -40,6 +46,14 @@ def test_rendering_setters_update_engines(): 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 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 index 1a892bca0..f534da493 100644 --- a/tests/unit/easydiffraction/project/test_display.py +++ b/tests/unit/easydiffraction/project/test_display.py @@ -47,12 +47,25 @@ def _recorder(*args, **kwargs): 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), + 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='full', + verbosity=SimpleNamespace(fit=SimpleNamespace(value='full')), ) return project, calls @@ -267,6 +280,67 @@ def fake_activity_indicator(label, *, verbosity): ] +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'] diff --git a/tests/unit/easydiffraction/project/test_project.py b/tests/unit/easydiffraction/project/test_project.py index e3dcf7a19..3afbd363e 100644 --- a/tests/unit/easydiffraction/project/test_project.py +++ b/tests/unit/easydiffraction/project/test_project.py @@ -30,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(): @@ -38,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(): diff --git a/tests/unit/easydiffraction/project/test_project_config.py b/tests/unit/easydiffraction/project/test_project_config.py index 3ba20eea1..be6576e94 100644 --- a/tests/unit/easydiffraction/project/test_project_config.py +++ b/tests/unit/easydiffraction/project/test_project_config.py @@ -23,8 +23,12 @@ def test_project_config_exposes_project_info_and_rendering_categories(): assert config.info.path is None assert isinstance(config.info.created, datetime.datetime) assert isinstance(config.info.last_modified, datetime.datetime) - assert config.categories == [config.info, config.rendering] - assert config.parameters == config.info.parameters + config.rendering.parameters + 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(): @@ -42,6 +46,29 @@ def test_project_config_as_cif_has_project_and_rendering_sections_without_data_h 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): @@ -57,6 +84,7 @@ def test_project_save_and_load_keep_project_config_section_format(tmp_path): 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' @@ -66,3 +94,36 @@ def test_project_save_and_load_keep_project_config_section_format(tmp_path): 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 e44c12608..f19b4392d 100644 --- a/tests/unit/easydiffraction/project/test_project_load.py +++ b/tests/unit/easydiffraction/project/test_project_load.py @@ -125,6 +125,169 @@ def test_round_trips_constraints(self, tmp_path): 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.""" diff --git a/tests/unit/easydiffraction/project/test_project_save.py b/tests/unit/easydiffraction/project/test_project_save.py index 27212251b..ff8a9b9be 100644 --- a/tests/unit/easydiffraction/project/test_project_save.py +++ b/tests/unit/easydiffraction/project/test_project_save.py @@ -62,3 +62,62 @@ def test_project_save_lists_existing_analysis_results_csv(tmp_path, monkeypatch, 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/test___main__.py b/tests/unit/easydiffraction/test___main__.py index 62ad8c68c..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): 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_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