From 98a2e4ed58d727fd8f6dda801d9e9b7c865ea94f Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 12 Jun 2026 00:27:35 +0200 Subject: [PATCH 01/12] Remove unused _allowed_attrs guard helper --- src/easydiffraction/core/guard.py | 10 ---------- 1 file changed, 10 deletions(-) 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__ From efb4a3cd0bbf93409f970e6d747bf14e96c33b89 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 12 Jun 2026 00:27:35 +0200 Subject: [PATCH 02/12] Remove dead pair-plot height helper and unused constants --- src/easydiffraction/display/plotting.py | 16 ---------------- 1 file changed, 16 deletions(-) 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.""" From 12529f549b5c1a7c1bcae7754510d352e629d07f Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 12 Jun 2026 00:27:35 +0200 Subject: [PATCH 03/12] Remove unused DEFAULT_CREDIBLE_INTERVAL_LEVELS alias --- src/easydiffraction/analysis/fit_helpers/bayesian.py | 1 - 1 file changed, 1 deletion(-) 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 From bcd0fdf257739d71eb79f0bb5c9b28f169868986 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 12 Jun 2026 07:55:28 +0200 Subject: [PATCH 04/12] Extract _summary_parameters_by_datablock helper --- src/easydiffraction/analysis/analysis.py | 29 ++++++++++++------------ 1 file changed, 14 insertions(+), 15 deletions(-) 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 From 79eacc60db5ae5b9531fb56d1ae52e6e7e6d6aa7 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 12 Jun 2026 07:56:08 +0200 Subject: [PATCH 05/12] Remove unused _set_created mutator --- src/easydiffraction/project/categories/info/default.py | 7 ------- 1 file changed, 7 deletions(-) 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.""" From f28b7bfdb8752faae6b12cac8ff6856e2a825e2d Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 12 Jun 2026 07:57:19 +0200 Subject: [PATCH 06/12] Remove never-wired lmfit iteration callback --- .../analysis/minimizers/base.py | 1 - .../analysis/minimizers/lmfit.py | 28 ------------------- 2 files changed, 29 deletions(-) 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 From a6e2c10ec4614ed6bd0e6be842ca73c7ae228380 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 12 Jun 2026 08:40:31 +0200 Subject: [PATCH 07/12] Fix stale API and inconsistencies in user docs --- docs/docs/index.md | 3 ++ .../user-guide/analysis-workflow/analysis.md | 4 +- docs/docs/user-guide/data-format.md | 4 +- docs/docs/user-guide/first-steps.md | 38 +++++++++---------- docs/docs/user-guide/glossary.md | 2 +- 5 files changed, 26 insertions(+), 25 deletions(-) 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/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]: # From f8b13460204fd0fe7b7345db875338766aece536 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 12 Jun 2026 08:40:42 +0200 Subject: [PATCH 08/12] Standardize single-crystal label on sc- in docs --- docs/docs/tutorials/index.md | 4 ++-- docs/docs/verification/ci_skip.txt | 2 +- .../tbti.int | 0 .../tbti.out | 0 .../tbti.pcr | 0 .../tbti.prf | 0 .../tbti.sum | 0 .../tbti.int | 0 .../tbti.out | 0 .../tbti.pcr | 0 .../tbti.prf | 0 .../tbti.sum | 0 .../prnio.cif | 0 .../prnio.int | 0 .../prnio.out | 0 .../prnio.pcr | 0 .../prnio.prf | 0 .../prnio.sum | 0 docs/docs/verification/index.md | 8 ++++---- ...t-iso_tbti.ipynb => sc-neut-cwl_ext-iso_tbti.ipynb} | 2 +- ...cwl_ext-iso_tbti.py => sc-neut-cwl_ext-iso_tbti.py} | 2 +- ...l_noext_tbti.ipynb => sc-neut-cwl_noext_tbti.ipynb} | 2 +- ...eut-cwl_noext_tbti.py => sc-neut-cwl_noext_tbti.py} | 2 +- ...eut-cwl_pr2nio4.ipynb => sc-neut-cwl_pr2nio4.ipynb} | 2 +- .../{sg-neut-cwl_pr2nio4.py => sc-neut-cwl_pr2nio4.py} | 2 +- docs/mkdocs.yml | 10 +++++----- 26 files changed, 18 insertions(+), 18 deletions(-) rename docs/docs/verification/fullprof/{sg-neut-cwl_ext-iso_tbti => sc-neut-cwl_ext-iso_tbti}/tbti.int (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_ext-iso_tbti => sc-neut-cwl_ext-iso_tbti}/tbti.out (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_ext-iso_tbti => sc-neut-cwl_ext-iso_tbti}/tbti.pcr (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_ext-iso_tbti => sc-neut-cwl_ext-iso_tbti}/tbti.prf (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_ext-iso_tbti => sc-neut-cwl_ext-iso_tbti}/tbti.sum (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_noext_tbti => sc-neut-cwl_noext_tbti}/tbti.int (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_noext_tbti => sc-neut-cwl_noext_tbti}/tbti.out (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_noext_tbti => sc-neut-cwl_noext_tbti}/tbti.pcr (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_noext_tbti => sc-neut-cwl_noext_tbti}/tbti.prf (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_noext_tbti => sc-neut-cwl_noext_tbti}/tbti.sum (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_pr2nio4 => sc-neut-cwl_pr2nio4}/prnio.cif (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_pr2nio4 => sc-neut-cwl_pr2nio4}/prnio.int (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_pr2nio4 => sc-neut-cwl_pr2nio4}/prnio.out (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_pr2nio4 => sc-neut-cwl_pr2nio4}/prnio.pcr (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_pr2nio4 => sc-neut-cwl_pr2nio4}/prnio.prf (100%) rename docs/docs/verification/fullprof/{sg-neut-cwl_pr2nio4 => sc-neut-cwl_pr2nio4}/prnio.sum (100%) rename docs/docs/verification/{sg-neut-cwl_ext-iso_tbti.ipynb => sc-neut-cwl_ext-iso_tbti.ipynb} (99%) rename docs/docs/verification/{sg-neut-cwl_ext-iso_tbti.py => sc-neut-cwl_ext-iso_tbti.py} (98%) rename docs/docs/verification/{sg-neut-cwl_noext_tbti.ipynb => sc-neut-cwl_noext_tbti.ipynb} (99%) rename docs/docs/verification/{sg-neut-cwl_noext_tbti.py => sc-neut-cwl_noext_tbti.py} (98%) rename docs/docs/verification/{sg-neut-cwl_pr2nio4.ipynb => sc-neut-cwl_pr2nio4.ipynb} (99%) rename docs/docs/verification/{sg-neut-cwl_pr2nio4.py => sc-neut-cwl_pr2nio4.py} (99%) 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/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: From 03078768da49188f92e18616675b0e33c2d3d159 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 12 Jun 2026 08:40:54 +0200 Subject: [PATCH 09/12] Promote documentation-ci-build ADR to accepted --- .../documentation-ci-build.md | 33 ++++++++++---- .../accepted/plotting-docs-performance.md | 2 +- .../accepted/test-suite-and-validation.md | 8 ++-- docs/dev/adrs/index.md | 2 +- docs/dev/issues/open.md | 43 +++++++++++++------ docs/dev/issues/recommended-priorities.md | 2 +- 6 files changed, 62 insertions(+), 28 deletions(-) rename docs/dev/adrs/{suggestions => accepted}/documentation-ci-build.md (72%) diff --git a/docs/dev/adrs/suggestions/documentation-ci-build.md b/docs/dev/adrs/accepted/documentation-ci-build.md similarity index 72% rename from docs/dev/adrs/suggestions/documentation-ci-build.md rename to docs/dev/adrs/accepted/documentation-ci-build.md index f3a858832..b0651bbac 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..140dc072f 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,14 +443,14 @@ 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) +- [Documentation CI and Build Verification](documentation-ci-build.md) — coordinated with §9. - [Lint Complexity Thresholds](../accepted/lint-complexity-thresholds.md) — sibling Quality guardrail. 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/issues/open.md b/docs/dev/issues/open.md index 8477f9203..23d2f14ad 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. From 7a4944b9083debec89c455bebd2ccddf47409a21 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 12 Jun 2026 08:41:03 +0200 Subject: [PATCH 10/12] Add documentation-snippet-tests implementation plan --- docs/dev/plans/documentation-snippet-tests.md | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 docs/dev/plans/documentation-snippet-tests.md diff --git a/docs/dev/plans/documentation-snippet-tests.md b/docs/dev/plans/documentation-snippet-tests.md new file mode 100644 index 000000000..42d9432de --- /dev/null +++ b/docs/dev/plans/documentation-snippet-tests.md @@ -0,0 +1,209 @@ +# 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. From 5a4c336c6decadd8b59d5311880c886d3a96646c Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 12 Jun 2026 08:55:31 +0200 Subject: [PATCH 11/12] Add model-sample-absorption ADR suggestion --- .../suggestions/model-sample-absorption.md | 379 ++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 docs/dev/adrs/suggestions/model-sample-absorption.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..b18bb7648 --- /dev/null +++ b/docs/dev/adrs/suggestions/model-sample-absorption.md @@ -0,0 +1,379 @@ +# 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). From 5b8663e84f27f4d836701b39b36fc7e6c659ae64 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 12 Jun 2026 09:02:39 +0200 Subject: [PATCH 12/12] Apply prettier formatting to ADR, plan, and issue docs --- .../adrs/accepted/documentation-ci-build.md | 20 +- .../accepted/test-suite-and-validation.md | 4 +- .../suggestions/model-sample-absorption.md | 194 +++++++++--------- docs/dev/issues/open.md | 4 +- docs/dev/plans/documentation-snippet-tests.md | 151 +++++++------- 5 files changed, 186 insertions(+), 187 deletions(-) diff --git a/docs/dev/adrs/accepted/documentation-ci-build.md b/docs/dev/adrs/accepted/documentation-ci-build.md index b0651bbac..e7e0e8885 100644 --- a/docs/dev/adrs/accepted/documentation-ci-build.md +++ b/docs/dev/adrs/accepted/documentation-ci-build.md @@ -82,14 +82,14 @@ allowlist for crystallographic terms, package names, and CIF tags. 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. | +| # | 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 @@ -198,8 +198,8 @@ Cons: question carried into that plan: extract fenced code blocks automatically or rely on explicitly named snippets. - 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). + 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 diff --git a/docs/dev/adrs/accepted/test-suite-and-validation.md b/docs/dev/adrs/accepted/test-suite-and-validation.md index 140dc072f..6d0744057 100644 --- a/docs/dev/adrs/accepted/test-suite-and-validation.md +++ b/docs/dev/adrs/accepted/test-suite-and-validation.md @@ -450,8 +450,8 @@ 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](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/suggestions/model-sample-absorption.md b/docs/dev/adrs/suggestions/model-sample-absorption.md index b18bb7648..838e71986 100644 --- a/docs/dev/adrs/suggestions/model-sample-absorption.md +++ b/docs/dev/adrs/suggestions/model-sample-absorption.md @@ -14,21 +14,21 @@ Experiment model. ## Context -This ADR follows the conventions in [`AGENTS.md`](../../../../AGENTS.md). +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). +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 +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 @@ -39,15 +39,13 @@ difference), while the μR = 0 page passes to corr 0.9999. See - **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θ. + 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: @@ -82,27 +80,27 @@ 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: +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 | +| 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). +(`_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: +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** @@ -120,8 +118,8 @@ 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. + 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), @@ -131,10 +129,10 @@ Two design consequences: ### 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 +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 @@ -159,28 +157,28 @@ 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). +(`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 | +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: @@ -188,16 +186,16 @@ Notes: 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. +- **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 @@ -222,15 +220,15 @@ read-only provenance (Deferred Work). ### 5. Equations -**Hewat** (cylinder, validated to 4 decimals vs FullProf; fit range -μR ≲ 1.5): +**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θ`): +**Lobanov–Alte da Veiga** (cylinder, extends to μR ≈ 10 via a branch at +μR = 3; `s ≡ sinθ`): ``` μR ≤ 3: @@ -244,8 +242,8 @@ at μR = 3; `s ≡ sinθ`): ``` (Lobanov constants transcribed from CrysFML08 -`Pow_Lorentz_Absorption.f90`; the implementation will copy them -verbatim and unit-test against that source.) +`Pow_Lorentz_Absorption.f90`; the implementation will copy them verbatim +and unit-test against that source.) **Flat plate, symmetric θ–2θ** (μt = μ·thickness): @@ -291,8 +289,8 @@ IUCr-aligned export (per 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 +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 @@ -309,23 +307,23 @@ backend round-trip. `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 +- **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. + 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. + FWHM·dA/dθ). Validated to 4 decimals against FullProf; acceptable and + documented. ## Alternatives Considered @@ -345,19 +343,19 @@ backend round-trip. 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. +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 +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. +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, @@ -371,9 +369,9 @@ on spec alone. - **`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. +- **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 23d2f14ad..76634fe41 100644 --- a/docs/dev/issues/open.md +++ b/docs/dev/issues/open.md @@ -33,8 +33,8 @@ A Lobanov–Alte-da-Veiga form covers `μR > 3`. **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. +— a switchable `experiment.absorption` category (mirroring `extinction`) +with a calculator-independent A(θ) envelope. **What the backends actually provide (corrected):** diff --git a/docs/dev/plans/documentation-snippet-tests.md b/docs/dev/plans/documentation-snippet-tests.md index 42d9432de..63d80ec77 100644 --- a/docs/dev/plans/documentation-snippet-tests.md +++ b/docs/dev/plans/documentation-snippet-tests.md @@ -30,13 +30,13 @@ shape would catch it before merge. ## 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: +- **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 @@ -45,10 +45,10 @@ shape would catch it before merge. `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. +- **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 @@ -82,21 +82,24 @@ shape would catch it before merge. ## 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. + 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). +- `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. + `` 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). + the matching Deferred Work bullet (final Phase 1 step before the + gate). ## Implementation steps (Phase 1) @@ -107,61 +110,59 @@ 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.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` + 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` + 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) @@ -200,10 +201,10 @@ Expectations: **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 +**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.