Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
2f07e74
Expose TOF microstructural size/strain broadening parameters
AndrewSazonov Jun 20, 2026
2d352f5
Add TOF size/strain verification example (known issue)
AndrewSazonov Jun 20, 2026
5cea901
Fix size/strain param formatting in verification index
AndrewSazonov Jun 20, 2026
8bfe61a
Add per-experiment eta-adaptive peak-range cutoff parameter
AndrewSazonov Jun 20, 2026
4a2ab3d
Cache Wyckoff orbit templates to speed up refinement
AndrewSazonov Jun 20, 2026
8f2de67
Skip excluded re-apply and cache included-point mask in fits
AndrewSazonov Jun 20, 2026
bdec293
Refresh TOF size/strain on minimizer fast-dict path
AndrewSazonov Jun 20, 2026
2ab24e0
Require positive cutoff_fwhm (reject zero/negative)
AndrewSazonov Jun 20, 2026
6634bc1
Promote peak-profile-cutoff ADR to accepted
AndrewSazonov Jun 20, 2026
5e2bc0e
Add performance follow-up issues 174-177
AndrewSazonov Jun 20, 2026
579cfba
Apply pixi run fix auto-fixes
AndrewSazonov Jun 20, 2026
1c13367
Correct CrysFML lattice-centering intensity scale
AndrewSazonov Jun 21, 2026
ff647c1
Assert LBCO fit by chi-square and well-constrained Biso
AndrewSazonov Jun 21, 2026
8996e5b
Default cutoff_fwhm to automatic tail-aware window
AndrewSazonov Jun 21, 2026
1f0797c
Match verification cutoffs to FullProf Wdt; assert fixed TOF JvD pages
AndrewSazonov Jun 21, 2026
5ba7161
Add cutoff_fwhm_auto_floor peak parameter for auto cutoff
AndrewSazonov Jun 22, 2026
0394a81
Add issue 179 on automatic cutoff_fwhm detection
AndrewSazonov Jun 22, 2026
9a56609
Use cryspy 0.12.0; rename cutoff key, drop inert auto-floor param
AndrewSazonov Jun 22, 2026
e50099a
Formatting and linting
AndrewSazonov Jun 22, 2026
2473108
Invalidate included-point cache on public data mutation
AndrewSazonov Jun 22, 2026
c95a858
Align cutoff_fwhm docs and tests with literal semantics
AndrewSazonov Jun 22, 2026
26067bd
Correct TOF JvD verification refinement note
AndrewSazonov Jun 22, 2026
25c9365
Mark microstructural size/strain available in features
AndrewSazonov Jun 22, 2026
0333ab5
Fix lint in included-point cache invalidation hooks
AndrewSazonov Jun 22, 2026
acacb16
Update ADR index cutoff summary to literal semantics
AndrewSazonov Jun 22, 2026
d1295e1
Un-gate LaB6 SyCos/SySin verification (agrees on cryspy 0.12.0)
AndrewSazonov Jun 22, 2026
c5939db
Refactor docstring formatting in PdDataBase to improve readability
AndrewSazonov Jun 22, 2026
100f5bf
Apply pixi run fix auto-fixes
AndrewSazonov Jun 22, 2026
abbcdf6
Refactor Si SEPD tutorial: streamline experiment setup and peak profi…
AndrewSazonov Jun 22, 2026
69bbc77
Update cryspy dependency to point to hotfix branch for performance im…
AndrewSazonov Jun 22, 2026
47e564a
Add cutoff_fwhm parameter to experiment peak configurations
AndrewSazonov Jun 22, 2026
4b5c4d9
Update scale factors for linked structures in NCAF WISH tutorial
AndrewSazonov Jun 22, 2026
d75bdaa
Source verification peak cutoff from FULLPROF_WDT constant
AndrewSazonov Jun 22, 2026
e32dc76
Refactor tutorial notebooks for clarity and consistency
AndrewSazonov Jun 22, 2026
0118dbf
Update cryspy dependency to version 0.12.1
AndrewSazonov Jun 23, 2026
99700a8
Add Darwin ARM64 tutorial benchmarks
AndrewSazonov Jun 23, 2026
7d363ba
Add directories to package structure docs
AndrewSazonov Jun 23, 2026
75303a5
Update tutorial parameters for microstructural size-strain
AndrewSazonov Jun 23, 2026
ce4e01c
Remove outdated fix references from tutorial index
AndrewSazonov Jun 23, 2026
fa4a60a
Fix cryspy flag for rhombohedral structures
AndrewSazonov Jun 23, 2026
aaaf257
Update notebooks
AndrewSazonov Jun 23, 2026
3aa899d
Document cryspy filter bug
AndrewSazonov Jun 23, 2026
8836176
List issue 180 in open-issue index
AndrewSazonov Jun 23, 2026
aff7ab1
Update tutorial baseline for new notebook results
AndrewSazonov Jun 23, 2026
a1b6666
Document cutoff_fwhm and size/strain peak parameters
AndrewSazonov Jun 23, 2026
f0ae439
Inline size/strain params; factor peak cutoff into common row
AndrewSazonov Jun 23, 2026
b5d41f3
Move TOF performance table to experiment guide; refine peak rows
AndrewSazonov Jun 23, 2026
f5d6743
Drop speculative Ikeda-Carpenter params (not implemented)
AndrewSazonov Jun 23, 2026
11e66f8
Add Ikeda-Carpenter to TOF table as work in progress
AndrewSazonov Jun 23, 2026
ef26c78
Raise clearly when CrysFML is used for time-of-flight
AndrewSazonov Jun 23, 2026
7beb270
Warn that CrysFML ignores peak.cutoff_fwhm
AndrewSazonov Jun 23, 2026
47f8cde
Untrack generated tutorial benchmark CSVs
AndrewSazonov Jun 23, 2026
be330f0
Give crysfml CWL test stubs an experiment_type
AndrewSazonov Jun 23, 2026
9392452
Record cutoff_fwhm auto-detection findings in issue 179
AndrewSazonov Jun 23, 2026
60b5252
Wrap long CrysFML comment lines under the doc limit
AndrewSazonov Jun 23, 2026
f2f90f9
Apply pixi run fix auto-fixes
AndrewSazonov Jun 23, 2026
844ea1f
Document CrysFML calculate_pattern Raises in docstring
AndrewSazonov Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions docs/dev/adrs/accepted/peak-profile-cutoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# ADR: Peak-Profile Range Cutoff (`cutoff_fwhm`)

