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]