diff --git a/.copier-answers.yml b/.copier-answers.yml
index d4d01f3ce..929409549 100644
--- a/.copier-answers.yml
+++ b/.copier-answers.yml
@@ -1,6 +1,6 @@
# WARNING: Do not edit this file manually.
# Any changes will be overwritten by Copier.
-_commit: v0.10.1-41-g508666e
+_commit: v0.11.0-7-gb5113cf
_src_path: gh:easyscience/templates
app_docs_url: https://easyscience.github.io/diffraction-app
app_doi: 10.5281/zenodo.18163581
@@ -17,9 +17,8 @@ lib_python_min: '3.12'
lib_repo_name: diffraction-lib
project_contact_email: support@easydiffraction.org
project_copyright_years: 2021-2026
-project_extended_description: A software for calculating neutron powder diffraction
- patterns based on a structural model and refining its parameters against experimental
- data
+project_extended_description: A software for calculating diffraction patterns based
+ on a structural model and refining its parameters against experimental data
project_name: EasyDiffraction
project_short_description: Diffraction data analysis
project_shortcut: ED
diff --git a/.github/actions/download-artifact/action.yml b/.github/actions/download-artifact/action.yml
index e4fd62f50..d1fff1a06 100644
--- a/.github/actions/download-artifact/action.yml
+++ b/.github/actions/download-artifact/action.yml
@@ -1,5 +1,5 @@
name: 'Download artifact'
-description: 'Generic wrapper for actions/download-artifact'
+description: 'Wrapper for actions/download-artifact'
inputs:
name:
description: 'Name of the artifact to download'
@@ -39,7 +39,7 @@ runs:
using: 'composite'
steps:
- name: Download artifact
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v8
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}
diff --git a/.github/actions/github-script/action.yml b/.github/actions/github-script/action.yml
index ab32da567..50de89b71 100644
--- a/.github/actions/github-script/action.yml
+++ b/.github/actions/github-script/action.yml
@@ -13,7 +13,7 @@ inputs:
runs:
using: 'composite'
steps:
- - uses: actions/github-script@v8
+ - uses: actions/github-script@v9
with:
script: ${{ inputs.script }}
github-token: ${{ inputs.github-token }}
diff --git a/.github/actions/setup-easyscience-bot/action.yml b/.github/actions/setup-easyscience-bot/action.yml
index 4b28eaf8a..e51eb01ac 100644
--- a/.github/actions/setup-easyscience-bot/action.yml
+++ b/.github/actions/setup-easyscience-bot/action.yml
@@ -22,9 +22,9 @@ runs:
steps:
- name: Create GitHub App installation token
id: app-token
- uses: actions/create-github-app-token@v2
+ uses: actions/create-github-app-token@v3
with:
- app-id: ${{ inputs.app-id }}
+ client-id: ${{ inputs.app-id }}
private-key: ${{ inputs.private-key }}
repositories: ${{ inputs.repositories }}
diff --git a/.github/actions/setup-pixi/action.yml b/.github/actions/setup-pixi/action.yml
index 167ee6233..ec7d7ba7b 100644
--- a/.github/actions/setup-pixi/action.yml
+++ b/.github/actions/setup-pixi/action.yml
@@ -1,5 +1,5 @@
name: 'Setup Pixi Environment'
-description: 'Sets up pixi with common configuration'
+description: 'Wrapper for prefix-dev/setup-pixi'
inputs:
environments:
description: 'Pixi environments to setup'
diff --git a/.github/actions/upload-artifact/action.yml b/.github/actions/upload-artifact/action.yml
index 825ac396c..fe8e46807 100644
--- a/.github/actions/upload-artifact/action.yml
+++ b/.github/actions/upload-artifact/action.yml
@@ -1,5 +1,5 @@
name: 'Upload artifact'
-description: 'Generic wrapper for actions/upload-artifact'
+description: 'Wrapper for actions/upload-artifact'
inputs:
name:
description: 'Artifact name'
@@ -38,7 +38,7 @@ runs:
using: 'composite'
steps:
- name: Upload artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}
diff --git a/.github/actions/upload-codecov/action.yml b/.github/actions/upload-codecov/action.yml
index 37d6298aa..0cb15d1fa 100644
--- a/.github/actions/upload-codecov/action.yml
+++ b/.github/actions/upload-codecov/action.yml
@@ -1,5 +1,5 @@
name: 'Upload coverage to Codecov'
-description: 'Generic wrapper for codecov/codecov-action@v5'
+description: 'Wrapper for codecov/codecov-action'
inputs:
name:
@@ -32,7 +32,7 @@ inputs:
runs:
using: composite
steps:
- - uses: codecov/codecov-action@v5
+ - uses: codecov/codecov-action@v6
with:
name: ${{ inputs.name }}
flags: ${{ inputs.flags }}
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index c73390a81..18b01c478 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -2,210 +2,192 @@
## Project Context
-- Python library for crystallographic diffraction analysis, such as
- refinement of the structural model against experimental data.
-- Support for
- - sample_form: powder and single crystal
- - beam_mode: time-of-flight and constant wavelength
- - radiation_probe: neutron and x-ray
- - scattering_type: bragg and total scattering
-- Calculations are done using external calculation libraries:
- - `cryspy` for Bragg diffraction
- - `crysfml` for Bragg diffraction
- - `pdffit2` for Total scattering
-- Follow CIF naming conventions where possible. In some places, we
- deviate for better API design, but we try to keep the spirit of the
- CIF names.
-- Reusing the concept of datablocks and categories from CIF. We have
- `DatablockItem` (structure or experiment) and `DatablockCollection`
- (collection of structures or experiments), as well as `CategoryItem`
- (single categories in CIF) and `CategoryCollection` (loop categories
- in CIF).
+- Python library for crystallographic diffraction analysis (refining
+ structural models against experimental data).
+- Domain axes: `sample_form` (powder, single crystal), `beam_mode`
+ (time-of-flight, constant wavelength), `radiation_probe` (neutron,
+ x-ray), `scattering_type` (bragg, total).
+- Calculation backends: `cryspy` and `crysfml` (Bragg), `pdffit2` (total
+ scattering).
+- CIF maps to `DatablockItem`/`DatablockCollection` and
+ `CategoryItem`/`CategoryCollection` (loops). Follow CIF naming;
+ deviate only for a clearly better API.
- Metadata via frozen dataclasses: `TypeInfo`, `Compatibility`,
`CalculatorSupport`.
-- The API is designed for scientists who use EasyDiffraction as a final
- product in a user-friendly, intuitive way. The target users are not
- software developers and may have little or no Python experience. The
- design is not oriented toward developers building their own tooling on
- top of the library, although experienced developers will find their
- own way. Prioritize discoverability, clear error messages, and safe
- defaults so that non-programmers are not stuck by standard API
- conventions.
-- This project must be developed to be as error-free as possible, with
- the same rigour applied to critical software (e.g. nuclear-plant
- control systems). Every code path must be tested, edge cases must be
- handled explicitly, and silent failures are not acceptable.
+- Audience is scientists, often non-programmers: prioritize
+ discoverability, clear errors, and safe defaults over developer
+ ergonomics.
+- Critical-software rigor: every code path tested, edge cases handled
+ explicitly, no silent failures.
## Code Style
-- Use snake_case for functions and variables, PascalCase for classes,
- and UPPER_SNAKE_CASE for constants.
-- Use `from __future__ import annotations` in every module.
-- Type-annotate all public function signatures.
-- Docstrings on all public classes and methods (numpy style). These must
- include sections Parameters, Returns and Raises, where applicable.
-- Docstring summary must be a single line no longer than 72 characters
- (the `max-doc-length` setting in `pyproject.toml`). If the summary
- does not fit, shorten the wording rather than wrapping to a second
- line.
-- Prefer flat over nested, explicit over clever.
-- Write straightforward code; do not add defensive checks for unlikely
- edge cases.
-- Prefer composition over deep inheritance.
-- One class per file when the class is substantial; group small related
- classes.
-- Avoid `**kwargs`; use explicit keyword arguments for clarity,
- autocomplete, and typo detection.
-- Do not use string-based dispatch (e.g. `getattr(self, f'_{name}')`) to
- route to attributes or methods. Instead, write explicit named methods
- (e.g. `_set_sample_form`, `_set_beam_mode`). This keeps the code
- greppable, autocomplete-friendly, and type-safe.
-- Public parameters and descriptors are either **editable** (property
- with both getter and setter) or **read-only** (property with getter
- only). If internal code needs to mutate a read-only property, add a
- private `_set_` method instead of exposing a public setter.
-- Lint complexity thresholds (`max-args`, `max-branches`,
- `max-statements`, `max-locals`, `max-nested-blocks`, etc. in
- `pyproject.toml`) are intentional code-quality guardrails. They are
- not arbitrary numbers — the project uses ruff's defaults (with
- `max-args` and `max-positional-args` set to 6 instead of 5 to account
- for ruff counting `self`/`cls`). When code violates a threshold, it is
- a signal that the function or class needs refactoring — not that the
- threshold needs raising. Do not raise thresholds, add `# noqa`
- comments, or use any other mechanism to silence complexity violations.
- Instead, refactor the code (extract helpers, introduce parameter
- objects, flatten nesting, etc.). For complex refactors that touch many
- lines or change public API, propose a refactoring plan and wait for
- approval before proceeding.
+- snake_case (functions/vars), PascalCase (classes), UPPER_SNAKE_CASE
+ (constants).
+- `from __future__ import annotations` in every module. Type-annotate
+ all public signatures.
+- Numpy-style docstrings on all public classes/methods (Parameters /
+ Returns / Raises where applicable). Summary is one line ≤72 chars
+ (`max-doc-length`); shorten wording rather than wrap.
+- Flat over nested, explicit over clever, composition over deep
+ inheritance. No defensive checks for unlikely edge cases.
+- 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`).
+- 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.
+- Lint complexity thresholds in `pyproject.toml` (`max-args`,
+ `max-branches`, `max-statements`, `max-locals`, `max-nested-blocks`,
+ …) are guardrails. A violation means refactor (extract helpers,
+ parameter objects, flatten) — do not raise thresholds, add `# noqa`,
+ or otherwise silence them. For complex refactors touching many lines
+ or public API, propose a plan and wait for approval.
## Architecture
-- Eager imports at the top of the module by default. Use lazy imports
- (inside a method body) only when necessary to break circular
- dependencies or to keep `core/` free of heavy utility imports on
- rarely-called paths (e.g. `help()`).
-- No `pkgutil` / `importlib` auto-discovery patterns.
-- No background/daemon threads.
-- No monkey-patching or runtime class mutation.
-- Do not use `__all__` in modules; instead, rely on explicit imports in
- `__init__.py` to control the public API.
-- Do not use redundant `import X as X` aliases in `__init__.py`. Use
- plain `from module import X`.
-- Concrete classes use `@Factory.register` decorators. To trigger
- registration, each package's `__init__.py` must explicitly import
- every concrete class (e.g.
- `from .chebyshev import ChebyshevPolynomialBackground`). When adding a
- new concrete class, always add its import to the corresponding
- `__init__.py`.
-- Switchable categories (those whose implementation can be swapped at
- runtime via a factory) follow a fixed naming convention on the owner
- (experiment, structure, or analysis): `` (read-only
- property), `_type` (getter + setter),
+- Eager top-of-module imports by default. Lazy imports only to break
+ circular deps or to keep `core/` free of heavy imports on rarely-
+ called paths (e.g. `help()`).
+- No `pkgutil`/`importlib` auto-discovery, no background threads, no
+ monkey-patching or runtime class mutation.
+- No `__all__`; control public API via explicit `__init__.py` imports.
+ No redundant `import X as X` aliases.
+- Concrete classes use `@Factory.register`. Each package's `__init__.py`
+ must explicitly import every concrete class to trigger registration —
+ always update it when adding a class.
+- Switchable categories (factory-swappable at runtime) follow this fixed
+ API on the owner (experiment / structure / analysis): ``
+ (read-only), `_type` (getter+setter),
`show_supported__types()`, `show_current__type()`.
- The owner class owns the type setter and the show methods; the show
- methods delegate to `Factory.show_supported(...)` passing context.
- Every factory-created category must have this full API, even if only
- one implementation exists today.
-- Categories are flat siblings within their owner (datablock or
- analysis). A category must never be a child of another category of a
- different type. Categories can reference each other via IDs, but not
- via parent-child nesting.
-- Every finite, closed set of values (factory tags, experiment axes,
- category descriptors with enumerated choices) must use a `(str, Enum)`
- class. Internal code compares against enum members, never raw strings.
-- Keep `core/` free of domain logic — only base classes and utilities.
-- Don't introduce a new abstraction until there is a concrete second use
- case.
-- Don't add dependencies without asking.
-
-## Tutorials
-
-- Jupyter notebooks (`docs/docs/tutorials/*.ipynb`) are **generated
- artifacts** — never edit them by hand. Edit only the corresponding
- `*.py` script, then run `pixi run notebook-prepare` to regenerate the
- notebook.
+ The owner owns the type setter and show methods; show methods delegate
+ to `Factory.show_supported(...)`. Required even if only one
+ implementation exists.
+- Categories are flat siblings within their owner. Never nest a category
+ as a child of another category of a different type; cross-reference
+ via IDs instead.
+- Every finite, closed set of values (factory tags, axes, enumerated
+ descriptors) is a `(str, Enum)`; compare against members, not raw
+ strings.
+- Keep `core/` free of domain logic (base classes and utilities only).
+- Don't introduce abstractions before a concrete second use case. Don't
+ add dependencies without asking.
## Testing
-- Every new module, class, or bug fix must ship with tests. See
- `docs/architecture/architecture.md` §10 for the full test strategy.
-- **Unit tests mirror the source tree:**
+- Every new module, class, or bug fix ships with tests. See
+ `docs/architecture/architecture.md` §10 for the full strategy.
+- Unit tests mirror the source tree:
`src/easydiffraction//.py` →
- `tests/unit/easydiffraction//test_.py`. Run
- `pixi run test-structure-check` to verify.
-- Category packages with only `default.py`/`factory.py` may use a single
- parent-level `test_.py` instead of per-file tests.
-- Supplementary test files use the pattern `test__coverage.py`.
-- Tests that expect `log.error()` to raise must `monkeypatch` Logger to
+ `tests/unit/easydiffraction//test_.py`. Verify with
+ `pixi run test-structure-check`. Supplementary tests:
+ `test__coverage.py`. Category packages with only
+ `default.py`/`factory.py` may use one parent-level
+ `test_.py`.
+- Tests expecting `log.error()` to raise must `monkeypatch` Logger to
RAISE mode (another test may have leaked WARN mode).
- `@typechecked` setters raise `typeguard.TypeCheckError`, not
`TypeError`.
- No test-ordering dependence, no network, no sleeping, no real
calculation engines in unit tests.
-- After adding or modifying tests, run `pixi run unit-tests` and confirm
- all tests pass.
-
-## Changes
-
-- Before implementing any structural or design change (new categories,
- new factories, switchable-category wiring, new datablocks, CIF
- serialisation changes), read `docs/architecture/architecture.md` to
- understand the current design choices and conventions. Follow the
- documented patterns (factory registration, switchable-category naming,
- metadata classification, etc.) to stay consistent with the rest of the
- codebase. For localised bug fixes or test updates, the rules in this
- file are sufficient.
-- The project is in beta; do not keep legacy code or add deprecation
- warnings. Instead, update tests and tutorials to follow the current
- API.
-- Minimal diffs: don't rewrite working code just to reformat it.
-- Never remove or replace existing functionality as part of a new change
- without explicit confirmation. If a refactor would drop features,
- options, or configurations, highlight every removal and wait for
- approval.
-- Fix only what's asked; flag adjacent issues as comments, don't fix
- them silently.
-- Don't add new features or refactor existing code unless explicitly
- asked.
-- Do not remove TODOs or comments unless the change fully resolves them.
+
+## Tutorials
+
+- Notebooks in `docs/docs/tutorials/*.ipynb` are generated artifacts.
+ Edit only the corresponding `*.py`, then run
+ `pixi run notebook-prepare`.
+
+## Change Discipline
+
+- 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.
+- 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;
+ flag adjacent issues as comments. Don't add features or refactor
+ unless asked. Don't remove TODOs or comments unless the change fully
+ 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).
-- Every change should be atomic and self-contained, small enough to be
- described by a single commit message. Make one change, suggest the
- commit message, then stop and wait for confirmation before starting
- the next change.
-- When in doubt, ask for clarification before making changes.
+- 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.
-## Workflow
+## Commits
-- Use a two-phase workflow for all non-trivial changes:
- - **Phase 1 — Implementation:** implement the change (source code,
- docs, architecture updates). Do not create new tests or run existing
- tests. Present the implementation for review and iterate until
- approved.
- - **Phase 2 — Verification:** once the implementation is approved, add
- or update tests, then run linting (`pixi run fix`, `pixi run check`)
- and all test suites (`pixi run unit-tests`,
- `pixi run integration-tests`, `pixi run script-tests`).
-- All open issues, design questions, and planned improvements are
- tracked in `docs/architecture/issues_open.md`, ordered by priority.
- When an issue is fully implemented, move it from that file to
- `docs/architecture/issues_closed.md`. When the resolution affects the
- architecture, update the relevant sections of
- `docs/architecture/architecture.md`.
-- After changes, run linting and formatting fixes with `pixi run fix`.
- This also regenerates `docs/architecture/package-structure-full.md`
- and `docs/architecture/package-structure-short.md` automatically — do
- not edit those files by hand. Do not check what was auto-fixed, just
- accept the fixes and move on. Then, run linting and formatting checks
- with `pixi run check` and address any remaining issues until the code
- is clean.
-- After changes, run unit tests with `pixi run unit-tests`.
-- After changes, run integration tests with
- `pixi run integration-tests`.
-- After changes, run tutorial tests with `pixi run script-tests`.
-- Suggest a concise commit message (as a code block) after each change
- (less than 72 characters, imperative mood, without prefixing with the
- type of change). E.g.:
+- Suggest a commit message after each change: code block, ≤72 chars,
+ imperative mood, no type prefix, no `Co-authored-by: Copilot`.
+ Examples:
- Add ChebyshevPolynomialBackground class
- Implement background_type setter on Experiment
- Standardize switchable-category naming convention
+- Stage only the files modified for the step, using explicit paths where
+ practical. Do not include data, project, CIF, or other generated
+ artifacts produced by integration/script/notebook tests unless the
+ user explicitly asked to update them.
+- Before each commit, inspect the worktree and avoid staging unrelated
+ user changes. If unrelated dirty files exist, leave them untouched and
+ mention them only when relevant.
+
+## Workflow
+
+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 2 — Verification.** Add/update tests, then run `pixi run fix`,
+ `pixi run check`, `pixi run unit-tests`, `pixi run integration-tests`,
+ `pixi run script-tests`.
+
+Notes:
+
+- `pixi run fix` regenerates `docs/architecture/package-structure-*.md`
+ automatically — never edit those by hand. Don't review auto-fixes;
+ accept and move on. Then `pixi run check` until clean.
+- 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.
+
+### Planning
+
+When asked to create a plan:
+
+- 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.
+- Include a status checklist with `[ ]` items; mark `[x]` as completed
+ during implementation.
+- Apply the two-phase workflow (Phase 1 implementation, Phase 2
+ verification) to non-trivial plans. Stop after Phase 1 and ask the
+ user to review before starting Phase 2.
+- The plan must explicitly state that, when an AI agent follows it,
+ 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. Follow the rules in
+ **Commits**. Keep commits atomic, single-purpose, and aligned with
+ plan steps.
+- If implementation uncovers a serious requirement, risk, design issue,
+ or scope change not covered by the plan, stop and ask the user for
+ clarification or approval before proceeding. Record the unresolved
+ issue in the plan when useful.
+- The plan should be easy to maintain while working: include concrete
+ 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.
+- 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
+ understand the benefit. Update it during implementation if extra
+ approved changes become important enough to mention in the PR title or
+ description.
diff --git a/.github/scripts/publish-dashboard.sh b/.github/scripts/publish-dashboard.sh
new file mode 100644
index 000000000..dcbd0b320
--- /dev/null
+++ b/.github/scripts/publish-dashboard.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+remote_repository="${DASHBOARD_REMOTE_REPOSITORY:?}"
+publish_branch="${DASHBOARD_PUBLISH_BRANCH:-master}"
+source_dir="${DASHBOARD_SOURCE_DIR:?}"
+token="${DASHBOARD_TOKEN:?}"
+git_user_name="${DASHBOARD_GIT_USER_NAME:-easyscience[bot]}"
+git_user_email="${DASHBOARD_GIT_USER_EMAIL:?}"
+commit_message="${DASHBOARD_COMMIT_MESSAGE:?}"
+max_attempts="${DASHBOARD_PUSH_ATTEMPTS:-3}"
+delay_seconds="${DASHBOARD_PUSH_DELAY_SECONDS:-15}"
+
+workspace_dir="$(mktemp -d)"
+repo_dir="${workspace_dir}/dashboard"
+remote_url="https://x-access-token:${token}@github.com/${remote_repository}.git"
+
+cleanup() {
+ rm -rf "${workspace_dir}"
+}
+
+prepare_worktree() {
+ if [[ ! -d "${repo_dir}/.git" ]]; then
+ git clone --branch "${publish_branch}" --depth 1 "${remote_url}" "${repo_dir}"
+ else
+ git -C "${repo_dir}" fetch origin "${publish_branch}"
+ git -C "${repo_dir}" checkout "${publish_branch}"
+ git -C "${repo_dir}" reset --hard "origin/${publish_branch}"
+ git -C "${repo_dir}" clean -fd
+ fi
+
+ git -C "${repo_dir}" config user.name "${git_user_name}"
+ git -C "${repo_dir}" config user.email "${git_user_email}"
+}
+
+sync_publish_dir() {
+ cp -R "${source_dir}/." "${repo_dir}/"
+ git -C "${repo_dir}" add .
+
+ if git -C "${repo_dir}" diff --cached --quiet; then
+ return 1
+ fi
+
+ git -C "${repo_dir}" commit -m "${commit_message}"
+}
+
+trap cleanup EXIT
+
+prepare_worktree
+
+if ! sync_publish_dir; then
+ echo "No dashboard changes to publish."
+ exit 0
+fi
+
+for ((attempt = 1; attempt <= max_attempts; attempt += 1)); do
+ if git -C "${repo_dir}" push origin "HEAD:${publish_branch}"; then
+ echo "Dashboard published on attempt ${attempt}."
+ exit 0
+ fi
+
+ if ((attempt == max_attempts)); then
+ echo "Dashboard publish failed after ${max_attempts} attempts." >&2
+ exit 1
+ fi
+
+ echo "Dashboard push attempt ${attempt} failed. Retrying in ${delay_seconds}s." >&2
+ sleep "${delay_seconds}"
+
+ prepare_worktree
+
+ if ! sync_publish_dir; then
+ echo "Dashboard changes already exist in the target repository."
+ exit 0
+ fi
+done
\ No newline at end of file
diff --git a/.github/workflows/backmerge.yml b/.github/workflows/backmerge.yml
index d8569f058..36ce6f547 100644
--- a/.github/workflows/backmerge.yml
+++ b/.github/workflows/backmerge.yml
@@ -20,11 +20,6 @@ concurrency:
group: backmerge-master-into-develop
cancel-in-progress: false
-# Opt into Node.js 24 for all JavaScript actions.
-# Remove once all referenced actions natively target Node 24.
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
jobs:
backmerge:
runs-on: ubuntu-latest
@@ -32,7 +27,7 @@ jobs:
steps:
- name: Checkout repository (for local actions)
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Setup easyscience[bot]
id: bot
@@ -43,7 +38,7 @@ jobs:
repositories: ${{ github.event.repository.name }}
- name: Checkout repository (with bot token)
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ steps.bot.outputs.token }}
diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml
index d3ebf3a20..21c72b38d 100644
--- a/.github/workflows/cleanup.yml
+++ b/.github/workflows/cleanup.yml
@@ -15,11 +15,11 @@ on:
days:
description: 'Number of days.'
required: true
- default: 30
+ default: '30'
minimum_runs:
description: 'The minimum runs to keep for each workflow.'
required: true
- default: 6
+ default: '6'
delete_workflow_pattern:
description:
'The name or filename of the workflow. if not set then it will target all
@@ -61,11 +61,6 @@ on:
- 'false'
- 'true'
-# Opt into Node.js 24 for all JavaScript actions.
-# Remove once all referenced actions natively target Node 24.
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
jobs:
del-runs:
runs-on: ubuntu-latest
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 1ad931394..95c190c63 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -26,9 +26,6 @@ concurrency:
# Set the environment variables to be used in all jobs defined in this workflow
env:
CI_BRANCH: ${{ github.head_ref || github.ref_name }}
- # Opt into Node.js 24 for all JavaScript actions.
- # Remove once all referenced actions natively target Node 24.
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# Job 1: Run docstring coverage
@@ -37,7 +34,7 @@ jobs:
steps:
- name: Check-out repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Set up pixi
uses: ./.github/actions/setup-pixi
@@ -51,7 +48,7 @@ jobs:
steps:
- name: Check-out repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Set up pixi
uses: ./.github/actions/setup-pixi
@@ -74,7 +71,7 @@ jobs:
steps:
- name: Check-out repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Set up pixi
uses: ./.github/actions/setup-pixi
diff --git a/.github/workflows/dashboard.yml b/.github/workflows/dashboard.yml
index 685134276..9d1f2b0bd 100644
--- a/.github/workflows/dashboard.yml
+++ b/.github/workflows/dashboard.yml
@@ -7,6 +7,10 @@ on:
permissions:
contents: read
+concurrency:
+ group: dashboard-publish-${{ github.repository }}
+ cancel-in-progress: false
+
# Set the environment variables to be used in all jobs defined in this workflow
env:
CI_BRANCH: ${{ github.head_ref || github.ref_name }}
@@ -14,9 +18,6 @@ env:
DEVELOP_BRANCH: develop
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
- # Opt into Node.js 24 for all JavaScript actions.
- # Remove once all referenced actions natively target Node 24.
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
dashboard:
@@ -24,7 +25,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -84,22 +85,25 @@ jobs:
${{ github.event.repository.name }}
dashboard
- # Publish to external dashboard repository with retry logic.
+ # Push to external dashboard repository with retry logic.
# Retry is needed to handle transient GitHub API/authentication issues
# that occasionally cause 403 errors when multiple workflows push concurrently.
# Uses personal_token (not github_token) as GITHUB_TOKEN cannot access external repos.
- - name: Publish to main branch of ${{ github.repository }}
- uses: Wandalen/wretry.action@v3.8.0
- with:
- attempt_limit: 3
- attempt_delay: 15000 # 15 seconds between retries
- action: peaceiris/actions-gh-pages@v4
- with: |
- publish_dir: ./_dashboard_publish
- keep_files: true
- external_repository: ${{ env.REPO_OWNER }}/dashboard
- publish_branch: master
- personal_token: ${{ steps.bot.outputs.token }}
+ - name:
+ Push to ${{ env.REPO_OWNER }}/dashboard/${{ env.REPO_NAME }}/${{ env.CI_BRANCH
+ }}
+ shell: bash
+ env:
+ DASHBOARD_COMMIT_MESSAGE: ${{ env.CI_BRANCH }}
+ DASHBOARD_GIT_USER_EMAIL:
+ ${{ vars.EASYSCIENCE_APP_ID }}+easyscience[bot]@users.noreply.github.com
+ DASHBOARD_PUSH_ATTEMPTS: '3'
+ DASHBOARD_PUSH_DELAY_SECONDS: '15'
+ DASHBOARD_PUBLISH_BRANCH: master
+ DASHBOARD_REMOTE_REPOSITORY: ${{ env.REPO_OWNER }}/dashboard
+ DASHBOARD_SOURCE_DIR: ./_dashboard_publish
+ DASHBOARD_TOKEN: ${{ steps.bot.outputs.token }}
+ run: bash ./.github/scripts/publish-dashboard.sh
- name: Add dashboard link to summary
run: |
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 870d6529f..117416a3f 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -48,9 +48,6 @@ env:
IS_RELEASE_TAG: ${{ startsWith(github.ref, 'refs/tags/v') }}
GITHUB_REPOSITORY: ${{ github.repository }}
NOTEBOOKS_DIR: tutorials
- # Opt into Node.js 24 for all JavaScript actions.
- # Remove once all referenced actions natively target Node 24.
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# Single job that builds and deploys documentation.
@@ -86,7 +83,7 @@ jobs:
# Check out the repository source code.
# Note: The gh-pages branch is fetched separately later for mike deployment.
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
# Activate dark mode to create documentation with Plotly charts in dark mode
# Need a better solution to automatically switch the chart colour theme based on the mkdocs material switcher
diff --git a/.github/workflows/issues-labels.yml b/.github/workflows/issues-labels.yml
index ed9d1c8ba..31a69ad20 100644
--- a/.github/workflows/issues-labels.yml
+++ b/.github/workflows/issues-labels.yml
@@ -11,11 +11,6 @@ on:
permissions:
issues: write
-# Opt into Node.js 24 for all JavaScript actions.
-# Remove once all referenced actions natively target Node 24.
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
jobs:
check-labels:
if: github.actor != 'easyscience[bot]'
@@ -28,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Setup easyscience[bot]
id: bot
diff --git a/.github/workflows/lint-format.yml b/.github/workflows/lint-format.yml
index 720c9a4ab..16dd95c95 100644
--- a/.github/workflows/lint-format.yml
+++ b/.github/workflows/lint-format.yml
@@ -34,9 +34,6 @@ permissions:
# Set the environment variables to be used in all jobs defined in this workflow
env:
CI_BRANCH: ${{ github.head_ref || github.ref_name }}
- # Opt into Node.js 24 for all JavaScript actions.
- # Remove once all referenced actions natively target Node 24.
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
lint-format:
@@ -44,7 +41,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Set up pixi
uses: ./.github/actions/setup-pixi
diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml
index 263d2200b..c51b8f865 100644
--- a/.github/workflows/pypi-publish.yml
+++ b/.github/workflows/pypi-publish.yml
@@ -10,11 +10,6 @@ on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
-# Opt into Node.js 24 for all JavaScript actions.
-# Remove once all referenced actions natively target Node 24.
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
jobs:
pypi-publish:
runs-on: ubuntu-latest
@@ -25,7 +20,7 @@ jobs:
steps:
- name: Check-out repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
fetch-depth: 0 # full history with tags to get the version number by versioningit
diff --git a/.github/workflows/pypi-test.yml b/.github/workflows/pypi-test.yml
index a472c1d1f..8266285b1 100644
--- a/.github/workflows/pypi-test.yml
+++ b/.github/workflows/pypi-test.yml
@@ -20,9 +20,6 @@ permissions:
env:
CI_BRANCH: ${{ github.head_ref || github.ref_name }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
- # Opt into Node.js 24 for all JavaScript actions.
- # Remove once all referenced actions natively target Node 24.
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# Job 1: Test installation from PyPI on multiple OS
@@ -35,7 +32,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Set up pixi
uses: ./.github/actions/setup-pixi
diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml
index 3a7cf92ee..a8f7761a7 100644
--- a/.github/workflows/release-notes.yml
+++ b/.github/workflows/release-notes.yml
@@ -10,11 +10,6 @@ on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
-# Opt into Node.js 24 for all JavaScript actions.
-# Remove once all referenced actions natively target Node 24.
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
jobs:
draft-release-notes:
permissions:
@@ -25,7 +20,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
fetch-depth: 0 # full history with tags to get the version number
@@ -66,7 +61,7 @@ jobs:
GITHUB_TOKEN: ${{ steps.bot.outputs.token }}
- name: Create GitHub draft release
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
with:
draft: true
tag_name: ${{ steps.draft.outputs.tag_name }}
diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml
index 5694a2a0a..0e716c27e 100644
--- a/.github/workflows/release-pr.yml
+++ b/.github/workflows/release-pr.yml
@@ -24,16 +24,13 @@ permissions:
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
SOURCE_BRANCH: ${{ inputs.source_branch || 'develop' }}
- # Opt into Node.js 24 for all JavaScript actions.
- # Remove once all referenced actions natively target Node 24.
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
create-pull-request:
runs-on: ubuntu-latest
steps:
- name: Checkout ${{ env.SOURCE_BRANCH }} branch
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
ref: ${{ env.SOURCE_BRANCH }}
diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml
index a53e9afc0..0f43db357 100644
--- a/.github/workflows/security.yml
+++ b/.github/workflows/security.yml
@@ -44,11 +44,6 @@ permissions:
contents: read
security-events: write
-# Opt into Node.js 24 for all JavaScript actions.
-# Remove once all referenced actions natively target Node 24.
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
jobs:
codeql:
name: Code scanning
@@ -66,13 +61,13 @@ jobs:
# focused on the active dev branch.
- name: Checkout repository (scheduled → develop)
if: ${{ github.event_name == 'schedule' }}
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
ref: develop
- name: Checkout repository
if: ${{ github.event_name != 'schedule' }}
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
diff --git a/.github/workflows/test-trigger.yml b/.github/workflows/test-trigger.yml
index f4a39e93d..440bae3c1 100644
--- a/.github/workflows/test-trigger.yml
+++ b/.github/workflows/test-trigger.yml
@@ -10,18 +10,13 @@ on:
permissions:
contents: read
-# Opt into Node.js 24 for all JavaScript actions.
-# Remove once all referenced actions natively target Node 24.
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
jobs:
code-tests-trigger:
runs-on: ubuntu-latest
steps:
- name: Checkout develop branch
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
ref: develop
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 801b8803f..d0298f95b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -46,9 +46,6 @@ env:
CI_BRANCH: ${{ github.head_ref || github.ref_name }}
PY_VERSIONS: '3.12 3.14'
PIXI_ENVS: 'py-312-env py-314-env'
- # Opt into Node.js 24 for all JavaScript actions.
- # Remove once all referenced actions natively target Node 24.
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# Job 1: Set up environment variables
@@ -83,7 +80,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Set up pixi
uses: ./.github/actions/setup-pixi
@@ -187,7 +184,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Download package (incl. extras) from previous job
uses: ./.github/actions/download-artifact
diff --git a/.github/workflows/tutorial-tests-colab.yaml b/.github/workflows/tutorial-tests-colab.yaml
index 30966aaaa..96bc7e856 100644
--- a/.github/workflows/tutorial-tests-colab.yaml
+++ b/.github/workflows/tutorial-tests-colab.yaml
@@ -24,22 +24,4 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v5
-
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
- with:
- python-version: ${{ matrix.python-version }}
-
- - name: Upgrade package installer for Python
- run: python -m pip install --upgrade pip
-
- - name: Install Python dependencies
- run:
- python -m pip install 'easydiffraction[visualization]' nbconvert nbmake pytest
- pytest-xdist
-
- - name: Check if Jupyter Notebooks run without errors
- run: >
- python -m pytest --nbmake docs/tutorials/ --nbmake-timeout=1200 --color=yes
- -n=auto
+ uses: actions/checkout@v6
diff --git a/.github/workflows/tutorial-tests-trigger.yml b/.github/workflows/tutorial-tests-trigger.yml
index f28d65279..4b160d87d 100644
--- a/.github/workflows/tutorial-tests-trigger.yml
+++ b/.github/workflows/tutorial-tests-trigger.yml
@@ -10,18 +10,13 @@ on:
permissions:
contents: read
-# Opt into Node.js 24 for all JavaScript actions.
-# Remove once all referenced actions natively target Node 24.
-env:
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
-
jobs:
tutorial-tests-trigger:
runs-on: ubuntu-latest
steps:
- name: Checkout develop branch
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
ref: develop
diff --git a/.github/workflows/tutorial-tests.yml b/.github/workflows/tutorial-tests.yml
index 34a5c955c..f924a31d3 100644
--- a/.github/workflows/tutorial-tests.yml
+++ b/.github/workflows/tutorial-tests.yml
@@ -27,9 +27,6 @@ concurrency:
# Set the environment variables to be used in all jobs defined in this workflow
env:
CI_BRANCH: ${{ github.head_ref || github.ref_name }}
- # Opt into Node.js 24 for all JavaScript actions.
- # Remove once all referenced actions natively target Node 24.
- FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# Job 1: Test tutorials as scripts and notebooks on multiple OS
@@ -43,7 +40,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Set up pixi
uses: ./.github/actions/setup-pixi
diff --git a/README.md b/README.md
index a9fccc670..c6b665d17 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,9 @@
-**EasyDiffraction** is a software for calculating neutron powder
-diffraction patterns based on a structural model and refining its
-parameters against experimental data.
+**EasyDiffraction** is a software for calculating diffraction patterns
+based on a structural model and refining its parameters against
+experimental data.
diff --git a/docs/architecture/package-structure-full.md b/docs/architecture/package-structure-full.md
index 765ac077a..be35bb076 100644
--- a/docs/architecture/package-structure-full.md
+++ b/docs/architecture/package-structure-full.md
@@ -6,6 +6,7 @@
│ ├── 📁 calculators
│ │ ├── 📄 __init__.py
│ │ ├── 📄 base.py
+│ │ │ ├── 🏷️ class PowderReflnRecord
│ │ │ └── 🏷️ class CalculatorBase
│ │ ├── 📄 crysfml.py
│ │ │ └── 🏷️ class CrysfmlCalculator
@@ -169,9 +170,6 @@
│ │ │ │ │ ├── 🏷️ class PdDataBase
│ │ │ │ │ ├── 🏷️ class PdCwlData
│ │ │ │ │ └── 🏷️ class PdTofData
-│ │ │ │ ├── 📄 bragg_sc.py
-│ │ │ │ │ ├── 🏷️ class Refln
-│ │ │ │ │ └── 🏷️ class ReflnData
│ │ │ │ ├── 📄 factory.py
│ │ │ │ │ └── 🏷️ class DataFactory
│ │ │ │ └── 📄 total_pd.py
@@ -257,6 +255,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
@@ -331,9 +343,12 @@
│ │ ├── 📄 ascii.py
│ │ │ └── 🏷️ class AsciiPlotter
│ │ ├── 📄 base.py
+│ │ │ ├── 🏷️ class BraggTickSet
+│ │ │ ├── 🏷️ class PowderMeasVsCalcSpec
│ │ │ ├── 🏷️ class XAxisType
│ │ │ └── 🏷️ class PlotterBase
│ │ └── 📄 plotly.py
+│ │ ├── 🏷️ class PowderCompositeRows
│ │ └── 🏷️ class PlotlyPlotter
│ ├── 📁 tablers
│ │ ├── 📄 __init__.py
@@ -349,6 +364,8 @@
│ │ └── 🏷️ class RendererFactoryBase
│ ├── 📄 plotting.py
│ │ ├── 🏷️ class PlotterEngineEnum
+│ │ ├── 🏷️ class _MeasVsCalcPlotOptions
+│ │ ├── 🏷️ class _PowderMeasVsCalcSeries
│ │ ├── 🏷️ class Plotter
│ │ └── 🏷️ class PlotterFactory
│ ├── 📄 tables.py
diff --git a/docs/architecture/package-structure-short.md b/docs/architecture/package-structure-short.md
index 30b4daf72..ba2a0b33b 100644
--- a/docs/architecture/package-structure-short.md
+++ b/docs/architecture/package-structure-short.md
@@ -85,7 +85,6 @@
│ │ │ ├── 📁 data
│ │ │ │ ├── 📄 __init__.py
│ │ │ │ ├── 📄 bragg_pd.py
-│ │ │ │ ├── 📄 bragg_sc.py
│ │ │ │ ├── 📄 factory.py
│ │ │ │ └── 📄 total_pd.py
│ │ │ ├── 📁 diffrn
@@ -128,6 +127,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/architecture/ROADMAP.md b/docs/dev/ROADMAP.md
similarity index 100%
rename from docs/architecture/ROADMAP.md
rename to docs/dev/ROADMAP.md
diff --git a/docs/architecture/adp_implementation.md b/docs/dev/adp_implementation.md
similarity index 100%
rename from docs/architecture/adp_implementation.md
rename to docs/dev/adp_implementation.md
diff --git a/docs/architecture/architecture.md b/docs/dev/architecture.md
similarity index 98%
rename from docs/architecture/architecture.md
rename to docs/dev/architecture.md
index c3ed05789..6cb8c67e2 100644
--- a/docs/architecture/architecture.md
+++ b/docs/dev/architecture.md
@@ -286,7 +286,8 @@ experiment.linked_phases # CategoryCollection
experiment.excluded_regions # CategoryCollection
experiment.instrument # CategoryItem
experiment.peak # CategoryItem
-experiment.data # CategoryCollection
+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(...)
@@ -461,7 +462,8 @@ from .line_segment import LineSegmentBackground
| `BackgroundFactory` | Background categories | `LineSegmentBackground`, `ChebyshevPolynomialBackground` |
| `PeakFactory` | Peak profiles | `CwlPseudoVoigt`, `TofJorgensen`, `TofJorgensenVonDreele`, … |
| `InstrumentFactory` | Instruments | `CwlPdInstrument`, `TofPdInstrument`, … |
-| `DataFactory` | Data collections | `PdCwlData`, `PdTofData`, `ReflnData`, `TotalData` |
+| `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` |
@@ -552,11 +554,18 @@ the choice.
| Tag | Class |
| -------------- | ----------- |
-| `bragg-pd-cwl` | `PdCwlData` |
+| `bragg-pd` | `PdCwlData` |
| `bragg-pd-tof` | `PdTofData` |
-| `bragg-sc` | `ReflnData` |
| `total-pd` | `TotalData` |
+**Refln tags**
+
+| Tag | Class |
+| -------------------- | -------------------- |
+| `bragg-sc` | `ReflnData` |
+| `bragg-pd-refln` | `PowderCwlReflnData` |
+| `bragg-pd-tof-refln` | `PowderTofReflnData` |
+
**Extinction tags**
| Tag | Class |
@@ -650,7 +659,9 @@ line-segment points.
| `PdCwlData` | `DataFactory` |
| `PdTofData` | `DataFactory` |
| `TotalData` | `DataFactory` |
-| `ReflnData` | `DataFactory` |
+| `ReflnData` | `ReflnFactory` |
+| `PowderCwlReflnData` | `ReflnFactory` |
+| `PowderTofReflnData` | `ReflnFactory` |
| `ExcludedRegions` | `ExcludedRegionsFactory` |
| `LinkedPhases` | `LinkedPhasesFactory` |
| `AtomSites` | `AtomSitesFactory` |
@@ -700,8 +711,9 @@ line-segment points.
The calculator performs the actual diffraction computation. It is
attached per-experiment on the `ExperimentBase` object. Each experiment
-auto-resolves its calculator on first access based on the data
-category's `calculator_support` metadata and
+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).
@@ -712,8 +724,8 @@ The experiment exposes a dedicated `calculation` category:
backend tag
- `calculation.calculator` — read-only access to the live backend
instance
-- `calculation.show_calculator_types()` — filtered by data category
- support and marks the current type
+- `calculation.show_calculator_types()` — filtered by the active support
+ category and marks the current type
### 6.2 Minimiser
@@ -1067,7 +1079,7 @@ 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`.
+- **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
@@ -1140,7 +1152,7 @@ project.display.show_tabler_types()
```
Available calculators are filtered by `engine_imported` (whether the
-library is installed) and by the experiment's data category
+library is installed) and by the experiment's active support category
`calculator_support` metadata.
### 9.6 Enums for Finite Value Sets
diff --git a/docs/architecture/cryspy-dwf-bug.md b/docs/dev/cryspy-dwf-bug.md
similarity index 100%
rename from docs/architecture/cryspy-dwf-bug.md
rename to docs/dev/cryspy-dwf-bug.md
diff --git a/docs/architecture/issues_closed.md b/docs/dev/issues_closed.md
similarity index 100%
rename from docs/architecture/issues_closed.md
rename to docs/dev/issues_closed.md
diff --git a/docs/architecture/issues_open.md b/docs/dev/issues_open.md
similarity index 97%
rename from docs/architecture/issues_open.md
rename to docs/dev/issues_open.md
index 298cf9a67..014aa4675 100644
--- a/docs/architecture/issues_open.md
+++ b/docs/dev/issues_open.md
@@ -621,6 +621,50 @@ are also marked.
---
+## 93. 🟢 Decide Future of `show_residual` in `plot_meas_vs_calc`
+
+**Type:** API cleanup
+
+Powder Bragg plots now show the residual row by default when
+`show_residual=None`, but the public `show_residual` argument still
+exists and some call sites still pass `show_residual=True` explicitly.
+The API should be clarified: either keep the argument as a compatibility
+option, remove it, or standardize a single meaning across powder and
+single-crystal plots.
+
+**TODOs:**
+
+- [plotting.py](src/easydiffraction/display/plotting.py#L459)
+- [\_\_main\_\_.py](src/easydiffraction/__main__.py#L105)
+
+**Depends on:** nothing.
+
+---
+
+## 94. 🟢 Revisit Powder `refln` Phase Labels and Row IDs
+
+**Type:** Naming / CIF UX
+
+The implemented powder reflection category uses `phase_id` throughout
+(`experiment.refln`, `PowderReflnRecord`, Bragg tick labels) and assigns
+global sequential row ids. This matches the current implementation, but
+the archived planning notes left two follow-up questions open:
+
+1. whether `structure_id` would be clearer than `phase_id` in the public
+ API / CIF output, and
+2. whether phase-prefixed row ids would make CIF inspection and
+ debugging easier than simple `1`, `2`, `3`, ...
+
+**TODOs:**
+
+- [refln_pd.py](src/easydiffraction/datablocks/experiment/categories/data/refln_pd.py#L37)
+- [refln_pd.py](src/easydiffraction/datablocks/experiment/categories/data/refln_pd.py#L154)
+- [base.py](src/easydiffraction/display/plotters/base.py#L24)
+
+**Depends on:** nothing.
+
+---
+
## 40. 🟢 Implement Resetting `.constrained` to `False`
**Type:** Feature
diff --git a/docs/dev/package-structure-full.md b/docs/dev/package-structure-full.md
new file mode 100644
index 000000000..ae233b21d
--- /dev/null
+++ b/docs/dev/package-structure-full.md
@@ -0,0 +1,412 @@
+# Package Structure (full)
+
+```
+📦 easydiffraction
+├── 📁 analysis
+│ ├── 📁 calculators
+│ │ ├── 📄 __init__.py
+│ │ ├── 📄 base.py
+│ │ │ └── 🏷️ class CalculatorBase
+│ │ ├── 📄 crysfml.py
+│ │ │ └── 🏷️ class CrysfmlCalculator
+│ │ ├── 📄 cryspy.py
+│ │ │ └── 🏷️ class CryspyCalculator
+│ │ ├── 📄 factory.py
+│ │ │ └── 🏷️ class CalculatorFactory
+│ │ └── 📄 pdffit.py
+│ │ └── 🏷️ class PdffitCalculator
+│ ├── 📁 categories
+│ │ ├── 📁 aliases
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 default.py
+│ │ │ │ ├── 🏷️ class Alias
+│ │ │ │ └── 🏷️ class Aliases
+│ │ │ └── 📄 factory.py
+│ │ │ └── 🏷️ class AliasesFactory
+│ │ ├── 📁 constraints
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 default.py
+│ │ │ │ ├── 🏷️ class Constraint
+│ │ │ │ └── 🏷️ class Constraints
+│ │ │ └── 📄 factory.py
+│ │ │ └── 🏷️ class ConstraintsFactory
+│ │ ├── 📁 fit
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 default.py
+│ │ │ │ └── 🏷️ class Fit
+│ │ │ ├── 📄 enums.py
+│ │ │ │ └── 🏷️ class FitModeEnum
+│ │ │ └── 📄 factory.py
+│ │ │ └── 🏷️ class FitFactory
+│ │ ├── 📁 joint_fit_experiments
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 default.py
+│ │ │ │ ├── 🏷️ class JointFitExperiment
+│ │ │ │ └── 🏷️ class JointFitExperiments
+│ │ │ └── 📄 factory.py
+│ │ │ └── 🏷️ class JointFitExperimentsFactory
+│ │ └── 📄 __init__.py
+│ ├── 📁 fit_helpers
+│ │ ├── 📄 __init__.py
+│ │ ├── 📄 metrics.py
+│ │ ├── 📄 reporting.py
+│ │ │ └── 🏷️ class FitResults
+│ │ └── 📄 tracking.py
+│ │ ├── 🏷️ class _TerminalLiveHandle
+│ │ └── 🏷️ class FitProgressTracker
+│ ├── 📁 minimizers
+│ │ ├── 📄 __init__.py
+│ │ ├── 📄 base.py
+│ │ │ └── 🏷️ class MinimizerBase
+│ │ ├── 📄 bumps.py
+│ │ │ ├── 🏷️ class _EasyDiffractionFitness
+│ │ │ └── 🏷️ class BumpsMinimizer
+│ │ ├── 📄 bumps_amoeba.py
+│ │ │ └── 🏷️ class BumpsAmoebaMinimizer
+│ │ ├── 📄 bumps_de.py
+│ │ │ └── 🏷️ class BumpsDEMinimizer
+│ │ ├── 📄 bumps_lm.py
+│ │ │ └── 🏷️ class BumpsLmMinimizer
+│ │ ├── 📄 dfols.py
+│ │ │ └── 🏷️ class DfolsMinimizer
+│ │ ├── 📄 enums.py
+│ │ │ └── 🏷️ class MinimizerTypeEnum
+│ │ ├── 📄 factory.py
+│ │ │ └── 🏷️ class MinimizerFactory
+│ │ ├── 📄 lmfit.py
+│ │ │ └── 🏷️ class LmfitMinimizer
+│ │ ├── 📄 lmfit_least_squares.py
+│ │ │ └── 🏷️ class LmfitLeastSquaresMinimizer
+│ │ └── 📄 lmfit_leastsq.py
+│ │ └── 🏷️ class LmfitLeastsqMinimizer
+│ ├── 📄 __init__.py
+│ ├── 📄 analysis.py
+│ │ ├── 🏷️ class AnalysisDisplay
+│ │ └── 🏷️ class Analysis
+│ ├── 📄 fitting.py
+│ │ └── 🏷️ class Fitter
+│ └── 📄 sequential.py
+│ └── 🏷️ class SequentialFitTemplate
+├── 📁 core
+│ ├── 📄 __init__.py
+│ ├── 📄 category.py
+│ │ ├── 🏷️ class CategoryItem
+│ │ └── 🏷️ class CategoryCollection
+│ ├── 📄 collection.py
+│ │ └── 🏷️ class CollectionBase
+│ ├── 📄 datablock.py
+│ │ ├── 🏷️ class DatablockItem
+│ │ └── 🏷️ class DatablockCollection
+│ ├── 📄 diagnostic.py
+│ │ └── 🏷️ class Diagnostics
+│ ├── 📄 factory.py
+│ │ └── 🏷️ class FactoryBase
+│ ├── 📄 guard.py
+│ │ └── 🏷️ class GuardedBase
+│ ├── 📄 identity.py
+│ │ └── 🏷️ class Identity
+│ ├── 📄 metadata.py
+│ │ ├── 🏷️ class TypeInfo
+│ │ ├── 🏷️ class Compatibility
+│ │ └── 🏷️ class CalculatorSupport
+│ ├── 📄 singleton.py
+│ │ ├── 🏷️ class SingletonBase
+│ │ └── 🏷️ class ConstraintsHandler
+│ ├── 📄 validation.py
+│ │ ├── 🏷️ class DataTypeHints
+│ │ ├── 🏷️ class DataTypes
+│ │ ├── 🏷️ class ValidationStage
+│ │ ├── 🏷️ class ValidatorBase
+│ │ ├── 🏷️ class TypeValidator
+│ │ ├── 🏷️ class RangeValidator
+│ │ ├── 🏷️ class MembershipValidator
+│ │ ├── 🏷️ class RegexValidator
+│ │ └── 🏷️ class AttributeSpec
+│ └── 📄 variable.py
+│ ├── 🏷️ class GenericDescriptorBase
+│ ├── 🏷️ class GenericStringDescriptor
+│ ├── 🏷️ class GenericNumericDescriptor
+│ ├── 🏷️ class GenericParameter
+│ ├── 🏷️ class StringDescriptor
+│ ├── 🏷️ class NumericDescriptor
+│ └── 🏷️ class Parameter
+├── 📁 crystallography
+│ ├── 📄 __init__.py
+│ ├── 📄 crystallography.py
+│ └── 📄 space_groups.py
+│ └── 🏷️ class _RestrictedUnpickler
+├── 📁 datablocks
+│ ├── 📁 experiment
+│ │ ├── 📁 categories
+│ │ │ ├── 📁 background
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 base.py
+│ │ │ │ │ └── 🏷️ class BackgroundBase
+│ │ │ │ ├── 📄 chebyshev.py
+│ │ │ │ │ ├── 🏷️ class PolynomialTerm
+│ │ │ │ │ └── 🏷️ class ChebyshevPolynomialBackground
+│ │ │ │ ├── 📄 enums.py
+│ │ │ │ │ └── 🏷️ class BackgroundTypeEnum
+│ │ │ │ ├── 📄 factory.py
+│ │ │ │ │ └── 🏷️ class BackgroundFactory
+│ │ │ │ └── 📄 line_segment.py
+│ │ │ │ ├── 🏷️ class LineSegment
+│ │ │ │ └── 🏷️ class LineSegmentBackground
+│ │ │ ├── 📁 calculation
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ │ └── 🏷️ class Calculation
+│ │ │ │ └── 📄 factory.py
+│ │ │ │ └── 🏷️ class CalculationFactory
+│ │ │ ├── 📁 data
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 bragg_pd.py
+│ │ │ │ │ ├── 🏷️ class PdDataPointBaseMixin
+│ │ │ │ │ ├── 🏷️ class PdCwlDataPointMixin
+│ │ │ │ │ ├── 🏷️ class PdTofDataPointMixin
+│ │ │ │ │ ├── 🏷️ class PdCwlDataPoint
+│ │ │ │ │ ├── 🏷️ class PdTofDataPoint
+│ │ │ │ │ ├── 🏷️ class PdDataBase
+│ │ │ │ │ ├── 🏷️ class PdCwlData
+│ │ │ │ │ └── 🏷️ class PdTofData
+│ │ │ │ ├── 📄 bragg_sc.py
+│ │ │ │ │ ├── 🏷️ class Refln
+│ │ │ │ │ └── 🏷️ class ReflnData
+│ │ │ │ ├── 📄 factory.py
+│ │ │ │ │ └── 🏷️ class DataFactory
+│ │ │ │ └── 📄 total_pd.py
+│ │ │ │ ├── 🏷️ class TotalDataPoint
+│ │ │ │ ├── 🏷️ class TotalDataBase
+│ │ │ │ └── 🏷️ class TotalData
+│ │ │ ├── 📁 diffrn
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ │ └── 🏷️ class DefaultDiffrn
+│ │ │ │ └── 📄 factory.py
+│ │ │ │ └── 🏷️ class DiffrnFactory
+│ │ │ ├── 📁 excluded_regions
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ │ ├── 🏷️ class ExcludedRegion
+│ │ │ │ │ └── 🏷️ class ExcludedRegions
+│ │ │ │ └── 📄 factory.py
+│ │ │ │ └── 🏷️ class ExcludedRegionsFactory
+│ │ │ ├── 📁 experiment_type
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ │ └── 🏷️ class ExperimentType
+│ │ │ │ └── 📄 factory.py
+│ │ │ │ └── 🏷️ class ExperimentTypeFactory
+│ │ │ ├── 📁 extinction
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 becker_coppens.py
+│ │ │ │ │ └── 🏷️ class BeckerCoppensExtinction
+│ │ │ │ └── 📄 factory.py
+│ │ │ │ └── 🏷️ class ExtinctionFactory
+│ │ │ ├── 📁 instrument
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 base.py
+│ │ │ │ │ └── 🏷️ class InstrumentBase
+│ │ │ │ ├── 📄 cwl.py
+│ │ │ │ │ ├── 🏷️ class CwlInstrumentBase
+│ │ │ │ │ ├── 🏷️ class CwlScInstrument
+│ │ │ │ │ └── 🏷️ class CwlPdInstrument
+│ │ │ │ ├── 📄 factory.py
+│ │ │ │ │ └── 🏷️ class InstrumentFactory
+│ │ │ │ └── 📄 tof.py
+│ │ │ │ ├── 🏷️ class TofScInstrument
+│ │ │ │ └── 🏷️ class TofPdInstrument
+│ │ │ ├── 📁 linked_crystal
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ │ └── 🏷️ class LinkedCrystal
+│ │ │ │ └── 📄 factory.py
+│ │ │ │ └── 🏷️ class LinkedCrystalFactory
+│ │ │ ├── 📁 linked_phases
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ │ ├── 🏷️ class LinkedPhase
+│ │ │ │ │ └── 🏷️ class LinkedPhases
+│ │ │ │ └── 📄 factory.py
+│ │ │ │ └── 🏷️ class LinkedPhasesFactory
+│ │ │ ├── 📁 peak
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 base.py
+│ │ │ │ │ └── 🏷️ class PeakBase
+│ │ │ │ ├── 📄 cwl.py
+│ │ │ │ │ ├── 🏷️ class CwlPseudoVoigt
+│ │ │ │ │ ├── 🏷️ class CwlPseudoVoigtEmpiricalAsymmetry
+│ │ │ │ │ └── 🏷️ class CwlThompsonCoxHastings
+│ │ │ │ ├── 📄 cwl_mixins.py
+│ │ │ │ │ ├── 🏷️ class CwlBroadeningMixin
+│ │ │ │ │ ├── 🏷️ class EmpiricalAsymmetryMixin
+│ │ │ │ │ └── 🏷️ class FcjAsymmetryMixin
+│ │ │ │ ├── 📄 factory.py
+│ │ │ │ │ └── 🏷️ class PeakFactory
+│ │ │ │ ├── 📄 tof.py
+│ │ │ │ │ ├── 🏷️ class TofPseudoVoigt
+│ │ │ │ │ ├── 🏷️ class TofJorgensen
+│ │ │ │ │ ├── 🏷️ class TofJorgensenVonDreele
+│ │ │ │ │ └── 🏷️ class TofDoubleJorgensenVonDreele
+│ │ │ │ ├── 📄 tof_mixins.py
+│ │ │ │ │ ├── 🏷️ class TofGaussianBroadeningMixin
+│ │ │ │ │ ├── 🏷️ class TofLorentzianBroadeningMixin
+│ │ │ │ │ ├── 🏷️ class TofBackToBackExponentialMixin
+│ │ │ │ │ └── 🏷️ class TofDoubleExponentialMixin
+│ │ │ │ ├── 📄 total.py
+│ │ │ │ │ └── 🏷️ class TotalGaussianDampedSinc
+│ │ │ │ └── 📄 total_mixins.py
+│ │ │ │ └── 🏷️ class TotalBroadeningMixin
+│ │ │ └── 📄 __init__.py
+│ │ ├── 📁 item
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 base.py
+│ │ │ │ ├── 🏷️ class ExperimentBase
+│ │ │ │ ├── 🏷️ class ScExperimentBase
+│ │ │ │ └── 🏷️ class PdExperimentBase
+│ │ │ ├── 📄 bragg_pd.py
+│ │ │ │ └── 🏷️ class BraggPdExperiment
+│ │ │ ├── 📄 bragg_sc.py
+│ │ │ │ ├── 🏷️ class CwlScExperiment
+│ │ │ │ └── 🏷️ class TofScExperiment
+│ │ │ ├── 📄 enums.py
+│ │ │ │ ├── 🏷️ class SampleFormEnum
+│ │ │ │ ├── 🏷️ class ScatteringTypeEnum
+│ │ │ │ ├── 🏷️ class RadiationProbeEnum
+│ │ │ │ ├── 🏷️ class BeamModeEnum
+│ │ │ │ ├── 🏷️ class CalculatorEnum
+│ │ │ │ ├── 🏷️ class PeakProfileTypeEnum
+│ │ │ │ └── 🏷️ class ExtinctionModelEnum
+│ │ │ ├── 📄 factory.py
+│ │ │ │ └── 🏷️ class ExperimentFactory
+│ │ │ └── 📄 total_pd.py
+│ │ │ └── 🏷️ class TotalPdExperiment
+│ │ ├── 📄 __init__.py
+│ │ └── 📄 collection.py
+│ │ └── 🏷️ class Experiments
+│ ├── 📁 structure
+│ │ ├── 📁 categories
+│ │ │ ├── 📁 atom_site_aniso
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ │ ├── 🏷️ class AtomSiteAniso
+│ │ │ │ │ └── 🏷️ class AtomSiteAnisoCollection
+│ │ │ │ └── 📄 factory.py
+│ │ │ │ └── 🏷️ class AtomSiteAnisoFactory
+│ │ │ ├── 📁 atom_sites
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ │ ├── 🏷️ class AtomSite
+│ │ │ │ │ └── 🏷️ class AtomSites
+│ │ │ │ ├── 📄 enums.py
+│ │ │ │ │ └── 🏷️ class AdpTypeEnum
+│ │ │ │ └── 📄 factory.py
+│ │ │ │ └── 🏷️ class AtomSitesFactory
+│ │ │ ├── 📁 cell
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ │ └── 🏷️ class Cell
+│ │ │ │ └── 📄 factory.py
+│ │ │ │ └── 🏷️ class CellFactory
+│ │ │ ├── 📁 space_group
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ │ └── 🏷️ class SpaceGroup
+│ │ │ │ └── 📄 factory.py
+│ │ │ │ └── 🏷️ class SpaceGroupFactory
+│ │ │ └── 📄 __init__.py
+│ │ ├── 📁 item
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 base.py
+│ │ │ │ └── 🏷️ class Structure
+│ │ │ └── 📄 factory.py
+│ │ │ └── 🏷️ class StructureFactory
+│ │ ├── 📄 __init__.py
+│ │ └── 📄 collection.py
+│ │ └── 🏷️ class Structures
+│ └── 📄 __init__.py
+├── 📁 display
+│ ├── 📁 plotters
+│ │ ├── 📄 __init__.py
+│ │ ├── 📄 ascii.py
+│ │ │ └── 🏷️ class AsciiPlotter
+│ │ ├── 📄 base.py
+│ │ │ ├── 🏷️ class BraggTickSet
+│ │ │ ├── 🏷️ class PowderMeasVsCalcSpec
+│ │ │ ├── 🏷️ class XAxisType
+│ │ │ └── 🏷️ class PlotterBase
+│ │ └── 📄 plotly.py
+│ │ ├── 🏷️ class PowderCompositeRows
+│ │ └── 🏷️ class PlotlyPlotter
+│ ├── 📁 tablers
+│ │ ├── 📄 __init__.py
+│ │ ├── 📄 base.py
+│ │ │ └── 🏷️ class TableBackendBase
+│ │ ├── 📄 pandas.py
+│ │ │ └── 🏷️ class PandasTableBackend
+│ │ └── 📄 rich.py
+│ │ └── 🏷️ class RichTableBackend
+│ ├── 📄 __init__.py
+│ ├── 📄 base.py
+│ │ ├── 🏷️ class RendererBase
+│ │ └── 🏷️ class RendererFactoryBase
+│ ├── 📄 plotting.py
+│ │ ├── 🏷️ class PlotterEngineEnum
+│ │ ├── 🏷️ class _MeasVsCalcPlotOptions
+│ │ ├── 🏷️ class Plotter
+│ │ └── 🏷️ class PlotterFactory
+│ ├── 📄 tables.py
+│ │ ├── 🏷️ class TableEngineEnum
+│ │ ├── 🏷️ class TableRenderer
+│ │ └── 🏷️ class TableRendererFactory
+│ └── 📄 utils.py
+│ └── 🏷️ class JupyterScrollManager
+├── 📁 io
+│ ├── 📁 cif
+│ │ ├── 📄 __init__.py
+│ │ ├── 📄 handler.py
+│ │ │ └── 🏷️ class CifHandler
+│ │ ├── 📄 parse.py
+│ │ └── 📄 serialize.py
+│ ├── 📄 __init__.py
+│ └── 📄 ascii.py
+├── 📁 project
+│ ├── 📁 categories
+│ │ ├── 📁 display
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 default.py
+│ │ │ │ └── 🏷️ class Display
+│ │ │ └── 📄 factory.py
+│ │ │ └── 🏷️ class DisplayFactory
+│ │ └── 📄 __init__.py
+│ ├── 📄 __init__.py
+│ ├── 📄 project.py
+│ │ └── 🏷️ class Project
+│ └── 📄 project_info.py
+│ └── 🏷️ class ProjectInfo
+├── 📁 summary
+│ ├── 📄 __init__.py
+│ └── 📄 summary.py
+│ └── 🏷️ class Summary
+├── 📁 utils
+│ ├── 📁 _vendored
+│ │ ├── 📁 jupyter_dark_detect
+│ │ │ ├── 📄 __init__.py
+│ │ │ └── 📄 detector.py
+│ │ ├── 📄 __init__.py
+│ │ └── 📄 theme_detect.py
+│ ├── 📄 __init__.py
+│ ├── 📄 enums.py
+│ │ └── 🏷️ class VerbosityEnum
+│ ├── 📄 environment.py
+│ ├── 📄 logging.py
+│ │ ├── 🏷️ class IconifiedRichHandler
+│ │ ├── 🏷️ class ConsoleManager
+│ │ ├── 🏷️ class LoggerConfig
+│ │ ├── 🏷️ class ExceptionHookManager
+│ │ ├── 🏷️ class Logger
+│ │ └── 🏷️ class ConsolePrinter
+│ └── 📄 utils.py
+├── 📄 __init__.py
+└── 📄 __main__.py
+```
diff --git a/docs/dev/package-structure-short.md b/docs/dev/package-structure-short.md
new file mode 100644
index 000000000..30b4daf72
--- /dev/null
+++ b/docs/dev/package-structure-short.md
@@ -0,0 +1,220 @@
+# Package Structure (short)
+
+```
+📦 easydiffraction
+├── 📁 analysis
+│ ├── 📁 calculators
+│ │ ├── 📄 __init__.py
+│ │ ├── 📄 base.py
+│ │ ├── 📄 crysfml.py
+│ │ ├── 📄 cryspy.py
+│ │ ├── 📄 factory.py
+│ │ └── 📄 pdffit.py
+│ ├── 📁 categories
+│ │ ├── 📁 aliases
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 default.py
+│ │ │ └── 📄 factory.py
+│ │ ├── 📁 constraints
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 default.py
+│ │ │ └── 📄 factory.py
+│ │ ├── 📁 fit
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 default.py
+│ │ │ ├── 📄 enums.py
+│ │ │ └── 📄 factory.py
+│ │ ├── 📁 joint_fit_experiments
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 default.py
+│ │ │ └── 📄 factory.py
+│ │ └── 📄 __init__.py
+│ ├── 📁 fit_helpers
+│ │ ├── 📄 __init__.py
+│ │ ├── 📄 metrics.py
+│ │ ├── 📄 reporting.py
+│ │ └── 📄 tracking.py
+│ ├── 📁 minimizers
+│ │ ├── 📄 __init__.py
+│ │ ├── 📄 base.py
+│ │ ├── 📄 bumps.py
+│ │ ├── 📄 bumps_amoeba.py
+│ │ ├── 📄 bumps_de.py
+│ │ ├── 📄 bumps_lm.py
+│ │ ├── 📄 dfols.py
+│ │ ├── 📄 enums.py
+│ │ ├── 📄 factory.py
+│ │ ├── 📄 lmfit.py
+│ │ ├── 📄 lmfit_least_squares.py
+│ │ └── 📄 lmfit_leastsq.py
+│ ├── 📄 __init__.py
+│ ├── 📄 analysis.py
+│ ├── 📄 fitting.py
+│ └── 📄 sequential.py
+├── 📁 core
+│ ├── 📄 __init__.py
+│ ├── 📄 category.py
+│ ├── 📄 collection.py
+│ ├── 📄 datablock.py
+│ ├── 📄 diagnostic.py
+│ ├── 📄 factory.py
+│ ├── 📄 guard.py
+│ ├── 📄 identity.py
+│ ├── 📄 metadata.py
+│ ├── 📄 singleton.py
+│ ├── 📄 validation.py
+│ └── 📄 variable.py
+├── 📁 crystallography
+│ ├── 📄 __init__.py
+│ ├── 📄 crystallography.py
+│ └── 📄 space_groups.py
+├── 📁 datablocks
+│ ├── 📁 experiment
+│ │ ├── 📁 categories
+│ │ │ ├── 📁 background
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 base.py
+│ │ │ │ ├── 📄 chebyshev.py
+│ │ │ │ ├── 📄 enums.py
+│ │ │ │ ├── 📄 factory.py
+│ │ │ │ └── 📄 line_segment.py
+│ │ │ ├── 📁 calculation
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ └── 📄 factory.py
+│ │ │ ├── 📁 data
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 bragg_pd.py
+│ │ │ │ ├── 📄 bragg_sc.py
+│ │ │ │ ├── 📄 factory.py
+│ │ │ │ └── 📄 total_pd.py
+│ │ │ ├── 📁 diffrn
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ └── 📄 factory.py
+│ │ │ ├── 📁 excluded_regions
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ └── 📄 factory.py
+│ │ │ ├── 📁 experiment_type
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ └── 📄 factory.py
+│ │ │ ├── 📁 extinction
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 becker_coppens.py
+│ │ │ │ └── 📄 factory.py
+│ │ │ ├── 📁 instrument
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 base.py
+│ │ │ │ ├── 📄 cwl.py
+│ │ │ │ ├── 📄 factory.py
+│ │ │ │ └── 📄 tof.py
+│ │ │ ├── 📁 linked_crystal
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ └── 📄 factory.py
+│ │ │ ├── 📁 linked_phases
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ └── 📄 factory.py
+│ │ │ ├── 📁 peak
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 base.py
+│ │ │ │ ├── 📄 cwl.py
+│ │ │ │ ├── 📄 cwl_mixins.py
+│ │ │ │ ├── 📄 factory.py
+│ │ │ │ ├── 📄 tof.py
+│ │ │ │ ├── 📄 tof_mixins.py
+│ │ │ │ ├── 📄 total.py
+│ │ │ │ └── 📄 total_mixins.py
+│ │ │ └── 📄 __init__.py
+│ │ ├── 📁 item
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 base.py
+│ │ │ ├── 📄 bragg_pd.py
+│ │ │ ├── 📄 bragg_sc.py
+│ │ │ ├── 📄 enums.py
+│ │ │ ├── 📄 factory.py
+│ │ │ └── 📄 total_pd.py
+│ │ ├── 📄 __init__.py
+│ │ └── 📄 collection.py
+│ ├── 📁 structure
+│ │ ├── 📁 categories
+│ │ │ ├── 📁 atom_site_aniso
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ └── 📄 factory.py
+│ │ │ ├── 📁 atom_sites
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ ├── 📄 enums.py
+│ │ │ │ └── 📄 factory.py
+│ │ │ ├── 📁 cell
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ └── 📄 factory.py
+│ │ │ ├── 📁 space_group
+│ │ │ │ ├── 📄 __init__.py
+│ │ │ │ ├── 📄 default.py
+│ │ │ │ └── 📄 factory.py
+│ │ │ └── 📄 __init__.py
+│ │ ├── 📁 item
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 base.py
+│ │ │ └── 📄 factory.py
+│ │ ├── 📄 __init__.py
+│ │ └── 📄 collection.py
+│ └── 📄 __init__.py
+├── 📁 display
+│ ├── 📁 plotters
+│ │ ├── 📄 __init__.py
+│ │ ├── 📄 ascii.py
+│ │ ├── 📄 base.py
+│ │ └── 📄 plotly.py
+│ ├── 📁 tablers
+│ │ ├── 📄 __init__.py
+│ │ ├── 📄 base.py
+│ │ ├── 📄 pandas.py
+│ │ └── 📄 rich.py
+│ ├── 📄 __init__.py
+│ ├── 📄 base.py
+│ ├── 📄 plotting.py
+│ ├── 📄 tables.py
+│ └── 📄 utils.py
+├── 📁 io
+│ ├── 📁 cif
+│ │ ├── 📄 __init__.py
+│ │ ├── 📄 handler.py
+│ │ ├── 📄 parse.py
+│ │ └── 📄 serialize.py
+│ ├── 📄 __init__.py
+│ └── 📄 ascii.py
+├── 📁 project
+│ ├── 📁 categories
+│ │ ├── 📁 display
+│ │ │ ├── 📄 __init__.py
+│ │ │ ├── 📄 default.py
+│ │ │ └── 📄 factory.py
+│ │ └── 📄 __init__.py
+│ ├── 📄 __init__.py
+│ ├── 📄 project.py
+│ └── 📄 project_info.py
+├── 📁 summary
+│ ├── 📄 __init__.py
+│ └── 📄 summary.py
+├── 📁 utils
+│ ├── 📁 _vendored
+│ │ ├── 📁 jupyter_dark_detect
+│ │ │ ├── 📄 __init__.py
+│ │ │ └── 📄 detector.py
+│ │ ├── 📄 __init__.py
+│ │ └── 📄 theme_detect.py
+│ ├── 📄 __init__.py
+│ ├── 📄 enums.py
+│ ├── 📄 environment.py
+│ ├── 📄 logging.py
+│ └── 📄 utils.py
+├── 📄 __init__.py
+└── 📄 __main__.py
+```
diff --git a/docs/dev/plan_powder-chart-y-range.md b/docs/dev/plan_powder-chart-y-range.md
new file mode 100644
index 000000000..c987fe47a
--- /dev/null
+++ b/docs/dev/plan_powder-chart-y-range.md
@@ -0,0 +1,138 @@
+# 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/introduction/index.md b/docs/docs/introduction/index.md
index ee1d21598..1a49f004c 100644
--- a/docs/docs/introduction/index.md
+++ b/docs/docs/introduction/index.md
@@ -6,9 +6,9 @@ icon: material/information-slab-circle
## Description
-**EasyDiffraction** is a software for calculating neutron powder
-diffraction patterns based on a structural model and refining its
-parameters against experimental data.
+**EasyDiffraction** is a software for calculating diffraction patterns
+based on a structural model and refining its parameters against
+experimental data.
**EasyDiffraction** is developed both as a Python library and as a
cross-platform desktop application.
diff --git a/docs/docs/tutorials/ed-1.ipynb b/docs/docs/tutorials/ed-1.ipynb
index ab98ddcde..53635a4c0 100644
--- a/docs/docs/tutorials/ed-1.ipynb
+++ b/docs/docs/tutorials/ed-1.ipynb
@@ -267,7 +267,7 @@
"outputs": [],
"source": [
"# Plot measured vs. calculated diffraction patterns\n",
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')"
]
}
],
diff --git a/docs/docs/tutorials/ed-1.py b/docs/docs/tutorials/ed-1.py
index a51e2f51f..0e2a5e553 100644
--- a/docs/docs/tutorials/ed-1.py
+++ b/docs/docs/tutorials/ed-1.py
@@ -107,4 +107,4 @@
# %%
# Plot measured vs. calculated diffraction patterns
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
diff --git a/docs/docs/tutorials/ed-16.ipynb b/docs/docs/tutorials/ed-16.ipynb
index 39a6f2836..f7a839dae 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', show_residual=False)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')"
]
},
{
@@ -465,7 +465,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='nomad', show_residual=False)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='nomad')"
]
},
{
@@ -589,7 +589,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', show_residual=False)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')"
]
},
{
@@ -599,7 +599,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='nomad', show_residual=False)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='nomad')"
]
}
],
diff --git a/docs/docs/tutorials/ed-16.py b/docs/docs/tutorials/ed-16.py
index 2981eb89e..91d48b1e6 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', show_residual=False)
+project.display.plotter.plot_meas_vs_calc(expt_name='sepd')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='nomad', show_residual=False)
+project.display.plotter.plot_meas_vs_calc(expt_name='nomad')
# %% [markdown]
# #### Set Fitting Parameters
@@ -245,7 +245,7 @@
# #### Plot Measured vs Calculated (After Fit)
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='sepd', show_residual=False)
+project.display.plotter.plot_meas_vs_calc(expt_name='sepd')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='nomad', show_residual=False)
+project.display.plotter.plot_meas_vs_calc(expt_name='nomad')
diff --git a/docs/docs/tutorials/ed-17.ipynb b/docs/docs/tutorials/ed-17.ipynb
index d2052de8e..d2ee917d1 100644
--- a/docs/docs/tutorials/ed-17.ipynb
+++ b/docs/docs/tutorials/ed-17.ipynb
@@ -587,7 +587,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='d20', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='d20')"
]
},
{
diff --git a/docs/docs/tutorials/ed-17.py b/docs/docs/tutorials/ed-17.py
index 37be84c4b..f929cd47c 100644
--- a/docs/docs/tutorials/ed-17.py
+++ b/docs/docs/tutorials/ed-17.py
@@ -286,7 +286,7 @@
# #### Compare measured and calculated patterns for the first fit.
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='d20', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(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', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(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', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='d20')
# %% [markdown]
# #### Plot Parameter Evolution
diff --git a/docs/docs/tutorials/ed-18.ipynb b/docs/docs/tutorials/ed-18.ipynb
index b46133a63..47a25154a 100644
--- a/docs/docs/tutorials/ed-18.ipynb
+++ b/docs/docs/tutorials/ed-18.ipynb
@@ -164,7 +164,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')"
]
}
],
diff --git a/docs/docs/tutorials/ed-18.py b/docs/docs/tutorials/ed-18.py
index d6c9d00b7..fb44972fa 100644
--- a/docs/docs/tutorials/ed-18.py
+++ b/docs/docs/tutorials/ed-18.py
@@ -53,4 +53,4 @@
project.display.plotter.plot_param_correlations()
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
diff --git a/docs/docs/tutorials/ed-2.ipynb b/docs/docs/tutorials/ed-2.ipynb
index b789c86d4..4a52b569c 100644
--- a/docs/docs/tutorials/ed-2.ipynb
+++ b/docs/docs/tutorials/ed-2.ipynb
@@ -365,7 +365,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')"
]
},
{
@@ -449,7 +449,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')"
]
},
{
@@ -517,7 +517,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')"
]
}
],
diff --git a/docs/docs/tutorials/ed-2.py b/docs/docs/tutorials/ed-2.py
index a9d05116a..fa4918c05 100644
--- a/docs/docs/tutorials/ed-2.py
+++ b/docs/docs/tutorials/ed-2.py
@@ -168,7 +168,7 @@
project.display.plotter.plot_param_correlations()
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %% [markdown]
# ## Step 5: Perform Analysis (with constraints)
@@ -205,7 +205,7 @@
project.display.plotter.plot_param_correlations()
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %% [markdown]
# ## Step 6: Switch calculator engine
@@ -226,4 +226,4 @@
project.display.plotter.plot_param_correlations()
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
diff --git a/docs/docs/tutorials/ed-20.ipynb b/docs/docs/tutorials/ed-20.ipynb
index 39cf27b1b..b9a47a281 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', show_residual=False)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2')"
]
},
{
@@ -525,7 +525,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2', show_residual=False)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2')"
]
},
{
@@ -752,7 +752,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2', show_residual=False)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2')"
]
},
{
@@ -762,7 +762,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2', show_residual=False)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2')"
]
},
{
diff --git a/docs/docs/tutorials/ed-20.py b/docs/docs/tutorials/ed-20.py
index 7c0488003..0ac55fdbf 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', show_residual=False)
+project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2', show_residual=False)
+project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2')
# %% [markdown]
# ## Perform Analysis
@@ -356,10 +356,10 @@
# Show full range in TOF.
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2', show_residual=False)
+project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2', show_residual=False)
+project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2')
# %% [markdown]
# Show selected peaks in d-spacing.
diff --git a/docs/docs/tutorials/ed-3.ipynb b/docs/docs/tutorials/ed-3.ipynb
index dc0957da4..f99298a2f 100644
--- a/docs/docs/tutorials/ed-3.ipynb
+++ b/docs/docs/tutorials/ed-3.ipynb
@@ -792,7 +792,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)"
]
},
{
@@ -1051,7 +1051,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)"
]
},
{
@@ -1157,7 +1157,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)"
]
},
{
@@ -1263,7 +1263,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)"
]
},
{
@@ -1411,7 +1411,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)"
]
},
{
@@ -1582,7 +1582,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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 b0f4d19f4..de1f74c63 100644
--- a/docs/docs/tutorials/ed-3.py
+++ b/docs/docs/tutorials/ed-3.py
@@ -334,10 +334,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)
# %% [markdown]
# #### Show Parameters
@@ -430,10 +430,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)
# %% [markdown]
# #### Save Project State
@@ -469,10 +469,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)
# %% [markdown]
# #### Save Project State
@@ -508,10 +508,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)
# %% [markdown]
# #### Save Project State
@@ -565,10 +565,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)
# %% [markdown]
# #### Save Project State
@@ -632,10 +632,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(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 99ae828ac..3a77ab8bb 100644
--- a/docs/docs/tutorials/ed-4.ipynb
+++ b/docs/docs/tutorials/ed-4.ipynb
@@ -687,9 +687,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(\n",
- " expt_name='npd', x_min=35.5, x_max=38.3, show_residual=True\n",
- ")"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='npd', x_min=35.5, x_max=38.3)"
]
},
{
@@ -699,9 +697,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(\n",
- " expt_name='xrd', x_min=29.0, x_max=30.4, show_residual=True\n",
- ")"
+ "project.display.plotter.plot_meas_vs_calc(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 6a6906793..8a868277f 100644
--- a/docs/docs/tutorials/ed-4.py
+++ b/docs/docs/tutorials/ed-4.py
@@ -315,11 +315,7 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(
- expt_name='npd', x_min=35.5, x_max=38.3, show_residual=True
-)
+project.display.plotter.plot_meas_vs_calc(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, show_residual=True
-)
+project.display.plotter.plot_meas_vs_calc(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 c1514e20b..02ef6045c 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', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='d20', x_min=41, x_max=54)"
]
},
{
@@ -610,7 +610,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='d20', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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 e7d5b87db..72e54feb9 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', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='d20')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='d20', x_min=41, x_max=54, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='d20', x_min=41, x_max=54)
# %% [markdown]
# #### Set Free Parameters
@@ -283,10 +283,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='d20', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='d20')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='d20', x_min=42, x_max=52, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(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 c1b443918..ae8f5a84d 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', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)"
]
},
{
@@ -483,7 +483,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)"
]
},
{
@@ -583,7 +583,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)"
]
},
{
@@ -681,7 +681,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)"
]
},
{
@@ -794,7 +794,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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, show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(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 35fc75c54..341178e61 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', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)
# %% [markdown]
# ### Perform Fit 1/4
@@ -215,10 +215,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)
# %% [markdown]
# ### Perform Fit 2/4
@@ -253,10 +253,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)
# %% [markdown]
# ### Perform Fit 3/4
@@ -289,10 +289,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)
# %% [markdown]
# ### Perform Fit 4/4
@@ -333,10 +333,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51, show_residual=True)
+project.display.plotter.plot_meas_vs_calc(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 aa606d301..7a66c6f66 100644
--- a/docs/docs/tutorials/ed-7.ipynb
+++ b/docs/docs/tutorials/ed-7.ipynb
@@ -345,10 +345,8 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', show_residual=True)\n",
- "project.display.plotter.plot_meas_vs_calc(\n",
- " expt_name='sepd', x_min=23200, x_max=23700, show_residual=True\n",
- ")"
+ "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)"
]
},
{
@@ -426,7 +424,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')"
]
},
{
@@ -436,9 +434,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(\n",
- " expt_name='sepd', x_min=23200, x_max=23700, show_residual=True\n",
- ")"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)"
]
},
{
@@ -514,7 +510,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')"
]
},
{
@@ -524,9 +520,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(\n",
- " expt_name='sepd', x_min=23200, x_max=23700, show_residual=True\n",
- ")"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)"
]
},
{
@@ -622,7 +616,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')"
]
},
{
@@ -632,9 +626,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(\n",
- " expt_name='sepd', x_min=23200, x_max=23700, show_residual=True\n",
- ")"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)"
]
},
{
@@ -731,7 +723,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')"
]
},
{
@@ -741,9 +733,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(\n",
- " expt_name='sepd', x_min=23200, x_max=23700, show_residual=True\n",
- ")"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)"
]
},
{
@@ -755,7 +745,7 @@
},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing')"
]
},
{
@@ -906,9 +896,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(\n",
- " expt_name='sepd', x_min=23200, x_max=23700, show_residual=True\n",
- ")"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)"
]
},
{
@@ -918,7 +906,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing')"
]
}
],
diff --git a/docs/docs/tutorials/ed-7.py b/docs/docs/tutorials/ed-7.py
index 71d3232c2..5305549a7 100644
--- a/docs/docs/tutorials/ed-7.py
+++ b/docs/docs/tutorials/ed-7.py
@@ -140,10 +140,8 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='sepd', show_residual=True)
-project.display.plotter.plot_meas_vs_calc(
- expt_name='sepd', x_min=23200, x_max=23700, show_residual=True
-)
+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)
# %% [markdown]
# ### Perform Fit 1/5
@@ -173,12 +171,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='sepd', show_residual=True)
+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, show_residual=True
-)
+project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)
# %% [markdown]
# ### Perform Fit 2/5
@@ -206,12 +202,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='sepd', show_residual=True)
+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, show_residual=True
-)
+project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)
# %% [markdown]
# ### Perform Fit 3/5
@@ -247,12 +241,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='sepd', show_residual=True)
+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, show_residual=True
-)
+project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)
# %% [markdown]
# ### Perform Fit 4/5
@@ -289,15 +281,13 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='sepd', show_residual=True)
+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, show_residual=True
-)
+project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing')
# %% [markdown]
@@ -356,9 +346,7 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(
- expt_name='sepd', x_min=23200, x_max=23700, show_residual=True
-)
+project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing')
diff --git a/docs/docs/tutorials/ed-8.ipynb b/docs/docs/tutorials/ed-8.ipynb
index 23b000e8e..5df563205 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', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6')"
]
},
{
@@ -640,7 +640,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7')"
]
},
{
@@ -678,7 +678,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6')"
]
},
{
@@ -688,7 +688,7 @@
"metadata": {},
"outputs": [],
"source": [
- "project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7', show_residual=True)"
+ "project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7')"
]
},
{
diff --git a/docs/docs/tutorials/ed-8.py b/docs/docs/tutorials/ed-8.py
index f81000fd2..a9adb7c8f 100644
--- a/docs/docs/tutorials/ed-8.py
+++ b/docs/docs/tutorials/ed-8.py
@@ -333,10 +333,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7')
# %% [markdown]
# #### Run Fitting
@@ -350,10 +350,10 @@
# #### Plot Measured vs Calculated
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6')
# %%
-project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7', show_residual=True)
+project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7')
# %% [markdown]
# ## Summary
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index b5eb08ac9..45c2ce5fb 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -200,7 +200,6 @@ nav:
- HS pd-neut-cwl: tutorials/ed-6.ipynb
- Si pd-neut-tof: tutorials/ed-7.ipynb
- NCAF pd-neut-tof: tutorials/ed-8.ipynb
- - LBCO+Si McStas: tutorials/ed-9.ipynb
- Single Crystal Diffraction:
- Tb2TiO7 sg-neut-cwl: tutorials/ed-14.ipynb
- Taurine sg-neut-tof: tutorials/ed-15.ipynb
diff --git a/pixi.lock b/pixi.lock
index 982cc8a8f..36a2ecb00 100644
--- a/pixi.lock
+++ b/pixi.lock
@@ -67,7 +67,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_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
@@ -106,7 +106,7 @@ environments:
- 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.5-h2d0b736_3.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
@@ -130,7 +130,7 @@ environments:
- 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-2.0.7-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda
@@ -178,10 +178,10 @@ environments:
- 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/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-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/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-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
@@ -189,9 +189,9 @@ environments:
- 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/75/3f/aa2458b3b88e59b0be1a06685f237c944375186f4652eb9b5d43bb5ebe21/copier-9.14.3-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/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/3e/f0/f7a0d5d1706768ced4015f3f9f713197e2cc017c199fe231001be9f04fbb/crysfml-0.6.0-cp314-cp314-manylinux_2_35_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
@@ -208,19 +208,19 @@ environments:
- 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/62/f3/5b2b5d392ddfd885f062e592fc0bcaf437d64370a67b2ff0c04a0f99e920/essdiffraction-26.4.1-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/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/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/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl
@@ -329,16 +329,16 @@ environments:
- 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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-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/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl
@@ -405,7 +405,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_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
@@ -440,7 +440,7 @@ environments:
- 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.5-h5e97a16_3.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
@@ -466,7 +466,7 @@ environments:
- 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-2.0.7-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda
@@ -514,10 +514,10 @@ environments:
- 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/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-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/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-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
@@ -525,9 +525,9 @@ environments:
- 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/75/3f/aa2458b3b88e59b0be1a06685f237c944375186f4652eb9b5d43bb5ebe21/copier-9.14.3-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/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl
- - pypi: https://files.pythonhosted.org/packages/83/3a/38071a19ddad863b19c3e8a04ecf0e7c899fe8dd5d38f559ac93a6f3e8ea/crysfml-0.6.0-cp314-cp314-macosx_14_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
@@ -544,19 +544,19 @@ environments:
- 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/62/f3/5b2b5d392ddfd885f062e592fc0bcaf437d64370a67b2ff0c04a0f99e920/essdiffraction-26.4.1-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/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/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/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl
@@ -664,16 +664,16 @@ environments:
- 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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-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/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz
- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl
@@ -737,7 +737,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_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
@@ -787,7 +787,7 @@ environments:
- 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-2.0.7-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda
@@ -843,19 +843,19 @@ environments:
- 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/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-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/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-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/75/3f/aa2458b3b88e59b0be1a06685f237c944375186f4652eb9b5d43bb5ebe21/copier-9.14.3-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/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl
- - pypi: https://files.pythonhosted.org/packages/ff/34/db1c40a59852c792ee15c15326267159a97c0353abc10496a24a1933ea44/crysfml-0.6.0-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
@@ -872,19 +872,19 @@ environments:
- 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/62/f3/5b2b5d392ddfd885f062e592fc0bcaf437d64370a67b2ff0c04a0f99e920/essdiffraction-26.4.1-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/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/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/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl
@@ -993,16 +993,16 @@ environments:
- 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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-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/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl
@@ -1076,7 +1076,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_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
@@ -1116,7 +1116,7 @@ environments:
- 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.5-h2d0b736_3.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
@@ -1140,7 +1140,7 @@ environments:
- 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-2.0.7-pyhd8ed1ab_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
@@ -1188,10 +1188,10 @@ environments:
- 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/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-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/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-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
@@ -1199,9 +1199,9 @@ environments:
- 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/75/3f/aa2458b3b88e59b0be1a06685f237c944375186f4652eb9b5d43bb5ebe21/copier-9.14.3-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/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/ee/bc/3c18f12e4b440e30a33e02a5ec3171ff2c77847a3a6a18ab3825d0c412d0/crysfml-0.6.0-cp312-cp312-manylinux_2_35_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
@@ -1218,19 +1218,19 @@ environments:
- 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/62/f3/5b2b5d392ddfd885f062e592fc0bcaf437d64370a67b2ff0c04a0f99e920/essdiffraction-26.4.1-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/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/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/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl
@@ -1339,16 +1339,16 @@ environments:
- 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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-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/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl
@@ -1415,7 +1415,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_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
@@ -1449,7 +1449,7 @@ environments:
- 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.5-h5e97a16_3.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
@@ -1475,7 +1475,7 @@ environments:
- 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-2.0.7-pyhd8ed1ab_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
@@ -1523,10 +1523,10 @@ environments:
- 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/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-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/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-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
@@ -1534,9 +1534,9 @@ environments:
- 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/75/3f/aa2458b3b88e59b0be1a06685f237c944375186f4652eb9b5d43bb5ebe21/copier-9.14.3-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/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl
- - pypi: https://files.pythonhosted.org/packages/0a/61/9382770d384685e23e32c9b1df6101d416104f9642c84feeba359685b97f/crysfml-0.6.0-cp312-cp312-macosx_14_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
@@ -1553,19 +1553,19 @@ environments:
- 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/62/f3/5b2b5d392ddfd885f062e592fc0bcaf437d64370a67b2ff0c04a0f99e920/essdiffraction-26.4.1-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/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/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/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl
@@ -1673,16 +1673,16 @@ environments:
- 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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-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/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl
- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl
@@ -1746,7 +1746,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_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
@@ -1795,7 +1795,7 @@ environments:
- 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-2.0.7-pyhd8ed1ab_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
@@ -1851,19 +1851,19 @@ environments:
- 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/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-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/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/7b/67/b1944235474aac3f0b0e1b232ce49547f9f9461ca4b943df1b88da5d3f1d/bumps-1.0.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/32/60/fca69c534602a7ced04280c952a246ad1edde2a6ca3a164f65d32ac41fe7/chardet-7.4.3-cp312-cp312-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl
- - pypi: https://files.pythonhosted.org/packages/75/3f/aa2458b3b88e59b0be1a06685f237c944375186f4652eb9b5d43bb5ebe21/copier-9.14.3-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/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl
- - pypi: https://files.pythonhosted.org/packages/a4/e6/d339c99d0ff7e51da38122b28e5c6676c1b5f475916c95e1e547fea40950/crysfml-0.6.0-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
@@ -1880,19 +1880,19 @@ environments:
- 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/62/f3/5b2b5d392ddfd885f062e592fc0bcaf437d64370a67b2ff0c04a0f99e920/essdiffraction-26.4.1-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/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/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/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl
@@ -2001,16 +2001,16 @@ environments:
- 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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-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/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl
@@ -2084,7 +2084,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_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
@@ -2123,7 +2123,7 @@ environments:
- 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.5-h2d0b736_3.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
@@ -2147,7 +2147,7 @@ environments:
- 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-2.0.7-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda
- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda
@@ -2195,10 +2195,10 @@ environments:
- 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/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-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/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-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
@@ -2206,9 +2206,9 @@ environments:
- 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/75/3f/aa2458b3b88e59b0be1a06685f237c944375186f4652eb9b5d43bb5ebe21/copier-9.14.3-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/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/3e/f0/f7a0d5d1706768ced4015f3f9f713197e2cc017c199fe231001be9f04fbb/crysfml-0.6.0-cp314-cp314-manylinux_2_35_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
@@ -2225,19 +2225,19 @@ environments:
- 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/62/f3/5b2b5d392ddfd885f062e592fc0bcaf437d64370a67b2ff0c04a0f99e920/essdiffraction-26.4.1-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/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/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/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl
@@ -2346,16 +2346,16 @@ environments:
- 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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-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/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl
@@ -2422,7 +2422,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_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
@@ -2457,7 +2457,7 @@ environments:
- 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.5-h5e97a16_3.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
@@ -2483,7 +2483,7 @@ environments:
- 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-2.0.7-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py314h6e9b3f0_1.conda
@@ -2531,10 +2531,10 @@ environments:
- 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/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-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/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-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
@@ -2542,9 +2542,9 @@ environments:
- 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/75/3f/aa2458b3b88e59b0be1a06685f237c944375186f4652eb9b5d43bb5ebe21/copier-9.14.3-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/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl
- - pypi: https://files.pythonhosted.org/packages/83/3a/38071a19ddad863b19c3e8a04ecf0e7c899fe8dd5d38f559ac93a6f3e8ea/crysfml-0.6.0-cp314-cp314-macosx_14_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
@@ -2561,19 +2561,19 @@ environments:
- 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/62/f3/5b2b5d392ddfd885f062e592fc0bcaf437d64370a67b2ff0c04a0f99e920/essdiffraction-26.4.1-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/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/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/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl
@@ -2681,16 +2681,16 @@ environments:
- 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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-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/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz
- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl
@@ -2754,7 +2754,7 @@ environments:
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.1-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda
- - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_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
@@ -2804,7 +2804,7 @@ environments:
- 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-2.0.7-pyhd8ed1ab_0.conda
+ - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda
- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-311-py314h8f8f202_1.conda
@@ -2860,19 +2860,19 @@ environments:
- 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/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-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/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-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/75/3f/aa2458b3b88e59b0be1a06685f237c944375186f4652eb9b5d43bb5ebe21/copier-9.14.3-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/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl
- - pypi: https://files.pythonhosted.org/packages/ff/34/db1c40a59852c792ee15c15326267159a97c0353abc10496a24a1933ea44/crysfml-0.6.0-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
@@ -2889,19 +2889,19 @@ environments:
- 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/62/f3/5b2b5d392ddfd885f062e592fc0bcaf437d64370a67b2ff0c04a0f99e920/essdiffraction-26.4.1-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/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/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/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl
@@ -3010,16 +3010,16 @@ environments:
- 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/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-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/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-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/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl
+ - pypi: https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl
- pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl
@@ -3501,20 +3501,20 @@ packages:
- pkg:pypi/backports-zstd?source=hash-mapping
size: 236635
timestamp: 1767045021157
-- pypi: https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl
name: backrefs
- version: '6.2'
- sha256: e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7
+ version: '7.0'
+ sha256: ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12
requires_dist:
- regex ; extra == 'extras'
- requires_python: '>=3.9'
-- pypi: https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl
+ requires_python: '>=3.10'
+- pypi: https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl
name: backrefs
- version: '6.2'
- sha256: c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90
+ version: '7.0'
+ sha256: a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9
requires_dist:
- regex ; extra == 'extras'
- requires_python: '>=3.9'
+ requires_python: '>=3.10'
- conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda
sha256: bf1e71c3c0a5b024e44ff928225a0874fc3c3356ec1a0b6fe719108e6d1288f6
md5: 5267bef8efea4127aacd1f4e1f149b6e
@@ -3664,10 +3664,10 @@ packages:
- pkg:pypi/brotli?source=hash-mapping
size: 335782
timestamp: 1764018443683
-- pypi: https://files.pythonhosted.org/packages/fa/88/6764e7a109dd84294850741501145da90d13cdeac9d4e614929464a37420/build-1.4.4-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl
name: build
- version: 1.4.4
- sha256: 8c3f48a6090b39edec1a273d2d57949aaf13723b01e02f9d518396887519f64d
+ version: 1.5.0
+ sha256: 13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f
requires_dist:
- packaging>=24.0
- pyproject-hooks
@@ -3676,10 +3676,9 @@ packages:
- tomli>=1.1.0 ; python_full_version < '3.11'
- keyring ; extra == 'keyring'
- uv>=0.1.18 ; extra == 'uv'
- - virtualenv>=20.11 ; python_full_version < '3.10' and extra == 'virtualenv'
- 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.9'
+ 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
@@ -4144,10 +4143,10 @@ packages:
- pytest-xdist ; extra == 'test-no-images'
- wurlitzer ; extra == 'test-no-images'
requires_python: '>=3.11'
-- pypi: https://files.pythonhosted.org/packages/75/3f/aa2458b3b88e59b0be1a06685f237c944375186f4652eb9b5d43bb5ebe21/copier-9.14.3-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl
name: copier
- version: 9.14.3
- sha256: b237bb8a7fba91fbe0580ee54292e7a4c915028f08389f1ee99332fb70d2cab1
+ version: 9.15.0
+ sha256: 0f59c2ea36df42f3ded85c091c3f1e2c8d3814b537504f0abc8c2e508f7e013d
requires_dist:
- colorama>=0.4.6
- dunamai>=1.7.0
@@ -4228,119 +4227,47 @@ packages:
purls: []
size: 49809
timestamp: 1775614256655
-- pypi: https://files.pythonhosted.org/packages/0a/61/9382770d384685e23e32c9b1df6101d416104f9642c84feeba359685b97f/crysfml-0.6.0-cp312-cp312-macosx_14_0_arm64.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
name: crysfml
- version: 0.6.0
- sha256: 881e7e5205c420c8c8fa12cee47e6ce081de2f1ebf64f81eee39e178ad478fbb
+ version: 0.6.1
+ sha256: aaa0c38132f99976fa95c7cb728ee98113137a0023819a60893461943dcefd93
requires_dist:
- numpy
- - build ; extra == 'ci'
- - check-wheel-contents ; extra == 'ci'
- - colorama ; extra == 'ci'
- - packaging ; extra == 'ci'
- - toml ; extra == 'ci'
- - validate-pyproject[all] ; extra == 'ci'
- - wheel ; extra == 'ci'
- - deepdiff ; extra == 'test'
- - matplotlib ; extra == 'test'
- - pygit2 ; extra == 'test'
- - pytest ; extra == 'test'
- - pytest-benchmark ; extra == 'test'
requires_python: '>=3.11,<3.15'
-- pypi: https://files.pythonhosted.org/packages/3e/f0/f7a0d5d1706768ced4015f3f9f713197e2cc017c199fe231001be9f04fbb/crysfml-0.6.0-cp314-cp314-manylinux_2_35_x86_64.whl
+- pypi: https://files.pythonhosted.org/packages/2c/58/d025d34259682d555db962eab098f5add29187443c31081cbaf5c7ec4bea/crysfml-0.6.1-cp312-cp312-win_amd64.whl
name: crysfml
- version: 0.6.0
- sha256: dbc5297fd7c85d90b139595041c90308b0292b68a5fbaeb044244e0b79f1ffb8
+ version: 0.6.1
+ sha256: 13c51e0021b70dd939cb6d38ac4e82dc11e173e7e43125d1cd4c55050371cf2f
requires_dist:
- numpy
- - build ; extra == 'ci'
- - check-wheel-contents ; extra == 'ci'
- - colorama ; extra == 'ci'
- - packaging ; extra == 'ci'
- - toml ; extra == 'ci'
- - validate-pyproject[all] ; extra == 'ci'
- - wheel ; extra == 'ci'
- - deepdiff ; extra == 'test'
- - matplotlib ; extra == 'test'
- - pygit2 ; extra == 'test'
- - pytest ; extra == 'test'
- - pytest-benchmark ; extra == 'test'
requires_python: '>=3.11,<3.15'
-- pypi: https://files.pythonhosted.org/packages/83/3a/38071a19ddad863b19c3e8a04ecf0e7c899fe8dd5d38f559ac93a6f3e8ea/crysfml-0.6.0-cp314-cp314-macosx_14_0_arm64.whl
+- 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.0
- sha256: 702877ce19c61915383493e3ce8098dbe8afb1fc4c025b2c3508fbb73d98764e
+ version: 0.6.1
+ sha256: 6321b1d45e29968976e2e504fba51cc9c01165a9911cde86a6b5b0473653f29a
requires_dist:
- numpy
- - build ; extra == 'ci'
- - check-wheel-contents ; extra == 'ci'
- - colorama ; extra == 'ci'
- - packaging ; extra == 'ci'
- - toml ; extra == 'ci'
- - validate-pyproject[all] ; extra == 'ci'
- - wheel ; extra == 'ci'
- - deepdiff ; extra == 'test'
- - matplotlib ; extra == 'test'
- - pygit2 ; extra == 'test'
- - pytest ; extra == 'test'
- - pytest-benchmark ; extra == 'test'
requires_python: '>=3.11,<3.15'
-- pypi: https://files.pythonhosted.org/packages/a4/e6/d339c99d0ff7e51da38122b28e5c6676c1b5f475916c95e1e547fea40950/crysfml-0.6.0-cp312-cp312-win_amd64.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
name: crysfml
- version: 0.6.0
- sha256: 0ad4685deb12b2d96296cc2aa801a520dd024f0ff4d20b997fc87118279b3c04
+ version: 0.6.1
+ sha256: 419f99e6fb4756185e00a2ca9f95377ae2ac1559fc0b965060bbec8a66a7eb4a
requires_dist:
- numpy
- - build ; extra == 'ci'
- - check-wheel-contents ; extra == 'ci'
- - colorama ; extra == 'ci'
- - packaging ; extra == 'ci'
- - toml ; extra == 'ci'
- - validate-pyproject[all] ; extra == 'ci'
- - wheel ; extra == 'ci'
- - deepdiff ; extra == 'test'
- - matplotlib ; extra == 'test'
- - pygit2 ; extra == 'test'
- - pytest ; extra == 'test'
- - pytest-benchmark ; extra == 'test'
requires_python: '>=3.11,<3.15'
-- pypi: https://files.pythonhosted.org/packages/ee/bc/3c18f12e4b440e30a33e02a5ec3171ff2c77847a3a6a18ab3825d0c412d0/crysfml-0.6.0-cp312-cp312-manylinux_2_35_x86_64.whl
+- 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.0
- sha256: 8306fb6acd45e87f4aff1cd7af85a03cb495b39b1e212b8b8b711c059deb9597
+ version: 0.6.1
+ sha256: 341ca456a1b4ee5a607df283e6a630db7e25585b560c166edc4fc35dbb4c27e3
requires_dist:
- numpy
- - build ; extra == 'ci'
- - check-wheel-contents ; extra == 'ci'
- - colorama ; extra == 'ci'
- - packaging ; extra == 'ci'
- - toml ; extra == 'ci'
- - validate-pyproject[all] ; extra == 'ci'
- - wheel ; extra == 'ci'
- - deepdiff ; extra == 'test'
- - matplotlib ; extra == 'test'
- - pygit2 ; extra == 'test'
- - pytest ; extra == 'test'
- - pytest-benchmark ; extra == 'test'
requires_python: '>=3.11,<3.15'
-- pypi: https://files.pythonhosted.org/packages/ff/34/db1c40a59852c792ee15c15326267159a97c0353abc10496a24a1933ea44/crysfml-0.6.0-cp314-cp314-win_amd64.whl
+- pypi: https://files.pythonhosted.org/packages/d9/1a/8a81b3a66f36969c8456f8af3a12f7d601fdd9cfed2ad5b4e72a2fb7ea8d/crysfml-0.6.1-cp314-cp314-win_amd64.whl
name: crysfml
- version: 0.6.0
- sha256: 3c987a1e9e59b7eb97852bcc1d6ef6268fbea2987c2fc43430675f95481a5ddc
+ version: 0.6.1
+ sha256: b4aa83292665847f0d9aafa44b69c7abb779d0e1e13b5eb1ed516bc4c811ca6f
requires_dist:
- numpy
- - build ; extra == 'ci'
- - check-wheel-contents ; extra == 'ci'
- - colorama ; extra == 'ci'
- - packaging ; extra == 'ci'
- - toml ; extra == 'ci'
- - validate-pyproject[all] ; extra == 'ci'
- - wheel ; extra == 'ci'
- - deepdiff ; extra == 'test'
- - matplotlib ; extra == 'test'
- - pygit2 ; extra == 'test'
- - pytest ; extra == 'test'
- - pytest-benchmark ; extra == 'test'
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
@@ -4656,8 +4583,8 @@ packages:
requires_python: '>=3.5'
- pypi: ./
name: easydiffraction
- version: 0.14.0+dev4
- sha256: fcc831ee2076ab9377f805e04d80a9de366848727833e83232d309f9b440d0f4
+ version: 0.15.0+devdirty9
+ sha256: ea566085dad9a9e2b2a0916bd7af16841d1af477107460060da3446971d2c2ad
requires_dist:
- asciichartpy
- asteval
@@ -4726,10 +4653,10 @@ packages:
- dnspython>=2.0.0
- idna>=2.0.0
requires_python: '>=3.8'
-- pypi: https://files.pythonhosted.org/packages/62/f3/5b2b5d392ddfd885f062e592fc0bcaf437d64370a67b2ff0c04a0f99e920/essdiffraction-26.4.1-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/c4/6d/82e65254354ba651dc966775270a9bbc02414a3eb3f1704e6c87dab2ea83/essdiffraction-26.5.1-py3-none-any.whl
name: essdiffraction
- version: 26.4.1
- sha256: 74b6aac78eea5e1f823c94ae56f33cdcbc3e68c4b786974dfb531d64f90da955
+ version: 26.5.1
+ sha256: 8a6c779078c71be250714619214069221ab7968a69580d4e4d3f4b3e9a1a53ad
requires_dist:
- dask>=2022.1.0
- essreduce>=26.4.0
@@ -4748,6 +4675,21 @@ packages:
- 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
@@ -5074,10 +5016,10 @@ packages:
version: 1.8.0
sha256: 34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746
requires_python: '>=3.9'
-- pypi: https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/d5/0c/043d5e551459da400957a1395e0febbf771446ff34291afcbe3d8be2a279/fsspec-2026.4.0-py3-none-any.whl
name: fsspec
- version: 2026.3.0
- sha256: d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4
+ version: 2026.4.0
+ sha256: 11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2
requires_dist:
- adlfs ; extra == 'abfs'
- adlfs ; extra == 'adl'
@@ -5233,10 +5175,10 @@ packages:
requires_dist:
- smmap>=3.0.1,<6
requires_python: '>=3.7'
-- pypi: https://files.pythonhosted.org/packages/f2/c5/a1bc0996af85757903cf2bf444a7824e68e0035ce63fb41d6f76f9def68b/gitpython-3.1.47-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/fd/6f/b842bfa6f21d6f87c57f9abf7194225e55279d96d869775e19e9f7236fc5/gitpython-3.1.49-py3-none-any.whl
name: gitpython
- version: 3.1.47
- sha256: 489f590edfd6d20571b2c0e72c6a6ac6915ee8b8cd04572330e3842207a78905
+ version: 3.1.49
+ sha256: 024b0422d7f84d15cd794844e029ffebd4c5d42a7eb9b936b458697ef550a02c
requires_dist:
- gitdb>=4.0.1,<5
- typing-extensions>=3.10.0.2 ; python_full_version < '3.10'
@@ -6001,9 +5943,9 @@ packages:
- pkg:pypi/jupyter-server-terminals?source=hash-mapping
size: 22052
timestamp: 1768574057200
-- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_0.conda
- sha256: 436a70259a9b4e13ce8b15faa8b37342835954d77a0a74d21dd24547e0871088
- md5: bcbb401d6fa84e0cee34d4926b0e9e93
+- 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
@@ -6023,9 +5965,9 @@ packages:
license: BSD-3-Clause
license_family: BSD
purls:
- - pkg:pypi/jupyterlab?source=hash-mapping
- size: 8245973
- timestamp: 1773240966438
+ - 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
@@ -7970,25 +7912,25 @@ packages:
requires_dist:
- numpy>=1.22
requires_python: '>=3.8'
-- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda
- sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586
- md5: 47e340acb35de30501a76c7c799c41d7
+- 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 >=13
+ - libgcc >=14
license: X11 AND BSD-3-Clause
purls: []
- size: 891641
- timestamp: 1738195959188
-- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda
- sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733
- md5: 068d497125e4bf8a66bf707254fff5ae
+ size: 918956
+ timestamp: 1777422145199
+- 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: 797030
- timestamp: 1738196177597
+ size: 805509
+ timestamp: 1777423252320
- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda
sha256: bb7b21d7fd0445ddc0631f64e66d91a179de4ba920b8381f29b9d006a42788c0
md5: 598fd7d4d0de2455fb74f56063969a97
@@ -9901,17 +9843,18 @@ packages:
purls: []
size: 49806
timestamp: 1775614307464
-- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda
- sha256: 4790787fe1f4e8da616edca4acf6a4f8ed4e7c6967aa31b920208fc8f95efcca
- md5: a61bf9ec79426938ff785eb69dbb1960
+- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-3.2.1-pyh332efcf_0.conda
+ sha256: 1c55116c22512cef7b01d55ae49697707f2c1fd829407183c19817e2d300fd8d
+ md5: 1cd2f3e885162ee1366312bd1b1677fd
depends:
- - python >=3.6
+ - python >=3.10
+ - typing_extensions
license: BSD-2-Clause
license_family: BSD
purls:
- - pkg:pypi/python-json-logger?source=hash-mapping
- size: 13383
- timestamp: 1677079727691
+ - 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
@@ -11640,10 +11583,10 @@ packages:
- pandas ; extra == 'test'
- xarray ; extra == 'test'
- pytest ; extra == 'test'
-- pypi: https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/d6/bb/fbdc4e57731efb86b4209ffdd2519520782bf27b3c961beac3e5c20d2b87/trove_classifiers-2026.4.28.13-py3-none-any.whl
name: trove-classifiers
- version: 2026.1.14.14
- sha256: 1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d
+ 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
@@ -11652,10 +11595,10 @@ packages:
- importlib-metadata>=3.6 ; python_full_version < '3.10'
- typing-extensions>=4.14.0
requires_python: '>=3.9'
-- pypi: https://files.pythonhosted.org/packages/9a/72/193d4e586ec5a4db834a36bbeb47641a62f951f114ffd0fe5b1b46e8d56f/typer-0.25.0-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl
name: typer
- version: 0.25.0
- sha256: ac01b48823d3db9a83c9e164338057eadbb1c9957a2a6b4eeb486669c560b5dc
+ version: 0.25.1
+ sha256: 75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89
requires_dist:
- click>=8.2.1
- shellingham>=1.3.0
@@ -11837,10 +11780,10 @@ packages:
- mypy ; extra == 'test'
- pretend ; extra == 'test'
- pytest ; extra == 'test'
-- pypi: https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl
+- pypi: https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl
name: virtualenv
- version: 21.2.4
- sha256: 29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac
+ version: 21.3.0
+ sha256: 4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7
requires_dist:
- distlib>=0.3.7,<1
- filelock>=3.24.2,<4 ; python_full_version >= '3.10'
diff --git a/pyproject.toml b/pyproject.toml
index b81b82af6..c0943384c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -218,7 +218,6 @@ preview = true # Enable new rules that are not yet stable, like DOC
docstring-code-format = true # Whether to format code snippets in docstrings
docstring-code-line-length = 72 # Line length for code snippets in docstrings
indent-style = 'space' # PEP 8 recommends using spaces over tabs
-line-ending = 'lf' # Line endings will be converted to \n
quote-style = 'single' # But double quotes in docstrings (PEP 8, PEP 257)
# Linting rules to use with Ruff
diff --git a/src/easydiffraction/analysis/calculators/base.py b/src/easydiffraction/analysis/calculators/base.py
index 5ef47e3b0..b0e74ba57 100644
--- a/src/easydiffraction/analysis/calculators/base.py
+++ b/src/easydiffraction/analysis/calculators/base.py
@@ -1,14 +1,35 @@
# SPDX-FileCopyrightText: 2025 EasyScience contributors
# SPDX-License-Identifier: BSD-3-Clause
+from __future__ import annotations
+
from abc import ABC
from abc import abstractmethod
+from dataclasses import dataclass
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ import numpy as np
+
+ from easydiffraction.datablocks.experiment.item.base import ExperimentBase
+ from easydiffraction.datablocks.structure.collection import Structures
+ from easydiffraction.datablocks.structure.item.base import Structure
-import numpy as np
-from easydiffraction.datablocks.experiment.item.base import ExperimentBase
-from easydiffraction.datablocks.structure.collection import Structures
-from easydiffraction.datablocks.structure.item.base import Structure
+@dataclass(frozen=True)
+class PowderReflnRecord:
+ """Calculated powder reflection metadata for one reflection row."""
+
+ phase_id: str
+ d_spacing: float
+ sin_theta_over_lambda: float
+ index_h: int
+ index_k: int
+ index_l: int
+ f_calc: float
+ f_squared_calc: float
+ two_theta: float | None = None
+ time_of_flight: float | None = None
class CalculatorBase(ABC):
@@ -60,3 +81,19 @@ def calculate_pattern(
np.ndarray
The calculated diffraction pattern as a NumPy array.
"""
+
+ def last_powder_refln_records(
+ self,
+ structure: Structure,
+ experiment: ExperimentBase,
+ *,
+ phase_id: str,
+ ) -> list[PowderReflnRecord] | None:
+ """
+ Return the last powder reflection records for one phase.
+
+ Backends that do not expose powder reflection metadata return
+ ``None`` so callers can clear stale reflection rows and warn.
+ """
+ del self, structure, experiment, phase_id
+ return None
diff --git a/src/easydiffraction/analysis/calculators/cryspy.py b/src/easydiffraction/analysis/calculators/cryspy.py
index 8e6a842ac..3c2da252c 100644
--- a/src/easydiffraction/analysis/calculators/cryspy.py
+++ b/src/easydiffraction/analysis/calculators/cryspy.py
@@ -1,21 +1,28 @@
# SPDX-FileCopyrightText: 2026 EasyScience contributors
# SPDX-License-Identifier: BSD-3-Clause
+from __future__ import annotations
+
import contextlib
import copy
import io
+from typing import TYPE_CHECKING
from typing import Any
import numpy as np
from easydiffraction.analysis.calculators.base import CalculatorBase
+from easydiffraction.analysis.calculators.base import PowderReflnRecord
from easydiffraction.analysis.calculators.factory import CalculatorFactory
from easydiffraction.core.metadata import TypeInfo
-from easydiffraction.datablocks.experiment.item.base import ExperimentBase
from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum
from easydiffraction.datablocks.experiment.item.enums import PeakProfileTypeEnum
from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum
-from easydiffraction.datablocks.structure.item.base import Structure
+from easydiffraction.utils.utils import sin_theta_over_lambda_to_d_spacing
+
+if TYPE_CHECKING:
+ from easydiffraction.datablocks.experiment.item.base import ExperimentBase
+ from easydiffraction.datablocks.structure.item.base import Structure
try:
import cryspy
@@ -31,6 +38,9 @@
cryspy = None
+EXPECTED_HKL_INDEX_ROWS = 3
+
+
@CalculatorFactory.register
class CryspyCalculator(CalculatorBase):
"""
@@ -56,6 +66,7 @@ def __init__(self) -> None:
self._cryspy_dicts: dict[str, dict[str, Any]] = {}
self._cached_peak_types: dict[str, str] = {}
self._cached_adp_types: dict[str, tuple[str, ...]] = {}
+ self._last_powder_phase_blocks: dict[str, dict[str, Any] | None] = {}
def _invalidate_stale_cache(
self,
@@ -185,6 +196,7 @@ def calculate_pattern(
"""
combined_name = f'{structure.name}_{experiment.name}'
self._invalidate_stale_cache(combined_name, experiment, structure)
+ self._last_powder_phase_blocks[combined_name] = None
if called_by_minimizer:
if self._cryspy_dicts and combined_name in self._cryspy_dicts:
@@ -236,15 +248,139 @@ def calculate_pattern(
return []
try:
- signal_plus = cryspy_in_out_dict[cryspy_block_name]['signal_plus']
- signal_minus = cryspy_in_out_dict[cryspy_block_name]['signal_minus']
+ powder_block = cryspy_in_out_dict[cryspy_block_name]
+ signal_plus = powder_block['signal_plus']
+ signal_minus = powder_block['signal_minus']
y_calc = signal_plus + signal_minus
+ self._last_powder_phase_blocks[combined_name] = powder_block.get(
+ f'dict_in_out_{structure.name}'
+ )
except KeyError:
print(f'[CryspyCalculator] Error: No calculated data for {cryspy_block_name}')
return []
return y_calc
+ def last_powder_refln_records(
+ self,
+ structure: Structure,
+ experiment: ExperimentBase,
+ *,
+ phase_id: str,
+ ) -> list[PowderReflnRecord] | None:
+ """
+ Return powder reflection records from the latest pattern run.
+ """
+ combined_name = f'{structure.name}_{experiment.name}'
+ phase_block = self._last_powder_phase_blocks.get(combined_name)
+ if phase_block is None:
+ return None
+
+ core_arrays = self._powder_refln_core_arrays(phase_block)
+ if core_arrays is None:
+ return None
+ x_values = self._powder_refln_x_values(phase_block, experiment.type.beam_mode.value)
+ if x_values is None:
+ return None
+
+ indices, sin_theta_over_lambda, f_nucl = core_arrays
+ d_spacing = self._powder_refln_d_spacing(phase_block, sin_theta_over_lambda)
+ f_calc = np.abs(f_nucl)
+ f_squared_calc = f_calc**2
+
+ return [
+ self._powder_refln_record(
+ phase_id=phase_id,
+ beam_mode=experiment.type.beam_mode.value,
+ hkl=(index_h, index_k, index_l),
+ values=(sthovl, d_value, x_value, f_value, f_sq_value),
+ )
+ for index_h, index_k, index_l, sthovl, d_value, x_value, f_value, f_sq_value in zip(
+ indices[0],
+ indices[1],
+ indices[2],
+ sin_theta_over_lambda,
+ d_spacing,
+ x_values,
+ f_calc,
+ f_squared_calc,
+ strict=True,
+ )
+ ]
+
+ @staticmethod
+ def _powder_refln_core_arrays(
+ phase_block: dict[str, Any],
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None:
+ 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
+
+ if indices.shape[0] != EXPECTED_HKL_INDEX_ROWS:
+ return None
+ return indices, sin_theta_over_lambda, f_nucl
+
+ @staticmethod
+ def _powder_refln_d_spacing(
+ phase_block: dict[str, Any],
+ sin_theta_over_lambda: np.ndarray,
+ ) -> np.ndarray:
+ d_spacing_raw = phase_block.get('d_hkl')
+ if d_spacing_raw is None:
+ return np.asarray(
+ sin_theta_over_lambda_to_d_spacing(sin_theta_over_lambda),
+ dtype=float,
+ )
+ return np.asarray(d_spacing_raw, dtype=float)
+
+ @staticmethod
+ def _powder_refln_x_values(
+ phase_block: dict[str, Any],
+ beam_mode: BeamModeEnum,
+ ) -> np.ndarray | None:
+ x_values = None
+ if beam_mode == BeamModeEnum.CONSTANT_WAVELENGTH:
+ x_raw = phase_block.get('ttheta_hkl')
+ if x_raw is not None:
+ x_values = np.degrees(np.asarray(x_raw, dtype=float))
+ elif beam_mode == BeamModeEnum.TIME_OF_FLIGHT:
+ x_raw = phase_block.get('time_hkl')
+ if x_raw is not None:
+ x_values = np.asarray(x_raw, dtype=float)
+
+ if x_values is None or x_values.size == 0:
+ return None
+ return x_values
+
+ @staticmethod
+ def _powder_refln_record(
+ *,
+ phase_id: str,
+ beam_mode: BeamModeEnum,
+ hkl: tuple[int, int, int],
+ values: tuple[float, float, float, float, float],
+ ) -> PowderReflnRecord:
+ index_h, index_k, index_l = hkl
+ sin_theta_over_lambda, d_spacing, x_value, f_calc, f_squared_calc = values
+ x_kwargs = {'two_theta': float(x_value)}
+ if beam_mode == BeamModeEnum.TIME_OF_FLIGHT:
+ x_kwargs = {'time_of_flight': float(x_value)}
+
+ return PowderReflnRecord(
+ phase_id=phase_id,
+ d_spacing=float(d_spacing),
+ sin_theta_over_lambda=float(sin_theta_over_lambda),
+ index_h=int(index_h),
+ index_k=int(index_k),
+ index_l=int(index_l),
+ f_calc=float(f_calc),
+ f_squared_calc=float(f_squared_calc),
+ **x_kwargs,
+ )
+
def _recreate_cryspy_dict(
self,
structure: Structure,
@@ -1045,7 +1181,7 @@ def _cif_measured_data_sc(
experiment: ExperimentBase,
) -> None:
"""Append single crystal measured data loop."""
- data = experiment.data
+ data = experiment.refln
cif_lines.extend((
'',
'loop_',
diff --git a/src/easydiffraction/analysis/fit_helpers/metrics.py b/src/easydiffraction/analysis/fit_helpers/metrics.py
index 61700006b..2de2064cd 100644
--- a/src/easydiffraction/analysis/fit_helpers/metrics.py
+++ b/src/easydiffraction/analysis/fit_helpers/metrics.py
@@ -7,6 +7,8 @@
import numpy as np
+from easydiffraction.datablocks.experiment.item.base import intensity_category_for
+
if TYPE_CHECKING:
from easydiffraction.datablocks.experiment.item.base import ExperimentBase
from easydiffraction.datablocks.structure.collection import Structures
@@ -179,9 +181,10 @@ def get_reliability_inputs(
structure._update_categories()
experiment._update_categories()
- y_calc = experiment.data.intensity_calc
- y_meas = experiment.data.intensity_meas
- y_meas_su = experiment.data.intensity_meas_su
+ intensity_category = intensity_category_for(experiment)
+ y_calc = intensity_category.intensity_calc
+ y_meas = intensity_category.intensity_meas
+ y_meas_su = intensity_category.intensity_meas_su
if y_meas is not None and y_calc is not None:
# If standard uncertainty is not provided, use ones
diff --git a/src/easydiffraction/analysis/fitting.py b/src/easydiffraction/analysis/fitting.py
index a29062037..9b0360143 100644
--- a/src/easydiffraction/analysis/fitting.py
+++ b/src/easydiffraction/analysis/fitting.py
@@ -12,6 +12,7 @@
from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum
from easydiffraction.analysis.minimizers.factory import MinimizerFactory
from easydiffraction.core.variable import Parameter
+from easydiffraction.datablocks.experiment.item.base import intensity_category_for
from easydiffraction.utils.enums import VerbosityEnum
if TYPE_CHECKING:
@@ -225,9 +226,10 @@ def _residual_function(
# Calculate the difference between measured and calculated
# patterns
- y_calc = experiment.data.intensity_calc
- y_meas = experiment.data.intensity_meas
- y_meas_su = experiment.data.intensity_meas_su
+ intensity_category = intensity_category_for(experiment)
+ y_calc = intensity_category.intensity_calc
+ y_meas = intensity_category.intensity_meas
+ y_meas_su = intensity_category.intensity_meas_su
diff = (y_meas - y_calc) / y_meas_su
# Residuals are squared before going into reduced
diff --git a/src/easydiffraction/core/variable.py b/src/easydiffraction/core/variable.py
index d5457d8fe..84bbd2f42 100644
--- a/src/easydiffraction/core/variable.py
+++ b/src/easydiffraction/core/variable.py
@@ -355,8 +355,7 @@ def free(self, v: bool) -> None:
)
if validated and self._symmetry_fixed:
log.warning(
- f"Parameter '{self.unique_name}' is fixed by symmetry "
- 'and cannot be refined. Ignoring free=True.'
+ f"Parameter '{self.unique_name}' is fixed by symmetry. Ignoring free=True."
)
self._free = False
return
diff --git a/src/easydiffraction/datablocks/experiment/categories/data/__init__.py b/src/easydiffraction/datablocks/experiment/categories/data/__init__.py
index 3599f3b58..5b2a41923 100644
--- a/src/easydiffraction/datablocks/experiment/categories/data/__init__.py
+++ b/src/easydiffraction/datablocks/experiment/categories/data/__init__.py
@@ -3,5 +3,4 @@
from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdCwlData
from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdTofData
-from easydiffraction.datablocks.experiment.categories.data.bragg_sc import ReflnData
from easydiffraction.datablocks.experiment.categories.data.total_pd import TotalData
diff --git a/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py b/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
index 1d12e383e..0f8633010 100644
--- a/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
+++ b/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
@@ -3,6 +3,8 @@
from __future__ import annotations
+from typing import TYPE_CHECKING
+
import numpy as np
from easydiffraction.core.category import CategoryCollection
@@ -22,9 +24,13 @@
from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum
from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum
from easydiffraction.io.cif.handler import CifHandler
+from easydiffraction.utils.logging import log
from easydiffraction.utils.utils import tof_to_d
from easydiffraction.utils.utils import twotheta_to_d
+if TYPE_CHECKING:
+ from easydiffraction.analysis.calculators.base import PowderReflnRecord
+
# Uncertainty values below this threshold are replaced with 1.0
_MIN_UNCERTAINTY = 0.0001
@@ -379,29 +385,89 @@ def _update(
project = experiments._parent
structures = project.structures
calculator = experiment.calculation.calculator
+ refln = experiment.refln
+
+ calc, refln_records, missing_refln_records = self._phase_calculation_results(
+ experiment=experiment,
+ structures=structures,
+ calculator=calculator,
+ called_by_minimizer=called_by_minimizer,
+ collect_refln_records=refln is not None,
+ )
+ self._set_intensity_calc(calc + self.intensity_bkg)
+ if refln is None:
+ return
+
+ if missing_refln_records:
+ refln._replace_from_records([])
+ log.warning(
+ 'Calculated powder reflection metadata is unavailable for '
+ f"experiment '{experiment.name}' with calculator "
+ f"'{calculator.name}'. Clearing experiment.refln.",
+ )
+ return
+
+ refln._replace_from_records(refln_records)
- initial_calc = np.zeros_like(self.x)
- calc = initial_calc
+ def _phase_calculation_results(
+ self,
+ *,
+ experiment: object,
+ structures: object,
+ calculator: object,
+ called_by_minimizer: bool,
+ collect_refln_records: bool,
+ ) -> tuple[np.ndarray, list[PowderReflnRecord], bool]:
+ calc = np.zeros_like(self.x)
+ refln_records: list[PowderReflnRecord] = []
+ missing_refln_records = False
- # TODO: refactor _get_valid_linked_phases to only be responsible
- # for returning list. Warning message should be defined here,
- # at least some of them.
- # TODO: Adapt following the _update method in bragg_sc.py
for linked_phase in experiment._get_valid_linked_phases(structures):
structure_id = linked_phase._identity.category_entry_name
- structure_scale = linked_phase.scale.value
structure = structures[structure_id]
-
- structure_calc = calculator.calculate_pattern(
- structure,
- experiment,
+ structure_scaled_calc, structure_refln_records = self._phase_result(
+ structure=structure,
+ experiment=experiment,
+ calculator=calculator,
+ linked_phase=linked_phase,
called_by_minimizer=called_by_minimizer,
+ collect_refln_records=collect_refln_records,
)
-
- structure_scaled_calc = structure_scale * structure_calc
calc += structure_scaled_calc
+ if not collect_refln_records:
+ continue
+ if structure_refln_records is None:
+ missing_refln_records = True
+ continue
+ refln_records.extend(structure_refln_records)
- self._set_intensity_calc(calc + self.intensity_bkg)
+ return calc, refln_records, missing_refln_records
+
+ @staticmethod
+ def _phase_result(
+ *,
+ structure: object,
+ experiment: object,
+ calculator: object,
+ linked_phase: object,
+ called_by_minimizer: bool,
+ collect_refln_records: bool,
+ ) -> tuple[np.ndarray, list[PowderReflnRecord] | None]:
+ structure_calc = calculator.calculate_pattern(
+ structure,
+ experiment,
+ called_by_minimizer=called_by_minimizer,
+ )
+ structure_scaled_calc = linked_phase.scale.value * structure_calc
+ if not collect_refln_records:
+ return structure_scaled_calc, []
+
+ structure_refln_records = calculator.last_powder_refln_records(
+ structure,
+ experiment,
+ phase_id=linked_phase.id.value,
+ )
+ return structure_scaled_calc, structure_refln_records
###################
# Public properties
diff --git a/src/easydiffraction/datablocks/experiment/categories/data/factory.py b/src/easydiffraction/datablocks/experiment/categories/data/factory.py
index 703a1fe73..5785491a8 100644
--- a/src/easydiffraction/datablocks/experiment/categories/data/factory.py
+++ b/src/easydiffraction/datablocks/experiment/categories/data/factory.py
@@ -30,8 +30,4 @@ class DataFactory(FactoryBase):
('sample_form', SampleFormEnum.POWDER),
('scattering_type', ScatteringTypeEnum.TOTAL),
}): 'total-pd',
- frozenset({
- ('sample_form', SampleFormEnum.SINGLE_CRYSTAL),
- ('scattering_type', ScatteringTypeEnum.BRAGG),
- }): 'bragg-sc',
}
diff --git a/src/easydiffraction/datablocks/experiment/categories/refln/__init__.py b/src/easydiffraction/datablocks/experiment/categories/refln/__init__.py
new file mode 100644
index 000000000..ddeb357a6
--- /dev/null
+++ b/src/easydiffraction/datablocks/experiment/categories/refln/__init__.py
@@ -0,0 +1,10 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors
+# SPDX-License-Identifier: BSD-3-Clause
+
+from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderCwlRefln
+from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderCwlReflnData
+from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderReflnBase
+from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderTofRefln
+from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderTofReflnData
+from easydiffraction.datablocks.experiment.categories.refln.bragg_sc import Refln
+from easydiffraction.datablocks.experiment.categories.refln.bragg_sc import ReflnData
diff --git a/src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py b/src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py
new file mode 100644
index 000000000..71d367ddd
--- /dev/null
+++ b/src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py
@@ -0,0 +1,302 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors
+# SPDX-License-Identifier: BSD-3-Clause
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import numpy as np
+
+from easydiffraction.core.category import CategoryCollection
+from easydiffraction.core.metadata import CalculatorSupport
+from easydiffraction.core.metadata import Compatibility
+from easydiffraction.core.metadata import TypeInfo
+from easydiffraction.core.validation import AttributeSpec
+from easydiffraction.core.validation import RangeValidator
+from easydiffraction.core.variable import NumericDescriptor
+from easydiffraction.core.variable import StringDescriptor
+from easydiffraction.datablocks.experiment.categories.refln.bragg_sc import (
+ Refln as SingleCrystalRefln,
+)
+from easydiffraction.datablocks.experiment.categories.refln.factory import ReflnFactory
+from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum
+from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum
+from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum
+from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum
+from easydiffraction.io.cif.handler import CifHandler
+
+if TYPE_CHECKING:
+ from collections.abc import Sequence
+
+ from easydiffraction.analysis.calculators.base import PowderReflnRecord
+
+
+class PowderReflnBase(SingleCrystalRefln):
+ """Single calculated powder reflection row."""
+
+ def __init__(self) -> None:
+ super().__init__()
+
+ self._phase_id = StringDescriptor(
+ name='phase_id',
+ description='Identifier of the linked phase for this reflection',
+ value_spec=AttributeSpec(default=''),
+ cif_handler=CifHandler(names=['_refln.phase_id']),
+ )
+ self._f_calc = NumericDescriptor(
+ name='f_calc',
+ description='Calculated structure-factor amplitude for this reflection',
+ value_spec=AttributeSpec(
+ default=0.0,
+ validator=RangeValidator(ge=0),
+ ),
+ cif_handler=CifHandler(names=['_refln.f_calc']),
+ )
+ self._f_squared_calc = NumericDescriptor(
+ name='f_squared_calc',
+ description='Calculated structure-factor amplitude squared for this reflection',
+ value_spec=AttributeSpec(
+ default=0.0,
+ validator=RangeValidator(ge=0),
+ ),
+ cif_handler=CifHandler(names=['_refln.f_squared_calc']),
+ )
+
+ @property
+ def phase_id(self) -> StringDescriptor:
+ """Linked-phase identifier for this reflection."""
+ return self._phase_id
+
+ @property
+ def f_calc(self) -> NumericDescriptor:
+ """Calculated structure-factor amplitude for this reflection."""
+ return self._f_calc
+
+ @property
+ def f_squared_calc(self) -> NumericDescriptor:
+ """Calculated structure-factor amplitude squared."""
+ return self._f_squared_calc
+
+ @property
+ def parameters(self) -> list:
+ """Powder reflection descriptors serialized in CIF loops."""
+ return [
+ self._id,
+ self._phase_id,
+ self._d_spacing,
+ self._sin_theta_over_lambda,
+ self._index_h,
+ self._index_k,
+ self._index_l,
+ self._f_calc,
+ self._f_squared_calc,
+ ]
+
+
+class PowderCwlRefln(PowderReflnBase):
+ """Single calculated powder reflection row for CWL experiments."""
+
+ def __init__(self) -> None:
+ super().__init__()
+
+ self._two_theta = NumericDescriptor(
+ name='two_theta',
+ description='Calculated 2theta position for this reflection',
+ units='deg',
+ value_spec=AttributeSpec(
+ default=0.0,
+ validator=RangeValidator(ge=0, le=180),
+ ),
+ cif_handler=CifHandler(names=['_refln.two_theta']),
+ )
+
+ @property
+ def two_theta(self) -> NumericDescriptor:
+ """Calculated 2theta position for this reflection."""
+ return self._two_theta
+
+ @property
+ def parameters(self) -> list:
+ """Powder CWL reflection descriptors serialized in CIF loops."""
+ return [*super().parameters, self._two_theta]
+
+
+class PowderTofRefln(PowderReflnBase):
+ """Single calculated powder reflection row for TOF experiments."""
+
+ def __init__(self) -> None:
+ super().__init__()
+
+ self._time_of_flight = NumericDescriptor(
+ name='time_of_flight',
+ description='Calculated time-of-flight position for this reflection',
+ units='μs',
+ value_spec=AttributeSpec(
+ default=0.0,
+ validator=RangeValidator(ge=0),
+ ),
+ cif_handler=CifHandler(names=['_refln.time_of_flight']),
+ )
+
+ @property
+ def time_of_flight(self) -> NumericDescriptor:
+ """Calculated time-of-flight position for this reflection."""
+ return self._time_of_flight
+
+ @property
+ def parameters(self) -> list:
+ """Powder TOF reflection descriptors serialized in CIF loops."""
+ return [*super().parameters, self._time_of_flight]
+
+
+class PowderReflnDataBase(CategoryCollection):
+ """Base collection for calculated powder reflection rows."""
+
+ _update_priority = 110
+
+ def _replace_from_records(self, records: Sequence[PowderReflnRecord]) -> None:
+ """Replace all rows from calculator reflection records."""
+ for item in self._items:
+ item._parent = None
+
+ new_items = []
+ for index, record in enumerate(records, start=1):
+ item = self._item_type()
+ item._parent = self
+ item.id._value = str(index)
+ item.phase_id._value = str(record.phase_id)
+ item.d_spacing._value = float(record.d_spacing)
+ item.sin_theta_over_lambda._value = float(record.sin_theta_over_lambda)
+ item.index_h._value = record.index_h
+ item.index_k._value = record.index_k
+ item.index_l._value = record.index_l
+ item.f_calc._value = float(record.f_calc)
+ item.f_squared_calc._value = float(record.f_squared_calc)
+ self._set_x_value(item=item, record=record)
+ new_items.append(item)
+
+ self._items = new_items
+ self._rebuild_index()
+
+ def _set_x_value(
+ self,
+ *,
+ item: PowderReflnBase,
+ record: PowderReflnRecord,
+ ) -> None:
+ """Set the beam-mode-specific x coordinate."""
+ del self, item, record
+
+ @property
+ def id(self) -> np.ndarray:
+ """Reflection identifiers for all rows."""
+ return np.fromiter((item.id.value for item in self._items), dtype=object)
+
+ @property
+ def phase_id(self) -> np.ndarray:
+ """Linked-phase identifiers for all rows."""
+ return np.fromiter((item.phase_id.value for item in self._items), dtype=object)
+
+ @property
+ def d_spacing(self) -> np.ndarray:
+ """D-spacing values for all rows."""
+ return np.fromiter((item.d_spacing.value for item in self._items), dtype=float)
+
+ @property
+ def sin_theta_over_lambda(self) -> np.ndarray:
+ """sin(theta)/lambda values for all rows."""
+ return np.fromiter(
+ (item.sin_theta_over_lambda.value for item in self._items),
+ dtype=float,
+ )
+
+ @property
+ def index_h(self) -> np.ndarray:
+ """Miller h indices for all rows."""
+ return np.fromiter((item.index_h.value for item in self._items), dtype=float)
+
+ @property
+ def index_k(self) -> np.ndarray:
+ """Miller k indices for all rows."""
+ return np.fromiter((item.index_k.value for item in self._items), dtype=float)
+
+ @property
+ def index_l(self) -> np.ndarray:
+ """Miller l indices for all rows."""
+ return np.fromiter((item.index_l.value for item in self._items), dtype=float)
+
+ @property
+ def f_calc(self) -> np.ndarray:
+ """Calculated structure-factor amplitudes for all rows."""
+ return np.fromiter((item.f_calc.value for item in self._items), dtype=float)
+
+ @property
+ def f_squared_calc(self) -> np.ndarray:
+ """
+ Calculated structure-factor amplitudes squared for all rows.
+ """
+ return np.fromiter((item.f_squared_calc.value for item in self._items), dtype=float)
+
+
+@ReflnFactory.register
+class PowderCwlReflnData(PowderReflnDataBase):
+ """Calculated powder reflection collection for CWL experiments."""
+
+ type_info = TypeInfo(tag='bragg-pd-refln', description='Bragg powder CWL reflection data')
+ compatibility = Compatibility(
+ sample_form=frozenset({SampleFormEnum.POWDER}),
+ scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
+ beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}),
+ )
+ calculator_support = CalculatorSupport(
+ calculators=frozenset({CalculatorEnum.CRYSPY}),
+ )
+
+ def __init__(self) -> None:
+ super().__init__(item_type=PowderCwlRefln)
+
+ def _set_x_value(
+ self,
+ *,
+ item: PowderReflnBase,
+ record: PowderReflnRecord,
+ ) -> None:
+ del self
+ item.two_theta._value = float(record.two_theta)
+
+ @property
+ def two_theta(self) -> np.ndarray:
+ """Calculated 2theta positions for all rows."""
+ return np.fromiter((item.two_theta.value for item in self._items), dtype=float)
+
+
+@ReflnFactory.register
+class PowderTofReflnData(PowderReflnDataBase):
+ """Calculated powder reflection collection for TOF experiments."""
+
+ type_info = TypeInfo(tag='bragg-pd-tof-refln', description='Bragg powder TOF reflection data')
+ compatibility = Compatibility(
+ sample_form=frozenset({SampleFormEnum.POWDER}),
+ scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
+ beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}),
+ )
+ calculator_support = CalculatorSupport(
+ calculators=frozenset({CalculatorEnum.CRYSPY}),
+ )
+
+ def __init__(self) -> None:
+ super().__init__(item_type=PowderTofRefln)
+
+ def _set_x_value(
+ self,
+ *,
+ item: PowderReflnBase,
+ record: PowderReflnRecord,
+ ) -> None:
+ del self
+ item.time_of_flight._value = float(record.time_of_flight)
+
+ @property
+ def time_of_flight(self) -> np.ndarray:
+ """Calculated time-of-flight positions for all rows."""
+ return np.fromiter((item.time_of_flight.value for item in self._items), dtype=float)
diff --git a/src/easydiffraction/datablocks/experiment/categories/data/bragg_sc.py b/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py
similarity index 99%
rename from src/easydiffraction/datablocks/experiment/categories/data/bragg_sc.py
rename to src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py
index 3e0669ecf..52329abb0 100644
--- a/src/easydiffraction/datablocks/experiment/categories/data/bragg_sc.py
+++ b/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py
@@ -15,7 +15,7 @@
from easydiffraction.core.validation import RegexValidator
from easydiffraction.core.variable import NumericDescriptor
from easydiffraction.core.variable import StringDescriptor
-from easydiffraction.datablocks.experiment.categories.data.factory import DataFactory
+from easydiffraction.datablocks.experiment.categories.refln.factory import ReflnFactory
from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum
from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum
from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum
@@ -236,7 +236,7 @@ def wavelength(self) -> NumericDescriptor:
return self._wavelength
-@DataFactory.register
+@ReflnFactory.register
class ReflnData(CategoryCollection):
"""Collection of reflections for single crystal diffraction data."""
diff --git a/src/easydiffraction/datablocks/experiment/categories/refln/factory.py b/src/easydiffraction/datablocks/experiment/categories/refln/factory.py
new file mode 100644
index 000000000..5069b1478
--- /dev/null
+++ b/src/easydiffraction/datablocks/experiment/categories/refln/factory.py
@@ -0,0 +1,39 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors
+# SPDX-License-Identifier: BSD-3-Clause
+"""Reflection collection factory — delegates to ``FactoryBase``."""
+
+from __future__ import annotations
+
+from typing import ClassVar
+
+from easydiffraction.core.factory import FactoryBase
+from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum
+from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum
+from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum
+
+
+class ReflnFactory(FactoryBase):
+ """Factory for creating reflection collections."""
+
+ _default_rules: ClassVar[dict] = {
+ frozenset({
+ ('sample_form', SampleFormEnum.SINGLE_CRYSTAL),
+ ('scattering_type', ScatteringTypeEnum.BRAGG),
+ ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH),
+ }): 'bragg-sc',
+ frozenset({
+ ('sample_form', SampleFormEnum.SINGLE_CRYSTAL),
+ ('scattering_type', ScatteringTypeEnum.BRAGG),
+ ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT),
+ }): 'bragg-sc',
+ frozenset({
+ ('sample_form', SampleFormEnum.POWDER),
+ ('scattering_type', ScatteringTypeEnum.BRAGG),
+ ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH),
+ }): 'bragg-pd-refln',
+ frozenset({
+ ('sample_form', SampleFormEnum.POWDER),
+ ('scattering_type', ScatteringTypeEnum.BRAGG),
+ ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT),
+ }): 'bragg-pd-tof-refln',
+ }
diff --git a/src/easydiffraction/datablocks/experiment/item/base.py b/src/easydiffraction/datablocks/experiment/item/base.py
index 1a4858bb0..563a62229 100644
--- a/src/easydiffraction/datablocks/experiment/item/base.py
+++ b/src/easydiffraction/datablocks/experiment/item/base.py
@@ -24,6 +24,7 @@
LinkedPhasesFactory,
)
from easydiffraction.datablocks.experiment.categories.peak.factory import PeakFactory
+from easydiffraction.datablocks.experiment.categories.refln.factory import ReflnFactory
from easydiffraction.io.cif.parse import read_cif_str
from easydiffraction.io.cif.serialize import experiment_to_cif
from easydiffraction.utils.logging import console
@@ -36,6 +37,25 @@
from easydiffraction.datablocks.structure.collection import Structures
+def intensity_category_for(experiment: object) -> object:
+ """Return the category exposing measured and calculated values."""
+ resolver = getattr(experiment, '_intensity_category', None)
+ if callable(resolver):
+ return resolver()
+
+ data = getattr(experiment, 'data', None)
+ if data is not None:
+ return data
+
+ refln = getattr(experiment, 'refln', None)
+ if refln is not None:
+ return refln
+
+ name = getattr(experiment, 'name', type(experiment).__name__)
+ msg = f"Experiment '{name}' has no intensity category."
+ raise AttributeError(msg)
+
+
class ExperimentBase(DatablockItem):
"""Base class for all experiment datablock items."""
@@ -199,7 +219,7 @@ def _set_calculator_type(
console.print(tag)
def _resolve_calculation(self) -> None:
- """Auto-resolve the default calculator from data category."""
+ """Auto-resolve the default calculator from category support."""
from easydiffraction.analysis.calculators.factory import CalculatorFactory # noqa: PLC0415
tag = self._default_calculator_tag()
@@ -214,19 +234,30 @@ def _supported_calculator_tags(self) -> list[str]:
"""
Return calculator tags supported by this experiment.
- Intersects the data category's ``calculator_support`` with
- calculators whose engines are importable.
+ Intersects the active support category's ``calculator_support``
+ with calculators whose engines are importable.
"""
from easydiffraction.analysis.calculators.factory import CalculatorFactory # noqa: PLC0415
available = CalculatorFactory.supported_tags()
- data = getattr(self, '_data', None)
- if data is not None:
- data_support = getattr(data, 'calculator_support', None)
+ support_category = self._calculator_support_category()
+ if support_category is not None:
+ data_support = getattr(support_category, 'calculator_support', None)
if data_support and data_support.calculators:
return [t for t in available if t in data_support.calculators]
return available
+ def _calculator_support_category(self) -> object | None:
+ """
+ Return the category that constrains calculator availability.
+ """
+ return getattr(self, '_data', None) or getattr(self, '_refln', None)
+
+ def _intensity_category(self) -> object:
+ """Return the experiment category exposing intensity arrays."""
+ msg = f"Experiment '{self.name}' has no intensity category."
+ raise AttributeError(msg)
+
class ScExperimentBase(ExperimentBase):
"""Base class for all single crystal experiments."""
@@ -249,12 +280,12 @@ def __init__(
sample_form=self.type.sample_form.value,
)
self._instrument = InstrumentFactory.create(self._instrument_type)
- self._data_type: str = DataFactory.default_tag(
+ self._refln_type: str = ReflnFactory.default_tag(
sample_form=self.type.sample_form.value,
beam_mode=self.type.beam_mode.value,
scattering_type=self.type.scattering_type.value,
)
- self._data = DataFactory.create(self._data_type)
+ self._refln = ReflnFactory.create(self._refln_type)
self._resolve_calculation()
@abstractmethod
@@ -347,14 +378,20 @@ def instrument(self) -> object:
"""Active instrument model for this experiment."""
return self._instrument
- # ------------------------------------------------------------------
- # Data (fixed at creation)
- # ------------------------------------------------------------------
-
@property
- def data(self) -> object:
- """Data collection for this experiment."""
- return self._data
+ def refln(self) -> object:
+ """Reflection collection for this experiment."""
+ return self._refln
+
+ def _calculator_support_category(self) -> object | None:
+ """
+ Return the reflection collection that constrains calculators.
+ """
+ return self._refln
+
+ def _intensity_category(self) -> object:
+ """Return the single-crystal reflection collection."""
+ return self._refln
class PdExperimentBase(ExperimentBase):
@@ -459,6 +496,16 @@ def data(self) -> object:
"""Data collection for this experiment."""
return self._data
+ def _calculator_support_category(self) -> object | None:
+ """
+ Return the powder data collection that constrains calculators.
+ """
+ return self._data
+
+ def _intensity_category(self) -> object:
+ """Return the powder intensity data collection."""
+ return self._data
+
@property
def peak(self) -> object:
"""Peak category object with profile parameters and mixins."""
diff --git a/src/easydiffraction/datablocks/experiment/item/bragg_pd.py b/src/easydiffraction/datablocks/experiment/item/bragg_pd.py
index 54f8a18c3..4ae7f79ec 100644
--- a/src/easydiffraction/datablocks/experiment/item/bragg_pd.py
+++ b/src/easydiffraction/datablocks/experiment/item/bragg_pd.py
@@ -11,8 +11,10 @@
from easydiffraction.core.metadata import TypeInfo
from easydiffraction.datablocks.experiment.categories.background.factory import BackgroundFactory
from easydiffraction.datablocks.experiment.categories.instrument.factory import InstrumentFactory
+from easydiffraction.datablocks.experiment.categories.refln.factory import ReflnFactory
from easydiffraction.datablocks.experiment.item.base import PdExperimentBase
from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum
+from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum
from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum
from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum
from easydiffraction.datablocks.experiment.item.factory import ExperimentFactory
@@ -62,6 +64,47 @@ def __init__(
self._instrument = InstrumentFactory.create(self._instrument_type)
self._background_type: str = BackgroundFactory.default_tag()
self._background = BackgroundFactory.create(self._background_type)
+ self._refln = None
+ self._sync_refln_category()
+
+ def _refln_collection_tag(self) -> str:
+ """
+ Return the reflection-collection tag for this beam mode.
+ """
+ return ReflnFactory.default_tag(
+ sample_form=self.type.sample_form.value,
+ beam_mode=self.type.beam_mode.value,
+ scattering_type=self.type.scattering_type.value,
+ )
+
+ def _refln_collection_type(self) -> type[object]:
+ """
+ Return the reflection-collection type for this beam mode.
+ """
+ refln_tag = self._refln_collection_tag()
+ return ReflnFactory._supported_map()[refln_tag]
+
+ def _sync_refln_category(self) -> None:
+ """Create or remove ``refln`` for the active calculator."""
+ calculator_type = self._calculator_type or self._default_calculator_tag()
+ refln_collection_type = self._refln_collection_type()
+ calculator = CalculatorEnum(calculator_type)
+ if refln_collection_type.calculator_support.supports(calculator):
+ if not isinstance(self._refln, refln_collection_type):
+ self._refln = ReflnFactory.create(self._refln_collection_tag())
+ return
+
+ self._refln = None
+
+ def _set_calculator_type(
+ self,
+ tag: str,
+ *,
+ announce: bool = True,
+ ) -> None:
+ """Switch calculator backend and sync ``refln`` availability."""
+ super()._set_calculator_type(tag, announce=announce)
+ self._sync_refln_category()
def _load_ascii_data_to_experiment(
self,
@@ -129,6 +172,11 @@ def instrument(self) -> object:
"""Active instrument model for this experiment."""
return self._instrument
+ @property
+ def refln(self) -> object | None:
+ """Calculated reflection metadata when supported."""
+ return self._refln
+
# ------------------------------------------------------------------
# Background (switchable-category pattern)
# ------------------------------------------------------------------
diff --git a/src/easydiffraction/datablocks/experiment/item/bragg_sc.py b/src/easydiffraction/datablocks/experiment/item/bragg_sc.py
index 7d2a95469..5e0659fae 100644
--- a/src/easydiffraction/datablocks/experiment/item/bragg_sc.py
+++ b/src/easydiffraction/datablocks/experiment/item/bragg_sc.py
@@ -47,7 +47,7 @@ def __init__(
def _load_ascii_data_to_experiment(self, data_path: str) -> int:
"""
- Load measured data from an ASCII file into the data category.
+ Load measured data from an ASCII file into the refln category.
The file format is space/column separated with 5 columns: ``h k
l Iobs sIobs``.
@@ -80,10 +80,10 @@ def _load_ascii_data_to_experiment(self, data_path: str) -> int:
integrated_intensities = data[:, 3]
integrated_intensities_su = data[:, 4]
- # Set the experiment data
- self.data._create_items_set_hkl_and_id(indices_h, indices_k, indices_l)
- self.data._set_intensity_meas(integrated_intensities)
- self.data._set_intensity_meas_su(integrated_intensities_su)
+ # Set the experiment reflections
+ self.refln._create_items_set_hkl_and_id(indices_h, indices_k, indices_l)
+ self.refln._set_intensity_meas(integrated_intensities)
+ self.refln._set_intensity_meas_su(integrated_intensities_su)
return len(indices_h)
@@ -112,7 +112,7 @@ def __init__(
def _load_ascii_data_to_experiment(self, data_path: str) -> int:
"""
- Load measured data from an ASCII file into the data category.
+ Load measured data from an ASCII file into the refln category.
The file format is space/column separated with 6 columns: ``h k
l Iobs sIobs wavelength``.
@@ -155,10 +155,10 @@ def _load_ascii_data_to_experiment(self, data_path: str) -> int:
# Extract wavelength values
wavelength = data[:, 5]
- # Set the experiment data
- self.data._create_items_set_hkl_and_id(indices_h, indices_k, indices_l)
- self.data._set_intensity_meas(integrated_intensities)
- self.data._set_intensity_meas_su(integrated_intensities_su)
- self.data._set_wavelength(wavelength)
+ # Set the experiment reflections
+ self.refln._create_items_set_hkl_and_id(indices_h, indices_k, indices_l)
+ self.refln._set_intensity_meas(integrated_intensities)
+ self.refln._set_intensity_meas_su(integrated_intensities_su)
+ self.refln._set_wavelength(wavelength)
return len(indices_h)
diff --git a/src/easydiffraction/display/plotters/ascii.py b/src/easydiffraction/display/plotters/ascii.py
index 1874faca9..a0c95fe11 100644
--- a/src/easydiffraction/display/plotters/ascii.py
+++ b/src/easydiffraction/display/plotters/ascii.py
@@ -16,6 +16,7 @@
from easydiffraction.display.plotters.base import DEFAULT_HEIGHT
from easydiffraction.display.plotters.base import SERIES_CONFIG
from easydiffraction.display.plotters.base import PlotterBase
+from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec
from easydiffraction.utils.logging import console
DEFAULT_COLORS = {
@@ -105,6 +106,34 @@ def plot_powder(
print(padded)
+ def plot_powder_meas_vs_calc(
+ self,
+ plot_spec: PowderMeasVsCalcSpec,
+ ) -> None:
+ """
+ Render a composite powder plot in the terminal.
+
+ The ASCII backend falls back to the existing single-chart view
+ for measured, calculated, and residual series. Bragg tick rows
+ are announced but not rendered graphically.
+ """
+ y_series = [plot_spec.y_meas, plot_spec.y_calc]
+ labels = ['meas', 'calc']
+ if plot_spec.y_resid is not None:
+ y_series.append(plot_spec.y_resid)
+ labels.append('resid')
+
+ self.plot_powder(
+ x=plot_spec.x,
+ y_series=y_series,
+ labels=labels,
+ axes_labels=plot_spec.axes_labels,
+ title=plot_spec.title,
+ height=plot_spec.height,
+ )
+ if plot_spec.bragg_tick_sets:
+ console.print('Bragg peak subplot rows are available with the Plotly engine only.')
+
@staticmethod
def plot_single_crystal(
x_calc: object,
diff --git a/src/easydiffraction/display/plotters/base.py b/src/easydiffraction/display/plotters/base.py
index 920fac810..b1a19a274 100644
--- a/src/easydiffraction/display/plotters/base.py
+++ b/src/easydiffraction/display/plotters/base.py
@@ -6,6 +6,7 @@
from abc import ABC
from abc import abstractmethod
+from dataclasses import dataclass
from enum import StrEnum
import numpy as np
@@ -19,6 +20,48 @@
DEFAULT_MAX = np.inf
+@dataclass(frozen=True)
+class BraggTickSet:
+ """
+ Bragg tick data for one linked phase row.
+
+ The plotting facade converts experiment reflection-category data
+ into this display-specific container so plotting backends stay
+ decoupled from experiment datablock internals.
+ """
+
+ phase_id: str
+ x: np.ndarray
+ h: np.ndarray
+ k: np.ndarray
+ ell: np.ndarray
+ f_squared_calc: np.ndarray
+ f_calc: np.ndarray
+
+
+@dataclass(frozen=True)
+class PowderMeasVsCalcSpec:
+ """
+ Specification for one composite powder plot.
+
+ The plotting facade assembles the measured, background, calculated,
+ residual, and Bragg-tick data into this display-specific object
+ before delegating to a backend.
+ """
+
+ x: np.ndarray
+ y_meas: np.ndarray
+ y_calc: np.ndarray
+ y_resid: np.ndarray | None
+ bragg_tick_sets: tuple[BraggTickSet, ...]
+ axes_labels: list[str]
+ title: str
+ residual_height_fraction: float
+ bragg_peaks_height_fraction: float
+ height: int | None = None
+ y_bkg: np.ndarray | None = None
+
+
class XAxisType(StrEnum):
"""
X-axis types for diffraction plots.
@@ -142,6 +185,10 @@ class XAxisType(StrEnum):
'mode': 'lines',
'name': 'Total calculated (Icalc)',
},
+ 'bkg': {
+ 'mode': 'lines',
+ 'name': 'Background (Ibkg)',
+ },
'meas': {
'mode': 'lines+markers',
'name': 'Measured (Imeas)',
@@ -200,6 +247,20 @@ def plot_powder(
Backend-specific height (text rows or pixels).
"""
+ @abstractmethod
+ def plot_powder_meas_vs_calc(
+ self,
+ plot_spec: PowderMeasVsCalcSpec,
+ ) -> None:
+ """
+ Render a composite powder plot with Bragg ticks and residual.
+
+ Parameters
+ ----------
+ plot_spec : PowderMeasVsCalcSpec
+ Composite powder-plot inputs and layout settings.
+ """
+
@abstractmethod
def plot_single_crystal(
self,
diff --git a/src/easydiffraction/display/plotters/plotly.py b/src/easydiffraction/display/plotters/plotly.py
index a876cff7b..887879dd3 100644
--- a/src/easydiffraction/display/plotters/plotly.py
+++ b/src/easydiffraction/display/plotters/plotly.py
@@ -10,10 +10,13 @@
from __future__ import annotations
+from dataclasses import dataclass
+
import darkdetect
import numpy as np
import plotly.graph_objects as go
import plotly.io as pio
+from plotly.subplots import make_subplots
try:
from IPython.display import HTML
@@ -22,18 +25,57 @@
display = None
HTML = None
+from easydiffraction.display.plotters.base import DEFAULT_HEIGHT
from easydiffraction.display.plotters.base import SERIES_CONFIG
+from easydiffraction.display.plotters.base import BraggTickSet
from easydiffraction.display.plotters.base import PlotterBase
+from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec
from easydiffraction.utils._vendored.theme_detect import is_dark
from easydiffraction.utils.environment import in_jupyter
from easydiffraction.utils.environment import in_pycharm
DEFAULT_COLORS = {
'meas': 'rgb(31, 119, 180)',
+ 'bkg': 'rgb(140, 140, 140)',
'calc': 'rgb(214, 39, 40)',
'resid': 'rgb(44, 160, 44)',
}
+MEASURED_LINE_WIDTH = 2.0
+BACKGROUND_LINE_WIDTH = 1.0
+CALCULATED_LINE_WIDTH = 2.0
+RESIDUAL_LINE_WIDTH = 2.0
+
+BRAGG_TICK_COLORS = (
+ 'rgb(255, 127, 14)',
+ 'rgb(23, 190, 207)',
+ 'rgb(140, 140, 140)',
+ 'rgb(188, 189, 34)',
+ 'rgb(148, 103, 189)',
+)
+
+NICE_AXIS_FRACTIONS = (1.0, 2.0, 5.0, 10.0)
+DISPLAY_TICK_FRACTIONS = (1.0, 2.0, 2.5, 4.0, 5.0, 7.5, 10.0)
+PLOTLY_HEIGHT_PER_UNIT = 24
+BRAGG_TICK_MARKER_SIZE = 12
+BRAGG_TICK_MARKER_LINE_WIDTH = 1
+BRAGG_TICK_SYMBOL_HEIGHT_SCALE = 1.4
+MAIN_INTENSITY_RANGE_MARGIN_FRACTION = 0.05
+COMPOSITE_VERTICAL_SPACING = 0.03
+COMPOSITE_MARGIN_RIGHT = 30
+COMPOSITE_MARGIN_TOP = 40
+COMPOSITE_MARGIN_BOTTOM = 45
+
+
+@dataclass(frozen=True)
+class PowderCompositeRows:
+ """Resolved row layout for the composite powder figure."""
+
+ row_count: int
+ row_heights: list[float]
+ bragg_row: int | None
+ residual_row: int | None
+
class PlotlyPlotter(PlotterBase):
"""Interactive plotter using Plotly for notebooks and browsers."""
@@ -115,6 +157,13 @@ def _correlation_grid_color(cls) -> str:
return 'rgba(110, 145, 190, 0.35)'
return 'rgba(120, 140, 160, 0.28)'
+ @classmethod
+ def _legend_background_color(cls) -> str:
+ """Return a half-transparent legend background color."""
+ if cls._is_dark_mode():
+ return 'rgba(0, 0, 0, 0.5)'
+ return 'rgba(255, 255, 255, 0.5)'
+
def plot_correlation_heatmap(
self,
corr_df: object,
@@ -326,6 +375,9 @@ def _get_powder_trace(
x: object,
y: object,
label: str,
+ *,
+ customdata: object | None = None,
+ hovertemplate: str | None = None,
) -> object:
"""
Create a Plotly trace for powder diffraction data.
@@ -337,7 +389,13 @@ def _get_powder_trace(
y : object
1D array- like of y-axis values.
label : str
- Series identifier (``'meas'``, ``'calc'``, or ``'resid'``).
+ Series identifier (``'meas'``, ``'bkg'``, ``'calc'``, or
+ ``'resid'``).
+ customdata : object | None, default=None
+ Optional per-point payload used by the hover template.
+ hovertemplate : str | None, default=None
+ Optional hover template overriding the default per-trace
+ one.
Returns
-------
@@ -347,7 +405,19 @@ def _get_powder_trace(
mode = SERIES_CONFIG[label]['mode']
name = SERIES_CONFIG[label]['name']
color = DEFAULT_COLORS[label]
- line = {'color': color}
+ line_width = {
+ 'meas': MEASURED_LINE_WIDTH,
+ 'bkg': BACKGROUND_LINE_WIDTH,
+ 'calc': CALCULATED_LINE_WIDTH,
+ 'resid': RESIDUAL_LINE_WIDTH,
+ }[label]
+ line = {'color': color, 'width': line_width}
+ legend_rank = {
+ 'meas': 10,
+ 'bkg': 20,
+ 'calc': 30,
+ 'resid': 40,
+ }[label]
return go.Scatter(
x=x,
@@ -355,6 +425,58 @@ def _get_powder_trace(
line=line,
mode=mode,
name=name,
+ legendrank=legend_rank,
+ customdata=customdata,
+ hovertemplate=(
+ hovertemplate
+ if hovertemplate is not None
+ else f'{name} x: %{{x}} y: %{{y}} '
+ ),
+ )
+
+ @staticmethod
+ def _powder_meas_vs_calc_hover_data(plot_spec: PowderMeasVsCalcSpec) -> np.ndarray:
+ """Return shared hover values for composite powder traces."""
+ residual_values = (
+ np.asarray(plot_spec.y_resid)
+ if plot_spec.y_resid is not None
+ else np.asarray(plot_spec.y_meas) - np.asarray(plot_spec.y_calc)
+ )
+ if plot_spec.y_bkg is None:
+ return np.column_stack((
+ np.asarray(plot_spec.y_meas),
+ np.asarray(plot_spec.y_calc),
+ residual_values,
+ ))
+
+ return np.column_stack((
+ np.asarray(plot_spec.y_meas),
+ np.asarray(plot_spec.y_bkg),
+ np.asarray(plot_spec.y_calc),
+ residual_values,
+ ))
+
+ @staticmethod
+ def _powder_meas_vs_calc_hover_template(plot_spec: PowderMeasVsCalcSpec) -> str:
+ """
+ Return a shared hover template for composite powder traces.
+ """
+ if plot_spec.y_bkg is None:
+ return (
+ 'x: %{x:,.2f} '
+ 'Imeas: %{customdata[0]:,.2f} '
+ 'Icalc: %{customdata[1]:,.2f} '
+ 'Imeas - Icalc: %{customdata[2]:,.2f}'
+ ' '
+ )
+
+ return (
+ 'x: %{x:,.2f} '
+ 'Imeas: %{customdata[0]:,.2f} '
+ 'Ibkg: %{customdata[1]:,.2f} '
+ 'Icalc: %{customdata[2]:,.2f} '
+ 'Imeas - Icalc: %{customdata[3]:,.2f}'
+ ' '
)
@staticmethod
@@ -435,6 +557,7 @@ def _get_config() -> dict:
A dict with display and mode bar settings.
"""
return {
+ 'displayModeBar': True,
'displaylogo': False,
'modeBarButtonsToRemove': [
'select2d',
@@ -445,6 +568,216 @@ def _get_config() -> dict:
],
}
+ @staticmethod
+ def _modebar_legend_toggle_post_script() -> str:
+ """
+ Return client-side code for a legend-toggle modebar button.
+ """
+ return r"""
+const graphDiv = document.getElementById('{plot_id}');
+if (!graphDiv) {
+ return;
+}
+
+const parseColor = function (colorValue) {
+ if (!colorValue) {
+ return null;
+ }
+
+ const rgbMatch = colorValue.match(/^rgba?\(([^)]+)\)$/);
+ if (rgbMatch) {
+ const channels = rgbMatch[1].split(',').slice(0, 3).map((value) => Number(value.trim()));
+ if (channels.every((value) => Number.isFinite(value))) {
+ return {red: channels[0], green: channels[1], blue: channels[2]};
+ }
+ }
+
+ const hexMatch = colorValue.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
+ if (!hexMatch) {
+ return null;
+ }
+
+ const normalizedHex = hexMatch[1].length === 3
+ ? hexMatch[1].split('').map((value) => value + value).join('')
+ : hexMatch[1];
+ return {
+ red: Number.parseInt(normalizedHex.slice(0, 2), 16),
+ green: Number.parseInt(normalizedHex.slice(2, 4), 16),
+ blue: Number.parseInt(normalizedHex.slice(4, 6), 16),
+ };
+};
+
+const resolveLegendButtonFill = function (opacity) {
+ const referencePath = graphDiv.querySelector('.modebar-btn path');
+ const referenceFill = referencePath ? window.getComputedStyle(referencePath).fill : null;
+ const fontColor = graphDiv._fullLayout && graphDiv._fullLayout.font
+ ? graphDiv._fullLayout.font.color
+ : null;
+ const parsedColor = (
+ parseColor(referenceFill)
+ || parseColor(fontColor)
+ || {red: 68, green: 68, blue: 68}
+ );
+ return (
+ 'rgba('
+ + parsedColor.red
+ + ', '
+ + parsedColor.green
+ + ', '
+ + parsedColor.blue
+ + ', '
+ + opacity
+ + ')'
+ );
+};
+
+const updateLegendButtonAppearance = function (legendVisible) {
+ const legendButton = graphDiv.querySelector('[data-legend-toggle="true"]');
+ if (!legendButton) {
+ return;
+ }
+
+ const legendIconPath = legendButton.querySelector('path');
+ if (!legendIconPath) {
+ return;
+ }
+
+ legendButton.classList.toggle('active', legendVisible);
+ legendButton.setAttribute('aria-pressed', String(legendVisible));
+ legendIconPath.setAttribute(
+ 'style',
+ 'fill: ' + resolveLegendButtonFill(legendVisible ? 0.7 : 0.3) + ';',
+ );
+};
+
+const applyLegendVisibility = function (legendVisible) {
+ const legend = graphDiv.querySelector('.legend');
+ if (legend) {
+ legend.style.display = legendVisible ? 'inline' : 'none';
+ legend.style.visibility = legendVisible ? 'visible' : 'hidden';
+ legend.style.pointerEvents = legendVisible ? '' : 'none';
+ }
+
+ if (graphDiv.layout) {
+ graphDiv.layout.showlegend = legendVisible;
+ }
+
+ if (graphDiv._fullLayout) {
+ graphDiv._fullLayout.showlegend = legendVisible;
+ }
+};
+
+const readLegendVisibility = function () {
+ if (graphDiv.dataset.legendVisible === 'true') {
+ return true;
+ }
+
+ if (graphDiv.dataset.legendVisible === 'false') {
+ return false;
+ }
+
+ const legend = graphDiv.querySelector('.legend');
+ if (legend) {
+ return (
+ window.getComputedStyle(legend).display !== 'none'
+ && window.getComputedStyle(legend).visibility !== 'hidden'
+ );
+ }
+
+ if (graphDiv.layout && typeof graphDiv.layout.showlegend === 'boolean') {
+ return graphDiv.layout.showlegend;
+ }
+
+ if (graphDiv._fullLayout && typeof graphDiv._fullLayout.showlegend === 'boolean') {
+ return graphDiv._fullLayout.showlegend;
+ }
+
+ return true;
+};
+
+const syncLegendVisibility = function (legendVisible) {
+ const resolvedLegendVisible = typeof legendVisible === 'boolean'
+ ? legendVisible
+ : readLegendVisibility();
+ graphDiv.dataset.legendVisible = String(resolvedLegendVisible);
+ applyLegendVisibility(resolvedLegendVisible);
+ updateLegendButtonAppearance(resolvedLegendVisible);
+ return resolvedLegendVisible;
+};
+
+const toggleLegend = function (event) {
+ if (event) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ const currentValue = readLegendVisibility();
+ const nextValue = !currentValue;
+ syncLegendVisibility(nextValue);
+};
+
+const installLegendToggleButton = function () {
+ const modebar = graphDiv.querySelector('.modebar');
+ if (!modebar) {
+ return;
+ }
+
+ if (!modebar.querySelector('.modebar-group')) {
+ return;
+ }
+
+ let legendButton = modebar.querySelector('[data-legend-toggle="true"]');
+ if (!legendButton) {
+ const legendButtonGroup = document.createElement('div');
+ legendButtonGroup.className = 'modebar-group';
+
+ legendButton = document.createElement('a');
+ legendButton.className = 'modebar-btn';
+ legendButton.href = 'javascript:void(0)';
+ legendButton.setAttribute('data-title', 'Toggle legend');
+ legendButton.setAttribute('data-legend-toggle', 'true');
+ legendButton.setAttribute('aria-label', 'Toggle legend');
+ legendButton.setAttribute('role', 'button');
+ legendButton.setAttribute('tabindex', '0');
+ legendButton.innerHTML = [
+ '',
+ ' ',
+ ' ',
+ ].join('');
+
+ legendButtonGroup.appendChild(legendButton);
+ modebar.appendChild(legendButtonGroup);
+ }
+
+ legendButton.onclick = toggleLegend;
+ legendButton.onkeydown = function (event) {
+ if (event.key === 'Enter' || event.key === ' ') {
+ toggleLegend(event);
+ }
+ };
+
+ syncLegendVisibility();
+};
+
+if (graphDiv.on) {
+ graphDiv.on('plotly_afterplot', installLegendToggleButton);
+ graphDiv.on('plotly_relayout', function (eventData) {
+ if (eventData && typeof eventData.showlegend === 'boolean') {
+ syncLegendVisibility(eventData.showlegend);
+ return;
+ }
+
+ syncLegendVisibility();
+ });
+}
+syncLegendVisibility();
+window.requestAnimationFrame(installLegendToggleButton);
+"""
+
@staticmethod
def _get_figure(
data: object,
@@ -472,6 +805,36 @@ def _get_figure(
fig.update_yaxes(tickformat=',.6~g', separatethousands=True)
return fig
+ @staticmethod
+ def _has_visible_legend(fig: object) -> bool:
+ """Return whether a figure exposes at least one legend entry."""
+
+ def _trace_value(trace: object, field_name: str) -> object:
+ value = getattr(trace, field_name, None)
+ if value is not None:
+ return value
+
+ trace_kwargs = getattr(trace, 'kwargs', None)
+ if isinstance(trace_kwargs, dict):
+ return trace_kwargs.get(field_name)
+
+ return None
+
+ layout = getattr(fig, 'layout', None)
+ layout_showlegend = getattr(layout, 'showlegend', None)
+ if layout_showlegend is False:
+ return False
+
+ for trace in getattr(fig, 'data', ()):
+ if _trace_value(trace, 'visible') is False:
+ continue
+ if _trace_value(trace, 'showlegend') is False:
+ continue
+ if _trace_value(trace, 'name'):
+ return True
+
+ return False
+
def _show_figure(
self,
fig: object,
@@ -492,16 +855,21 @@ 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()
html_fig = pio.to_html(
fig,
include_plotlyjs='cdn',
full_html=False,
config=config,
+ post_script=post_script,
)
display(HTML(html_fig))
- @staticmethod
+ @classmethod
def _get_layout(
+ cls,
title: str,
axes_labels: object,
shapes: list | None = None,
@@ -534,6 +902,7 @@ def _get_layout(
'text': title,
},
legend={
+ 'bgcolor': cls._legend_background_color(),
'xanchor': 'right',
'x': 1.0,
'yanchor': 'top',
@@ -601,6 +970,406 @@ def plot_powder(
fig = self._get_figure(data, layout)
self._show_figure(fig)
+ @staticmethod
+ def _get_bragg_tick_trace(
+ tick_set: BraggTickSet,
+ row_y: float,
+ color: str,
+ ) -> object:
+ """
+ Create a hover-capable Bragg tick trace for one linked phase.
+ """
+ y = np.full(tick_set.x.shape, row_y, dtype=float)
+ hover_text = []
+ for idx, x_value in enumerate(tick_set.x):
+ index_h = int(tick_set.h[idx])
+ index_k = int(tick_set.k[idx])
+ index_l = int(tick_set.ell[idx])
+ hover_text.append(
+ f'{tick_set.phase_id} '
+ f'x: {float(x_value):,.2f} '
+ f'Miller indices: ({index_h} {index_k} {index_l}) '
+ # f'F²cal:{float(tick_set.f_squared_calc[idx]):.6g} '
+ # f'Fcalc:{float(tick_set.f_calc[idx]):.6g}'
+ ' '
+ )
+
+ return go.Scatter(
+ x=tick_set.x,
+ y=y,
+ mode='markers',
+ marker={
+ 'symbol': 'line-ns-open',
+ 'size': BRAGG_TICK_MARKER_SIZE,
+ 'line': {'width': BRAGG_TICK_MARKER_LINE_WIDTH},
+ 'color': color,
+ },
+ name=f'Bragg peaks: {tick_set.phase_id}',
+ text=hover_text,
+ hoverlabel={
+ 'font': {'color': 'white'},
+ 'bordercolor': 'white',
+ },
+ hovertemplate='%{text}',
+ )
+
+ @staticmethod
+ def _nice_axis_limit(raw_limit: float) -> float:
+ """Round a positive axis limit up to a readable value."""
+ if raw_limit <= 0:
+ return 1.0
+
+ exponent = float(np.floor(np.log10(raw_limit)))
+ base = 10.0**exponent
+ fraction = raw_limit / base
+
+ for nice_fraction in NICE_AXIS_FRACTIONS:
+ if fraction <= nice_fraction:
+ return nice_fraction * base
+ return NICE_AXIS_FRACTIONS[-1] * base
+
+ @staticmethod
+ def _get_display_tick_limit(raw_limit: float) -> float:
+ """Return a rounded positive tick limit within ``raw_limit``."""
+ if raw_limit <= 0:
+ return 1.0
+
+ exponent = float(np.floor(np.log10(raw_limit)))
+ base = 10.0**exponent
+ fraction = raw_limit / base
+
+ for nice_fraction in reversed(DISPLAY_TICK_FRACTIONS):
+ if fraction >= nice_fraction:
+ return nice_fraction * base
+ return DISPLAY_TICK_FRACTIONS[0] * base
+
+ @staticmethod
+ def _base_composite_height_pixels(plot_spec: PowderMeasVsCalcSpec) -> float:
+ """Return the baseline figure height for a single-phase plot."""
+ if plot_spec.height is None:
+ return float(DEFAULT_HEIGHT * PLOTLY_HEIGHT_PER_UNIT)
+ return float(plot_spec.height)
+
+ @staticmethod
+ def _composite_plot_area_height(full_height: float) -> float:
+ """
+ Return the drawable plot area height after vertical margins.
+ """
+ return max(full_height - COMPOSITE_MARGIN_TOP - COMPOSITE_MARGIN_BOTTOM, 1.0)
+
+ @staticmethod
+ def _subplot_available_height_fraction(row_count: int) -> float:
+ """
+ Return the fraction of plot height available for subplot rows.
+ """
+ return 1.0 - COMPOSITE_VERTICAL_SPACING * max(row_count - 1, 0)
+
+ @staticmethod
+ def _bragg_tick_symbol_height_pixels() -> float:
+ """Return rendered pixel height for one Bragg tick marker."""
+ return (
+ BRAGG_TICK_MARKER_SIZE * BRAGG_TICK_SYMBOL_HEIGHT_SCALE + BRAGG_TICK_MARKER_LINE_WIDTH
+ )
+
+ @staticmethod
+ def _bragg_row_height_pixels(plot_spec: PowderMeasVsCalcSpec) -> float:
+ """
+ Return the exact Bragg-row pixel height for the current phases.
+ """
+ return float(
+ len(plot_spec.bragg_tick_sets) * PlotlyPlotter._bragg_tick_symbol_height_pixels()
+ )
+
+ @classmethod
+ def _baseline_non_bragg_row_heights(
+ cls,
+ plot_spec: PowderMeasVsCalcSpec,
+ row_count: int,
+ *,
+ has_bragg_ticks: bool,
+ has_residual: bool,
+ ) -> tuple[float, float | None]:
+ """Return baseline main and residual row heights in pixels."""
+ baseline_height = cls._base_composite_height_pixels(plot_spec)
+ plot_area_height = cls._composite_plot_area_height(baseline_height)
+ available_row_pixels = plot_area_height * cls._subplot_available_height_fraction(row_count)
+ baseline_bragg_pixels = float(
+ cls._bragg_tick_symbol_height_pixels() if has_bragg_ticks else 0
+ )
+ non_bragg_pixels = max(available_row_pixels - baseline_bragg_pixels, 1.0)
+
+ if not has_residual:
+ return non_bragg_pixels, None
+
+ main_pixels = non_bragg_pixels / (1.0 + plot_spec.residual_height_fraction)
+ residual_pixels = main_pixels * plot_spec.residual_height_fraction
+ return main_pixels, residual_pixels
+
+ @staticmethod
+ def _get_powder_composite_rows(plot_spec: PowderMeasVsCalcSpec) -> PowderCompositeRows:
+ """Resolve subplot rows for the composite powder figure."""
+ has_bragg_ticks = bool(plot_spec.bragg_tick_sets)
+ has_residual = plot_spec.y_resid is not None
+ row_count = 1 + int(has_bragg_ticks) + int(has_residual)
+ main_row_height, residual_row_height = PlotlyPlotter._baseline_non_bragg_row_heights(
+ plot_spec=plot_spec,
+ row_count=row_count,
+ has_bragg_ticks=has_bragg_ticks,
+ has_residual=has_residual,
+ )
+ row_heights = [main_row_height]
+ bragg_row = None
+ residual_row = None
+ next_row = 2
+
+ if has_bragg_ticks:
+ bragg_row = next_row
+ next_row += 1
+ row_heights.append(PlotlyPlotter._bragg_row_height_pixels(plot_spec))
+ if has_residual:
+ residual_row = next_row
+ row_heights.append(residual_row_height if residual_row_height is not None else 1.0)
+
+ return PowderCompositeRows(
+ row_count=row_count,
+ row_heights=row_heights,
+ bragg_row=bragg_row,
+ residual_row=residual_row,
+ )
+
+ @classmethod
+ def _composite_figure_height(
+ cls,
+ plot_spec: PowderMeasVsCalcSpec,
+ layout: PowderCompositeRows,
+ ) -> float:
+ """Return figure height for Bragg row growth."""
+ base_pixels = cls._base_composite_height_pixels(plot_spec)
+ phase_count = len(plot_spec.bragg_tick_sets)
+ if phase_count <= 1:
+ return base_pixels
+
+ added_bragg_pixels = float((phase_count - 1) * cls._bragg_tick_symbol_height_pixels())
+ growth_pixels = added_bragg_pixels / cls._subplot_available_height_fraction(
+ layout.row_count
+ )
+ return base_pixels + growth_pixels
+
+ @classmethod
+ def _get_main_intensity_range(cls, plot_spec: PowderMeasVsCalcSpec) -> tuple[float, float]:
+ """
+ Return an explicit y-range for the main powder intensity row.
+ """
+ y_meas = np.asarray(plot_spec.y_meas)
+ y_calc = np.asarray(plot_spec.y_calc)
+ 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_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))
+ main_y_range = main_y_max - main_y_min
+ if main_y_range > 0.0:
+ main_y_margin = main_y_range * MAIN_INTENSITY_RANGE_MARGIN_FRACTION
+ return main_y_min - main_y_margin, main_y_max + main_y_margin
+
+ return main_y_min - 1.0, main_y_max + 1.0
+
+ @classmethod
+ def _get_residual_limit(cls, plot_spec: PowderMeasVsCalcSpec) -> float:
+ """Return a symmetric residual limit matched to the main row."""
+ if plot_spec.y_resid is None:
+ return 1.0
+
+ y_meas = np.asarray(plot_spec.y_meas)
+ y_calc = np.asarray(plot_spec.y_calc)
+ y_resid = np.asarray(plot_spec.y_resid)
+ if min(y_meas.size, y_calc.size, y_resid.size) == 0:
+ return 1.0
+
+ main_y_min, main_y_max = cls._get_main_intensity_range(plot_spec)
+ main_y_range = max(main_y_max - main_y_min, 0.0)
+ scale_matched_half_range = 0.5 * main_y_range * plot_spec.residual_height_fraction
+ if scale_matched_half_range > 0.0:
+ return scale_matched_half_range
+
+ return cls._nice_axis_limit(float(np.max(np.abs(y_resid))))
+
+ @staticmethod
+ def _composite_x_range(x_values: np.ndarray) -> tuple[float | None, float | None]:
+ """Return the explicit x-range for the composite powder plot."""
+ if x_values.size == 0:
+ return None, None
+ return float(np.min(x_values)), float(np.max(x_values))
+
+ def plot_powder_meas_vs_calc(
+ self,
+ plot_spec: PowderMeasVsCalcSpec,
+ ) -> None:
+ """
+ Render a composite powder plot with optional Bragg ticks.
+
+ The main row shows measured and calculated intensities. The
+ Bragg row is added only when tick data is available. The
+ residual row is added only when residual data is requested.
+ """
+ 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 = make_subplots(
+ rows=layout.row_count,
+ cols=1,
+ shared_xaxes=True,
+ vertical_spacing=COMPOSITE_VERTICAL_SPACING,
+ 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),
+ )
+ )
+ for label, y_values in main_traces:
+ fig.add_trace(
+ self._get_powder_trace(
+ plot_spec.x,
+ y_values,
+ label,
+ customdata=hover_data,
+ hovertemplate=hover_template,
+ ),
+ 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,
+ )
+
+ if layout.residual_row is not None and plot_spec.y_resid is not 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,
+ )
+
+ fig.update_layout(
+ height=self._composite_figure_height(plot_spec, layout),
+ margin={
+ 'autoexpand': True,
+ 'r': COMPOSITE_MARGIN_RIGHT,
+ 't': COMPOSITE_MARGIN_TOP,
+ 'b': COMPOSITE_MARGIN_BOTTOM,
+ },
+ title={'text': plot_spec.title},
+ legend={
+ 'bgcolor': self._legend_background_color(),
+ 'xanchor': 'right',
+ 'x': 1.0,
+ 'yanchor': 'top',
+ 'y': 1.0,
+ },
+ )
+
+ for row_idx in range(1, layout.row_count + 1):
+ x_axis_kwargs = {
+ 'matches': 'x',
+ 'showline': True,
+ 'mirror': True,
+ 'zeroline': False,
+ 'tickformat': ',.6~g',
+ 'separatethousands': True,
+ }
+ if x_min is not None and x_max is not None:
+ x_axis_kwargs['range'] = [x_min, x_max]
+ fig.update_xaxes(row=row_idx, col=1, **x_axis_kwargs)
+ fig.update_yaxes(
+ showline=True,
+ mirror=True,
+ zeroline=False,
+ tickformat=',.6~g',
+ separatethousands=True,
+ row=row_idx,
+ col=1,
+ )
+
+ fig.update_xaxes(showticklabels=(layout.row_count == 1), row=1, col=1)
+ fig.update_yaxes(
+ title_text=plot_spec.axes_labels[1],
+ range=[main_y_min, main_y_max],
+ row=1,
+ 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)
+
+ self._show_figure(fig)
+
def plot_single_crystal(
self,
x_calc: object,
@@ -683,7 +1452,7 @@ def plot_scatter(
'array': sy,
'visible': True,
},
- hovertemplate='x: %{x} y: %{y} ',
+ hovertemplate='x: %{x:,.2f} y: %{y:,.2f} ',
)
layout = self._get_layout(
diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py
index 11b61c7c7..61e93f683 100644
--- a/src/easydiffraction/display/plotting.py
+++ b/src/easydiffraction/display/plotting.py
@@ -10,11 +10,15 @@
from __future__ import annotations
import pathlib
+from dataclasses import dataclass
from enum import StrEnum
import numpy as np
import pandas as pd
+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.base import RendererBase
from easydiffraction.display.base import RendererFactoryBase
from easydiffraction.display.plotters.ascii import AsciiPlotter
@@ -23,12 +27,16 @@
from easydiffraction.display.plotters.base import DEFAULT_MAX
from easydiffraction.display.plotters.base import DEFAULT_MIN
from easydiffraction.display.plotters.base import DEFAULT_X_AXIS
+from easydiffraction.display.plotters.base import BraggTickSet
+from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec
from easydiffraction.display.plotters.base import XAxisType
from easydiffraction.display.plotters.plotly import PlotlyPlotter
from easydiffraction.display.tables import TableRenderer
from easydiffraction.utils.environment import in_jupyter
from easydiffraction.utils.logging import console
from easydiffraction.utils.logging import log
+from easydiffraction.utils.utils import tof_to_d
+from easydiffraction.utils.utils import twotheta_to_d
class PlotterEngineEnum(StrEnum):
@@ -57,6 +65,29 @@ def description(self) -> str:
DEFAULT_CORRELATION_THRESHOLD = 0.7
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
+
+
+@dataclass(frozen=True)
+class _MeasVsCalcPlotOptions:
+ """Internal options for a measured-vs-calculated plot request."""
+
+ x_min: float | None = None
+ x_max: float | None = None
+ show_residual: bool | None = None
+ x: object | None = None
+
+
+@dataclass(frozen=True)
+class _PowderMeasVsCalcSeries:
+ """Filtered y-series for a composite powder plot."""
+
+ y_meas: np.ndarray
+ y_calc: np.ndarray
+ y_bkg: np.ndarray | None = None
class Plotter(RendererBase):
@@ -72,7 +103,8 @@ def __init__(self) -> None:
self._x_min = DEFAULT_MIN
self._x_max = DEFAULT_MAX
# Chart height
- self.height = DEFAULT_HEIGHT
+ self._height = DEFAULT_HEIGHT
+ self._height_is_explicit = False
# Back-reference to the owning Project (set via _set_project)
self._project = None
@@ -171,7 +203,9 @@ def _filtered_y_array(
if x_max is None:
x_max = self.x_max
- mask = (x_array >= x_min) & (x_array <= x_max)
+ lower_bound = min(x_min, x_max)
+ upper_bound = max(x_min, x_max)
+ mask = (x_array >= lower_bound) & (x_array <= upper_bound)
return y_array[mask]
@staticmethod
@@ -233,14 +267,22 @@ def _prepare_powder_context(
# Filter x
x_filtered = self._filtered_y_array(x_array, x_array, x_min, x_max)
+ resolved_x_min = self.x_min if x_min is None else float(x_min)
+ resolved_x_max = self.x_max if x_max is None else float(x_max)
+ if x_filtered.size > 0:
+ if x_min is None:
+ resolved_x_min = float(np.min(x_filtered))
+ if x_max is None:
+ resolved_x_max = float(np.max(x_filtered))
axes_labels = self._get_axes_labels(sample_form, scattering_type, x_axis)
return {
'x_filtered': x_filtered,
'x_array': x_array,
- 'x_min': x_min,
- 'x_max': x_max,
+ 'x_min': resolved_x_min,
+ 'x_max': resolved_x_max,
+ 'x_axis': x_axis,
'axes_labels': axes_labels,
}
@@ -331,8 +373,16 @@ def height(self, value: object) -> None:
"""
if value is not None:
self._height = value
+ self._height_is_explicit = True
else:
self._height = DEFAULT_HEIGHT
+ self._height_is_explicit = False
+
+ def _composite_plot_height(self) -> int | None:
+ """Return explicit composite height or backend default."""
+ if self._height_is_explicit:
+ return self._height
+ return None
# ------------------------------------------------------------------
# Public methods
@@ -377,7 +427,7 @@ def plot_meas(
self._update_project_categories(expt_name)
experiment = self._project.experiments[expt_name]
self._plot_meas_data(
- experiment.data,
+ intensity_category_for(experiment),
expt_name,
experiment.type,
x_min=x_min,
@@ -409,7 +459,7 @@ def plot_calc(
self._update_project_categories(expt_name)
experiment = self._project.experiments[expt_name]
self._plot_calc_data(
- experiment.data,
+ intensity_category_for(experiment),
expt_name,
experiment.type,
x_min=x_min,
@@ -423,7 +473,7 @@ def plot_meas_vs_calc(
x_min: float | None = None,
x_max: float | None = None,
*,
- show_residual: bool = False,
+ show_residual: bool | None = None,
x: object | None = None,
) -> None:
"""
@@ -437,21 +487,26 @@ def plot_meas_vs_calc(
Lower bound for the x-axis range.
x_max : float | None, default=None
Upper bound for the x-axis range.
- show_residual : bool, default=False
- When ``True``, include the residual (difference) curve.
+ show_residual : bool | None, default=None
+ When ``None``, powder Bragg plots include the residual by
+ default while other measured-vs-calculated plots keep the
+ historical no-residual default.
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]
- self._plot_meas_vs_calc_data(
- experiment,
- expt_name,
+ plot_options = _MeasVsCalcPlotOptions(
x_min=x_min,
x_max=x_max,
show_residual=show_residual,
x=x,
)
+ self._plot_meas_vs_calc_data(
+ experiment=experiment,
+ expt_name=expt_name,
+ plot_options=plot_options,
+ )
def plot_param_series(
self,
@@ -1069,11 +1124,7 @@ def _plot_meas_vs_calc_data(
self,
experiment: object,
expt_name: str,
- x_min: object = None,
- x_max: object = None,
- *,
- show_residual: bool = False,
- x: object = None,
+ plot_options: _MeasVsCalcPlotOptions,
) -> None:
"""
Plot measured and calculated series and optional residual.
@@ -1090,23 +1141,20 @@ def _plot_meas_vs_calc_data(
Parameters
----------
experiment : object
- Experiment instance with ``.data`` and ``.type`` attributes.
+ Experiment instance with an intensity category and
+ ``.type``.
expt_name : str
Experiment name for the title.
- x_min : object, default=None
- Optional minimum x-axis limit.
- x_max : object, default=None
- Optional maximum x-axis limit.
- show_residual : bool, default=False
- If ``True``, add residual series (powder only).
- x : object, default=None
- X-axis type. If ``None``, auto-detected from sample form and
- beam mode.
+ plot_options : _MeasVsCalcPlotOptions
+ X-range, residual, and x-axis selection options.
"""
- pattern = experiment.data
+ pattern = intensity_category_for(experiment)
expt_type = experiment.type
- x_axis, _, sample_form, scattering_type, _ = self._resolve_x_axis(expt_type, x)
+ x_axis, _, sample_form, scattering_type, _ = self._resolve_x_axis(
+ expt_type,
+ plot_options.x,
+ )
# Validate required data (before x-array check, matching
# original behavior for plot_meas_vs_calc)
@@ -1121,21 +1169,13 @@ def _plot_meas_vs_calc_data(
# Single crystal scatter plot (I²calc vs I²meas)
if x_axis in {XAxisType.INTENSITY_CALC, 'intensity_calc'}:
- axes_labels = self._get_axes_labels(sample_form, scattering_type, x_axis)
-
- if pattern.intensity_meas_su is None:
- log.warning(f'No measurement uncertainties for experiment {expt_name}')
- meas_su = np.zeros_like(pattern.intensity_meas)
- else:
- meas_su = pattern.intensity_meas_su
-
- self._backend.plot_single_crystal(
- x_calc=pattern.intensity_calc,
- y_meas=pattern.intensity_meas,
- y_meas_su=meas_su,
- axes_labels=axes_labels,
- title=f"Measured vs Calculated data for experiment 🔬 '{expt_name}'",
- height=self.height,
+ self._plot_single_crystal_meas_vs_calc(
+ pattern=pattern,
+ expt_name=expt_name,
+ sample_form=sample_form,
+ scattering_type=scattering_type,
+ x_axis=x_axis,
+ title=title,
)
return
@@ -1144,25 +1184,134 @@ def _plot_meas_vs_calc_data(
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
- y_series = []
- y_labels = []
y_meas = self._filtered_y_array(
pattern.intensity_meas, ctx['x_array'], ctx['x_min'], ctx['x_max']
)
- y_series.append(y_meas)
- y_labels.append('meas')
y_calc = self._filtered_y_array(
pattern.intensity_calc, ctx['x_array'], ctx['x_min'], ctx['x_max']
)
- y_series.append(y_calc)
- y_labels.append('calc')
+ 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
+ )
+
+ powder_series = _PowderMeasVsCalcSeries(
+ y_meas=y_meas,
+ y_calc=y_calc,
+ y_bkg=y_bkg,
+ )
+
+ if sample_form == SampleFormEnum.POWDER and scattering_type == ScatteringTypeEnum.BRAGG:
+ self._plot_powder_bragg_meas_vs_calc(
+ experiment=experiment,
+ expt_name=expt_name,
+ ctx=ctx,
+ series=powder_series,
+ plot_options=plot_options,
+ title=title,
+ )
+ return
+
+ self._plot_line_meas_vs_calc(
+ ctx=ctx,
+ y_meas=y_meas,
+ y_calc=y_calc,
+ show_residual=False
+ if plot_options.show_residual is None
+ else plot_options.show_residual,
+ title=title,
+ )
+
+ def _plot_single_crystal_meas_vs_calc(
+ self,
+ pattern: object,
+ expt_name: str,
+ sample_form: SampleFormEnum,
+ scattering_type: ScatteringTypeEnum,
+ x_axis: XAxisType | str,
+ title: str,
+ ) -> None:
+ """
+ Render the single-crystal measured-vs-calculated scatter plot.
+ """
+ axes_labels = self._get_axes_labels(sample_form, scattering_type, x_axis)
+ if pattern.intensity_meas_su is None:
+ log.warning(f'No measurement uncertainties for experiment {expt_name}')
+ meas_su = np.zeros_like(pattern.intensity_meas)
+ else:
+ meas_su = pattern.intensity_meas_su
+
+ self._backend.plot_single_crystal(
+ x_calc=pattern.intensity_calc,
+ y_meas=pattern.intensity_meas,
+ y_meas_su=meas_su,
+ axes_labels=axes_labels,
+ title=title,
+ height=self.height,
+ )
+
+ def _plot_powder_bragg_meas_vs_calc(
+ self,
+ experiment: object,
+ expt_name: str,
+ ctx: dict[str, object],
+ series: _PowderMeasVsCalcSeries,
+ plot_options: _MeasVsCalcPlotOptions,
+ title: str,
+ ) -> 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:
+ 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=series.y_meas,
+ y_calc=series.y_calc,
+ y_resid=y_resid,
+ bragg_tick_sets=bragg_tick_sets,
+ axes_labels=ctx['axes_labels'],
+ title=title,
+ residual_height_fraction=DEFAULT_RESID_HEIGHT,
+ bragg_peaks_height_fraction=DEFAULT_BRAGG_ROW,
+ height=self._composite_plot_height(),
+ y_bkg=series.y_bkg,
+ )
+ self._backend.plot_powder_meas_vs_calc(plot_spec=plot_spec)
+
+ def _plot_line_meas_vs_calc(
+ self,
+ ctx: dict[str, object],
+ y_meas: np.ndarray,
+ y_calc: np.ndarray,
+ *,
+ show_residual: bool,
+ title: str,
+ ) -> None:
+ """
+ Render the non-composite line version of measured-vs-calculated.
+ """
+ y_series = [y_meas, y_calc]
+ y_labels = ['meas', 'calc']
if show_residual:
y_series.append(y_meas - y_calc)
y_labels.append('resid')
@@ -1176,6 +1325,170 @@ def _plot_meas_vs_calc_data(
height=self.height,
)
+ @staticmethod
+ def _extract_bragg_tick_sets(
+ experiment: object,
+ expt_name: str,
+ x_axis: object,
+ x_min: float | None,
+ x_max: float | None,
+ ) -> tuple[BraggTickSet, ...]:
+ """
+ Convert experiment reflection data into Bragg tick display rows.
+ """
+ refln = getattr(experiment, 'refln', None)
+ if refln is None:
+ return ()
+
+ x_values = Plotter._bragg_tick_x_values(
+ refln=refln,
+ experiment=experiment,
+ expt_name=expt_name,
+ x_axis=x_axis,
+ )
+ arrays = Plotter._bragg_tick_arrays(refln=refln, expt_name=expt_name)
+ if x_values is None or arrays is None:
+ return ()
+
+ arrays['x'] = np.asarray(x_values)
+ if arrays['x'].size == 0:
+ return ()
+
+ mask = Plotter._bragg_tick_mask(arrays['x'], x_min=x_min, x_max=x_max)
+ if not np.any(mask):
+ return ()
+
+ return Plotter._group_bragg_tick_sets(arrays=arrays, mask=mask)
+
+ @staticmethod
+ def _bragg_tick_x_values(
+ *,
+ refln: object,
+ experiment: object,
+ expt_name: str,
+ x_axis: object,
+ ) -> object | None:
+ x_name = getattr(x_axis, 'value', x_axis)
+ if x_name == XAxisType.D_SPACING:
+ return Plotter._bragg_tick_d_spacing(refln=refln, experiment=experiment)
+ if x_name == XAxisType.TWO_THETA:
+ return Plotter._bragg_tick_attr(refln, x_name, expt_name)
+ if x_name == XAxisType.TIME_OF_FLIGHT:
+ return Plotter._bragg_tick_attr(refln, x_name, expt_name)
+
+ log.warning(
+ f"Unsupported Bragg tick x axis '{x_name}' for experiment '{expt_name}'. "
+ 'Skipping the Bragg subplot.',
+ )
+ return None
+
+ @staticmethod
+ def _bragg_tick_attr(
+ refln: object,
+ name: str,
+ expt_name: str,
+ ) -> object | None:
+ value = getattr(refln, name, None)
+ if value is not None:
+ return value
+
+ log.warning(
+ f"Experiment '{expt_name}' reflection data does not expose '{name}'. "
+ 'Skipping the Bragg subplot.',
+ )
+ return None
+
+ @staticmethod
+ def _bragg_tick_arrays(
+ *,
+ refln: object,
+ expt_name: str,
+ ) -> dict[str, np.ndarray] | None:
+ arrays: dict[str, np.ndarray] = {}
+ for name in (
+ 'phase_id',
+ 'index_h',
+ 'index_k',
+ 'index_l',
+ 'f_squared_calc',
+ 'f_calc',
+ ):
+ value = getattr(refln, name, None)
+ if value is None:
+ log.warning(
+ f"Experiment '{expt_name}' reflection data is missing '{name}'. "
+ 'Skipping the Bragg subplot.',
+ )
+ return None
+ arrays[name] = np.asarray(value)
+ return arrays
+
+ @staticmethod
+ def _bragg_tick_mask(
+ x_values: np.ndarray,
+ *,
+ x_min: float | None,
+ x_max: float | None,
+ ) -> np.ndarray:
+ lower_bound = DEFAULT_MIN if x_min is None else min(x_min, x_max)
+ upper_bound = DEFAULT_MAX if x_max is None else max(x_min, x_max)
+ return (x_values >= lower_bound) & (x_values <= upper_bound)
+
+ @staticmethod
+ def _group_bragg_tick_sets(
+ *,
+ arrays: dict[str, np.ndarray],
+ mask: np.ndarray,
+ ) -> tuple[BraggTickSet, ...]:
+ phase_ids = arrays['phase_id'][mask]
+ unique_phase_ids = []
+ for raw_phase_id in phase_ids:
+ if not any(
+ np.array_equal(raw_phase_id, existing_phase_id)
+ for existing_phase_id in unique_phase_ids
+ ):
+ unique_phase_ids.append(raw_phase_id)
+
+ tick_sets = []
+ for raw_phase_id in unique_phase_ids:
+ phase_mask = mask & (arrays['phase_id'] == raw_phase_id)
+ tick_sets.append(
+ BraggTickSet(
+ phase_id=str(raw_phase_id),
+ x=arrays['x'][phase_mask],
+ h=arrays['index_h'][phase_mask],
+ k=arrays['index_k'][phase_mask],
+ ell=arrays['index_l'][phase_mask],
+ f_squared_calc=arrays['f_squared_calc'][phase_mask],
+ f_calc=arrays['f_calc'][phase_mask],
+ )
+ )
+
+ return tuple(tick_sets)
+
+ @staticmethod
+ def _bragg_tick_d_spacing(
+ *,
+ refln: object,
+ experiment: object,
+ ) -> object:
+ """
+ Resolve Bragg tick d-spacing in the plotted coordinate system.
+ """
+ if hasattr(refln, 'two_theta'):
+ return twotheta_to_d(
+ refln.two_theta,
+ experiment.instrument.setup_wavelength.value,
+ )
+ if hasattr(refln, 'time_of_flight'):
+ return tof_to_d(
+ refln.time_of_flight,
+ experiment.instrument.calib_d_to_tof_offset.value,
+ experiment.instrument.calib_d_to_tof_linear.value,
+ experiment.instrument.calib_d_to_tof_quad.value,
+ )
+ return refln.d_spacing
+
def _plot_param_series_from_csv(
self,
csv_path: str,
diff --git a/tests/unit/easydiffraction/analysis/calculators/test_cryspy.py b/tests/unit/easydiffraction/analysis/calculators/test_cryspy.py
index 178faab59..5fdda3ee4 100644
--- a/tests/unit/easydiffraction/analysis/calculators/test_cryspy.py
+++ b/tests/unit/easydiffraction/analysis/calculators/test_cryspy.py
@@ -3,6 +3,9 @@
from types import SimpleNamespace
+import numpy as np
+import pytest
+
def test_module_import():
import easydiffraction.analysis.calculators.cryspy as MUT
@@ -81,9 +84,6 @@ def test_update_structure_zeroes_biso_for_anisotropic_atoms():
def test_update_structure_restores_wyckoff_multiplicity_after_coordinate_wrapping():
- import numpy as np
- import pytest
-
pytest.importorskip('cryspy')
from easydiffraction.analysis.calculators.cryspy import CryspyCalculator
@@ -114,3 +114,62 @@ def test_update_structure_restores_wyckoff_multiplicity_after_coordinate_wrappin
assert cryspy_model_dict['atom_fract_xyz'][1][0] == -0.20587714
assert cryspy_model_dict['atom_multiplicity'][0] == 18
+
+
+def test_last_powder_refln_records_converts_cwl_two_theta_to_degrees():
+ 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], [0], [1]]),
+ 'sthovl': np.array([0.25]),
+ 'ttheta_hkl': np.array([np.pi / 2]),
+ 'f_nucl': np.array([-3.0 + 4.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-a')
+
+ assert len(records) == 1
+ assert records[0].phase_id == 'phase-a'
+ assert records[0].two_theta == pytest.approx(90.0)
+ assert records[0].d_spacing == pytest.approx(2.0)
+ assert records[0].f_calc == pytest.approx(5.0)
+ assert records[0].f_squared_calc == pytest.approx(25.0)
+
+
+def test_last_powder_refln_records_reads_tof_time_and_d_spacing():
+ 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([[2], [1], [0]]),
+ 'sthovl': np.array([0.1]),
+ 'd_hkl': np.array([3.21]),
+ 'time_hkl': np.array([1234.0]),
+ 'f_nucl': np.array([6.0 + 0.0j]),
+ }
+ }
+ structure = SimpleNamespace(name='phase')
+ experiment = SimpleNamespace(
+ name='exp',
+ type=SimpleNamespace(beam_mode=SimpleNamespace(value=BeamModeEnum.TIME_OF_FLIGHT)),
+ )
+
+ records = calculator.last_powder_refln_records(structure, experiment, phase_id='phase-b')
+
+ assert len(records) == 1
+ assert records[0].phase_id == 'phase-b'
+ assert records[0].time_of_flight == pytest.approx(1234.0)
+ 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)
diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_factory.py b/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_factory.py
index 5132591a1..af0096c1d 100644
--- a/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_factory.py
+++ b/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_factory.py
@@ -16,11 +16,8 @@ def test_data_factory_default_and_errors():
obj2 = DataFactory.create('bragg-pd-tof')
assert obj2.__class__.__name__ == 'PdTofData'
- obj3 = DataFactory.create('bragg-sc')
- assert obj3.__class__.__name__ == 'ReflnData'
-
- obj4 = DataFactory.create('total-pd')
- assert obj4.__class__.__name__ == 'TotalData'
+ obj3 = DataFactory.create('total-pd')
+ assert obj3.__class__.__name__ == 'TotalData'
# Unsupported tag should raise ValueError
with pytest.raises(
@@ -60,13 +57,6 @@ def test_data_factory_default_tag_resolution():
)
assert tag == 'total-pd'
- # Context-dependent default: single crystal
- tag = DataFactory.default_tag(
- sample_form=SampleFormEnum.SINGLE_CRYSTAL,
- scattering_type=ScatteringTypeEnum.BRAGG,
- )
- assert tag == 'bragg-sc'
-
def test_data_factory_supported_tags():
# Ensure concrete classes are registered
@@ -75,5 +65,4 @@ def test_data_factory_supported_tags():
tags = DataFactory.supported_tags()
assert 'bragg-pd' in tags
assert 'bragg-pd-tof' in tags
- assert 'bragg-sc' in tags
assert 'total-pd' in tags
diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_pd.py b/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_pd.py
new file mode 100644
index 000000000..27ab4e533
--- /dev/null
+++ b/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_pd.py
@@ -0,0 +1,196 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors
+# SPDX-License-Identifier: BSD-3-Clause
+
+import numpy as np
+import pytest
+
+from easydiffraction.analysis.calculators.base import PowderReflnRecord
+from easydiffraction.datablocks.experiment.item.factory import ExperimentFactory
+from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum
+
+
+def test_powder_cwl_refln_defaults():
+ from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderCwlRefln
+
+ refln = PowderCwlRefln()
+
+ assert refln.id.value == '0'
+ assert refln.phase_id.value == ''
+ assert refln.two_theta.value == 0.0
+ assert refln.d_spacing.value == 0.0
+ assert refln.f_calc.value == 0.0
+ assert refln.f_squared_calc.value == 0.0
+ assert refln._identity.category_code == 'refln'
+
+
+def test_powder_cwl_refln_data_replace_from_records_sets_arrays():
+ from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import (
+ PowderCwlReflnData,
+ )
+
+ refln = PowderCwlReflnData()
+ refln._replace_from_records([
+ PowderReflnRecord(
+ phase_id='alpha',
+ d_spacing=2.1,
+ sin_theta_over_lambda=0.25,
+ index_h=1,
+ index_k=0,
+ index_l=1,
+ f_calc=3.0,
+ f_squared_calc=9.0,
+ two_theta=14.5,
+ ),
+ PowderReflnRecord(
+ phase_id='beta',
+ d_spacing=1.5,
+ sin_theta_over_lambda=0.33,
+ index_h=2,
+ index_k=1,
+ index_l=0,
+ f_calc=4.0,
+ f_squared_calc=16.0,
+ two_theta=22.0,
+ ),
+ ])
+
+ assert [item.id.value for item in refln._items] == ['1', '2']
+ np.testing.assert_array_equal(refln.phase_id, np.array(['alpha', 'beta']))
+ np.testing.assert_allclose(refln.d_spacing, np.array([2.1, 1.5]))
+ np.testing.assert_allclose(refln.two_theta, np.array([14.5, 22.0]))
+ np.testing.assert_allclose(refln.f_calc, np.array([3.0, 4.0]))
+ np.testing.assert_allclose(refln.f_squared_calc, np.array([9.0, 16.0]))
+
+
+def test_powder_tof_refln_data_replace_from_records_sets_arrays():
+ from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import (
+ PowderTofReflnData,
+ )
+
+ refln = PowderTofReflnData()
+ refln._replace_from_records([
+ PowderReflnRecord(
+ phase_id='gamma',
+ d_spacing=3.2,
+ sin_theta_over_lambda=0.15,
+ index_h=1,
+ index_k=1,
+ index_l=0,
+ f_calc=5.0,
+ f_squared_calc=25.0,
+ time_of_flight=1200.0,
+ )
+ ])
+
+ assert [item.id.value for item in refln._items] == ['1']
+ np.testing.assert_array_equal(refln.phase_id, np.array(['gamma']))
+ np.testing.assert_allclose(refln.time_of_flight, np.array([1200.0]))
+ np.testing.assert_allclose(refln.d_spacing, np.array([3.2]))
+
+
+def test_powder_refln_is_cryspy_only():
+ from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import (
+ PowderCwlReflnData,
+ )
+ from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import (
+ PowderTofReflnData,
+ )
+
+ assert PowderCwlReflnData.calculator_support.calculators == frozenset({CalculatorEnum.CRYSPY})
+ assert PowderTofReflnData.calculator_support.calculators == frozenset({CalculatorEnum.CRYSPY})
+
+
+def test_powder_refln_replace_from_records_rebuilds_index_and_parents():
+ from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import (
+ PowderCwlReflnData,
+ )
+
+ refln = PowderCwlReflnData()
+ refln._replace_from_records([
+ PowderReflnRecord(
+ phase_id='alpha',
+ d_spacing=2.1,
+ sin_theta_over_lambda=0.25,
+ index_h=1,
+ index_k=0,
+ index_l=1,
+ f_calc=3.0,
+ f_squared_calc=9.0,
+ two_theta=14.5,
+ )
+ ])
+
+ old_item = refln['1']
+ assert old_item._parent is refln
+
+ refln._replace_from_records([])
+
+ assert len(refln) == 0
+ assert old_item._parent is None
+ with pytest.raises(KeyError):
+ refln['1']
+
+ refln._replace_from_records([
+ PowderReflnRecord(
+ phase_id='beta',
+ d_spacing=1.5,
+ sin_theta_over_lambda=0.33,
+ index_h=2,
+ index_k=1,
+ index_l=0,
+ f_calc=4.0,
+ f_squared_calc=16.0,
+ two_theta=22.0,
+ )
+ ])
+
+ new_item = refln['1']
+ assert new_item is refln._items[0]
+ assert new_item._parent is refln
+ assert new_item.phase_id.value == 'beta'
+
+
+def test_powder_refln_round_trips_via_experiment_cif():
+ experiment = ExperimentFactory.from_scratch(
+ name='powder',
+ sample_form='powder',
+ beam_mode='constant wavelength',
+ radiation_probe='neutron',
+ scattering_type='bragg',
+ )
+ experiment.refln._replace_from_records([
+ PowderReflnRecord(
+ phase_id='alpha',
+ d_spacing=2.1,
+ sin_theta_over_lambda=0.25,
+ index_h=1,
+ index_k=0,
+ index_l=1,
+ f_calc=3.0,
+ f_squared_calc=9.0,
+ two_theta=14.5,
+ ),
+ PowderReflnRecord(
+ phase_id='beta',
+ d_spacing=1.5,
+ sin_theta_over_lambda=0.33,
+ index_h=2,
+ index_k=1,
+ index_l=0,
+ f_calc=4.0,
+ f_squared_calc=16.0,
+ two_theta=22.0,
+ ),
+ ])
+ experiment._need_categories_update = False
+
+ cif = experiment.as_cif
+ loaded = ExperimentFactory.from_cif_str(cif)
+
+ assert '_refln.phase_id' in cif
+ assert '_refln.f_calc' in cif
+ assert '_refln.f_squared_calc' in cif
+ np.testing.assert_array_equal(loaded.refln.phase_id, np.array(['alpha', 'beta']))
+ np.testing.assert_allclose(loaded.refln.two_theta, np.array([14.5, 22.0]))
+ np.testing.assert_allclose(loaded.refln.f_calc, np.array([3.0, 4.0]))
+ np.testing.assert_allclose(loaded.refln.f_squared_calc, np.array([9.0, 16.0]))
diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_bragg_sc.py b/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_sc.py
similarity index 79%
rename from tests/unit/easydiffraction/datablocks/experiment/categories/data/test_bragg_sc.py
rename to tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_sc.py
index 534fd1923..fc7175d4a 100644
--- a/tests/unit/easydiffraction/datablocks/experiment/categories/data/test_bragg_sc.py
+++ b/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_sc.py
@@ -5,7 +5,7 @@
def test_refln_data_point_defaults():
- from easydiffraction.datablocks.experiment.categories.data.bragg_sc import Refln
+ from easydiffraction.datablocks.experiment.categories.refln.bragg_sc import Refln
pt = Refln()
assert pt.id.value == '0'
@@ -22,51 +22,42 @@ def test_refln_data_point_defaults():
def test_refln_data_collection_create_and_properties():
- from easydiffraction.datablocks.experiment.categories.data.bragg_sc import ReflnData
+ from easydiffraction.datablocks.experiment.categories.refln.bragg_sc import ReflnData
coll = ReflnData()
- # Create items with hkl
h = np.array([1.0, 2.0, 0.0])
k = np.array([0.0, 1.0, 0.0])
l = np.array([0.0, 0.0, 2.0])
coll._create_items_set_hkl_and_id(h, k, l)
assert len(coll._items) == 3
-
- # Check hkl arrays
np.testing.assert_array_almost_equal(coll.index_h, h)
np.testing.assert_array_almost_equal(coll.index_k, k)
np.testing.assert_array_almost_equal(coll.index_l, l)
-
- # Check IDs are sequential
assert coll._items[0].id.value == '1'
assert coll._items[1].id.value == '2'
assert coll._items[2].id.value == '3'
- # Set and read measured intensities
meas = np.array([50.0, 100.0, 150.0])
coll._set_intensity_meas(meas)
np.testing.assert_array_almost_equal(coll.intensity_meas, meas)
- # Set and read su
su = np.array([5.0, 10.0, 15.0])
coll._set_intensity_meas_su(su)
np.testing.assert_array_almost_equal(coll.intensity_meas_su, su)
- # Set wavelength
wl = np.array([0.84, 0.84, 0.84])
coll._set_wavelength(wl)
np.testing.assert_array_almost_equal(coll.wavelength, wl)
- # Set and read calculated intensities
calc = np.array([48.0, 102.0, 148.0])
coll._set_intensity_calc(calc)
np.testing.assert_array_almost_equal(coll.intensity_calc, calc)
def test_refln_data_d_spacing_and_stol():
- from easydiffraction.datablocks.experiment.categories.data.bragg_sc import ReflnData
+ from easydiffraction.datablocks.experiment.categories.refln.bragg_sc import ReflnData
coll = ReflnData()
h = np.array([1.0, 2.0])
@@ -74,19 +65,17 @@ def test_refln_data_d_spacing_and_stol():
l = np.array([0.0, 0.0])
coll._create_items_set_hkl_and_id(h, k, l)
- # Set d-spacing
d = np.array([5.43, 2.715])
coll._set_d_spacing(d)
np.testing.assert_array_almost_equal(coll.d_spacing, d)
- # Set sin(theta)/lambda
stol = np.array([0.092, 0.184])
coll._set_sin_theta_over_lambda(stol)
np.testing.assert_array_almost_equal(coll.sin_theta_over_lambda, stol)
def test_refln_data_type_info():
- from easydiffraction.datablocks.experiment.categories.data.bragg_sc import ReflnData
+ from easydiffraction.datablocks.experiment.categories.refln.bragg_sc import ReflnData
assert ReflnData.type_info.tag == 'bragg-sc'
assert ReflnData.type_info.description == 'Bragg single-crystal reflection data'
diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_factory.py b/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_factory.py
new file mode 100644
index 000000000..76ac4b49a
--- /dev/null
+++ b/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_factory.py
@@ -0,0 +1,67 @@
+# SPDX-FileCopyrightText: 2026 EasyScience contributors
+# SPDX-License-Identifier: BSD-3-Clause
+
+import pytest
+
+
+def test_refln_factory_default_and_errors():
+ from easydiffraction.datablocks.experiment.categories.refln.factory import ReflnFactory
+
+ obj = ReflnFactory.create('bragg-sc')
+ assert obj.__class__.__name__ == 'ReflnData'
+
+ obj2 = ReflnFactory.create('bragg-pd-refln')
+ assert obj2.__class__.__name__ == 'PowderCwlReflnData'
+
+ obj3 = ReflnFactory.create('bragg-pd-tof-refln')
+ assert obj3.__class__.__name__ == 'PowderTofReflnData'
+
+ with pytest.raises(
+ ValueError,
+ match=r"Unsupported type: 'nonexistent'\. Supported: .*",
+ ):
+ ReflnFactory.create('nonexistent')
+
+
+def test_refln_factory_default_tag_resolution():
+ from easydiffraction.datablocks.experiment.categories.refln.factory import ReflnFactory
+ from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum
+ from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum
+ from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum
+
+ tag = ReflnFactory.default_tag(
+ sample_form=SampleFormEnum.SINGLE_CRYSTAL,
+ scattering_type=ScatteringTypeEnum.BRAGG,
+ beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH,
+ )
+ assert tag == 'bragg-sc'
+
+ tag = ReflnFactory.default_tag(
+ sample_form=SampleFormEnum.SINGLE_CRYSTAL,
+ scattering_type=ScatteringTypeEnum.BRAGG,
+ beam_mode=BeamModeEnum.TIME_OF_FLIGHT,
+ )
+ assert tag == 'bragg-sc'
+
+ tag = ReflnFactory.default_tag(
+ sample_form=SampleFormEnum.POWDER,
+ scattering_type=ScatteringTypeEnum.BRAGG,
+ beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH,
+ )
+ assert tag == 'bragg-pd-refln'
+
+ tag = ReflnFactory.default_tag(
+ sample_form=SampleFormEnum.POWDER,
+ scattering_type=ScatteringTypeEnum.BRAGG,
+ beam_mode=BeamModeEnum.TIME_OF_FLIGHT,
+ )
+ assert tag == 'bragg-pd-tof-refln'
+
+
+def test_refln_factory_supported_tags():
+ from easydiffraction.datablocks.experiment.categories.refln.factory import ReflnFactory
+
+ tags = ReflnFactory.supported_tags()
+ assert 'bragg-sc' in tags
+ assert 'bragg-pd-refln' in tags
+ assert 'bragg-pd-tof-refln' in tags
diff --git a/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_pd.py b/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_pd.py
index 57a610f3d..2c8d17a95 100644
--- a/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_pd.py
+++ b/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_pd.py
@@ -4,10 +4,18 @@
import numpy as np
import pytest
+from easydiffraction.analysis.calculators.base import PowderReflnRecord
from easydiffraction.datablocks.experiment.categories.background.factory import BackgroundFactory
from easydiffraction.datablocks.experiment.categories.experiment_type import ExperimentType
+from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import (
+ PowderCwlReflnData,
+)
+from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import (
+ PowderTofReflnData,
+)
from easydiffraction.datablocks.experiment.item.bragg_pd import BraggPdExperiment
from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum
+from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum
from easydiffraction.datablocks.experiment.item.enums import RadiationProbeEnum
from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum
from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum
@@ -22,6 +30,15 @@ def _mk_type_powder_cwl_bragg():
return et
+def _mk_type_powder_tof_bragg():
+ et = ExperimentType()
+ et._set_sample_form(SampleFormEnum.POWDER.value)
+ et._set_beam_mode(BeamModeEnum.TIME_OF_FLIGHT.value)
+ et._set_radiation_probe(RadiationProbeEnum.NEUTRON.value)
+ et._set_scattering_type(ScatteringTypeEnum.BRAGG.value)
+ return et
+
+
def test_background_defaults_and_change():
expt = BraggPdExperiment(name='e1', type=_mk_type_powder_cwl_bragg())
# default background type
@@ -71,3 +88,162 @@ def test_load_ascii_data_rounds_and_defaults_sy(tmp_path: pytest.TempPathFactory
np.savetxt(pinv, np.ones((5, 1)))
with pytest.raises(IndexError, match='tuple index out of range'):
expt._load_ascii_data_to_experiment(str(pinv))
+
+
+def test_bragg_pd_experiment_creates_beam_mode_specific_refln_collection():
+ cwl_experiment = BraggPdExperiment(name='cwl', type=_mk_type_powder_cwl_bragg())
+ tof_experiment = BraggPdExperiment(name='tof', type=_mk_type_powder_tof_bragg())
+
+ assert isinstance(cwl_experiment.refln, PowderCwlReflnData)
+ assert isinstance(tof_experiment.refln, PowderTofReflnData)
+
+
+def test_bragg_pd_experiment_disables_refln_for_crysfml_and_restores_it_for_cryspy():
+ experiment = BraggPdExperiment(name='powder', type=_mk_type_powder_cwl_bragg())
+
+ assert isinstance(experiment.refln, PowderCwlReflnData)
+
+ experiment._calculator_type = CalculatorEnum.CRYSFML.value
+ experiment._sync_refln_category()
+ assert experiment.refln is None
+
+ experiment._calculator_type = CalculatorEnum.CRYSPY.value
+ experiment._sync_refln_category()
+ assert isinstance(experiment.refln, PowderCwlReflnData)
+
+
+def test_pd_data_update_populates_and_clears_refln():
+ from collections import UserDict
+
+ class FakeStructures(UserDict):
+ @property
+ def names(self):
+ return list(self.data.keys())
+
+ class FakeCalculator:
+ name = 'fake'
+
+ def __init__(self):
+ self.return_records = True
+
+ def calculate_pattern(self, structure, experiment, *, called_by_minimizer=False):
+ del experiment, called_by_minimizer
+ return structure.pattern
+
+ def last_powder_refln_records(self, structure, experiment, *, phase_id):
+ del experiment, phase_id
+ if not self.return_records:
+ return None
+ return structure.records
+
+ class FakeStructure:
+ def __init__(self, name, pattern, records):
+ self.name = name
+ self.pattern = pattern
+ self.records = records
+
+ experiment = BraggPdExperiment(name='powder', type=_mk_type_powder_cwl_bragg())
+ experiment.linked_phases.create(id='phase_a', scale=2.0)
+ experiment.linked_phases.create(id='phase_b', scale=3.0)
+ experiment.data._create_items_set_xcoord_and_id(np.array([10.0, 20.0, 30.0]))
+ experiment.data._set_intensity_meas(np.array([100.0, 110.0, 120.0]))
+
+ structures = FakeStructures({
+ 'phase_a': FakeStructure(
+ 'phase_a',
+ np.array([1.0, 2.0, 3.0]),
+ [
+ PowderReflnRecord(
+ phase_id='phase_a',
+ d_spacing=2.1,
+ sin_theta_over_lambda=0.25,
+ index_h=1,
+ index_k=0,
+ index_l=1,
+ f_calc=3.0,
+ f_squared_calc=9.0,
+ two_theta=14.5,
+ )
+ ],
+ ),
+ 'phase_b': FakeStructure(
+ 'phase_b',
+ np.array([4.0, 5.0, 6.0]),
+ [
+ PowderReflnRecord(
+ phase_id='phase_b',
+ d_spacing=1.8,
+ sin_theta_over_lambda=0.28,
+ index_h=2,
+ index_k=1,
+ index_l=0,
+ f_calc=4.0,
+ f_squared_calc=16.0,
+ two_theta=18.5,
+ )
+ ],
+ ),
+ })
+ project = type('Project', (), {'structures': structures})()
+ experiments = type('Experiments', (), {'_parent': project})()
+ experiment._parent = experiments
+ experiment._calculator = FakeCalculator()
+
+ experiment.data._update()
+
+ np.testing.assert_allclose(experiment.data.intensity_calc, np.array([14.0, 19.0, 24.0]))
+ np.testing.assert_array_equal(experiment.refln.phase_id, np.array(['phase_a', 'phase_b']))
+ np.testing.assert_allclose(experiment.refln.two_theta, np.array([14.5, 18.5]))
+
+ experiment._calculator.return_records = False
+ experiment.data._update()
+
+ assert len(experiment.refln._items) == 0
+
+
+def test_pd_data_update_skips_refln_records_when_category_is_disabled():
+ from collections import UserDict
+
+ class FakeStructures(UserDict):
+ @property
+ def names(self):
+ return list(self.data.keys())
+
+ class FakeCalculator:
+ name = 'fake'
+
+ def calculate_pattern(self, structure, experiment, *, called_by_minimizer=False):
+ del experiment, called_by_minimizer
+ return structure.pattern
+
+ def last_powder_refln_records(self, structure, experiment, *, phase_id):
+ del structure, experiment, phase_id
+ msg = 'Powder reflection metadata should not be requested'
+ raise AssertionError(msg)
+
+ class FakeStructure:
+ def __init__(self, name, pattern):
+ self.name = name
+ self.pattern = pattern
+
+ experiment = BraggPdExperiment(name='powder', type=_mk_type_powder_cwl_bragg())
+ experiment.linked_phases.create(id='phase_a', scale=2.0)
+ experiment.data._create_items_set_xcoord_and_id(np.array([10.0, 20.0, 30.0]))
+ experiment.data._set_intensity_meas(np.array([100.0, 110.0, 120.0]))
+
+ structures = FakeStructures({
+ 'phase_a': FakeStructure(
+ 'phase_a',
+ np.array([1.0, 2.0, 3.0]),
+ )
+ })
+ project = type('Project', (), {'structures': structures})()
+ experiments = type('Experiments', (), {'_parent': project})()
+ experiment._parent = experiments
+ experiment._calculator = FakeCalculator()
+ experiment._refln = None
+
+ experiment.data._update()
+
+ np.testing.assert_allclose(experiment.data.intensity_calc, np.array([2.0, 4.0, 6.0]))
+ assert experiment.refln is None
diff --git a/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_sc_coverage.py b/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_sc_coverage.py
index 8d86c6d94..9a196b67c 100644
--- a/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_sc_coverage.py
+++ b/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_sc_coverage.py
@@ -74,8 +74,8 @@ def test_switchable_categories(self):
assert ex.linked_crystal is not None
# instrument
assert ex.instrument is not None
- # data
- assert ex.data is not None
+ # refln
+ assert ex.refln is not None
def test_extinction_type_invalid(self):
ex = CwlScExperiment(name='cwl_sc', type=_mk_type_sc_cwl())
diff --git a/tests/unit/easydiffraction/display/plotters/test_ascii.py b/tests/unit/easydiffraction/display/plotters/test_ascii.py
index e0c04f8b3..49dd76537 100644
--- a/tests/unit/easydiffraction/display/plotters/test_ascii.py
+++ b/tests/unit/easydiffraction/display/plotters/test_ascii.py
@@ -48,3 +48,40 @@ def test_ascii_plotter_plot_single_crystal(capsys):
assert '●' in out
# Verify diagonal reference line (· character)
assert '·' in out
+
+
+def test_ascii_plotter_plot_powder_meas_vs_calc_announces_plotly_only_bragg_row(capsys):
+ from easydiffraction.display.plotters.ascii import AsciiPlotter
+ from easydiffraction.display.plotters.base import BraggTickSet
+ from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec
+
+ plotter = AsciiPlotter()
+ plotter.plot_powder_meas_vs_calc(
+ plot_spec=PowderMeasVsCalcSpec(
+ x=np.array([0.0, 1.0, 2.0]),
+ y_meas=np.array([3.0, 4.0, 5.0]),
+ y_calc=np.array([2.5, 4.5, 4.0]),
+ y_resid=np.array([0.5, -0.5, 1.0]),
+ bragg_tick_sets=(
+ BraggTickSet(
+ phase_id='phase-a',
+ x=np.array([0.5]),
+ h=np.array([1]),
+ k=np.array([0]),
+ ell=np.array([1]),
+ f_squared_calc=np.array([100.0]),
+ f_calc=np.array([10.0]),
+ ),
+ ),
+ axes_labels=['2θ (degree)', 'Intensity (arb. units)'],
+ title='Powder plot',
+ residual_height_fraction=0.25,
+ bragg_peaks_height_fraction=0.15,
+ height=8,
+ ),
+ )
+
+ out = capsys.readouterr().out
+ assert 'Legend:' in out
+ assert 'Residual (Imeas - Icalc)' in out
+ assert 'Bragg peak subplot rows are available with the Plotly engine only.' in out
diff --git a/tests/unit/easydiffraction/display/plotters/test_plotly.py b/tests/unit/easydiffraction/display/plotters/test_plotly.py
index 5335a6871..3905ee278 100644
--- a/tests/unit/easydiffraction/display/plotters/test_plotly.py
+++ b/tests/unit/easydiffraction/display/plotters/test_plotly.py
@@ -1,6 +1,9 @@
# SPDX-FileCopyrightText: 2026 EasyScience contributors
# SPDX-License-Identifier: BSD-3-Clause
+import numpy as np
+import pytest
+
def test_module_import():
import easydiffraction.display.plotters.plotly as MUT
@@ -46,6 +49,22 @@ def test_correlation_colorscale_uses_white_center_in_light_mode(monkeypatch):
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)'
+
+
def test_get_trace_and_plot(monkeypatch):
import easydiffraction.display.plotters.plotly as pp
@@ -84,7 +103,7 @@ def __init__(self, **kwargs):
class DummyPIO:
@staticmethod
- def to_html(fig, include_plotlyjs=None, full_html=None, config=None):
+ def to_html(fig, include_plotlyjs=None, full_html=None, config=None, post_script=None):
return 'plot
'
dummy_display_calls = {'count': 0}
@@ -110,6 +129,7 @@ def __init__(self, html):
assert hasattr(trace, 'kwargs')
assert trace.kwargs['x'] == x
assert trace.kwargs['y'] == y
+ assert trace.kwargs['line']['width'] == pp.CALCULATED_LINE_WIDTH
# Exercise plot_powder (non-PyCharm, display path)
plotter.plot_powder(
@@ -125,6 +145,139 @@ def __init__(self, html):
assert dummy_display_calls['count'] == 1 or shown['count'] == 1
+def test_show_figure_adds_legend_toggle_script_to_html_output(monkeypatch):
+ import easydiffraction.display.plotters.plotly as pp
+
+ monkeypatch.setattr(pp, 'in_pycharm', lambda: False)
+
+ captured = {}
+
+ class DummyFig:
+ def update_xaxes(self, **kwargs):
+ pass
+
+ def update_yaxes(self, **kwargs):
+ pass
+
+ def show(self, **kwargs):
+ captured['show_called'] = True
+
+ class DummyScatter:
+ def __init__(self, **kwargs):
+ self.kwargs = kwargs
+
+ class DummyGO:
+ class Scatter(DummyScatter):
+ pass
+
+ class Figure(DummyFig):
+ def __init__(self, data=None, layout=None):
+ self.data = data
+ self.layout = layout
+
+ class Layout:
+ def __init__(self, **kwargs):
+ self.kwargs = kwargs
+
+ class DummyPIO:
+ @staticmethod
+ def to_html(fig, include_plotlyjs=None, full_html=None, config=None, post_script=None):
+ captured['config'] = config
+ 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, 'go', DummyGO)
+ monkeypatch.setattr(pp, 'pio', DummyPIO)
+ monkeypatch.setattr(pp, 'display', dummy_display)
+ monkeypatch.setattr(pp, 'HTML', DummyHTML)
+
+ plotter = pp.PlotlyPlotter()
+ plotter.plot_powder(
+ [0, 1, 2],
+ y_series=[[1, 2, 3]],
+ labels=['calc'],
+ axes_labels=['x', 'y'],
+ title='t',
+ height=None,
+ )
+
+ assert captured.get('show_called') is not True
+ assert captured['config']['displayModeBar'] is True
+ assert captured['config']['displaylogo'] is False
+ assert 'data-legend-toggle="true"' in captured['post_script']
+ assert 'Toggle legend' in captured['post_script']
+ assert 'graphDiv.dataset.legendVisible' in captured['post_script']
+ assert 'const applyLegendVisibility = function (legendVisible) {' in captured['post_script']
+ assert "legend.style.display = legendVisible ? 'inline' : 'none';" in captured['post_script']
+ assert 'const readLegendVisibility = function () {' in captured['post_script']
+ assert (
+ "if (graphDiv.layout && typeof graphDiv.layout.showlegend === 'boolean')"
+ in captured['post_script']
+ )
+ assert "legendButton.classList.toggle('active', legendVisible);" in captured['post_script']
+ assert "graphDiv.on('plotly_relayout', function (eventData) {" in captured['post_script']
+ assert 'legendButton.onclick = toggleLegend;' in captured['post_script']
+ assert 'resolveLegendButtonFill(legendVisible ? 0.7 : 0.3)' in captured['post_script']
+ assert "legendButtonGroup.className = 'modebar-group';" in captured['post_script']
+ assert 'modebar.appendChild(legendButtonGroup);' in captured['post_script']
+ assert 'legendButton.innerHTML' in captured['post_script']
+ assert 'height="1em" width="1em"' in captured['post_script']
+ assert captured['displayed_html'] == 'plot
'
+
+
+def test_show_figure_skips_legend_toggle_script_without_legend(monkeypatch):
+ import easydiffraction.display.plotters.plotly as pp
+
+ monkeypatch.setattr(pp, 'in_pycharm', lambda: False)
+
+ captured = {}
+
+ class DummyTrace:
+ def __init__(self, name=None, showlegend=None, visible=None):
+ self.name = name
+ self.showlegend = showlegend
+ self.visible = visible
+
+ class DummyFig:
+ def __init__(self):
+ self.data = [DummyTrace(name=None, showlegend=False)]
+ self.layout = type('DummyLayout', (), {'showlegend': None})()
+
+ 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 captured['displayed_html'] == 'plot
'
+
+
def test_plotly_single_crystal_trace_and_plot(monkeypatch):
import easydiffraction.display.plotters.plotly as pp
@@ -162,7 +315,7 @@ def __init__(self, **kwargs):
class DummyPIO:
@staticmethod
- def to_html(fig, include_plotlyjs=None, full_html=None, config=None):
+ def to_html(fig, include_plotlyjs=None, full_html=None, config=None, post_script=None):
return 'plot
'
dummy_display_calls = {'count': 0}
@@ -209,3 +362,553 @@ def __init__(self, html):
)
# One display call expected
assert dummy_display_calls['count'] == 1 or shown['count'] == 1
+
+
+def test_get_bragg_tick_trace_includes_peak_metadata():
+ from easydiffraction.display.plotters.base import BraggTickSet
+ from easydiffraction.display.plotters.plotly import PlotlyPlotter
+
+ trace = PlotlyPlotter._get_bragg_tick_trace(
+ tick_set=BraggTickSet(
+ phase_id='phase-a',
+ x=np.array([1.5, 2.5]),
+ h=np.array([1, 2]),
+ k=np.array([0, 1]),
+ ell=np.array([1, 0]),
+ f_squared_calc=np.array([100.0, 80.0]),
+ f_calc=np.array([10.0, 9.0]),
+ ),
+ row_y=2.0,
+ color='#123456',
+ )
+
+ assert list(trace.x) == [1.5, 2.5]
+ assert list(trace.y) == [2.0, 2.0]
+ assert trace.mode == 'markers'
+ assert trace.marker.symbol == 'line-ns-open'
+ assert trace.hovertemplate == '%{text}'
+ assert 'phase-a' in trace.text[0]
+ assert 'Miller indices: (1 0 1)' in trace.text[0]
+ assert 'x: 1.50' in trace.text[0]
+
+
+def test_plot_powder_meas_vs_calc_creates_synced_three_panel_figure(monkeypatch):
+ import easydiffraction.display.plotters.plotly as pp
+
+ from easydiffraction.display.plotters.base import BraggTickSet
+ 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)
+
+ 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=(
+ BraggTickSet(
+ phase_id='phase-a',
+ x=np.array([1.5]),
+ h=np.array([1]),
+ k=np.array([0]),
+ ell=np.array([1]),
+ f_squared_calc=np.array([100.0]),
+ f_calc=np.array([10.0]),
+ ),
+ BraggTickSet(
+ phase_id='phase-b',
+ x=np.array([2.5]),
+ h=np.array([2]),
+ k=np.array([1]),
+ ell=np.array([0]),
+ f_squared_calc=np.array([80.0]),
+ f_calc=np.array([9.0]),
+ ),
+ ),
+ axes_labels=['2θ (degree)', 'Intensity (arb. units)'],
+ title='Powder',
+ residual_height_fraction=0.25,
+ bragg_peaks_height_fraction=0.10,
+ height=None,
+ )
+
+ plotter = pp.PlotlyPlotter()
+ plotter.plot_powder_meas_vs_calc(plot_spec=plot_spec)
+
+ fig = captured['fig']
+ assert len(fig.data) == 5
+ assert fig.layout.xaxis.matches == 'x'
+ assert fig.layout.xaxis2.matches == 'x'
+ assert fig.layout.xaxis3.matches == 'x'
+ assert fig.layout.yaxis3.scaleanchor == 'y'
+ assert fig.layout.yaxis3.scaleratio == pytest.approx(1.0)
+
+ plot_area_height = fig.layout.height - fig.layout.margin.t - fig.layout.margin.b
+ main_height = fig.layout.yaxis.domain[1] - fig.layout.yaxis.domain[0]
+ bragg_height = fig.layout.yaxis2.domain[1] - fig.layout.yaxis2.domain[0]
+ residual_height = fig.layout.yaxis3.domain[1] - fig.layout.yaxis3.domain[0]
+ assert residual_height == pytest.approx(main_height * 0.25)
+ assert plot_area_height * bragg_height == pytest.approx(
+ 2 * pp.PlotlyPlotter._bragg_tick_symbol_height_pixels()
+ )
+
+ expected_hovertemplate = (
+ 'x: %{x:,.2f} '
+ 'Imeas: %{customdata[0]:,.2f} '
+ 'Icalc: %{customdata[1]:,.2f} '
+ 'Imeas - Icalc: %{customdata[2]:,.2f}'
+ ' '
+ )
+ meas_trace = next(trace for trace in fig.data if trace.name == 'Measured (Imeas)')
+ calc_trace = next(trace for trace in fig.data if trace.name == 'Total calculated (Icalc)')
+ residual_trace = next(trace for trace in fig.data if trace.name == 'Residual (Imeas - Icalc)')
+ assert meas_trace.hovertemplate == expected_hovertemplate
+ assert calc_trace.hovertemplate == expected_hovertemplate
+ assert residual_trace.hovertemplate == expected_hovertemplate
+ assert meas_trace.line.width == pp.MEASURED_LINE_WIDTH
+ assert calc_trace.line.width == pp.CALCULATED_LINE_WIDTH
+ assert residual_trace.line.width == pp.RESIDUAL_LINE_WIDTH
+ assert list(meas_trace.customdata[0]) == pytest.approx([10.0, 9.0, 1.0])
+ assert list(calc_trace.customdata[0]) == pytest.approx([10.0, 9.0, 1.0])
+ assert list(residual_trace.customdata[0]) == pytest.approx([10.0, 9.0, 1.0])
+
+ bragg_traces = [trace for trace in fig.data if trace.name.startswith('Bragg')]
+ assert [trace.name for trace in bragg_traces] == [
+ 'Bragg peaks: phase-a',
+ 'Bragg peaks: phase-b',
+ ]
+ assert list(bragg_traces[0].y) == [1.0]
+ assert list(bragg_traces[1].y) == [2.0]
+ assert list(fig.layout.yaxis2.ticktext) == ['phase-a', 'phase-b']
+ assert list(fig.layout.yaxis2.range) == [2.5, 0.5]
+ assert fig.layout.yaxis2.title.text is None
+ assert fig.layout.yaxis3.title.text is None
+ assert fig.layout.yaxis3.zeroline is False
+ assert fig.layout.xaxis3.title.text == '2θ (degree)'
+ assert 'Miller indices: (1 0 1)' in bragg_traces[0].text[0]
+ assert 'phase-a' in bragg_traces[0].text[0]
+
+
+def test_plot_powder_meas_vs_calc_adds_background_curve(monkeypatch):
+ import easydiffraction.display.plotters.plotly as pp
+
+ from easydiffraction.display.plotters.base import BraggTickSet
+ 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)
+
+ 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=(
+ BraggTickSet(
+ phase_id='phase-a',
+ x=np.array([1.5]),
+ h=np.array([1]),
+ k=np.array([0]),
+ ell=np.array([1]),
+ f_squared_calc=np.array([100.0]),
+ f_calc=np.array([10.0]),
+ ),
+ ),
+ axes_labels=['2θ (degree)', 'Intensity (arb. units)'],
+ title='Powder',
+ residual_height_fraction=0.25,
+ bragg_peaks_height_fraction=0.10,
+ height=None,
+ y_bkg=np.array([1.5, 1.5, 1.5]),
+ )
+
+ plotter = pp.PlotlyPlotter()
+ plotter.plot_powder_meas_vs_calc(plot_spec=plot_spec)
+
+ fig = captured['fig']
+ assert len(fig.data) == 5
+ assert [trace.name for trace in fig.data[:3]] == [
+ 'Measured (Imeas)',
+ 'Background (Ibkg)',
+ 'Total calculated (Icalc)',
+ ]
+ background_trace = next(trace for trace in fig.data if trace.name == 'Background (Ibkg)')
+ meas_trace = next(trace for trace in fig.data if trace.name == 'Measured (Imeas)')
+ calc_trace = next(trace for trace in fig.data if trace.name == 'Total calculated (Icalc)')
+ residual_trace = next(trace for trace in fig.data if trace.name == 'Residual (Imeas - Icalc)')
+ assert list(background_trace.y) == pytest.approx([1.5, 1.5, 1.5])
+ assert background_trace.mode == 'lines'
+ assert background_trace.line.color == pp.DEFAULT_COLORS['bkg']
+ assert background_trace.line.width == pp.BACKGROUND_LINE_WIDTH
+ raw_min = 1.5
+ raw_max = 12.0
+ raw_range = raw_max - raw_min
+ margin = raw_range * pp.MAIN_INTENSITY_RANGE_MARGIN_FRACTION
+ assert fig.layout.yaxis.range[0] == pytest.approx(raw_min - margin)
+ assert fig.layout.yaxis.range[1] == pytest.approx(raw_max + margin)
+ assert meas_trace.legendrank < background_trace.legendrank < calc_trace.legendrank
+ assert residual_trace.legendrank > calc_trace.legendrank
+ for trace in (meas_trace, background_trace, calc_trace, residual_trace):
+ assert 'Ibkg: %{customdata[1]' in trace.hovertemplate
+ assert list(trace.customdata[0]) == pytest.approx([10.0, 1.5, 9.0, 1.0])
+
+
+def test_get_main_intensity_range_uses_unit_padding_for_flat_series():
+ from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec
+ from easydiffraction.display.plotters.plotly import PlotlyPlotter
+
+ plot_spec = PowderMeasVsCalcSpec(
+ x=np.array([1.0]),
+ y_meas=np.array([5.0]),
+ y_calc=np.array([5.0]),
+ y_resid=None,
+ bragg_tick_sets=(),
+ axes_labels=['2θ (degree)', 'Intensity (arb. units)'],
+ title='Powder',
+ residual_height_fraction=0.25,
+ bragg_peaks_height_fraction=0.10,
+ height=None,
+ y_bkg=np.array([5.0]),
+ )
+
+ assert PlotlyPlotter._get_main_intensity_range(plot_spec) == pytest.approx((4.0, 6.0))
+
+
+def test_bragg_row_height_pixels_scale_linearly_with_phase_count():
+ from easydiffraction.display.plotters.base import BraggTickSet
+ from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec
+ from easydiffraction.display.plotters.plotly import PlotlyPlotter
+
+ single_phase = PowderMeasVsCalcSpec(
+ x=np.array([1.0, 2.0]),
+ y_meas=np.array([1.0, 2.0]),
+ y_calc=np.array([1.0, 2.0]),
+ y_resid=np.array([0.0, 0.0]),
+ bragg_tick_sets=(
+ BraggTickSet(
+ phase_id='phase-a',
+ x=np.array([1.5]),
+ h=np.array([1]),
+ k=np.array([0]),
+ ell=np.array([1]),
+ f_squared_calc=np.array([100.0]),
+ f_calc=np.array([10.0]),
+ ),
+ ),
+ axes_labels=['2θ (degree)', 'Intensity (arb. units)'],
+ title='Powder',
+ residual_height_fraction=0.25,
+ bragg_peaks_height_fraction=0.10,
+ height=None,
+ )
+ two_phase = PowderMeasVsCalcSpec(
+ x=single_phase.x,
+ y_meas=single_phase.y_meas,
+ y_calc=single_phase.y_calc,
+ y_resid=single_phase.y_resid,
+ bragg_tick_sets=(
+ single_phase.bragg_tick_sets[0],
+ BraggTickSet(
+ phase_id='phase-b',
+ x=np.array([2.5]),
+ h=np.array([2]),
+ k=np.array([1]),
+ ell=np.array([0]),
+ f_squared_calc=np.array([80.0]),
+ f_calc=np.array([9.0]),
+ ),
+ ),
+ axes_labels=single_phase.axes_labels,
+ title=single_phase.title,
+ residual_height_fraction=single_phase.residual_height_fraction,
+ bragg_peaks_height_fraction=single_phase.bragg_peaks_height_fraction,
+ height=single_phase.height,
+ )
+
+ symbol_height = PlotlyPlotter._bragg_tick_symbol_height_pixels()
+ single_height = PlotlyPlotter._bragg_row_height_pixels(single_phase)
+ two_phase_height = PlotlyPlotter._bragg_row_height_pixels(two_phase)
+ assert single_height == pytest.approx(symbol_height)
+ assert two_phase_height == pytest.approx(2 * symbol_height)
+
+
+def test_plot_powder_meas_vs_calc_grows_total_height_for_many_phases(monkeypatch):
+ import easydiffraction.display.plotters.plotly as pp
+
+ from easydiffraction.display.plotters.base import BraggTickSet
+ from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec
+
+ captured = {}
+
+ def fake_show_figure(self, fig):
+ captured.setdefault('figures', []).append(fig)
+
+ monkeypatch.setattr(pp.PlotlyPlotter, '_show_figure', fake_show_figure)
+
+ def plot_spec(phase_count: int) -> PowderMeasVsCalcSpec:
+ bragg_tick_sets = tuple(
+ BraggTickSet(
+ phase_id=f'phase-{idx}',
+ x=np.array([1.0 + idx]),
+ h=np.array([idx + 1]),
+ k=np.array([0]),
+ ell=np.array([1]),
+ f_squared_calc=np.array([100.0 - idx]),
+ f_calc=np.array([10.0 - 0.1 * idx]),
+ )
+ for idx in range(phase_count)
+ )
+ return 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=bragg_tick_sets,
+ axes_labels=['2θ (degree)', 'Intensity (arb. units)'],
+ title='Powder',
+ residual_height_fraction=0.25,
+ bragg_peaks_height_fraction=0.10,
+ height=None,
+ )
+
+ plotter = pp.PlotlyPlotter()
+ plotter.plot_powder_meas_vs_calc(plot_spec=plot_spec(1))
+ plotter.plot_powder_meas_vs_calc(plot_spec=plot_spec(10))
+
+ single_fig, multi_fig = captured['figures']
+
+ def row_height_pixels(fig, axis_name: str) -> float:
+ axis = getattr(fig.layout, axis_name)
+ plot_area_height = fig.layout.height - fig.layout.margin.t - fig.layout.margin.b
+ return plot_area_height * (axis.domain[1] - axis.domain[0])
+
+ assert multi_fig.layout.height > single_fig.layout.height
+ assert row_height_pixels(multi_fig, 'yaxis') == pytest.approx(
+ row_height_pixels(single_fig, 'yaxis')
+ )
+ assert row_height_pixels(multi_fig, 'yaxis3') == pytest.approx(
+ row_height_pixels(single_fig, 'yaxis3')
+ )
+ assert row_height_pixels(multi_fig, 'yaxis2') == pytest.approx(
+ row_height_pixels(single_fig, 'yaxis2') * 10
+ )
+
+
+def test_plot_powder_meas_vs_calc_uses_explicit_plotly_height_as_pixels(monkeypatch):
+ import easydiffraction.display.plotters.plotly as pp
+
+ from easydiffraction.display.plotters.base import BraggTickSet
+ 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=None,
+ bragg_tick_sets=(
+ BraggTickSet(
+ phase_id='phase-a',
+ x=np.array([1.5]),
+ h=np.array([1]),
+ k=np.array([0]),
+ ell=np.array([1]),
+ f_squared_calc=np.array([100.0]),
+ f_calc=np.array([10.0]),
+ ),
+ ),
+ axes_labels=['2θ (degree)', 'Intensity (arb. units)'],
+ title='Powder',
+ residual_height_fraction=0.25,
+ bragg_peaks_height_fraction=0.10,
+ height=800,
+ ),
+ )
+
+ assert captured['fig'].layout.height == 800
+
+
+def test_plot_powder_meas_vs_calc_skips_bragg_row_when_no_ticks(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,
+ ),
+ )
+
+ fig = captured['fig']
+ assert len(fig.data) == 3
+ assert fig.layout.xaxis.matches == 'x'
+ assert fig.layout.xaxis2.matches == 'x'
+ assert fig.layout.yaxis2.title.text is None
+ assert fig.layout.xaxis2.title.text == '2θ (degree)'
+ assert [trace.name for trace in fig.data] == [
+ 'Measured (Imeas)',
+ 'Total calculated (Icalc)',
+ 'Residual (Imeas - Icalc)',
+ ]
+
+
+def test_plot_powder_meas_vs_calc_keeps_exact_residual_scale_match(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([200.0, 3600.0, 220.0]),
+ y_calc=np.array([180.0, 3400.0, 210.0]),
+ y_resid=np.array([20.0, 200.0, 10.0]),
+ 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,
+ ),
+ )
+
+ fig = captured['fig']
+ raw_min = 180.0
+ raw_max = 3600.0
+ raw_range = raw_max - raw_min
+ margin = raw_range * pp.MAIN_INTENSITY_RANGE_MARGIN_FRACTION
+ expected_main_min = raw_min - margin
+ expected_main_max = raw_max + margin
+ expected_limit = 0.5 * (expected_main_max - expected_main_min) * 0.25
+ assert fig.layout.yaxis2.scaleanchor == 'y'
+ assert fig.layout.yaxis2.scaleratio == pytest.approx(1.0)
+ assert fig.layout.yaxis.range[0] == pytest.approx(expected_main_min)
+ assert fig.layout.yaxis.range[1] == pytest.approx(expected_main_max)
+ assert fig.layout.yaxis2.range[0] == pytest.approx(-expected_limit)
+ assert fig.layout.yaxis2.range[1] == pytest.approx(expected_limit)
+ plot_area_height = fig.layout.height - fig.layout.margin.t - fig.layout.margin.b
+ main_pixels = plot_area_height * (fig.layout.yaxis.domain[1] - fig.layout.yaxis.domain[0])
+ residual_pixels = plot_area_height * (
+ fig.layout.yaxis2.domain[1] - fig.layout.yaxis2.domain[0]
+ )
+ main_units_per_pixel = (fig.layout.yaxis.range[1] - fig.layout.yaxis.range[0]) / main_pixels
+ residual_units_per_pixel = (
+ fig.layout.yaxis2.range[1] - fig.layout.yaxis2.range[0]
+ ) / residual_pixels
+ assert residual_units_per_pixel == pytest.approx(main_units_per_pixel)
+ assert list(fig.layout.yaxis2.tickvals) == pytest.approx([-400.0, 0.0, 400.0])
+
+
+def test_plot_powder_meas_vs_calc_clips_large_residual_spikes(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([200.0, 3600.0, 220.0]),
+ y_calc=np.array([180.0, 3400.0, 210.0]),
+ y_resid=np.array([20.0, 1200.0, 10.0]),
+ 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,
+ ),
+ )
+
+ fig = captured['fig']
+ raw_min = 180.0
+ raw_max = 3600.0
+ raw_range = raw_max - raw_min
+ margin = raw_range * pp.MAIN_INTENSITY_RANGE_MARGIN_FRACTION
+ expected_limit = 0.5 * ((raw_max + margin) - (raw_min - margin)) * 0.25
+ assert fig.layout.yaxis2.range[0] == pytest.approx(-expected_limit)
+ assert fig.layout.yaxis2.range[1] == pytest.approx(expected_limit)
+ assert list(fig.layout.yaxis2.tickvals) == pytest.approx([-400.0, 0.0, 400.0])
+
+
+def test_plot_powder_meas_vs_calc_accepts_empty_filtered_range(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([], dtype=float),
+ y_meas=np.array([], dtype=float),
+ y_calc=np.array([], dtype=float),
+ y_resid=np.array([], dtype=float),
+ 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,
+ ),
+ )
+
+ fig = captured['fig']
+ assert len(fig.data) == 3
+ assert fig.layout.yaxis2.range[0] == pytest.approx(-1.0)
+ assert fig.layout.yaxis2.range[1] == pytest.approx(1.0)
diff --git a/tests/unit/easydiffraction/display/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py
index a40b2ba1a..5e1cc4daf 100644
--- a/tests/unit/easydiffraction/display/test_plotting.py
+++ b/tests/unit/easydiffraction/display/test_plotting.py
@@ -67,6 +67,7 @@ def test_plotter_error_paths_and_filtering(capsys, 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
from easydiffraction.utils.logging import Logger
monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.WARN, raising=True)
@@ -113,18 +114,21 @@ def __init__(self, pattern, expt_type):
p._plot_meas_vs_calc_data(
Expt(Ptn(two_theta=None, intensity_meas=None, intensity_calc=None), ExptType()),
'E',
+ _MeasVsCalcPlotOptions(),
)
out = capsys.readouterr().out
assert 'No measured data available for experiment E' in out
p._plot_meas_vs_calc_data(
Expt(Ptn(two_theta=[1], intensity_meas=None, intensity_calc=[1]), ExptType()),
'E',
+ _MeasVsCalcPlotOptions(),
)
out = capsys.readouterr().out
assert 'No measured data available for experiment E' in out
p._plot_meas_vs_calc_data(
Expt(Ptn(two_theta=[1], intensity_meas=[1], intensity_calc=None), ExptType()),
'E',
+ _MeasVsCalcPlotOptions(),
)
out = capsys.readouterr().out
assert 'No calculated data available for experiment E' in out
@@ -175,6 +179,429 @@ def __init__(self):
assert 'Measured data' in called['title']
+def test_extract_bragg_tick_sets_groups_and_filters():
+ import numpy as np
+
+ from easydiffraction.display.plotting import Plotter
+ from easydiffraction.display.plotting import XAxisType
+
+ class Refln:
+ phase_id = np.array(['phase-a', 'phase-a', 'phase-b', 'phase-b'])
+ two_theta = np.array([0.5, 1.5, 2.5, 3.5])
+ index_h = np.array([1, 2, 3, 4])
+ index_k = np.array([0, 1, 1, 2])
+ index_l = np.array([1, 0, 2, 1])
+ f_squared_calc = np.array([10.0, 20.0, 30.0, 40.0])
+ f_calc = np.array([3.0, 4.0, 5.0, 6.0])
+
+ class Experiment:
+ refln = Refln()
+
+ tick_sets = Plotter()._extract_bragg_tick_sets(
+ experiment=Experiment(),
+ expt_name='E1',
+ x_axis=XAxisType.TWO_THETA,
+ x_min=1.0,
+ x_max=3.0,
+ )
+
+ assert [tick_set.phase_id for tick_set in tick_sets] == ['phase-a', 'phase-b']
+ assert np.allclose(tick_sets[0].x, np.array([1.5]))
+ assert np.array_equal(tick_sets[0].h, np.array([2]))
+ assert np.array_equal(tick_sets[1].h, np.array([3]))
+ assert np.array_equal(tick_sets[1].k, np.array([1]))
+ assert np.array_equal(tick_sets[1].ell, np.array([2]))
+ assert np.allclose(tick_sets[0].f_squared_calc, np.array([20.0]))
+ assert np.allclose(tick_sets[1].f_calc, np.array([5.0]))
+
+
+def test_extract_bragg_tick_sets_returns_empty_without_category():
+ from easydiffraction.display.plotting import Plotter
+ from easydiffraction.display.plotting import XAxisType
+
+ class Experiment:
+ pass
+
+ tick_sets = Plotter()._extract_bragg_tick_sets(
+ experiment=Experiment(),
+ expt_name='E1',
+ x_axis=XAxisType.TWO_THETA,
+ x_min=1.0,
+ x_max=3.0,
+ )
+
+ assert tick_sets == ()
+
+
+def test_extract_bragg_tick_sets_uses_derived_d_spacing_for_cwl_ticks():
+ import numpy as np
+
+ from easydiffraction.display.plotting import Plotter
+ from easydiffraction.display.plotting import XAxisType
+ from easydiffraction.utils.utils import twotheta_to_d
+
+ class Refln:
+ phase_id = np.array(['phase-a'])
+ two_theta = np.array([20.0])
+ d_spacing = np.array([999.0])
+ index_h = np.array([1])
+ index_k = np.array([0])
+ index_l = np.array([1])
+ f_squared_calc = np.array([10.0])
+ f_calc = np.array([3.0])
+
+ class Instrument:
+ setup_wavelength = type('Wavelength', (), {'value': 1.0})()
+
+ class Experiment:
+ refln = Refln()
+ instrument = Instrument()
+
+ tick_sets = Plotter()._extract_bragg_tick_sets(
+ experiment=Experiment(),
+ expt_name='E1',
+ x_axis=XAxisType.D_SPACING,
+ x_min=0.1,
+ x_max=10.0,
+ )
+
+ np.testing.assert_allclose(tick_sets[0].x, twotheta_to_d(np.array([20.0]), 1.0))
+
+
+def test_plot_meas_vs_calc_routes_powder_bragg_to_composite_backend():
+ import numpy as np
+
+ 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 = {}
+
+ class FakeBackend:
+ def plot_powder_meas_vs_calc(self, **kwargs):
+ captured['powder_meas_vs_calc'] = kwargs['plot_spec']
+
+ def plot_powder(self, **kwargs):
+ captured['powder'] = kwargs
+
+ class Pattern:
+ two_theta = np.array([0.0, 1.0, 2.0, 3.0])
+ d_spacing = two_theta
+ intensity_meas = np.array([10.0, 20.0, 30.0, 40.0])
+ intensity_bkg = np.array([1.0, 2.0, 3.0, 4.0])
+ intensity_calc = np.array([9.0, 18.0, 27.0, 39.0])
+
+ class Refln:
+ phase_id = np.array(['phase-a', 'phase-a', 'phase-b'])
+ two_theta = np.array([0.5, 1.5, 2.0])
+ index_h = np.array([1, 2, 3])
+ index_k = np.array([0, 1, 1])
+ index_l = np.array([1, 0, 2])
+ f_squared_calc = np.array([100.0, 80.0, 60.0])
+ f_calc = np.array([10.0, 9.0, 8.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(x_min=1.0, x_max=2.0),
+ )
+
+ assert 'powder_meas_vs_calc' in captured
+ assert 'powder' not in captured
+ call = captured['powder_meas_vs_calc']
+ assert np.allclose(call.x, np.array([1.0, 2.0]))
+ assert np.allclose(call.y_meas, np.array([20.0, 30.0]))
+ assert np.allclose(call.y_bkg, np.array([2.0, 3.0]))
+ assert np.allclose(call.y_calc, np.array([18.0, 27.0]))
+ assert np.allclose(call.y_resid, np.array([2.0, 3.0]))
+ assert [tick_set.phase_id for tick_set in call.bragg_tick_sets] == [
+ 'phase-a',
+ 'phase-b',
+ ]
+ assert np.allclose(call.bragg_tick_sets[0].x, np.array([1.5]))
+ assert np.allclose(call.bragg_tick_sets[1].x, np.array([2.0]))
+
+
+def test_plot_meas_vs_calc_extracts_bragg_ticks_with_default_bounds():
+ import numpy as np
+
+ 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 = {}
+
+ class FakeBackend:
+ def plot_powder_meas_vs_calc(self, **kwargs):
+ captured['powder_meas_vs_calc'] = kwargs['plot_spec']
+
+ class Pattern:
+ time_of_flight = np.array([10.0, 11.0, 12.0])
+ intensity_meas = np.array([100.0, 110.0, 105.0])
+ intensity_calc = np.array([99.0, 108.0, 104.0])
+
+ class Refln:
+ phase_id = np.array(['phase-a'])
+ time_of_flight = np.array([11.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.TIME_OF_FLIGHT})()
+
+ 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(),
+ )
+
+ call = captured['powder_meas_vs_calc']
+ assert np.allclose(call.x, np.array([10.0, 11.0, 12.0]))
+ assert [tick_set.phase_id for tick_set in call.bragg_tick_sets] == ['phase-a']
+ assert np.allclose(call.bragg_tick_sets[0].x, np.array([11.0]))
+
+
+def test_plot_meas_vs_calc_groups_numeric_bragg_structure_ids():
+ import numpy as np
+
+ 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 = {}
+
+ class FakeBackend:
+ def plot_powder_meas_vs_calc(self, **kwargs):
+ captured['powder_meas_vs_calc'] = kwargs['plot_spec']
+
+ class Pattern:
+ time_of_flight = np.array([10.0, 11.0, 12.0])
+ intensity_meas = np.array([100.0, 110.0, 105.0])
+ intensity_calc = np.array([99.0, 108.0, 104.0])
+
+ class Refln:
+ phase_id = np.array([1, 1, 2])
+ time_of_flight = np.array([10.0, 11.0, 12.0])
+ index_h = np.array([1, 2, 3])
+ index_k = np.array([0, 1, 1])
+ index_l = np.array([1, 0, 2])
+ f_squared_calc = np.array([50.0, 40.0, 30.0])
+ f_calc = np.array([7.0, 6.0, 5.0])
+
+ class ExptType:
+ sample_form = type('SF', (), {'value': SampleFormEnum.POWDER})()
+ scattering_type = type('S', (), {'value': ScatteringTypeEnum.BRAGG})()
+ beam_mode = type('B', (), {'value': BeamModeEnum.TIME_OF_FLIGHT})()
+
+ 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(),
+ )
+
+ call = captured['powder_meas_vs_calc']
+ assert [tick_set.phase_id for tick_set in call.bragg_tick_sets] == ['1', '2']
+ assert np.allclose(call.bragg_tick_sets[0].x, np.array([10.0, 11.0]))
+ assert np.allclose(call.bragg_tick_sets[1].x, np.array([12.0]))
+
+
+def test_plot_meas_vs_calc_skips_bragg_ticks_when_filtered_pattern_is_empty():
+ import numpy as np
+
+ 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 = {}
+
+ class FakeBackend:
+ def plot_powder_meas_vs_calc(self, **kwargs):
+ captured['powder_meas_vs_calc'] = kwargs['plot_spec']
+
+ class Pattern:
+ time_of_flight = np.array([10.0, 11.0, 12.0])
+ intensity_meas = np.array([100.0, 110.0, 105.0])
+ intensity_calc = np.array([99.0, 108.0, 104.0])
+
+ class Refln:
+ phase_id = np.array(['phase-a'])
+ time_of_flight = np.array([8.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.TIME_OF_FLIGHT})()
+
+ 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(x_min=7.0, x_max=9.0),
+ )
+
+ call = captured['powder_meas_vs_calc']
+ assert call.x.size == 0
+ assert call.bragg_tick_sets == ()
+
+
+def test_plot_meas_vs_calc_does_not_accept_layout_fraction_overrides():
+ from easydiffraction.display.plotting import Plotter
+
+ plotter = Plotter()
+
+ with pytest.raises(TypeError, match='residual_height_fraction'):
+ plotter.plot_meas_vs_calc('E1', residual_height_fraction=0.20)
+
+ with pytest.raises(TypeError, match='bragg_peaks_height_fraction'):
+ plotter.plot_meas_vs_calc('E1', bragg_peaks_height_fraction=0.20)
+
+
+def test_plot_meas_vs_calc_keeps_single_crystal_routing():
+ import numpy as np
+
+ 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 = {}
+
+ class FakeBackend:
+ def plot_single_crystal(self, **kwargs):
+ captured['single_crystal'] = kwargs
+
+ def plot_powder_meas_vs_calc(self, **kwargs):
+ captured['powder_meas_vs_calc'] = kwargs['plot_spec']
+
+ class Pattern:
+ intensity_calc = np.array([1.0, 2.0, 3.0])
+ intensity_meas = np.array([1.1, 1.9, 3.2])
+ intensity_meas_su = np.array([0.1, 0.1, 0.1])
+
+ class ExptType:
+ sample_form = type('SF', (), {'value': SampleFormEnum.SINGLE_CRYSTAL})()
+ scattering_type = type('S', (), {'value': ScatteringTypeEnum.BRAGG})()
+ beam_mode = type('B', (), {'value': BeamModeEnum.CONSTANT_WAVELENGTH})()
+
+ class Experiment:
+ data = Pattern()
+ type = ExptType()
+
+ plotter = Plotter()
+ plotter._backend = FakeBackend()
+ plotter._plot_meas_vs_calc_data(
+ experiment=Experiment(),
+ expt_name='E1',
+ plot_options=_MeasVsCalcPlotOptions(),
+ )
+
+ assert 'single_crystal' in captured
+ assert 'powder_meas_vs_calc' not in captured
+ assert np.allclose(captured['single_crystal']['x_calc'], np.array([1.0, 2.0, 3.0]))
+ assert np.allclose(captured['single_crystal']['y_meas'], np.array([1.1, 1.9, 3.2]))
+ assert np.allclose(captured['single_crystal']['y_meas_su'], np.array([0.1, 0.1, 0.1]))
+
+
+def test_plot_meas_vs_calc_keeps_default_residual_off_for_line_paths():
+ import numpy as np
+
+ 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 = {}
+
+ class FakeBackend:
+ def plot_powder(self, **kwargs):
+ captured['powder'] = kwargs
+
+ def plot_single_crystal(self, **kwargs):
+ captured['single_crystal'] = kwargs
+
+ def plot_powder_meas_vs_calc(self, **kwargs):
+ captured['powder_meas_vs_calc'] = kwargs['plot_spec']
+
+ class Pattern:
+ d_spacing = np.array([1.0, 2.0, 3.0])
+ intensity_meas = np.array([5.0, 6.0, 7.0])
+ intensity_calc = np.array([4.5, 6.1, 7.2])
+
+ class ExptType:
+ sample_form = type('SF', (), {'value': SampleFormEnum.SINGLE_CRYSTAL})()
+ scattering_type = type('S', (), {'value': ScatteringTypeEnum.BRAGG})()
+ beam_mode = type('B', (), {'value': BeamModeEnum.CONSTANT_WAVELENGTH})()
+
+ class Experiment:
+ data = Pattern()
+ type = ExptType()
+
+ plotter = Plotter()
+ plotter._backend = FakeBackend()
+ plotter._plot_meas_vs_calc_data(
+ experiment=Experiment(),
+ expt_name='E1',
+ plot_options=_MeasVsCalcPlotOptions(x='d_spacing'),
+ )
+
+ assert 'powder' in captured
+ assert 'single_crystal' not in captured
+ assert 'powder_meas_vs_calc' not in captured
+ assert captured['powder']['labels'] == ['meas', 'calc']
+
+
def test_plot_param_correlations_renders_ascii_table(monkeypatch):
import numpy as np
diff --git a/tests/unit/easydiffraction/display/test_plotting_coverage.py b/tests/unit/easydiffraction/display/test_plotting_coverage.py
index e290842b5..806e68da2 100644
--- a/tests/unit/easydiffraction/display/test_plotting_coverage.py
+++ b/tests/unit/easydiffraction/display/test_plotting_coverage.py
@@ -90,6 +90,7 @@ def test_height_setter_with_value(self):
p = Plotter()
p.height = 50
assert p.height == 50
+ assert p._composite_plot_height() == 50
def test_height_setter_with_none_resets_default(self):
from easydiffraction.display.plotters.base import DEFAULT_HEIGHT
@@ -99,6 +100,15 @@ def test_height_setter_with_none_resets_default(self):
p.height = 99
p.height = None
assert p.height == DEFAULT_HEIGHT
+ assert p._composite_plot_height() is None
+
+ def test_default_height_uses_backend_composite_default(self):
+ from easydiffraction.display.plotters.base import DEFAULT_HEIGHT
+ from easydiffraction.display.plotting import Plotter
+
+ p = Plotter()
+ assert p.height == DEFAULT_HEIGHT
+ assert p._composite_plot_height() is None
# ------------------------------------------------------------------
@@ -489,6 +499,9 @@ class FakeBackend:
def plot_powder(self, **kwargs):
calls.append(('powder', kwargs))
+ def plot_powder_meas_vs_calc(self, **kwargs):
+ calls.append(('powder_meas_vs_calc', kwargs['plot_spec']))
+
p = Plotter()
p._set_project(FakeProject())
p._backend = FakeBackend()
@@ -511,11 +524,14 @@ def test_plot_meas_vs_calc(self, monkeypatch):
p, calls = self._make_plotter_with_project(monkeypatch)
p.plot_meas_vs_calc('E1')
assert len(calls) == 1
- assert 'meas' in calls[0][1]['labels']
- assert 'calc' in calls[0][1]['labels']
+ assert calls[0][0] == 'powder_meas_vs_calc'
+ assert calls[0][1].y_resid is not None
+ assert calls[0][1].bragg_tick_sets == ()
- def test_plot_meas_vs_calc_with_residual(self, monkeypatch):
+ def test_plot_meas_vs_calc_without_residual(self, monkeypatch):
p, calls = self._make_plotter_with_project(monkeypatch)
- p.plot_meas_vs_calc('E1', show_residual=True)
+ p.plot_meas_vs_calc('E1', show_residual=False)
assert len(calls) == 1
- assert 'resid' in calls[0][1]['labels']
+ assert calls[0][0] == 'powder_meas_vs_calc'
+ assert calls[0][1].y_resid is None
+ assert calls[0][1].bragg_tick_sets == ()
diff --git a/tools/license_headers.py b/tools/license_headers.py
index 47d235242..f276ca1b1 100644
--- a/tools/license_headers.py
+++ b/tools/license_headers.py
@@ -25,7 +25,9 @@
def load_pyproject(repo_path: Union[str, Path]) -> dict[str, Any]:
- """Load and return parsed ``pyproject.toml`` data for the repository."""
+ """
+ Load and return parsed ``pyproject.toml`` data for the repository.
+ """
repo_root = find_repository_root(repo_path)
pyproject_path = repo_root / 'pyproject.toml'
@@ -56,7 +58,9 @@ def get_exclude_patterns(
exclude_values: list[str],
exclude_from_pyproject_toml: Optional[str],
) -> list[str]:
- """Return normalized exclude patterns from CLI and ``pyproject.toml``."""
+ """
+ Return normalized exclude patterns from CLI and ``pyproject.toml``.
+ """
pyproject_data = load_pyproject(repo_path)
patterns: list[str] = []
@@ -119,7 +123,9 @@ def get_file_creation_year(file_path: Union[str, Path]) -> str:
def get_org_url(repo_path: Union[str, Path]) -> str:
- """Return the organization URL derived from the repository source URL."""
+ """
+ Return the organization URL derived from the repository source URL.
+ """
pyproject_data = load_pyproject(repo_path)
repo_url = pyproject_data['project']['urls']['Source Code']
return repo_url.rsplit('/', 1)[0]