## Status

Accepted.

## Date

2026-06-20

## Group

Experiment model.

## Context

For powder data the cryspy backend evaluates each reflection's peak
profile at **every** point of the pattern. Concretely the TOF
back-to-back-exponential profile (`tof_Jorgensen`,
`tof_Jorgensen_VonDreele`) and the constant-wavelength pseudo-Voigt
(`calc_profile_pseudo_voight`) each build a dense `(n_points × n_hkl)`
matrix and run the transcendental kernels (`erfc`, `exp`, the complex
exponential integral `exp1`, Lorentzian and Finger-Cox-Jephcoat
asymmetry) over all of it — including the vast region far from each peak
where the contribution is numerically negligible. The cost grows with
`n_points × n_hkl`, which is large for wide constant-wavelength scans
with many reflections.

FullProf solves this with its `WDT` parameter: each peak is only
calculated within a window of a few FWHMs around its centre. cryspy has
no equivalent, so the wasted far-field evaluation is paid on every
profile computation, i.e. on the refinement iterations that re-evaluate
the profile (when broadening parameters `U/V/W/X/Y`, `σ/γ`, or the unit
cell are refined).

The right cutoff value is data-dependent: a sharp, Gaussian-dominated
peak needs only a few FWHMs, while the slow `1/Δ²` Lorentzian tail needs
a much wider window. The **binding accuracy metric is the integrated
peak-area ratio**, not Rwp: truncating the Lorentzian tail removes area
(absorbed by the scale factor, so Rwp barely moves) and the verification
suite requires the area ratio to stay within `0.99–1.01`. Empirically a
strong-Lorentzian case with preferred orientation (LBCO) needs a much
wider window than a weak-Lorentzian one (LaB6), so a single hard-coded
constant is either too slow (sized for the worst case everywhere) or
unsafe (too aggressive for some data). Users therefore need a per-
experiment knob, mirroring FullProf's `.pcr` `WDT`.

