diff --git a/docs/dev/adrs/suggestions/documentation-ci-build.md b/docs/dev/adrs/accepted/documentation-ci-build.md similarity index 67% rename from docs/dev/adrs/suggestions/documentation-ci-build.md rename to docs/dev/adrs/accepted/documentation-ci-build.md index f3a858832..e7e0e8885 100644 --- a/docs/dev/adrs/suggestions/documentation-ci-build.md +++ b/docs/dev/adrs/accepted/documentation-ci-build.md @@ -1,6 +1,6 @@ # ADR: Documentation CI and Build Verification -**Status:** Proposed +**Status:** Accepted **Date:** 2026-05-31 ## Group @@ -77,6 +77,20 @@ Use `codespell` first for low-noise spelling checks. Consider `Vale` after the project has a small EasyDiffraction style vocabulary and an allowlist for crystallographic terms, package names, and CIF tags. +## Implementation Status + +Most of this ADR is already in place; it is accepted to record the +chosen direction and to track the remaining gaps. + +| # | Decision | Status | +| --- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | MkDocs `--strict` build | **Done** — `docs-build` pixi task runs `mkdocs build --strict`; wired into the `lint-format.yml` "docs strict build" gate and the `docs.yml` deploy workflow. | +| 2 | `mkdocstrings` for API pages | **Done** — `mkdocstrings` + `mkdocstrings-python` configured in `docs/mkdocs.yml` (handler `paths: ['src']`); the `api-reference/*.md` pages use `:::` directives. | +| 3 | Snippet smoke tests | **Not done** — no task imports or executes the user-facing snippets in `quick-reference/`, `user-guide/first-steps.md`, or `user-guide/analysis-workflow/*.md`. Highest-value remaining gap. | +| 4 | Tutorial freshness check | **Partial** — `notebook-prepare` plus `notebook-tests`/`notebook-exec-ci` exist, but no no-write task asserts that `notebook-prepare` leaves the committed `.ipynb` unchanged. | +| 5 | `lychee` link checking | **Done for local/relative links** — `link-check` pixi task (config in `lychee.toml`) is wired into `lint-format.yml`. External-URL checking is deferred (see issue 114). | +| 6 | `codespell`, then `Vale` | **codespell done** — `spell-check` pixi task wired into `lint-format.yml`. `Vale` deferred. | + ## Options Considered ### MkDocs strict build @@ -179,11 +193,14 @@ Cons: ## Deferred Work -- Decide whether link checking runs on every pull request, nightly, or - both. -- Decide whether snippet smoke tests extract fenced code blocks +- Add snippet smoke tests for user-facing examples (decision 3). Tracked + by the `documentation-snippet-tests` implementation plan. Open + question carried into that plan: extract fenced code blocks automatically or rely on explicitly named snippets. -- Decide whether docs CI should build only source Markdown or also build - rendered notebooks. -- Add the chosen checks to `pixi.toml`, CI configuration, and developer - documentation after this ADR is accepted. +- Add a no-write `notebook-prepare-check` task that fails CI when the + committed notebooks are out of date with their `.py` sources (decision + 4). +- Enable external-URL link checking in the docs gate (decision 5), + scheduled or cached to avoid flakiness. Tracked by issue 114. +- Adopt `Vale` prose linting once an EasyDiffraction style vocabulary + and crystallographic-term allowlist exist (decision 6). diff --git a/docs/dev/adrs/accepted/plotting-docs-performance.md b/docs/dev/adrs/accepted/plotting-docs-performance.md index 3d68101a4..d6a9109fd 100644 --- a/docs/dev/adrs/accepted/plotting-docs-performance.md +++ b/docs/dev/adrs/accepted/plotting-docs-performance.md @@ -450,7 +450,7 @@ Settled in discussion on 2026-06-02: payload + faster draw) — a separate, data-side optimization. - A docs CI budget check (page weight / figure count) to catch regressions, aligning with - [`documentation-ci-build.md`](../suggestions/documentation-ci-build.md). + [`documentation-ci-build.md`](documentation-ci-build.md). - Hoist a single importmap into the **report** template `` for standalone reports that render multiple Three.js scenes (the same per-scene-importmap bug as docs, but governed by diff --git a/docs/dev/adrs/accepted/test-suite-and-validation.md b/docs/dev/adrs/accepted/test-suite-and-validation.md index b35aea302..6d0744057 100644 --- a/docs/dev/adrs/accepted/test-suite-and-validation.md +++ b/docs/dev/adrs/accepted/test-suite-and-validation.md @@ -66,7 +66,7 @@ accumulated: slow and therefore runs on pull requests only. There is no fast, every-push check that the site builds strictly, links resolve, and prose is clean. This overlaps the unimplemented - [Documentation CI and Build Verification](../suggestions/documentation-ci-build.md) + [Documentation CI and Build Verification](documentation-ci-build.md) suggestion. This ADR amends [Test Strategy](../accepted/test-strategy.md): the @@ -338,7 +338,7 @@ prompt drift feedback. It is deliberately **separate from `docs.yml`**, which executes all tutorials and then builds and deploys — slow, and therefore pull-request-only. The detailed catalogue of documentation checks is owned by -[Documentation CI and Build Verification](../suggestions/documentation-ci-build.md), +[Documentation CI and Build Verification](documentation-ci-build.md), which this ADR coordinates with: that ADR defines _what_ the checks are; this ADR's decision is that the cheap, deterministic subset runs as part of the every-push test workflow. Promoting that ADR is part of this @@ -443,15 +443,15 @@ drafting conversation, per the dependency-approval rule): - `pytest-benchmark` — performance-regression benchmarks (§7). Coordinated with -[Documentation CI and Build Verification](../suggestions/documentation-ci-build.md), +[Documentation CI and Build Verification](documentation-ci-build.md), which carries the documentation-check tools (`codespell`, a link checker such as `lychee`, and later `Vale`) used by §9. ## Related ADRs - [Test Strategy](../accepted/test-strategy.md) — amended by this ADR. -- [Documentation CI and Build Verification](../suggestions/documentation-ci-build.md) - — coordinated with §9. +- [Documentation CI and Build Verification](documentation-ci-build.md) — + coordinated with §9. - [Lint Complexity Thresholds](../accepted/lint-complexity-thresholds.md) — sibling Quality guardrail. - [Notebook Generation Source of Truth](../accepted/notebook-generation.md) diff --git a/docs/dev/adrs/index.md b/docs/dev/adrs/index.md index 17e2d85a1..b4e98e0d9 100644 --- a/docs/dev/adrs/index.md +++ b/docs/dev/adrs/index.md @@ -33,7 +33,7 @@ folders. | Documentation | Accepted | Help Method Discoverability | Requires primary public objects and facades to expose consistent `help()` output. | [`help-discoverability.md`](accepted/help-discoverability.md) | | Documentation | Accepted | Notebook Generation Source of Truth | Treats tutorial `.py` files as editable sources and notebooks as generated artifacts. | [`notebook-generation.md`](accepted/notebook-generation.md) | | Documentation | Accepted | Plotting & Docs Performance for Interactive Figures | Self-hosts a lazy, shared figure runtime so docs pages load fast and progressively while staying interactive. | [`plotting-docs-performance.md`](accepted/plotting-docs-performance.md) | -| Documentation | Suggestion | Documentation CI and Build Verification | Proposes strict MkDocs builds, API-derived docs, snippet smoke tests, link checks, and prose/spelling checks. | [`documentation-ci-build.md`](suggestions/documentation-ci-build.md) | +| Documentation | Accepted | Documentation CI and Build Verification | Strict MkDocs builds, API-derived docs, snippet smoke tests, link checks, and prose/spelling checks. | [`documentation-ci-build.md`](accepted/documentation-ci-build.md) | | Experiment model | Accepted | Immutable Experiment Type | Makes experiment type axes creation-time state rather than mutable runtime state. | [`immutable-experiment-type.md`](accepted/immutable-experiment-type.md) | | Experiment model | Accepted | Automatic Line-Segment Background Estimation | Detects line-segment background control points from the measured pattern, peak-insensitive and editable. | [`background-auto-estimate.md`](accepted/background-auto-estimate.md) | | Experiment model | Accepted | Calculation Without Measured Data | Adds a writable `data_range` category so a structure-only experiment is calculable and plottable without loaded data. | [`calculation-without-measured-data.md`](accepted/calculation-without-measured-data.md) | diff --git a/docs/dev/adrs/suggestions/model-sample-absorption.md b/docs/dev/adrs/suggestions/model-sample-absorption.md new file mode 100644 index 000000000..838e71986 --- /dev/null +++ b/docs/dev/adrs/suggestions/model-sample-absorption.md @@ -0,0 +1,377 @@ +# ADR: Model Sample Absorption (Debye–Scherrer, μR) + +## Status + +Proposed. + +## Date + +2026-06-12 + +## Group + +Experiment model. + +## Context + +This ADR follows the conventions in +[`AGENTS.md`](../../../../AGENTS.md). + +The calculators (`cryspy`, `crysfml`) currently apply **no** +sample-absorption correction. For a cylindrical sample in Debye–Scherrer +geometry the transmission through the sample is an angle-dependent +factor that attenuates low-angle peaks more than high-angle peaks; +omitting it leaves an angle-dependent intensity residual that scales +with the sample's μR (linear absorption coefficient × radius). + +This is not hypothetical. The verification reference +`pd-neut-cwl_tch-fcj_lab6` was refined in FullProf with **μR = 0.7**; +the unmodelled correction is the _entire_ intensity residual on the +companion `pd-neut-cwl_tch-fcj_abs_lab6` page (≈5 % profile difference), +while the μR = 0 page passes to corr 0.9999. See +[issue #119](../../issues/open.md). + +### What the three reference sources provide + +**FullProf** splits absorption into a refineable **magnitude** and a +**correction type**, on two different axes: + +- **CW (and symmetric θ–2θ flat plate):** `muR` on the `.pcr` Lambda + line — a single value (μ·R). In the LaB₆ reference it is **fixed** + (the Lambda line carries no refinement codeword), confirming that + absorption is typically entered as a known constant, not refined. A + `2nd-muR` field on the same line models a second coaxial cylinder (the + sample container / capillary wall); `Cthm` and `Rpolarz` on that line + are **polarization**, not absorption, and are out of scope here. +- **TOF:** `Iabscor` selects the correction _form_ — `1` flat plate ⟂ + incident beam, `2` cylindrical, `3` exponential `A = exp(−ABS·λᶜ)`. + TOF absorption is wavelength-dependent, not a pure function of 2θ. + +**CrysFML08** (`Src/CFML_Powder/Pow_Lorentz_Absorption.f90`) already +implements the CW formulas in Fortran: + +- `Lorentz_abs_CW(sinth, costh, postt, tmv, …, ilor, cabs, …)` with + `tmv = μR`, `ilor` = geometry (`DBS`, `BB`, `TBG`, `TFX`, `FILMS`…) + and `cabs ∈ {HEWAT, LOBANOV}`. +- `Powder_Lorentz_IntegInt_CW(…, muR, …)` — the bare Hewat form. + +**However**, these routines are **not wrapped** in CrysFML's +`PythonAPI/`, and the high-level entry our backend actually calls +(`cw_powder_pattern_from_dict`) computes a plain Lorentz factor +`0.5/(sin²θ·cosθ)` with no absorption term. So the issue's claim that +absorption is "reachable via `Lorentz_abs_CW` through pycrysfml" is +**not true today** — it would require upstream wrapping or an upstream +call-site change we do not control. + +**cryspy** has **no absorption code at all** (only Debye–Waller and +sphere _extinction_, which are different physics). CW intensity is +assembled in `procedure_rhochi/rhochi_pd.py` as +`0.5 · scale · Lorentz(θ) · |F|² · mult`, with no slot for an A(θ) +factor. + +### Consequence of the source survey + +Neither backend can apply the correction internally without changes we +do not own. The only way to get **identical** results across both +calculators is to compute A(θ) **ourselves in EasyDiffraction** and +apply it to the calculated pattern. This makes absorption a +**calculator-independent** correction — unlike `extinction`, which is +threaded into cryspy's own dict and is therefore `cryspy`-only. + +### CIF dictionary support (`tmp/iucr-dicts`) + +There is **no** standard data name for μR or for the Hewat coefficient. +The standard items cover the _physical provenance_ only: + +| Quantity | Standard CIF item | Units | +| ------------------------- | ------------------------------------------------------------- | ----- | +| Linear absorption μ | `_exptl_absorpt.coefficient_mu`, `_pd_char.atten_coef_mu_*` | mm⁻¹ | +| Sample radius / thickness | `_pd_spec.size_axial/_equat/_thick` | mm | +| Sample shape | `_pd_spec.shape` ∈ {`cylinder`, `flat_sheet`, `irregular`} | code | +| Correction type | `_exptl_absorpt.correction_type` (incl. `cylinder`, `sphere`) | code | +| Beam path | `_pd_spec.mount_mode` ∈ {`reflection`, `transmission`} | code | + +So the refineable μR itself needs a **project-namespaced** +(`_easydiffraction_absorption.*`) tag, with the standard items available +later as optional provenance (see Deferred Work). + +### Evidence from the FullProf example suite + +A survey of all 68 `.pcr` files shipped with FullProf (`Examples/`) +shows that **every** absorption example — CW and TOF — is **cylindrical +(Debye–Scherrer)**; not one uses flat-plate or exponential absorption, +and the container term is never used: + +- **CW (cylindrical `muR`):** `dy*`, `DyMnGe*`, `cuf1k`, `hocu`, + `si3n4r`, `sin_3t2` — μR ∈ {0.068, 0.15, 0.40, 1.28}, all **fixed** + (no refinement codeword on the Lambda line). Note μR reaches **1.28**, + slightly past Hewat's nominal ≈1.0 validity — the practical motivation + for the Lobanov form later. +- **TOF (`Iabscor`):** `Ceo2_PEARL`, `nac-osiris(n)`, `hrpd`, `arg_si`, + `cecual`, `cecoal`, `lamn_pol` — **all `Iabscor = 2` (cylindrical)**, + with `ABSCOR1` non-zero and **refined** (non-zero codewords). +- **Never observed:** `Iabscor = 1` (flat plate), `Iabscor = 3` + (exponential), or a non-zero `2nd-muR` (container) in any file. + +Two design consequences: + +1. **Ship a single cylindrical type now.** The cylinder is the only + geometry the reference toolchain actually exercises, so Phase 1 + builds `none` + `cylinder-hewat` only; everything else becomes a + documented future extension (§Deferred Work) that plugs into the same + switchable category without rework. +2. **Refineable, default-fixed.** CW practice fixes `muR`; TOF practice + refines it. So `mu_r` is a normal refineable `Parameter` but ships + `free = False` (matching the CW reference and the Biso degeneracy), + not a constant. + +## Decision + +### 1. Add a switchable `absorption` category on the experiment + +Introduce `experiment.absorption`, mirroring `experiment.extinction`: a +`SwitchableCategoryBase` whose concrete classes are registered with an +`AbsorptionFactory`, gated by `Compatibility` and `CalculatorSupport`. +It follows +[`switchable-category-owned-selectors.md`](../accepted/switchable-category-owned-selectors.md): + +```python +experiment.absorption.type # writable selector (str) +experiment.absorption.show_supported() # supported types, active starred +``` + +The owner exposes only `experiment.absorption` and a private +`_swap_absorption` hook (Family A: the hook **replaces the category +instance** when `type` changes), exactly as `extinction` does today. + +### 2. Application point — a pointwise envelope on the calculated pattern + +Because A(θ) is a **slowly-varying smooth envelope** of sin²θ (its +fractional change across one peak FWHM is ≪ 1 for any realistic μR), +applying it per-reflection vs. pointwise to the summed pattern differs +only at second order in (FWHM · dA/dθ) — negligible. We therefore apply +it once, after pattern assembly, in **both** backends: + +``` +y_corrected(2θ_i) = A(θ_i) · y_calc(2θ_i) +``` + +This is a single shared helper +(`analysis/calculators/absorption.py::factor(two_theta, params)`) called +from the post-calculation step of both `cryspy.py` and `crysfml.py`. The +two backends thus stay bit-for-bit consistent on the absorption term, +and the helper is unit-testable in isolation against FullProf output +(validated to 4 decimals in the issue). + +### 3. Supported types — the taxonomy + +The `type` selector lists factory tags gated by `Compatibility` (sample +form, beam mode, scattering type, radiation) and `CalculatorSupport`. +**Phase 1 builds only the first two rows;** the rest are the planned +extension surface, designed here so they later plug into the same +category by registering a class (see Deferred Work). + +| Tag | Beam mode | Sample form | Radiation | Calculators | Parameters | Status | +| ------------------ | --------- | ----------- | ------------- | --------------- | --------------- | ----------- | +| `none` | any | any | neutron, xray | cryspy, crysfml | — | **Phase 1** | +| `cylinder-hewat` | CWL | powder | neutron, xray | cryspy, crysfml | `mu_r` | **Phase 1** | +| `cylinder-lobanov` | CWL | powder | neutron, xray | cryspy, crysfml | `mu_r` | future | +| `tof-cylinder` | TOF | powder | neutron | cryspy, crysfml | `mu_r` (λ-dep.) | future | +| `flat-plate` | CWL | powder | neutron, xray | cryspy, crysfml | `mu_t` | future | +| `tof-exponential` | TOF | powder | neutron | cryspy, crysfml | `coeff`, `exp` | future | + +Notes: + +- `none` is the **default** (A ≡ 1). The category always exists for + powder so the user can discover and switch it on via + `show_supported()`; opting out is `type = 'none'`, not deleting the + category. (`scattering_type = 'total'` / pdffit is out of scope.) +- **Why a single built type is correct now:** the FullProf example suite + uses only cylindrical absorption (§"Evidence from the FullProf example + suite"), and our sole verification reference is CW cylindrical (LaB₆, + μR = 0.7). Building `cylinder-hewat` alone closes the known gap with a + tested oracle; the others would ship untested physics. This also + respects [`AGENTS.md`](../../../../AGENTS.md) §Architecture ("don't + introduce abstractions before a concrete second use case") — but the + switchable-category contract is still required even for a single + implementation (as `extinction` is today with only `becker-coppens`), + which is what keeps the extension surface free. +- Radiation is **not** a discriminator for the cylindrical geometric + envelope — the Hewat/Lobanov constants depend on geometry, not on + neutron vs X-ray. X-ray simply tends to larger μ; the same formula + applies. Both are supported. +- The future rows are designed (tags, parameters, CIF) but **not + built**. TOF in particular is λ-dependent and the slow-envelope + argument does not carry over unchanged (each detector bin mixes + wavelengths), so it needs its own application path; it is listed so + the taxonomy and CIF tags are settled once. + +### 4. The μR parameter + +The single refineable/settable quantity is **`mu_r`** (μ·R), matching +FullProf's `muR` and CrysFML's `tmv`. It is a `Parameter` +(`RangeValidator(ge=0.0)`, default `0.0`), **free=False by default** +(absorption is normally fixed, per the LaB₆ reference). Storing μ and R +separately is rejected (see Alternatives); they can be added later as +read-only provenance (Deferred Work). + +`flat-plate` uses `mu_t` (μ·thickness); the TOF exponential form uses +`coeff` and `exp` (`A = exp(−coeff·λ^exp)`). + +### 5. Equations + +**Hewat** (cylinder, validated to 4 decimals vs FullProf; fit range μR ≲ +1.5): + +``` +A(θ) = exp( −(1.7133 − 0.0368·sin²θ)·μR + (0.0927 + 0.375·sin²θ)·μR² ) +``` + +**Lobanov–Alte da Veiga** (cylinder, extends to μR ≈ 10 via a branch at +μR = 3; `s ≡ sinθ`): + +``` +μR ≤ 3: + k1 = (25.99978 − 0.01911·s^0.25)·exp(−0.024514·s) + 0.109561·√s − 26.0456 + k2 = −0.02489 − 0.39499·s + 1.219077·s^1.5 − 1.31268·s² + 0.871081·s^2.5 − 0.2327·s³ + k3 = 0.003045 + 0.018167·s − 0.03305·s² + A = exp( −((k3·μR + k2)·μR + k1)·μR ) (normalised; k0 = 1.697653) + +μR > 3: + A = k7 + (k4 − k7) / (1 + k5·(μR − 3))^k6 (k4…k7 polynomials in s) +``` + +(Lobanov constants transcribed from CrysFML08 +`Pow_Lorentz_Absorption.f90`; the implementation will copy them verbatim +and unit-test against that source.) + +**Flat plate, symmetric θ–2θ** (μt = μ·thickness): + +``` +A(θ) = exp( −2·μt / sinθ ) # transmission, symmetric reflection +``` + +**TOF exponential** (deferred; per FullProf `Iabscor = 3`): + +``` +A(λ) = exp( −coeff · λ^exp ) +``` + +### 6. User-facing API + +```python +# Discover what is available for this experiment (Phase 1) +experiment.absorption.show_supported() +# -> none (*), cylinder-hewat + +# Turn on the cylindrical Debye–Scherrer correction +experiment.absorption.type = 'cylinder-hewat' +experiment.absorption.mu_r = 0.7 # fixed value from the beamline + +# (Optional) refine it — off by default because it is near-degenerate +# with Biso and scale +experiment.absorption.mu_r.free = True +``` + +`experiment.absorption.type = 'none'` restores A ≡ 1. + +### 7. CIF mapping + +Project-namespaced block, one identity tag plus the magnitude: + +``` +_absorption.type cylinder-hewat +_absorption.mu_r 0.7 +``` + +IUCr-aligned export (per +[`iucr-cif-tag-alignment.md`](../accepted/iucr-cif-tag-alignment.md)) +uses `_easydiffraction_absorption.type` / `.mu_r`, and may additionally +emit the standard provenance `_exptl_absorpt.correction_type cylinder`. +`flat-plate` writes `_absorption.mu_t`; TOF forms write their own +fields. The `_absorption.type` tag is the single source of truth for the +active type (no owner-level selector tag), per +[`switchable-category-owned-selectors.md`](../accepted/switchable-category-owned-selectors.md). + +### 8. Cost + +A(θ) is one vectorised `exp` over the 2θ grid per pattern evaluation — +microseconds, utterly dominated by the diffraction calculation itself. +Refining `mu_r` adds one parameter; each fit iteration recomputes the +envelope at negligible cost. There is no per-reflection loop and no +backend round-trip. + +## Consequences + +- **Closes the LaB₆ absorption residual.** The + `pd-neut-cwl_tch-fcj_abs_lab6` verification page becomes the + acceptance test: with `cylinder-hewat`, `mu_r = 0.7` it should reach + the same corr as the μR = 0 page. +- **Calculator-consistent by construction.** Both backends call the same + helper, so the absorption term can never drift between `cryspy` and + `crysfml`. New backends inherit it for free. +- **No upstream dependency.** We do not wait on cryspy or CrysFML Python + wrappers; nothing in `pyproject.toml` changes. +- **New switchable category to wire.** Owner attribute, `_swap_*` hook, + factory, `__init__.py` registration, enums, CIF round-trip, and the + `none` default — the full + [`switchable-category-owned-selectors.md`](../accepted/switchable-category-owned-selectors.md) + surface. Mitigated by mirroring `extinction` closely. +- **Degeneracy is documented, not hidden.** `mu_r.free = False` by + default; help text warns that refining μR together with Biso/scale + correlates strongly. Explicit modelling is preferred precisely so Biso + is not biased by soaking up absorption. +- **Pointwise envelope is an approximation** (second order in + FWHM·dA/dθ). Validated to 4 decimals against FullProf; acceptable and + documented. + +## Alternatives Considered + +1. **Per-reflection multiply inside each backend.** More "correct" in + principle, but cryspy has no slot (would need to patch its intensity + loop) and crysfml's routine is in unwrapped Fortran. Two divergent + code paths, an upstream dependency, and no measurable accuracy gain + over the envelope. Rejected. +2. **Flat instrument parameter `instrument.mu_r`** (as the issue + sketches). Simpler, but it cannot carry a correction-type selector + (Hewat vs Lobanov vs flat-plate vs TOF), breaks the + switchable-category uniformity the project standardised on, and has + no natural home for TOF forms. Rejected in favour of the switchable + category. +3. **Store μ and R separately, compute μR.** Matches the CIF items, but + the two inputs are perfectly correlated for the correction and only + their product matters; FullProf and CrysFML both parametrise by the + product. Storing them separately invites a confusing two-knob UI for + one degree of freedom. Deferred to optional provenance. +4. **Let Biso absorb it.** The status quo. Biases the thermal parameters + and fails the `_abs_` verification page. Rejected — this ADR exists + to avoid exactly that. + +## Deferred Work + +All of these are designed into the taxonomy and CIF tags above but **not +built in Phase 1** — each is a new class registered on the same +`AbsorptionFactory`, gated by `Compatibility`/`CalculatorSupport`, with +no change to the category, the swap hook, or the application helper. +None appears in the FullProf example suite (only the cylinder does), so +none is urgent; each should land **with a verification dataset**, not on +spec alone. + +- **`cylinder-lobanov`** — extends valid μR to ≈10 (branch at μR = 3). + Real CW neutron examples reach μR = 1.28, past Hewat's ≈1.0 validity, + so this is a genuine eventual want, not hypothetical. Add when a + high-μR reference exists; Hewat alone meets the current LaB₆ case. +- **TOF absorption** (`tof-cylinder`, `tof-exponential`): the + λ-dependent forms (FullProf `Iabscor = 2 / 3`). The pointwise-2θ + envelope does not transfer directly — needs its own application path. + FullProf TOF examples all use `Iabscor = 2` (cylindrical) and refine + it, so `tof-cylinder` is the natural next target. +- **`flat-plate`** (CW symmetric θ–2θ, `mu_t`) and other geometries + (`Iabscor = 1`): present in the file formats but used by **zero** + FullProf examples — lowest priority. +- **Optional (μ, R) provenance** mapped to + `_exptl_absorpt.coefficient_mu` and `_pd_spec.size_*`, read-only, with + `mu_r` remaining the single refineable knob. +- **Container / `2nd-muR`** (sample-in-holder coaxial cylinder): never + used in any FullProf example; revisit only if a case needs it. +- **Single-crystal absorption** (different formalism entirely). diff --git a/docs/dev/issues/open.md b/docs/dev/issues/open.md index 8477f9203..76634fe41 100644 --- a/docs/dev/issues/open.md +++ b/docs/dev/issues/open.md @@ -31,19 +31,30 @@ A(θ) = exp( -(1.7133 − 0.0368·sin²θ)·μR + (0.0927 + 0.375·sin²θ)·μR A Lobanov–Alte-da-Veiga form covers `μR > 3`. -**Implementation sketch:** - -- Add a `μR` instrument parameter for CWL powder (Debye–Scherrer). -- `crysfml`: CrysFML08 already implements this — reachable via - `Lorentz_abs_CW(..., ilor='DBS', cabs='HEWAT', tmv=μR)` through - pycrysfml. -- `cryspy`: multiply each reflection's intensity by `A(θ_hkl)`, - analogous to the existing Lorentz factor (one extra term). +**Design:** captured in +[`adrs/suggestions/model-sample-absorption.md`](../adrs/suggestions/model-sample-absorption.md) +— a switchable `experiment.absorption` category (mirroring `extinction`) +with a calculator-independent A(θ) envelope. + +**What the backends actually provide (corrected):** + +- `cryspy`: **no** absorption code at all (only Debye–Waller and sphere + _extinction_); its CW intensity loop has no slot to multiply A(θ). +- `crysfml`: CrysFML08 implements `Lorentz_abs_CW` in Fortran, but it is + **not** wrapped in `PythonAPI/`, and the high-level + `cw_powder_pattern_from_dict` path we call applies a plain Lorentz + factor with no absorption. So it is **not** reachable through + pycrysfml today without upstream changes. + +**Implication:** neither backend can apply the correction internally +without changes we do not own. The chosen approach computes A(θ) in +EasyDiffraction and applies it as a pointwise envelope on the calculated +pattern, identically for both calculators (see the ADR). **Note:** absorption is nearly degenerate with Biso + scale (its angle term is linear in `sin²θ`, like the Debye–Waller), so refining Biso can partly absorb it — but that biases Biso, so an explicit correction is -preferable. +preferable. In FullProf `μR` is normally **fixed**, not refined. **References:** @@ -52,10 +63,16 @@ preferable. - CrysFML08: [`Src/CFML_Powder/Pow_Lorentz_Absorption.f90`](https://code.ill.fr/scientific-software/CrysFML2008/-/blob/master/Src/CFML_Powder/Pow_Lorentz_Absorption.f90), `Lorentz_abs_CW`. -- FullProf `μR`: `.pcr` Lambda line field 7; `iabscor = 2` selects - HEWAT. +- FullProf splits absorption into a refineable **magnitude** and a + **type**: CW uses `μR` on the `.pcr` Lambda line (fixed there — no + refinement codeword), with the cylindrical Hewat form implied; TOF + uses `Iabscor` (`1` flat plate, `2` cylinder, `3` exponential + `exp(−ABS·λᶜ)`). `Cthm`/`Rpolarz`/`2nd-muR` on the Lambda line are + polarization and container terms, not the primary absorption knob. -**Depends on:** adding a `μR` instrument parameter. +**Depends on:** the switchable `experiment.absorption` category in the +ADR above (supersedes the earlier "add a `μR` instrument parameter" +sketch). --- @@ -2041,7 +2058,7 @@ The fast docs gate (`docs-build` + `link-check` + `spell-check`) catches broken nav/internal links and typos on every push, but does not yet check external URLs. Add a `lychee` link checker (with an allowlist for rate-limited/unstable domains), coordinated with the -[Documentation CI and Build Verification](../adrs/suggestions/documentation-ci-build.md) +[Documentation CI and Build Verification](../adrs/accepted/documentation-ci-build.md) ADR. Run it nightly or on pull requests to avoid flakiness from external sites. Also covers link-checking of URLs that appear only inside executed notebook output cells (a feature that does not exist yet). diff --git a/docs/dev/issues/recommended-priorities.md b/docs/dev/issues/recommended-priorities.md index a70b716de..9c8457024 100644 --- a/docs/dev/issues/recommended-priorities.md +++ b/docs/dev/issues/recommended-priorities.md @@ -83,7 +83,7 @@ session: - **[`cif-numeric-precision.md`](../adrs/suggestions/cif-numeric-precision.md)** — s.u.-aware CIF serialization (file size + meaningful precision). - **[`fit-output-files-and-data-exports.md`](../adrs/suggestions/fit-output-files-and-data-exports.md)**. -- **[`documentation-ci-build.md`](../adrs/suggestions/documentation-ci-build.md)**. +- **[`documentation-ci-build.md`](../adrs/accepted/documentation-ci-build.md)**. - **[`in-house-calculation-engine.md`](../adrs/suggestions/in-house-calculation-engine.md)** — drafted 2026-06-10 (in review): own the core (neutron powder Rietveld) in-repo, keep cryspy/crysfml/pdffit for the frontier. diff --git a/docs/dev/plans/documentation-snippet-tests.md b/docs/dev/plans/documentation-snippet-tests.md new file mode 100644 index 000000000..63d80ec77 --- /dev/null +++ b/docs/dev/plans/documentation-snippet-tests.md @@ -0,0 +1,210 @@ +# Plan: Documentation snippet smoke tests + +This plan follows [`AGENTS.md`](../../../AGENTS.md). No deliberate +exceptions to those instructions are required. + +## ADR + +Implements decision 3 ("Add snippet smoke tests for user-facing +examples") of the accepted ADR +[`documentation-ci-build.md`](../adrs/accepted/documentation-ci-build.md). +No new ADR is required. The ADR's "Implementation Status" table marks +this decision as the highest-value remaining gap, and its "Deferred +Work" section points at this plan. + +## Motivation + +User-facing code snippets drift from the public API. A recent pass found +`from easydiffraction import Structure / Experiment` (neither exported) +and `download_from_repository(...)` (does not exist) live in +`user-guide/first-steps.md`. The strict MkDocs build and link checker do +not execute Python, so this class of breakage reaches readers. A small, +fast, backend-free smoke test that exercises the documented public API +shape would catch it before merge. + +## Branch and PR + +- Branch: `documentation-snippet-tests` (flat slug, off `develop`). + Created and checked out by `/draft-impl-1`. +- PR targets `develop`, not `master`. Do not push unless asked. + +## Decisions + +- **Explicit markers, not blanket extraction.** Only fenced + ` ```python ` blocks explicitly opted in are executed. Many documented + snippets are intentionally non-self-contained (they reference a + `project` built in an earlier block, download data, or run `fit()` + against a real backend); auto-running every block would force heavy + fixtures and network/backends, which the ADR rules out. The opt-in + marker is an HTML comment on the line immediately before the fence: + ``. This keeps the test set curated and the + intent visible in the source Markdown. +- **API shape only, no computation.** Marked snippets construct small + in-memory objects and assert public names exist (`Project()`, + `project.structures.create(...)`, `experiment.peak.type = ...`, + `project.analysis.minimizer.show_supported()`, + `project.display.parameters.all()`). They must not download data, run + `fit()`, or select a real calculator/sampler backend. +- **No network, no real backends, no notebooks.** The runner sets a + guard (monkeypatched `download_data`/`download_tutorial` that raise, + and a check that no marked snippet imports a calculator backend). + Snippets run in a unique temp working directory. +- **Test tier: `tests/functional/`.** These are fast, in-process, + backend-free checks of the public API as documented — the same tier as + the existing functional suite (`pixi run functional-tests`, no + `-n auto`, no backends). They are not unit tests (they do not mirror a + single `src/` module) and not integration tests (no network/backends), + so `tests/unit/` structure mirroring (`test-structure-check`) is + unaffected. +- **One always-on shape check, independent of markers.** In addition to + marked-snippet execution, a parametrised test scans the doc set for + `from easydiffraction import ` and `ed.` references and + asserts each resolves against the installed package. This alone would + have caught the `first-steps.md` regression and needs no per-snippet + curation. +- **Scope of pages (initial).** Per the ADR: + `docs/docs/quick-reference/index.md`, + `docs/docs/user-guide/first-steps.md`, + `docs/docs/user-guide/analysis-workflow/*.md`. Expandable later. + +## Open questions + +- Marker syntax: `` HTML comment (recommended, + invisible in rendered docs) vs. a fenced info string like + ` ```python title="api-shape-test" `. Recommendation: HTML comment. + Confirm during `/draft-impl-1`. +- Whether to wire the new pixi task into the `lint-format.yml` gate now + or fold it into `functional-tests` (already in `pixi run all` and the + test workflow). Recommendation: fold into `functional-tests` so no CI + wiring change is needed; add a thin `docs-snippet-tests` alias for + local runs. + +## Concrete files likely to change + +- New: `tests/functional/test_docs_snippets.py` — the runner: snippet + extraction (reuse the Markdown-walking style of + `tools/test_scripts.py` and the skip-list pattern of + `docs/docs/conftest.py`), the marked-snippet execution test, and the + always-on import-shape test. +- New (optional): a tiny helper module if extraction logic is shared, + e.g. `tests/functional/_docs_snippets.py`. +- `pixi.toml` — add a `docs-snippet-tests` convenience task (and, per + the open question, optionally have `functional-tests` already cover + it). +- `docs/docs/quick-reference/index.md`, + `docs/docs/user-guide/first-steps.md`, + `docs/docs/user-guide/analysis-workflow/*.md` — add + `` markers above the curated safe snippets; + make minimal edits only where a snippet must be self-contained to run. +- `docs/dev/adrs/accepted/documentation-ci-build.md` — flip decision 3 + in the Implementation Status table from "Not done" to "Done" and drop + the matching Deferred Work bullet (final Phase 1 step before the + gate). + +## Implementation steps (Phase 1) + +Each `- [ ]` step is one atomic commit. An AI agent following this plan +must edit the checkbox to `- [x]`, stage the listed files with explicit +paths, and commit locally with the step's `Commit:` line **before** +moving to the next step (per AGENTS.md → Commits). Do not create or run +the test suite as a debugging tool during Phase 1; Phase 2 owns +verification. + +- [ ] **P1.1 — Add the import-shape test (always-on).** Create + `tests/functional/test_docs_snippets.py` with the doc-page list + and a parametrised test that extracts every + `from easydiffraction import ` and `ed.` reference + from the listed pages and asserts each name resolves on the + installed `easydiffraction` package. Files: + `tests/functional/test_docs_snippets.py`. Commit: + `Add import-shape smoke test for doc snippets` + +- [ ] **P1.2 — Add the marked-snippet extractor and runner.** Extend the + test module to collect ` ```python ` blocks preceded by + ``, and exec each page's marked blocks in a + shared namespace inside a unique temp cwd, with `download_data` / + `download_tutorial` monkeypatched to raise and a guard rejecting + any real-backend selection. No snippets are marked yet, so the + test is a no-op collection at this point. Files: + `tests/functional/test_docs_snippets.py` (+ optional + `tests/functional/_docs_snippets.py`). Commit: + `Add marked-snippet runner for doc smoke tests` + +- [ ] **P1.3 — Mark and (minimally) adapt Quick Reference snippets.** + Add `` to the backend-free, self-contained + snippets in `quick-reference/index.md` (session start, + build-a-project in code, show/select-type blocks). Make the + smallest edits needed for them to run standalone; do not change + documented behaviour. Files: `docs/docs/quick-reference/index.md`, + `tests/functional/test_docs_snippets.py` (if fixtures needed). + Commit: `Mark Quick Reference snippets for smoke testing` + +- [ ] **P1.4 — Mark and adapt First Steps and Analysis Workflow + snippets.** Same treatment for `user-guide/first-steps.md` and + `user-guide/analysis-workflow/*.md`. Files: + `docs/docs/user-guide/first-steps.md`, + `docs/docs/user-guide/analysis-workflow/*.md`. Commit: + `Mark user-guide snippets for smoke testing` + +- [ ] **P1.5 — Add the `docs-snippet-tests` pixi task.** Add a + convenience task running the new file (e.g. + `docs-snippet-tests = 'python -m pytest tests/functional/test_docs_snippets.py --color=yes -v'`). + Confirm the open question on `functional-tests` coverage; if + folding in, no workflow change is required. Files: `pixi.toml`. + Commit: `Add docs-snippet-tests pixi task` + +- [ ] **P1.6 — Update the ADR Implementation Status.** In + `documentation-ci-build.md`, flip decision 3 to "Done" with a + pointer to the new task, and remove the snippet-tests bullet from + Deferred Work. Files: + `docs/dev/adrs/accepted/documentation-ci-build.md`. Commit: + `Mark snippet smoke tests done in documentation-ci-build ADR` + +- [ ] **P1.7 — Phase 1 review gate (no code).** Mark this item `[x]` and + commit the checklist update alone. Commit: + `Reach Phase 1 review gate` + +## Verification (Phase 2) + +Run after Phase 1 review. Capture logs with the zsh-safe pattern where +output is needed for analysis. + +```bash +pixi run fix +pixi run check > /tmp/easydiffraction-check.log 2>&1; check_exit_code=$?; tail -n 200 /tmp/easydiffraction-check.log; exit $check_exit_code +pixi run unit-tests +pixi run functional-tests > /tmp/easydiffraction-functional.log 2>&1; functional_tests_exit_code=$?; tail -n 200 /tmp/easydiffraction-functional.log; exit $functional_tests_exit_code +pixi run integration-tests +pixi run script-tests +``` + +Expectations: + +- `pixi run functional-tests` (covering the new + `tests/functional/test_docs_snippets.py`) passes, and the import-shape + test fails loudly if a documented symbol is later removed or renamed. +- `pixi run check` stays clean, including `spell-check`, `link-check`, + and the strict docs build. + +## Status checklist + +- [ ] P1.1 Import-shape test +- [ ] P1.2 Marked-snippet runner +- [ ] P1.3 Quick Reference snippets marked +- [ ] P1.4 User-guide snippets marked +- [ ] P1.5 `docs-snippet-tests` pixi task +- [ ] P1.6 ADR Implementation Status updated +- [ ] P1.7 Phase 1 review gate +- [ ] Phase 2 verification complete + +## Suggested Pull Request + +**Title:** Catch broken code examples in the documentation automatically + +**Description:** EasyDiffraction now checks its own documentation: a +fast test confirms that the Python commands shown in the Quick +Reference, First Steps, and Analysis Workflow pages still match the +current software. If a future change renames or removes something used +in an example, the check fails before the documentation goes out, so the +commands you copy from the guides keep working. The check runs entirely +offline and does not perform any real calculations, so it stays quick. diff --git a/docs/docs/index.md b/docs/docs/index.md index f3f434005..f0b4404f6 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -16,6 +16,9 @@ Here is a brief overview of the main documentation sections: - [:material-school: Tutorials](tutorials/index.md) – Offers practical, step-by-step examples demonstrating common workflows and data analysis tasks. +- [:material-check-decagram: Verification](verification/index.md) – + Cross-checks EasyDiffraction calculations against reference results + from external software (FullProf) across supported experiment types. - [:material-console: Command-Line Interface](cli/index.md) – Describes how to use EasyDiffraction from the terminal for batch fitting and other tasks. diff --git a/docs/docs/tutorials/index.md b/docs/docs/tutorials/index.md index cfb3da1dd..64511d5a0 100644 --- a/docs/docs/tutorials/index.md +++ b/docs/docs/tutorials/index.md @@ -83,10 +83,10 @@ containing Bayesian fit state. ## Single Crystal Diffraction -- [Tb2TiO7 `sg-neut-cwl`](ed-14.ipynb) – Demonstrates structure +- [Tb2TiO7 `sc-neut-cwl`](ed-14.ipynb) – Demonstrates structure refinement of Tb2TiO7 using constant wavelength neutron single crystal diffraction data from HEiDi at FRM II. -- [Taurine `sg-neut-tof`](ed-15.ipynb) – Demonstrates structure +- [Taurine `sc-neut-tof`](ed-15.ipynb) – Demonstrates structure refinement of Taurine using time-of-flight neutron single crystal diffraction data from SENJU at J-PARC. diff --git a/docs/docs/user-guide/analysis-workflow/analysis.md b/docs/docs/user-guide/analysis-workflow/analysis.md index 3819765b7..c3a71d747 100644 --- a/docs/docs/user-guide/analysis-workflow/analysis.md +++ b/docs/docs/user-guide/analysis-workflow/analysis.md @@ -392,8 +392,8 @@ for Ba will be equal to that of La during the refinement process. ### Viewing Constraints -To view the defined constraints, you can use the `show_constraints` -method: +To view the defined constraints, you can use the `show` method on +`project.analysis.constraints`: ```python project.analysis.constraints.show() diff --git a/docs/docs/user-guide/data-format.md b/docs/docs/user-guide/data-format.md index d81346f1c..9b8b505e1 100644 --- a/docs/docs/user-guide/data-format.md +++ b/docs/docs/user-guide/data-format.md @@ -62,7 +62,7 @@ isotropic displacement parameters (_Biso_) | Label | Type | x | y | z | occ | Biso | | ----- | ---- | --- | --- | --- | --- | ------ | | La | La | 0 | 0 | 0 | 0.5 | 0.4958 | -| Ba | Ba | 0 | 0 | 0 | 0.5 | 0.4958 | +| Ba | Ba | 0 | 0 | 0 | 0.5 | 0.4943 | | Co | Co | 0.5 | 0.5 | 0.5 | 1.0 | 0.2567 | | O | O | 0 | 0.5 | 0.5 | 1.0 | 1.4041 | @@ -97,7 +97,7 @@ loop_ _atom_site.ADP_type _atom_site.B_iso_or_equiv La La 0 0 0 a 0.5 Biso 0.4958 -Ba Ba 0 0 0 a 0.5 Biso 0.4958 +Ba Ba 0 0 0 a 0.5 Biso 0.4943 Co Co 0.5 0.5 0.5 b 1 Biso 0.2567 O O 0 0.5 0.5 c 1 Biso 1.4041 diff --git a/docs/docs/user-guide/first-steps.md b/docs/docs/user-guide/first-steps.md index 81e901c70..e8f235e46 100644 --- a/docs/docs/user-guide/first-steps.md +++ b/docs/docs/user-guide/first-steps.md @@ -37,14 +37,14 @@ A complete tutorial using the `import` syntax can be found ### Importing specific parts Alternatively, you can import specific classes or methods from the -package. For example, you can import the `Project`, `Structure`, -`Experiment` classes and `download_from_repository` method like this: +package. For example, you can import the `Project`, `StructureFactory`, +`ExperimentFactory` classes and `download_data` method like this: ```python from easydiffraction import Project -from easydiffraction import Structure -from easydiffraction import Experiment -from easydiffraction import download_from_repository +from easydiffraction import StructureFactory +from easydiffraction import ExperimentFactory +from easydiffraction import download_data ``` This enables you to use these classes and methods directly without the @@ -62,28 +62,26 @@ A complete tutorial using the `from` syntax can be found ## Utility functions EasyDiffraction also provides several utility functions that can -simplify your workflow. One of them is the `download_from_repository` -function, which allows you to download data files from our remote -repository, making it easy to access and use them while experimenting -with EasyDiffraction. +simplify your workflow. One of them is the `download_data` function, +which allows you to download example datasets by their numeric ID from +our remote repository, making it easy to access and use them while +experimenting with EasyDiffraction. -For example, you can download a data file like this: +You can list the available datasets and their IDs with `list_data()`, +then download one like this: ```python import easydiffraction as ed -ed.download_from_repository( - 'hrpt_lbco.xye', - branch='docs', - destination='data', -) +ed.list_data() + +data_path = ed.download_data(id=3, destination='data') ``` -This command will download the `hrpt_lbco.xye` file from the `docs` -branch of the EasyDiffraction repository and save it in the `data` -directory of your current working directory. This is particularly useful -for quickly accessing example datasets without having to manually -download them. +This command downloads the dataset with ID `3` and saves it in the +`data` directory of your current working directory, returning the full +path to the downloaded file. This is particularly useful for quickly +accessing example datasets without having to manually download them. ## Help methods diff --git a/docs/docs/user-guide/glossary.md b/docs/docs/user-guide/glossary.md index 75f0300c0..16bf0e4f2 100644 --- a/docs/docs/user-guide/glossary.md +++ b/docs/docs/user-guide/glossary.md @@ -36,7 +36,7 @@ experiment types: ### X-ray Diffraction -- [pd-xray][0]{:.label-experiment} Powder X-ray diffraction. +- [pd-xray][0]{:.label-experiment} – Powder X-ray diffraction. [0]: # diff --git a/docs/docs/verification/ci_skip.txt b/docs/docs/verification/ci_skip.txt index e64d34537..0c1e04bdc 100644 --- a/docs/docs/verification/ci_skip.txt +++ b/docs/docs/verification/ci_skip.txt @@ -19,4 +19,4 @@ pd-neut-cwl_tch-fcj-noabs_lab6 # FCJ asymmetry (S_L/D_L) not implemented in cry pd-neut-cwl_tch-fcj_lab6 # unmodelled sample absorption (muR=0.7, HEWAT) + FCJ asymmetry; see issues/open.md pd-neut-tof_j_si # ed-crysfml TOF Jorgensen profile ~8.5% off after fitting scale (area ratio 1.09, corr 0.997); cryspy matches FullProf pd-neut-tof_jvd_si # cryspy TOF Lorentzian discrepancy; see issues/open.md -sg-neut-cwl_ext-iso_tbti # Different asymmetry types in cryspy vs FullProf \ No newline at end of file +sc-neut-cwl_ext-iso_tbti # Different asymmetry types in cryspy vs FullProf \ No newline at end of file diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_ext-iso_tbti/tbti.int b/docs/docs/verification/fullprof/sc-neut-cwl_ext-iso_tbti/tbti.int similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_ext-iso_tbti/tbti.int rename to docs/docs/verification/fullprof/sc-neut-cwl_ext-iso_tbti/tbti.int diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_ext-iso_tbti/tbti.out b/docs/docs/verification/fullprof/sc-neut-cwl_ext-iso_tbti/tbti.out similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_ext-iso_tbti/tbti.out rename to docs/docs/verification/fullprof/sc-neut-cwl_ext-iso_tbti/tbti.out diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_ext-iso_tbti/tbti.pcr b/docs/docs/verification/fullprof/sc-neut-cwl_ext-iso_tbti/tbti.pcr similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_ext-iso_tbti/tbti.pcr rename to docs/docs/verification/fullprof/sc-neut-cwl_ext-iso_tbti/tbti.pcr diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_ext-iso_tbti/tbti.prf b/docs/docs/verification/fullprof/sc-neut-cwl_ext-iso_tbti/tbti.prf similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_ext-iso_tbti/tbti.prf rename to docs/docs/verification/fullprof/sc-neut-cwl_ext-iso_tbti/tbti.prf diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_ext-iso_tbti/tbti.sum b/docs/docs/verification/fullprof/sc-neut-cwl_ext-iso_tbti/tbti.sum similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_ext-iso_tbti/tbti.sum rename to docs/docs/verification/fullprof/sc-neut-cwl_ext-iso_tbti/tbti.sum diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_noext_tbti/tbti.int b/docs/docs/verification/fullprof/sc-neut-cwl_noext_tbti/tbti.int similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_noext_tbti/tbti.int rename to docs/docs/verification/fullprof/sc-neut-cwl_noext_tbti/tbti.int diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_noext_tbti/tbti.out b/docs/docs/verification/fullprof/sc-neut-cwl_noext_tbti/tbti.out similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_noext_tbti/tbti.out rename to docs/docs/verification/fullprof/sc-neut-cwl_noext_tbti/tbti.out diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_noext_tbti/tbti.pcr b/docs/docs/verification/fullprof/sc-neut-cwl_noext_tbti/tbti.pcr similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_noext_tbti/tbti.pcr rename to docs/docs/verification/fullprof/sc-neut-cwl_noext_tbti/tbti.pcr diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_noext_tbti/tbti.prf b/docs/docs/verification/fullprof/sc-neut-cwl_noext_tbti/tbti.prf similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_noext_tbti/tbti.prf rename to docs/docs/verification/fullprof/sc-neut-cwl_noext_tbti/tbti.prf diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_noext_tbti/tbti.sum b/docs/docs/verification/fullprof/sc-neut-cwl_noext_tbti/tbti.sum similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_noext_tbti/tbti.sum rename to docs/docs/verification/fullprof/sc-neut-cwl_noext_tbti/tbti.sum diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_pr2nio4/prnio.cif b/docs/docs/verification/fullprof/sc-neut-cwl_pr2nio4/prnio.cif similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_pr2nio4/prnio.cif rename to docs/docs/verification/fullprof/sc-neut-cwl_pr2nio4/prnio.cif diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_pr2nio4/prnio.int b/docs/docs/verification/fullprof/sc-neut-cwl_pr2nio4/prnio.int similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_pr2nio4/prnio.int rename to docs/docs/verification/fullprof/sc-neut-cwl_pr2nio4/prnio.int diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_pr2nio4/prnio.out b/docs/docs/verification/fullprof/sc-neut-cwl_pr2nio4/prnio.out similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_pr2nio4/prnio.out rename to docs/docs/verification/fullprof/sc-neut-cwl_pr2nio4/prnio.out diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_pr2nio4/prnio.pcr b/docs/docs/verification/fullprof/sc-neut-cwl_pr2nio4/prnio.pcr similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_pr2nio4/prnio.pcr rename to docs/docs/verification/fullprof/sc-neut-cwl_pr2nio4/prnio.pcr diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_pr2nio4/prnio.prf b/docs/docs/verification/fullprof/sc-neut-cwl_pr2nio4/prnio.prf similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_pr2nio4/prnio.prf rename to docs/docs/verification/fullprof/sc-neut-cwl_pr2nio4/prnio.prf diff --git a/docs/docs/verification/fullprof/sg-neut-cwl_pr2nio4/prnio.sum b/docs/docs/verification/fullprof/sc-neut-cwl_pr2nio4/prnio.sum similarity index 100% rename from docs/docs/verification/fullprof/sg-neut-cwl_pr2nio4/prnio.sum rename to docs/docs/verification/fullprof/sc-neut-cwl_pr2nio4/prnio.sum diff --git a/docs/docs/verification/index.md b/docs/docs/verification/index.md index f26badd1d..b1c61f093 100644 --- a/docs/docs/verification/index.md +++ b/docs/docs/verification/index.md @@ -29,7 +29,7 @@ effect; each such page states the reason below. Pages are grouped by **experiment type** (sample form, radiation probe, and beam mode). Coverage grows to span every supported combination — -`pd-neut-cwl`, `pd-neut-tof`, `pd-xray`, `sg-neut-cwl`, `sg-neut-tof`, +`pd-neut-cwl`, `pd-neut-tof`, `pd-xray`, `sc-neut-cwl`, `sc-neut-tof`, and so on. The list below notes only what is specific to each page. ## Powder, neutron, constant wavelength @@ -72,15 +72,15 @@ and so on. The list below notes only what is specific to each page. ## Single crystal, neutron, constant wavelength -- [Pr₂NiO₄ `sg-neut-cwl` (no extinction)](sg-neut-cwl_pr2nio4.ipynb) – +- [Pr₂NiO₄ `sc-neut-cwl` (no extinction)](sc-neut-cwl_pr2nio4.ipynb) – Strontium-doped praseodymium nickelate (Pr₂NiO₄:Sr, K₂NiF₄-type, _Fmmm_); per-reflection F² against FullProf reference with anisotropic ADPs. -- [Tb₂Ti₂O₇ `sg-neut-cwl` (no extinction)](sg-neut-cwl_noext_tbti.ipynb) +- [Tb₂Ti₂O₇ `sc-neut-cwl` (no extinction)](sc-neut-cwl_noext_tbti.ipynb) – Terbium titanate (Tb₂Ti₂O₇, _F d -3 m_); per-reflection F² against a FullProf-no-extinction reference with anisotropic ADPs. Scale is initialized from the FullProf and refined. -- [Tb₂Ti₂O₇ `sg-neut-cwl` (isotropic extinction)](sg-neut-cwl_ext-iso_tbti.ipynb) +- [Tb₂Ti₂O₇ `sc-neut-cwl` (isotropic extinction)](sc-neut-cwl_ext-iso_tbti.ipynb) – Terbium titanate (Tb₂Ti₂O₇, _F d -3 m_); per-reflection F² against FullProf reference with anisotropic ADPs and empirical extinction. Cryspy extinction (`becker-coppens`, `gauss`) uses two parameters, diff --git a/docs/docs/verification/sg-neut-cwl_ext-iso_tbti.ipynb b/docs/docs/verification/sc-neut-cwl_ext-iso_tbti.ipynb similarity index 99% rename from docs/docs/verification/sg-neut-cwl_ext-iso_tbti.ipynb rename to docs/docs/verification/sc-neut-cwl_ext-iso_tbti.ipynb index 413811c6a..3d486427a 100644 --- a/docs/docs/verification/sg-neut-cwl_ext-iso_tbti.ipynb +++ b/docs/docs/verification/sc-neut-cwl_ext-iso_tbti.ipynb @@ -161,7 +161,7 @@ "metadata": {}, "outputs": [], "source": [ - "FULLPROF_PROJECT_DIR = 'sg-neut-cwl_ext-iso_tbti'\n", + "FULLPROF_PROJECT_DIR = 'sc-neut-cwl_ext-iso_tbti'\n", "FULLPROF_OUT_FILE = 'tbti.out'\n", "FULLPROF_SCALE = 0.37517014 # FullProf Scale\n", "FULLPROF_WAVELENGTH = 0.7930 # FullProf Lambda\n", diff --git a/docs/docs/verification/sg-neut-cwl_ext-iso_tbti.py b/docs/docs/verification/sc-neut-cwl_ext-iso_tbti.py similarity index 98% rename from docs/docs/verification/sg-neut-cwl_ext-iso_tbti.py rename to docs/docs/verification/sc-neut-cwl_ext-iso_tbti.py index 6628b683c..68d359b5b 100644 --- a/docs/docs/verification/sg-neut-cwl_ext-iso_tbti.py +++ b/docs/docs/verification/sc-neut-cwl_ext-iso_tbti.py @@ -86,7 +86,7 @@ # ## Load the FullProf reference # %% -FULLPROF_PROJECT_DIR = 'sg-neut-cwl_ext-iso_tbti' +FULLPROF_PROJECT_DIR = 'sc-neut-cwl_ext-iso_tbti' FULLPROF_OUT_FILE = 'tbti.out' FULLPROF_SCALE = 0.37517014 # FullProf Scale FULLPROF_WAVELENGTH = 0.7930 # FullProf Lambda diff --git a/docs/docs/verification/sg-neut-cwl_noext_tbti.ipynb b/docs/docs/verification/sc-neut-cwl_noext_tbti.ipynb similarity index 99% rename from docs/docs/verification/sg-neut-cwl_noext_tbti.ipynb rename to docs/docs/verification/sc-neut-cwl_noext_tbti.ipynb index e44d2c79a..6dcd17ac6 100644 --- a/docs/docs/verification/sg-neut-cwl_noext_tbti.ipynb +++ b/docs/docs/verification/sc-neut-cwl_noext_tbti.ipynb @@ -161,7 +161,7 @@ "metadata": {}, "outputs": [], "source": [ - "FULLPROF_PROJECT_DIR = 'sg-neut-cwl_noext_tbti'\n", + "FULLPROF_PROJECT_DIR = 'sc-neut-cwl_noext_tbti'\n", "FULLPROF_OUT_FILE = 'tbti.out'\n", "FULLPROF_SCALE = 0.28749475 # FullProf Scale\n", "FULLPROF_WAVELENGTH = 0.7930 # FullProf Lambda\n", diff --git a/docs/docs/verification/sg-neut-cwl_noext_tbti.py b/docs/docs/verification/sc-neut-cwl_noext_tbti.py similarity index 98% rename from docs/docs/verification/sg-neut-cwl_noext_tbti.py rename to docs/docs/verification/sc-neut-cwl_noext_tbti.py index 803fbeb2f..da2aecd97 100644 --- a/docs/docs/verification/sg-neut-cwl_noext_tbti.py +++ b/docs/docs/verification/sc-neut-cwl_noext_tbti.py @@ -86,7 +86,7 @@ # ## Load the FullProf reference # %% -FULLPROF_PROJECT_DIR = 'sg-neut-cwl_noext_tbti' +FULLPROF_PROJECT_DIR = 'sc-neut-cwl_noext_tbti' FULLPROF_OUT_FILE = 'tbti.out' FULLPROF_SCALE = 0.28749475 # FullProf Scale FULLPROF_WAVELENGTH = 0.7930 # FullProf Lambda diff --git a/docs/docs/verification/sg-neut-cwl_pr2nio4.ipynb b/docs/docs/verification/sc-neut-cwl_pr2nio4.ipynb similarity index 99% rename from docs/docs/verification/sg-neut-cwl_pr2nio4.ipynb rename to docs/docs/verification/sc-neut-cwl_pr2nio4.ipynb index bfb620839..d92c5cd23 100644 --- a/docs/docs/verification/sg-neut-cwl_pr2nio4.ipynb +++ b/docs/docs/verification/sc-neut-cwl_pr2nio4.ipynb @@ -196,7 +196,7 @@ "metadata": {}, "outputs": [], "source": [ - "FULLPROF_PROJECT_DIR = 'sg-neut-cwl_pr2nio4'\n", + "FULLPROF_PROJECT_DIR = 'sc-neut-cwl_pr2nio4'\n", "FULLPROF_OUT_FILE = 'prnio.out'\n", "FULLPROF_SCALE = 0.06298 # FullProf Scale\n", "FULLPROF_WAVELENGTH = 0.8302 # FullProf Lambda\n", diff --git a/docs/docs/verification/sg-neut-cwl_pr2nio4.py b/docs/docs/verification/sc-neut-cwl_pr2nio4.py similarity index 99% rename from docs/docs/verification/sg-neut-cwl_pr2nio4.py rename to docs/docs/verification/sc-neut-cwl_pr2nio4.py index 5f6984856..faa916e40 100644 --- a/docs/docs/verification/sg-neut-cwl_pr2nio4.py +++ b/docs/docs/verification/sc-neut-cwl_pr2nio4.py @@ -121,7 +121,7 @@ # ## Load the FullProf reference # %% -FULLPROF_PROJECT_DIR = 'sg-neut-cwl_pr2nio4' +FULLPROF_PROJECT_DIR = 'sc-neut-cwl_pr2nio4' FULLPROF_OUT_FILE = 'prnio.out' FULLPROF_SCALE = 0.06298 # FullProf Scale FULLPROF_WAVELENGTH = 0.8302 # FullProf Lambda diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 29192b99d..b525f40c6 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -232,8 +232,8 @@ nav: - Si pd-neut-tof: tutorials/ed-28.ipynb - NaCl pd-xray: tutorials/ed-29.ipynb - Single Crystal Diffraction: - - Tb2TiO7 sg-neut-cwl: tutorials/ed-14.ipynb - - Taurine sg-neut-tof: tutorials/ed-15.ipynb + - Tb2TiO7 sc-neut-cwl: tutorials/ed-14.ipynb + - Taurine sc-neut-tof: tutorials/ed-15.ipynb - Pair Distribution Function: - Ni pd-neut-cwl: tutorials/ed-10.ipynb - Si pd-neut-tof: tutorials/ed-11.ipynb @@ -267,9 +267,9 @@ nav: - Si (Jorgensen–Von Dreele): verification/pd-neut-tof_jvd_si.ipynb - NaCaAlF: verification/pd-neut-tof_jvd_ncaf.ipynb - Single crystal, neutron, constant wavelength: - - Pr2NiO4 (no extinction): verification/sg-neut-cwl_pr2nio4.ipynb - - Tb2Ti2O7 (no extinction): verification/sg-neut-cwl_noext_tbti.ipynb - - Tb2Ti2O7: verification/sg-neut-cwl_ext-iso_tbti.ipynb + - Pr2NiO4 (no extinction): verification/sc-neut-cwl_pr2nio4.ipynb + - Tb2Ti2O7 (no extinction): verification/sc-neut-cwl_noext_tbti.ipynb + - Tb2Ti2O7: verification/sc-neut-cwl_ext-iso_tbti.ipynb - Command-Line: - Command-Line: cli/index.md - API Reference: diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index 364d053b7..7b3dfc4ff 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -194,6 +194,16 @@ def _summary_parameters( if param._identity.category_code not in _SUMMARY_HIDDEN_PARAMETER_CATEGORIES ] + def _summary_parameters_by_datablock( + self, + ) -> dict[str, list[GenericDescriptorBase]]: + """Return summary parameters grouped by datablock kind.""" + project = self._analysis.project + return { + 'structures': self._summary_parameters(project.structures.parameters), + 'experiments': self._summary_parameters(project.experiments.parameters), + } + def all_params(self) -> None: """Print all parameters for structures and experiments.""" project = self._analysis.project @@ -307,14 +317,9 @@ def how_to_access_parameters(self) -> None: code. """ project = self._analysis.project - structures_params = self._summary_parameters(project.structures.parameters) - experiments_params = self._summary_parameters(project.experiments.parameters) - all_params = { - 'structures': structures_params, - 'experiments': experiments_params, - } + all_params = self._summary_parameters_by_datablock() - if not structures_params and not experiments_params: + if not all_params['structures'] and not all_params['experiments']: log.warning('No parameters found.') return @@ -370,15 +375,9 @@ def parameter_cif_uids(self) -> None: The output explains which unique identifiers are used when creating CIF-based constraints. """ - project = self._analysis.project - structures_params = self._summary_parameters(project.structures.parameters) - experiments_params = self._summary_parameters(project.experiments.parameters) - all_params = { - 'structures': structures_params, - 'experiments': experiments_params, - } + all_params = self._summary_parameters_by_datablock() - if not structures_params and not experiments_params: + if not all_params['structures'] and not all_params['experiments']: log.warning('No parameters found.') return diff --git a/src/easydiffraction/analysis/fit_helpers/bayesian.py b/src/easydiffraction/analysis/fit_helpers/bayesian.py index 773d3dacc..1ee864946 100644 --- a/src/easydiffraction/analysis/fit_helpers/bayesian.py +++ b/src/easydiffraction/analysis/fit_helpers/bayesian.py @@ -28,7 +28,6 @@ ESS_BULK_CONVERGENCE_THRESHOLD = 400.0 POSTERIOR_SAMPLE_NDIM = 3 DEFAULT_CI_LEVELS = (0.68, 0.95) -DEFAULT_CREDIBLE_INTERVAL_LEVELS = DEFAULT_CI_LEVELS IntervalLevels = tuple[float, ...] SettingsMap = dict[str, object] | None DiagnosticsMap = dict[str, object] | None diff --git a/src/easydiffraction/analysis/minimizers/base.py b/src/easydiffraction/analysis/minimizers/base.py index fd2af7bad..c8a50b716 100644 --- a/src/easydiffraction/analysis/minimizers/base.py +++ b/src/easydiffraction/analysis/minimizers/base.py @@ -51,7 +51,6 @@ def __init__( self._max_iterations: int | None = max_iterations self.result: FitResults | None = None self._previous_chi2: float | None = None - self._iteration: int | None = None self._best_chi2: float | None = None self._best_iteration: int | None = None self._fitting_time: float | None = None diff --git a/src/easydiffraction/analysis/minimizers/lmfit.py b/src/easydiffraction/analysis/minimizers/lmfit.py index b564d3512..e3b7c5ed9 100644 --- a/src/easydiffraction/analysis/minimizers/lmfit.py +++ b/src/easydiffraction/analysis/minimizers/lmfit.py @@ -130,31 +130,3 @@ def _check_success(self, raw_result: object) -> bool: # noqa: PLR6301 True if the optimization was successful, False otherwise. """ return getattr(raw_result, 'success', False) - - def _iteration_callback( - self, - params: lmfit.Parameters, - iter: int, - resid: object, - *args: object, - **kwargs: object, - ) -> None: - """ - Handle each iteration callback of the minimizer. - - Parameters - ---------- - params : lmfit.Parameters - The current parameters. - iter : int - The current iteration number. - resid : object - The residuals. - *args : object - Additional positional arguments. - **kwargs : object - Additional keyword arguments. - """ - # Intentionally unused, required by callback signature - del params, resid, args, kwargs - self._iteration = iter diff --git a/src/easydiffraction/core/guard.py b/src/easydiffraction/core/guard.py index 51715bdc7..8226960a2 100644 --- a/src/easydiffraction/core/guard.py +++ b/src/easydiffraction/core/guard.py @@ -145,16 +145,6 @@ def _public_writable_attrs(cls) -> set[str]: """Public properties with a setter.""" return {key for key, prop in cls._iter_properties() if prop.fset is not None} - def _allowed_attrs( - self, - *, - writable_only: bool = False, - ) -> set[str]: - cls = type(self) - if writable_only: - return cls._public_writable_attrs() - return cls._public_attrs() - @property def _log_name(self) -> str: return self.unique_name or type(self).__name__ diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index 0f99dfb93..1334dbd98 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -153,7 +153,6 @@ class PosteriorPairPlotStyleEnum(StrEnum): POSTERIOR_PAIR_AUTO_MAX_CONTOUR_PARAMETERS = 6 PAIR_PLOT_CELL_SIZE_PIXELS = 190 PAIR_PLOT_MIN_CELL_SIZE_PIXELS = 90 -PAIR_PLOT_MIN_SIZE_PIXELS = 680 PAIR_PLOT_MARGIN_PIXELS = 120 PAIR_PLOT_ESTIMATED_CONTAINER_WIDTH_PIXELS = 980 PAIR_PLOT_SUBPLOT_SPACING = 0.01 @@ -164,7 +163,6 @@ class PosteriorPairPlotStyleEnum(StrEnum): POSTERIOR_PAIR_X_TITLE_YSHIFT_PIXELS = 10 SQUARE_MATRIX_TITLE_YSHIFT_PIXELS = 12 POSTERIOR_PAIR_GUIDE_LINE_COLOR = 'rgba(125, 140, 173, 0.18)' -SQUARE_MATRIX_FIXED_ASPECT_RATIO = '1 / 1' SQUARE_MATRIX_FIXED_ASPECT_META_KEY = 'fixed_aspect_wrapper' SQUARE_MATRIX_LEFT_MARGIN_PIXELS = 40 SQUARE_MATRIX_RIGHT_MARGIN_PIXELS = 24 @@ -2631,20 +2629,6 @@ def _posterior_pair_cell_size_pixels( ) ) - @classmethod - def _posterior_pair_figure_height_pixels(cls, n_parameters: int) -> int: - """ - Return the initial figure height for a responsive pair plot. - """ - cell_size = cls._posterior_pair_cell_size_pixels( - n_parameters, - available_width_pixels=PAIR_PLOT_ESTIMATED_CONTAINER_WIDTH_PIXELS, - ) - return max( - PAIR_PLOT_MIN_SIZE_PIXELS, - cell_size * n_parameters + PAIR_PLOT_MARGIN_PIXELS, - ) - @staticmethod def _posterior_pair_contour_panel_count(n_parameters: int) -> int: """Return the number of lower-triangle contour panels.""" diff --git a/src/easydiffraction/project/categories/info/default.py b/src/easydiffraction/project/categories/info/default.py index 61a85fdad..20aab0a88 100644 --- a/src/easydiffraction/project/categories/info/default.py +++ b/src/easydiffraction/project/categories/info/default.py @@ -153,13 +153,6 @@ def created(self) -> datetime.datetime: """Return the creation timestamp.""" return self._parse_timestamp(self._created_descriptor.value) - def _set_created(self, value: datetime.datetime | str) -> None: - """Set the creation timestamp from runtime or CIF input.""" - if isinstance(value, datetime.datetime): - self._created_descriptor.value = self._format_timestamp(value) - return - self._created_descriptor.value = value - @property def last_modified(self) -> datetime.datetime: """Return the last modified timestamp."""