This relates to the upstream capability-request workflow
([`upstream-capability-request-evidence.md`](../suggestions/upstream-capability-request-evidence.md)):
the literal cutoff was proposed upstream and released in cryspy 0.12.0,
so EasyDiffraction drives it through the released `profile_cutoff_fwhm`
key without requiring a cryspy CIF-schema change.

## Decision

Expose a per-experiment peak-profile range cutoff and feed it to cryspy,
using cryspy's released **literal** cutoff (cryspy ≥ 0.12.0).

1. **Public API.** Add `experiment.peak.cutoff_fwhm` to the TOF and CWL
peak categories as a non-refinable `NumericDescriptor` (a calculation
control, not a fittable quantity). The value is the cutoff measured
in **FWHMs**, which is what the name states; it equals FullProf's
`WDT`. The name `cutoff_fwhm` is preferred over `cutoff_lorentz` (the
cutoff trims the whole pseudo-Voigt window, not only the Lorentzian
part) and over `cutoff_wdt` (cryptic outside FullProf).

2. **Literal window (matches FullProf `WDT` and cryspy ≥ 0.12.0).**
`cutoff_fwhm` is a single literal half-width in FWHMs applied around
each peak centre. cryspy keeps only the points within

```
|Δ| ≤ cutoff_fwhm · (FWHM + 1/α + 1/β) (TOF)
|z| ≤ cutoff_fwhm (CWL, z in FWHM units)
```

For TOF the back-to-back exponential e-folding tails (`1/α`, `1/β`)
are added to the window so the asymmetric tails are retained. This is
exactly FullProf's `WDT` semantics, so setting `cutoff_fwhm` to a
`.pcr` `WDT` value gives an apples-to-apples comparison. A per-point
η-adaptive window is **not** part of this decision — see Deferred
Work.

3. **Backend hand-off (no cryspy CIF-schema change).** The cryspy
profile functions take a cutoff argument that defaults to a module
constant; the cryspy `rhochi` drivers read it from the experiment
dictionary key `profile_cutoff_fwhm`, falling back to the constant
when absent. The EasyDiffraction cryspy calculator injects
`cryspy_dict[<expt>]["profile_cutoff_fwhm"] = peak.cutoff_fwhm.value`
in the peak-update step, which runs on **both** the object-recreate
path and the minimizer fast-dict path, so the value reaches every
calculation without serialising a new CIF item.

4. **Default `cutoff_fwhm = 0` = no cutoff.** The default is `0` for
both TOF and CWL, which cryspy treats as "no cutoff" (the full range
is computed — slower but maximally accurate). A positive value is an
opt-in literal cutoff that trades accuracy for speed. There is no
safe auto-tuned default value to choose, because the right cutoff is
data-dependent (see Context); the verification suite sets each page's
`cutoff_fwhm` explicitly to that case's FullProf `.pcr` `WDT`.

## Consequences

- With a positive `cutoff_fwhm` the peak-profile function is markedly
cheaper (TOF Jorgensen-Von Dreele ≈ 10× and CWL pseudo-Voigt ≈ 4–7×
faster at the verification `WDT` values, with the FullProf area ratio
and Rwp unchanged). The default `0` keeps the full profile, so the
speed-up is opt-in per experiment.
- The speed-up is realised on profile-re-evaluating refinement
iterations and on single `calculate()` calls. It is **not** the
current minimization bottleneck: profiling shows refinement time is
dominated by EasyDiffraction's per-iteration Wyckoff symmetry-
constraint solve, not by cryspy (see Deferred Work).
- `cutoff_fwhm` persists in the experiment CIF
(`_easydiffraction_peak.cutoff_fwhm`) like other peak settings; it is
never refined.
- Correct results require a cryspy that honours `profile_cutoff_fwhm`
(released in cryspy 0.12.0). An older cryspy ignores the key and
computes the full profile (slower but identical numerically), so the
parameter degrades safely.
- The accuracy contract is stated in area-ratio terms (the verification
suite requires the integrated peak-area ratio to stay within
`0.99–1.01`), giving a clear rule for choosing or validating any
`cutoff_fwhm` value.

## Alternatives Considered

- **Fixed module constant, no user control.** Simplest, but cannot be
both safe and fast across data with different Lorentzian content;
gives users no lever. Rejected.
- **Auto-tuned positive default.** Choosing a single non-zero default
(e.g. `10` TOF / `80` CWL) was prototyped but rejected: the safe value
is data-dependent, so any constant is either too slow (sized for the
worst case) or unsafe (truncates strong-Lorentzian tails). Defaulting
to `0` (no cutoff) is always correct; users opt in to the speed-up.
- **Per-point η-adaptive window**
(`half_width = max(4·FWHM, cutoff_fwhm·η)`). Faster than a uniform
literal window on mixed-η patterns at equal accuracy, but it must run
**inside** the cryspy profile kernels (per-point η is not available to
the EasyDiffraction calculator, which can only inject one scalar) and
released cryspy exposes only the literal cutoff. Deferred to upstream
cryspy work; see Deferred Work.
- **Names `cutoff_lorentz` / `cutoff_wdt`.** Rejected: the first
mis-implies a Lorentzian-only effect, the second is opaque.
- **Serialise `WDT` as a new cryspy CIF item.** Avoided; dict injection
needs no upstream schema change and works on both calculation paths.

## Deferred Work

- **Automatic cutoff selection** (issue 179): compute a safe per-
experiment literal `cutoff_fwhm` in EasyDiffraction from the peak
parameters, so `0` could mean "auto" instead of "no cutoff" without
the user guessing a value. This stays a single injected scalar and
needs no cryspy change, but is a per-experiment (worst-case-η) bound,
not a per-point optimum.
- **Per-point η-adaptive window upstream:** a future cryspy PR could add
the η-adaptive window inside the profile kernels (where per-point η is
known) for the extra speed-up on mixed-η patterns.
- The dominant **minimization** cost is EasyDiffraction-side, not the
profile: `crystallography._orbit_template_residual` re-solves
`numpy.linalg.lstsq` over 27 lattice shifts per orbit template per
atom site on every iteration (~45 % of a fit iteration in profiling),
even though the Wyckoff orbit assignment is fixed for the duration of
a fit. Caching the per-site orbit template at fit setup is the larger
refinement-speed win and is out of scope for this ADR.
3 changes: 0 additions & 3 deletions docs/dev/adrs/accepted/preferred-orientation-category.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ Three independent sources confirm the same simple, widely used model —
`r`, and the exported `.r` is portable across engines.

Two names are deliberately **excluded**:

- **`.hkl`** stores the direction as a bracketed array `[ 1 0 4 ]`.
gemmi's loop reader does not reliably round-trip this array syntax,
so we use the three scalar integer columns `.index_h/_k/_l`
Expand Down Expand Up @@ -315,7 +314,6 @@ Alternatives Considered.

- **CrysPy** (`analysis/calculators/cryspy.py`). Two paths, matching how
every other experiment parameter is handled:

1. **CIF construction** (`_convert_experiment_to_cryspy_cif`): after
`_cif_phase_section`, emit a `_texture_*` loop for the matching
`pref_orient` row. **Constant-wavelength only** for now — TOF is
Expand Down Expand Up @@ -389,7 +387,6 @@ Alternatives Considered.
has a March–Dollase routine,
`CFML_Powder/Pow_Preferred_Orientation.f90`), the mapping differs from
CrysPy and **must not reuse `_march_r_to_cryspy_g1`**:

- **`march_r` passes through unchanged** — CrysFML uses the _standard_
March coefficient (`r²cos²α + sin²α/r`, `par(1) = r`). The `1/r`
inversion is CrysPy-specific; do **not** apply it for CrysFML.
Expand Down
Loading
Loading