From 87a180227f020454491b32ee8cc08fa86b0205c4 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 13 May 2026 23:27:53 +0200 Subject: [PATCH 01/10] Add display UX ADR and plan --- docs/dev/adr_display-ux.md | 227 ++++++++++++++++++++++++++++++++++ docs/dev/plan_display-ux.md | 238 ++++++++++++++++++++++++++++++++++++ 2 files changed, 465 insertions(+) create mode 100644 docs/dev/adr_display-ux.md create mode 100644 docs/dev/plan_display-ux.md diff --git a/docs/dev/adr_display-ux.md b/docs/dev/adr_display-ux.md new file mode 100644 index 00000000..264092e2 --- /dev/null +++ b/docs/dev/adr_display-ux.md @@ -0,0 +1,227 @@ +# ADR: Display UX Facade + +## Status + +Accepted. + +## Context + +The current user-facing display API mixes presentation actions, analysis +reports, and renderer configuration: + +```python +project.display.plotter.plot_meas(expt_name='hrpt') +project.display.plotter.plot_calc(expt_name='hrpt') +project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.plotter.plot_param_correlations() +project.display.plotter.plot_posterior_pairs() +project.display.plotter.plot_param_distribution(param) +project.display.plotter.plot_posterior_predictive(expt_name='hrpt') + +project.analysis.display.free_params() +project.analysis.display.fit_results() +``` + +This has several UX problems: + +- `plot_` is redundant below any display or chart object. +- `plotter` and `tabler` expose backend implementation language to + scientists using notebooks. +- `project.display` and `project.analysis.display` overlap without a + clear user-facing rule. +- `plot_meas`, `plot_calc`, and `plot_meas_vs_calc` force users to + choose a plot state that the project can often infer. +- Bayesian and deterministic chart names are not systematic. +- The existing `project.display` category is serialized to CIF, so it + should not also become a broad transient display facade. + +EasyDiffraction is aimed at scientists, often non-programmers, so the +display API should prioritize discoverability, clear names, and safe +defaults. + +## Decision + +Use `project.display` as the user-facing facade for display actions. +Move serialized renderer settings out of that facade and into a separate +project category named `project.rendering`. + +Renderer settings: + +```python +project.rendering.chart_engine = 'plotly' +project.rendering.table_engine = 'pandas' +project.rendering.show_chart_engines() +project.rendering.show_table_engines() +project.rendering.show_config() +``` + +Suggested CIF names: + +- `_rendering.chart_engine` +- `_rendering.table_engine` + +No legacy loader is required for `_display.plotter_type` or +`_display.tabler_type`. The project is in beta, so this cleanup may +break old project files rather than carrying compatibility code. + +The selected display API is grouped: + +```python +project.display.pattern(expt_name='hrpt') + +project.display.parameters.free() +project.display.parameters.fittable() +project.display.parameters.all() +project.display.parameters.access() +project.display.parameters.cif_uids() + +project.display.fit.results() +project.display.fit.correlations() +project.display.fit.series(param, versus=temperature) + +project.display.posterior.pairs() +project.display.posterior.distribution(param) +project.display.posterior.predictive(expt_name='hrpt') + +project.display.show_pattern_options(expt_name='hrpt') +``` + +`project.analysis.display` is removed from the primary public API. Its +current responsibilities move to clearer homes: + +| Current method | New home | +| ---------------------------- | -------------------------------------------------------------- | +| `all_params()` | `project.display.parameters.all()` | +| `fittable_params()` | `project.display.parameters.fittable()` | +| `free_params()` | `project.display.parameters.free()` | +| `how_to_access_parameters()` | `project.display.parameters.access()` | +| `parameter_cif_uids()` | `project.display.parameters.cif_uids()` | +| `fit_results()` | `project.display.fit.results()` | +| `constraints()` | `project.analysis.constraints.show()` | +| `as_cif()` | `project.analysis.as_cif` and `project.analysis.show_as_cif()` | + +`project.analysis` and `project.info` should follow the same CIF display +pattern as structures and experiments: + +- `as_cif` is a read-only property returning CIF text as a string. +- `show_as_cif()` pretty-prints the CIF text with a header. + +## Pattern Display + +Use `pattern()` as the main experiment chart: + +```python +project.display.pattern(expt_name='hrpt') +project.display.pattern(expt_name='hrpt', x_min=40, x_max=55) +``` + +By default, `pattern()` uses `include='auto'` and displays as much +useful information as the project state supports: + +- measured data if present +- calculated data if a model/calculation is available +- background if defined and relevant +- Bragg ticks if phases/reflections are available +- residual if both measured and calculated data are available and the + experiment type supports a residual panel +- excluded regions if available +- uncertainty bands where posterior predictive data exists + +Specific subsets are selected with `include`: + +```python +project.display.pattern(expt_name='hrpt', include='auto') +project.display.pattern(expt_name='hrpt', include='measured') +project.display.pattern(expt_name='hrpt', include='calculated') +project.display.pattern( + expt_name='hrpt', + include=('measured', 'calculated', 'background', 'residual', 'bragg'), +) +``` + +`include` was chosen over alternatives: + +| Name | Reason not selected | +| ------------- | ----------------------------------------------- | +| `layers` | Sounds graphical rather than user intent. | +| `components` | Precise, but longer. | +| `content` | Too broad. | +| `view` | Better for presets than arbitrary combinations. | +| `series` | Does not fit residual rows or Bragg ticks well. | +| boolean flags | Explicit, but scales poorly. | + +Add discovery for supported pattern content: + +```python +project.display.show_pattern_options(expt_name='hrpt') +``` + +The table should show option name, description, availability for the +experiment, whether `include='auto'` includes it, and the reason an +option is unavailable. + +Initial option names: + +- `auto` +- `measured` +- `calculated` +- `background` +- `residual` +- `bragg` +- `excluded` +- `uncertainty` + +`uncertainty` should be implemented immediately where posterior +predictive data exists. It should be unavailable, with a clear reason, +when no posterior predictive data is present. + +## Deterministic And Bayesian Consistency + +Use these naming rules: + +- `pattern()` shows the current point-estimate experiment view. +- `fit.results()` reports the latest fit result. +- `fit.correlations()` shows parameter relationships from the latest + fit. +- `fit.series(param, versus=...)` shows fitted parameter values across a + sequence of fit results or experiments. +- `posterior.*` names are used only when posterior samples are required. + +## Rejected Alternatives + +Flat display facade: + +```python +project.display.pattern(expt_name='hrpt') +project.display.parameters(scope='free') +project.display.fit_results() +project.display.correlations() +project.display.parameter_series(param, versus=temperature) +project.display.posterior_pairs() +project.display.posterior_distribution(param) +project.display.posterior_predictive(expt_name='hrpt') +``` + +This is shorter but would make `project.display` grow into a long flat +list. + +Separate `charts` and `tables` namespaces were also rejected because +users should not need to decide the output type before asking for +information. Some outputs may render as a chart or a table depending on +backend and state. + +Separate `measured()` and `calculated()` methods were rejected because +they duplicate `pattern(..., include=...)`. + +## Consequences + +- The main display workflow becomes more discoverable through grouped + namespaces and tab completion. +- Renderer configuration becomes clearly separate from display actions. +- Existing tutorials and public API docs must be updated to the selected + API. +- Constraints remain owned by the analysis constraints category. +- There is no legacy CIF compatibility path for `_display.plotter_type` + or `_display.tabler_type`. +- `project.analysis` and `project.info` need CIF access cleanup for + consistency with structure and experiment objects. diff --git a/docs/dev/plan_display-ux.md b/docs/dev/plan_display-ux.md new file mode 100644 index 00000000..549b46be --- /dev/null +++ b/docs/dev/plan_display-ux.md @@ -0,0 +1,238 @@ +# Plan: Display UX Facade + +## Goal + +Implement the display UX approach described in +`docs/dev/adr_display-ux.md`. + +The end-user API should become: + +```python +project.display.pattern(expt_name='hrpt') + +project.display.parameters.free() +project.display.parameters.fittable() +project.display.parameters.all() +project.display.parameters.access() +project.display.parameters.cif_uids() + +project.display.fit.results() +project.display.fit.correlations() +project.display.fit.series(param, versus=temperature) + +project.display.posterior.pairs() +project.display.posterior.distribution(param) +project.display.posterior.predictive(expt_name='hrpt') + +project.display.show_pattern_options(expt_name='hrpt') +``` + +Renderer configuration should move to: + +```python +project.rendering.chart_engine = 'plotly' +project.rendering.table_engine = 'pandas' +project.rendering.show_chart_engines() +project.rendering.show_table_engines() +project.rendering.show_config() +``` + +## Branch + +Use implementation branch: + +```text +feature/display-ux +``` + +## Decisions + +- Use grouped display namespaces under `project.display`. +- Use `project.display.fit.series(param, versus=...)` for sequential fit + parameter plots. +- Use `pattern(..., include='auto')` as the default experiment chart. +- Use `include` rather than `layers`, `components`, `content`, `view`, + `series`, or boolean flags. +- Do not add `project.display.constraints()`. +- Move constraint reporting to `project.analysis.constraints.show()`. +- Rename the serialized project display category to `rendering`. +- Do not add legacy CIF loading for `_display.plotter_type` or + `_display.tabler_type`. +- Standardize CIF display helpers on `project.analysis` and + `project.info`: `as_cif` should be a read-only property returning CIF + text, and `show_as_cif()` should pretty-print CIF text with a header. +- Implement `include='uncertainty'` immediately where posterior + predictive data exists. +- No compatibility aliases or deprecation warnings are required unless + release policy separately requires them. + +## Likely Files To Change + +- `src/easydiffraction/project/project.py` +- `src/easydiffraction/project/project_info.py` +- `src/easydiffraction/project/categories/display/default.py` +- `src/easydiffraction/project/categories/display/factory.py` +- `src/easydiffraction/project/categories/display/__init__.py` +- new `src/easydiffraction/project/categories/rendering/...` +- `src/easydiffraction/display/plotting.py` +- `src/easydiffraction/display/tables.py` +- `src/easydiffraction/analysis/analysis.py` +- `src/easydiffraction/analysis/categories/constraints/default.py` +- `src/easydiffraction/__main__.py` +- `docs/dev/architecture.md` +- `docs/docs/user-guide/**/*.md` +- `docs/docs/tutorials/*.py` +- tests under `tests/unit/easydiffraction/` +- tests under `tests/integration/fitting/` + +Do not edit generated tutorial notebooks directly. Update tutorial `.py` +files and regenerate notebooks in Phase 2 if required. + +## Resolved Questions + +- Legacy CIF `_display.plotter_type` and `_display.tabler_type` do not + need to load into `project.rendering`. No legacy code is required. +- `project.analysis` and `project.info` need consistency with structure + and experiment objects: `as_cif` should be a read-only property that + returns CIF text, and `show_as_cif()` should pretty-print CIF text + with a header. +- `include='uncertainty'` should be implemented immediately where + posterior predictive data exists. + +## Phase 1 - Implementation + +Do not create or run tests in Phase 1 unless explicitly requested. Every +completed Phase 1 implementation step must be staged with explicit paths +and committed locally before moving to the next implementation step or +the Phase 1 review gate. Use atomic commits, inspect the worktree before +each commit, and stage only the files changed for that step. + +- [ ] Rename the serialized project display category to rendering. + - Move or recreate the category package as + `src/easydiffraction/project/categories/rendering/`. + - Rename user-facing settings from `plotter_type` and `tabler_type` to + `chart_engine` and `table_engine`. + - Add `show_chart_engines()`, `show_table_engines()`, and + `show_config()`. + - Update CIF names to `_rendering.chart_engine` and + `_rendering.table_engine`. + - Do not add legacy loading for `_display.plotter_type` or + `_display.tabler_type`. + +- [ ] Add the new `project.display` facade. + - Add a facade object that is not the serialized rendering category. + - Add `pattern(...)` and `show_pattern_options(...)`. + - Add `parameters`, `fit`, and `posterior` namespace objects. + +- [ ] Implement `pattern(..., include='auto')`. + - Replace common user-facing calls to `plot_meas`, `plot_calc`, and + `plot_meas_vs_calc` with one state-aware method. + - Support explicit includes for `measured`, `calculated`, + `background`, `residual`, `bragg`, `excluded`, and `uncertainty` + where data is available. + - Implement `uncertainty` immediately for experiments with posterior + predictive data. + - Render a clear warning or error when a requested include is not + available. + +- [ ] Move parameter table displays under `project.display.parameters`. + - Implement `all()`, `fittable()`, `free()`, `access()`, and + `cif_uids()`. + - Remove the primary public need for `project.analysis.display`. + +- [ ] Move fit displays under `project.display.fit`. + - Implement `results()`. + - Implement `correlations()`. + - Implement `series(param, versus=...)`. + +- [ ] Move Bayesian displays under `project.display.posterior`. + - Implement `pairs()`. + - Implement `distribution(param)`. + - Implement `predictive(expt_name=...)`. + +- [ ] Move constraint reporting to + `project.analysis.constraints.show()`. + - Do not add `project.display.constraints()`. + +- [ ] Standardize CIF display helpers. + - Convert `project.analysis.as_cif()` to a read-only + `project.analysis.as_cif` property. + - Ensure `project.analysis.show_as_cif()` pretty-prints CIF text with + a header. + - Convert `project.info.as_cif()` to a read-only `project.info.as_cif` + property. + - Ensure `project.info.show_as_cif()` pretty-prints CIF text with a + header. + +- [ ] Update docs, tutorials, and architecture text. + - Replace old public display examples with the selected API. + - Update `docs/dev/architecture.md`. + - Update tutorial `.py` files only; regenerate notebooks in Phase 2 if + required. + +- [ ] Stop at the Phase 1 review gate. + - Summarize changed files and open questions. + - Suggest next verification commands. + - Wait for user approval before Phase 2. + +Suggested Phase 1 commit messages: + +```text +Rename display settings category to rendering +Add grouped display facade +Implement state-aware pattern display +Move analysis display reports to display facade +Standardize CIF display helpers +Update display UX documentation +``` + +## Phase 2 - Verification + +After Phase 1 is reviewed and approved: + +- [ ] Add or update unit tests for the rendering category. +- [ ] Add or update unit tests for the display facade namespaces. +- [ ] Add or update plotting integration tests for `pattern(...)`. +- [ ] Add or update analysis display integration tests for parameter and + fit report methods. +- [ ] Regenerate tutorial notebooks if tutorial `.py` files changed. +- [ ] Run formatting and checks. +- [ ] Run unit tests. +- [ ] Run integration tests. +- [ ] Run script tests. + +Verification commands: + +```sh +pixi run notebook-prepare +pixi run fix +pixi run check +pixi run unit-tests +pixi run integration-tests +pixi run script-tests +``` + +Run `pixi run notebook-prepare` only if tutorial `.py` files changed. +Run `pixi run integration-tests` only after the relevant unit and +focused integration tests are passing. + +## Suggested Commit Message + +```text +Plan display UX facade implementation +``` + +## Suggested Pull Request + +Title: + +```text +Improve chart and table display API +``` + +Description: + +This change makes display commands easier to discover and use in +notebooks. Experiment patterns, parameter tables, fit reports, and +Bayesian plots are grouped under `project.display`, while renderer +settings move to `project.rendering`. From 0888b2d40845b88a713bc639b23d99a39426f63f Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 13 May 2026 23:37:47 +0200 Subject: [PATCH 02/10] Rename display settings category to rendering --- docs/dev/plan_display-ux.md | 2 +- src/easydiffraction/io/cif/serialize.py | 12 +- .../project/categories/rendering/__init__.py | 8 ++ .../project/categories/rendering/default.py | 131 ++++++++++++++++++ .../project/categories/rendering/factory.py | 17 +++ src/easydiffraction/project/project.py | 18 ++- 6 files changed, 175 insertions(+), 13 deletions(-) create mode 100644 src/easydiffraction/project/categories/rendering/__init__.py create mode 100644 src/easydiffraction/project/categories/rendering/default.py create mode 100644 src/easydiffraction/project/categories/rendering/factory.py diff --git a/docs/dev/plan_display-ux.md b/docs/dev/plan_display-ux.md index 549b46be..4de989d0 100644 --- a/docs/dev/plan_display-ux.md +++ b/docs/dev/plan_display-ux.md @@ -107,7 +107,7 @@ and committed locally before moving to the next implementation step or the Phase 1 review gate. Use atomic commits, inspect the worktree before each commit, and stage only the files changed for that step. -- [ ] Rename the serialized project display category to rendering. +- [x] Rename the serialized project display category to rendering. - Move or recreate the category package as `src/easydiffraction/project/categories/rendering/`. - Rename user-facing settings from `plotter_type` and `tabler_type` to diff --git a/src/easydiffraction/io/cif/serialize.py b/src/easydiffraction/io/cif/serialize.py index 0b868b6c..bd74c822 100644 --- a/src/easydiffraction/io/cif/serialize.py +++ b/src/easydiffraction/io/cif/serialize.py @@ -334,9 +334,9 @@ def _as_cif_text(section: object) -> str: def project_config_to_cif(project: object) -> str: """Render project-level configuration to ``project.cif`` text.""" lines: list[str] = [_as_cif_text(project.info)] - display = getattr(project, 'display', None) - if display is not None: - lines.extend(('', _as_cif_text(display))) + rendering = getattr(project, 'rendering', None) + if rendering is not None: + lines.extend(('', _as_cif_text(rendering))) return '\n'.join(lines) @@ -455,9 +455,9 @@ def project_config_from_cif(project: object, cif_text: str) -> None: _populate_project_info_from_block(project.info, block) - display = getattr(project, 'display', None) - if display is not None: - display.from_cif(block) + rendering = getattr(project, 'rendering', None) + if rendering is not None: + rendering.from_cif(block) def analysis_from_cif(analysis: object, cif_text: str) -> None: diff --git a/src/easydiffraction/project/categories/rendering/__init__.py b/src/easydiffraction/project/categories/rendering/__init__.py new file mode 100644 index 00000000..84662b01 --- /dev/null +++ b/src/easydiffraction/project/categories/rendering/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Project rendering category exports.""" + +from __future__ import annotations + +from easydiffraction.project.categories.rendering.default import Rendering +from easydiffraction.project.categories.rendering.factory import RenderingFactory \ No newline at end of file diff --git a/src/easydiffraction/project/categories/rendering/default.py b/src/easydiffraction/project/categories/rendering/default.py new file mode 100644 index 00000000..3e923366 --- /dev/null +++ b/src/easydiffraction/project/categories/rendering/default.py @@ -0,0 +1,131 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Project rendering category.""" + +from __future__ import annotations + +from easydiffraction.core.category import CategoryItem +from easydiffraction.core.metadata import TypeInfo +from easydiffraction.core.validation import AttributeSpec +from easydiffraction.core.validation import MembershipValidator +from easydiffraction.core.variable import StringDescriptor +from easydiffraction.display.plotting import Plotter +from easydiffraction.display.plotting import PlotterEngineEnum +from easydiffraction.display.tables import TableEngineEnum +from easydiffraction.display.tables import TableRenderer +from easydiffraction.io.cif.handler import CifHandler +from easydiffraction.io.cif.parse import read_cif_str +from easydiffraction.project.categories.rendering.factory import RenderingFactory +from easydiffraction.utils.logging import console +from easydiffraction.utils.utils import render_table + + +@RenderingFactory.register +class Rendering(CategoryItem): + """Chart and table engine selection for a project.""" + + type_info = TypeInfo( + tag='default', + description='Project rendering category', + ) + + def __init__(self) -> None: + super().__init__() + + self._plotter = Plotter() + self._tabler = TableRenderer.get() + + self._chart_engine = StringDescriptor( + name='chart_engine', + description='Chart renderer backend type', + value_spec=AttributeSpec( + default=self._plotter.engine, + validator=MembershipValidator( + allowed=[member.value for member in PlotterEngineEnum], + ), + ), + cif_handler=CifHandler(names=['_rendering.chart_engine']), + ) + self._table_engine = StringDescriptor( + name='table_engine', + description='Table renderer backend type', + value_spec=AttributeSpec( + default=self._tabler.engine, + validator=MembershipValidator( + allowed=[member.value for member in TableEngineEnum], + ), + ), + cif_handler=CifHandler(names=['_rendering.table_engine']), + ) + + self._identity.category_code = 'rendering' + + @property + def chart_engine(self) -> StringDescriptor: + """Chart renderer backend type.""" + return self._chart_engine + + @chart_engine.setter + def chart_engine(self, value: str) -> None: + self._plotter.engine = value + self._chart_engine.value = self._plotter.engine + + @property + def table_engine(self) -> StringDescriptor: + """Table renderer backend type.""" + return self._table_engine + + @table_engine.setter + def table_engine(self, value: str) -> None: + self._tabler.engine = value + self._table_engine.value = self._tabler.engine + + @property + def plotter(self) -> Plotter: + """Live plotting facade bound to the owning project.""" + parent = getattr(self, '_parent', None) + if parent is not None: + self._plotter._set_project(parent) + return self._plotter + + @property + def tabler(self) -> TableRenderer: + """Live table-rendering facade.""" + return self._tabler + + def show_chart_engines(self) -> None: + """Print supported chart renderer backends.""" + self.plotter.show_supported_engines() + + def show_table_engines(self) -> None: + """Print supported table renderer backends.""" + self.tabler.show_supported_engines() + + def show_config(self) -> None: + """Print the current rendering configuration.""" + console.paragraph('Current rendering configuration') + render_table( + columns_headers=['Setting', 'Value'], + columns_alignment=['left', 'left'], + columns_data=[ + ['Chart engine', self.chart_engine.value], + ['Table engine', self.table_engine.value], + ], + ) + + def from_cif(self, block: object, idx: int = 0) -> None: + """Populate this rendering category from a CIF block.""" + del idx + chart_engine = read_cif_str(block, '_rendering.chart_engine') + if chart_engine is not None: + if chart_engine == self._plotter.engine: + self._chart_engine.value = chart_engine + else: + self.chart_engine = chart_engine + + table_engine = read_cif_str(block, '_rendering.table_engine') + if table_engine is not None: + if table_engine == self._tabler.engine: + self._table_engine.value = table_engine + else: + self.table_engine = table_engine \ No newline at end of file diff --git a/src/easydiffraction/project/categories/rendering/factory.py b/src/easydiffraction/project/categories/rendering/factory.py new file mode 100644 index 00000000..acaa03f3 --- /dev/null +++ b/src/easydiffraction/project/categories/rendering/factory.py @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Factory for project rendering categories.""" + +from __future__ import annotations + +from typing import ClassVar + +from easydiffraction.core.factory import FactoryBase + + +class RenderingFactory(FactoryBase): + """Create project rendering category instances.""" + + _default_rules: ClassVar[dict] = { + frozenset(): 'default', + } \ No newline at end of file diff --git a/src/easydiffraction/project/project.py b/src/easydiffraction/project/project.py index 58d7e97f..0cb46528 100644 --- a/src/easydiffraction/project/project.py +++ b/src/easydiffraction/project/project.py @@ -16,8 +16,8 @@ from easydiffraction.datablocks.structure.collection import Structures from easydiffraction.io.cif.serialize import project_config_to_cif from easydiffraction.io.cif.serialize import project_to_cif -from easydiffraction.project.categories.display import Display -from easydiffraction.project.categories.display import DisplayFactory +from easydiffraction.project.categories.rendering import Rendering +from easydiffraction.project.categories.rendering import RenderingFactory from easydiffraction.project.project_info import ProjectInfo from easydiffraction.summary.summary import Summary from easydiffraction.utils.enums import VerbosityEnum @@ -82,8 +82,9 @@ def __init__( self._info: ProjectInfo = ProjectInfo(name, title, description) self._structures = Structures() self._experiments = Experiments() - self._display = DisplayFactory.create('default') - self._display._parent = self + self._rendering = RenderingFactory.create('default') + self._rendering._parent = self + self._display = self._rendering self._analysis = Analysis(self) self._summary = Summary(self) self._saved = False @@ -152,8 +153,13 @@ def experiments(self, experiments: Experiments) -> None: self._experiments = experiments @property - def display(self) -> Display: - """Display configuration and facades bound to the project.""" + def rendering(self) -> Rendering: + """Rendering configuration bound to the project.""" + return self._rendering + + @property + def display(self) -> Rendering: + """Current display entry-point bound to the project.""" return self._display @property From eeb7e4b2ea2a4695f980a937bcd0c0f15098fcdd Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 13 May 2026 23:47:46 +0200 Subject: [PATCH 03/10] Add grouped display facade --- docs/dev/plan_display-ux.md | 2 +- src/easydiffraction/__main__.py | 6 +- src/easydiffraction/project/display.py | 443 +++++++++++++++++++++++++ src/easydiffraction/project/project.py | 5 +- 4 files changed, 450 insertions(+), 6 deletions(-) create mode 100644 src/easydiffraction/project/display.py diff --git a/docs/dev/plan_display-ux.md b/docs/dev/plan_display-ux.md index 4de989d0..d0dbac57 100644 --- a/docs/dev/plan_display-ux.md +++ b/docs/dev/plan_display-ux.md @@ -119,7 +119,7 @@ each commit, and stage only the files changed for that step. - Do not add legacy loading for `_display.plotter_type` or `_display.tabler_type`. -- [ ] Add the new `project.display` facade. +- [x] Add the new `project.display` facade. - Add a facade object that is not the serialized rendering category. - Add `pattern(...)` and `show_pattern_options(...)`. - Add `parameters`, `fit`, and `posterior` namespace objects. diff --git a/src/easydiffraction/__main__.py b/src/easydiffraction/__main__.py index bd5ae9be..18220904 100644 --- a/src/easydiffraction/__main__.py +++ b/src/easydiffraction/__main__.py @@ -99,10 +99,10 @@ def fit( if dry: project.info._path = None project.analysis.fit() - project.analysis.display.fit_results() - project.display.plotter.plot_param_correlations() + project.display.fit.results() + project.display.fit.correlations() for expt in project.experiments: - project.display.plotter.plot_meas_vs_calc(expt_name=expt.name, show_residual=True) + project.display.pattern(expt_name=expt.name) # project.summary.show_report() diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py new file mode 100644 index 00000000..d3f05c38 --- /dev/null +++ b/src/easydiffraction/project/display.py @@ -0,0 +1,443 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Project display facade grouping charts and reports.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from easydiffraction.datablocks.experiment.item.base import intensity_category_for +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.display.plotting import PlotterEngineEnum +from easydiffraction.display.plotting import PosteriorPairPlotStyleEnum +from easydiffraction.utils.utils import render_table + +if TYPE_CHECKING: + from easydiffraction.project.project import Project + + +_PATTERN_OPTION_DESCRIPTIONS: dict[str, str] = { + 'auto': 'Show the most informative available pattern view.', + 'measured': 'Measured diffraction intensities.', + 'calculated': 'Calculated diffraction intensities.', + 'background': 'Calculated background intensities when present.', + 'residual': 'Measured minus calculated residuals when supported.', + 'bragg': 'Bragg reflection tick marks when reflection data exists.', + 'excluded': 'Excluded fitting regions when defined on the experiment.', + 'uncertainty': 'Posterior predictive uncertainty bands when available.', +} + + +@dataclass(frozen=True, slots=True) +class PatternOptionStatus: + """Availability metadata for one ``display.pattern`` option.""" + + name: str + description: str + available: bool + auto_included: bool + reason: str + + +class ParameterDisplay: + """Parameter-table namespace under ``project.display``.""" + + def __init__(self, project: Project) -> None: + self._project = project + + def all(self) -> None: + """Show all structure and experiment parameters.""" + self._project.analysis.display.all_params() + + def fittable(self) -> None: + """Show all currently fittable parameters.""" + self._project.analysis.display.fittable_params() + + def free(self) -> None: + """Show all currently free parameters.""" + self._project.analysis.display.free_params() + + def access(self) -> None: + """Show Python access paths for all parameters.""" + self._project.analysis.display.how_to_access_parameters() + + def cif_uids(self) -> None: + """Show CIF unique identifiers for all parameters.""" + self._project.analysis.display.parameter_cif_uids() + + +class FitDisplay: + """Fit-report namespace under ``project.display``.""" + + def __init__(self, project: Project) -> None: + self._project = project + + def results(self) -> None: + """Show the latest fit summary and fitted parameter table.""" + self._project.analysis.display.fit_results() + + def correlations( + self, + threshold: float | None = None, + precision: int = 2, + *, + max_parameters: int = 6, + show_diagonal: bool = True, + ) -> None: + """Show parameter correlations from the latest fit.""" + self._project.rendering.plotter.plot_param_correlations( + threshold=threshold, + precision=precision, + max_parameters=max_parameters, + show_diagonal=show_diagonal, + ) + + def series( + self, + param: object, + versus: object | None = None, + ) -> None: + """Plot one fitted parameter across sequential results.""" + self._project.rendering.plotter.plot_param_series(param=param, versus=versus) + + +class PosteriorDisplay: + """Posterior-plot namespace under ``project.display``.""" + + def __init__(self, project: Project) -> None: + self._project = project + + def pairs( + self, + parameters: list[object] | None = None, + style: PosteriorPairPlotStyleEnum | str = PosteriorPairPlotStyleEnum.AUTO, + *, + threshold: float | None = None, + max_parameters: int = 6, + ) -> None: + """Plot posterior pair relationships for sampled parameters.""" + self._project.rendering.plotter.plot_posterior_pairs( + parameters=parameters, + style=style, + threshold=threshold, + max_parameters=max_parameters, + ) + + def distribution(self, param: object) -> None: + """Plot one sampled parameter's posterior distribution.""" + self._project.rendering.plotter.plot_param_distribution(param) + + def predictive( + self, + expt_name: str, + style: str = 'band', + x_min: float | None = None, + x_max: float | None = None, + *, + show_residual: bool | None = None, + x: object | None = None, + ) -> None: + """Plot posterior predictive summaries for one experiment.""" + self._project.rendering.plotter.plot_posterior_predictive( + expt_name=expt_name, + style=style, + x_min=x_min, + x_max=x_max, + show_residual=show_residual, + x=x, + ) + + +class ProjectDisplay: + """Grouped display facade exposed as ``project.display``.""" + + def __init__(self, project: Project) -> None: + self._project = project + self._parameters = ParameterDisplay(project) + self._fit = FitDisplay(project) + self._posterior = PosteriorDisplay(project) + + @property + def parameters(self) -> ParameterDisplay: + """Parameter-table namespace.""" + return self._parameters + + @property + def fit(self) -> FitDisplay: + """Fit-report namespace.""" + return self._fit + + @property + def posterior(self) -> PosteriorDisplay: + """Posterior-plot namespace.""" + return self._posterior + + def pattern( + self, + expt_name: str, + x_min: float | None = None, + x_max: float | None = None, + include: str | tuple[str, ...] = 'auto', + *, + x: object | None = None, + ) -> None: + """Show a pattern view for one experiment.""" + normalized_include = self._normalize_include(include) + + if normalized_include == ('auto',): + auto_options = self._pattern_option_statuses(expt_name) + if self._status_by_name(auto_options, 'uncertainty').available: + self.posterior.predictive( + expt_name=expt_name, + style='band', + x_min=x_min, + x_max=x_max, + x=x, + ) + return + self._show_point_estimate_pattern( + expt_name=expt_name, + x_min=x_min, + x_max=x_max, + include=('measured', 'calculated'), + x=x, + ) + return + + if normalized_include == ('measured',): + self._project.rendering.plotter.plot_meas( + expt_name=expt_name, + x_min=x_min, + x_max=x_max, + x=x, + ) + return + + if normalized_include == ('calculated',): + self._project.rendering.plotter.plot_calc( + expt_name=expt_name, + x_min=x_min, + x_max=x_max, + x=x, + ) + return + + if 'uncertainty' in normalized_include: + self.posterior.predictive( + expt_name=expt_name, + style='band', + x_min=x_min, + x_max=x_max, + show_residual=True if 'residual' in normalized_include else None, + x=x, + ) + return + + self._show_point_estimate_pattern( + expt_name=expt_name, + x_min=x_min, + x_max=x_max, + include=normalized_include, + x=x, + ) + + def show_pattern_options(self, expt_name: str) -> None: + """Show available ``pattern(include=...)`` options.""" + statuses = self._pattern_option_statuses(expt_name) + render_table( + columns_headers=['Option', 'Description', 'Available', 'Auto', 'Reason'], + columns_alignment=['left', 'left', 'center', 'center', 'left'], + columns_data=[ + [ + status.name, + status.description, + 'yes' if status.available else 'no', + 'yes' if status.auto_included else 'no', + status.reason or '-', + ] + for status in statuses + ], + ) + + @staticmethod + def _normalize_include(include: str | tuple[str, ...]) -> tuple[str, ...]: + """Validate and normalize a ``pattern(include=...)`` value.""" + values = (include,) if isinstance(include, str) else include + if not values: + msg = 'include must contain at least one option.' + raise ValueError(msg) + + normalized = tuple(dict.fromkeys(values)) + unknown = [value for value in normalized if value not in _PATTERN_OPTION_DESCRIPTIONS] + if unknown: + msg = f'Unknown pattern include option(s): {unknown}.' + raise ValueError(msg) + if 'auto' in normalized and len(normalized) > 1: + msg = "include='auto' cannot be combined with other options." + raise ValueError(msg) + return normalized + + @staticmethod + def _status_by_name( + statuses: list[PatternOptionStatus], + option_name: str, + ) -> PatternOptionStatus: + """Return one pattern option status by name.""" + for status in statuses: + if status.name == option_name: + return status + msg = f'Unknown pattern option: {option_name}.' + raise ValueError(msg) + + def _show_point_estimate_pattern( + self, + *, + expt_name: str, + x_min: float | None, + x_max: float | None, + include: tuple[str, ...], + x: object | None, + ) -> None: + """Dispatch a point-estimate pattern view to the live plotter.""" + include_set = set(include) + if include_set == {'measured'}: + self._project.rendering.plotter.plot_meas( + expt_name=expt_name, + x_min=x_min, + x_max=x_max, + x=x, + ) + return + if include_set == {'calculated'}: + self._project.rendering.plotter.plot_calc( + expt_name=expt_name, + x_min=x_min, + x_max=x_max, + x=x, + ) + return + if {'measured', 'calculated'}.issubset(include_set): + self._project.rendering.plotter.plot_meas_vs_calc( + expt_name=expt_name, + x_min=x_min, + x_max=x_max, + show_residual='residual' in include_set, + x=x, + ) + return + + msg = ( + 'Point-estimate pattern views currently support include values ' + "'measured', 'calculated', or combinations containing both." + ) + raise ValueError(msg) + + def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: + """Return availability details for the requested experiment.""" + experiment = self._project.experiments[expt_name] + pattern = intensity_category_for(experiment) + sample_form = experiment.type.sample_form.value + scattering_type = experiment.type.scattering_type.value + + measured_available = getattr(pattern, 'intensity_meas', None) is not None + calculated_available = getattr(pattern, 'intensity_calc', None) is not None + background_available = getattr(pattern, 'intensity_bkg', None) is not None + bragg_available = ( + sample_form == SampleFormEnum.POWDER.value + and scattering_type == ScatteringTypeEnum.BRAGG.value + and getattr(experiment, 'refln', None) is not None + ) + residual_available = ( + sample_form == SampleFormEnum.POWDER.value + and measured_available + and calculated_available + ) + excluded_regions = getattr(experiment, 'excluded_regions', None) + excluded_available = excluded_regions is not None and len(excluded_regions) > 0 + uncertainty_available, uncertainty_reason = self._uncertainty_status() + + auto_uses_uncertainty = uncertainty_available + auto_measured = measured_available + auto_calculated = calculated_available + auto_background = background_available and calculated_available + auto_residual = residual_available + auto_bragg = bragg_available and measured_available and calculated_available + + return [ + PatternOptionStatus( + name='auto', + description=_PATTERN_OPTION_DESCRIPTIONS['auto'], + available=measured_available or calculated_available or uncertainty_available, + auto_included=True, + reason='' if measured_available or calculated_available or uncertainty_available else ( + 'No measured, calculated, or posterior predictive data is available.' + ), + ), + PatternOptionStatus( + name='measured', + description=_PATTERN_OPTION_DESCRIPTIONS['measured'], + available=measured_available, + auto_included=auto_measured, + reason='' if measured_available else 'Measured intensities are unavailable.', + ), + PatternOptionStatus( + name='calculated', + description=_PATTERN_OPTION_DESCRIPTIONS['calculated'], + available=calculated_available, + auto_included=auto_calculated, + reason='' if calculated_available else 'Calculated intensities are unavailable.', + ), + PatternOptionStatus( + name='background', + description=_PATTERN_OPTION_DESCRIPTIONS['background'], + available=background_available, + auto_included=auto_background, + reason='' if background_available else 'Background intensities are unavailable.', + ), + PatternOptionStatus( + name='residual', + description=_PATTERN_OPTION_DESCRIPTIONS['residual'], + available=residual_available, + auto_included=auto_residual, + reason='' if residual_available else 'Residuals currently require powder measured and calculated data.', + ), + PatternOptionStatus( + name='bragg', + description=_PATTERN_OPTION_DESCRIPTIONS['bragg'], + available=bragg_available, + auto_included=auto_bragg, + reason='' if bragg_available else 'Bragg tick marks require powder Bragg reflection data.', + ), + PatternOptionStatus( + name='excluded', + description=_PATTERN_OPTION_DESCRIPTIONS['excluded'], + available=excluded_available, + auto_included=False, + reason='' if excluded_available else 'No excluded regions are defined for this experiment.', + ), + PatternOptionStatus( + name='uncertainty', + description=_PATTERN_OPTION_DESCRIPTIONS['uncertainty'], + available=uncertainty_available, + auto_included=auto_uses_uncertainty, + reason=uncertainty_reason, + ), + ] + + def _uncertainty_status(self) -> tuple[bool, str]: + """Return whether posterior predictive uncertainty is available.""" + fit_results = getattr(self._project.analysis, 'fit_results', None) + if fit_results is None: + return False, 'No fit results are available.' + + posterior_samples = getattr(fit_results, 'posterior_samples', None) + posterior_predictive = getattr(fit_results, 'posterior_predictive', None) + if posterior_samples is None or posterior_predictive is None: + return False, 'Posterior predictive data is unavailable.' + + if self._project.rendering.chart_engine.value != PlotterEngineEnum.PLOTLY.value: + return False, 'Uncertainty bands currently require the Plotly chart engine.' + + return True, '' \ No newline at end of file diff --git a/src/easydiffraction/project/project.py b/src/easydiffraction/project/project.py index 0cb46528..b9c0c7e0 100644 --- a/src/easydiffraction/project/project.py +++ b/src/easydiffraction/project/project.py @@ -16,6 +16,7 @@ from easydiffraction.datablocks.structure.collection import Structures from easydiffraction.io.cif.serialize import project_config_to_cif from easydiffraction.io.cif.serialize import project_to_cif +from easydiffraction.project.display import ProjectDisplay from easydiffraction.project.categories.rendering import Rendering from easydiffraction.project.categories.rendering import RenderingFactory from easydiffraction.project.project_info import ProjectInfo @@ -84,7 +85,7 @@ def __init__( self._experiments = Experiments() self._rendering = RenderingFactory.create('default') self._rendering._parent = self - self._display = self._rendering + self._display = ProjectDisplay(self) self._analysis = Analysis(self) self._summary = Summary(self) self._saved = False @@ -158,7 +159,7 @@ def rendering(self) -> Rendering: return self._rendering @property - def display(self) -> Rendering: + def display(self) -> ProjectDisplay: """Current display entry-point bound to the project.""" return self._display From 9bf3ad8464bbd9b5ea05efc4c3afa0220628e7e9 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 13 May 2026 23:55:30 +0200 Subject: [PATCH 04/10] Move constraint display and standardize CIF helpers --- docs/dev/plan_display-ux.md | 10 +++---- src/easydiffraction/analysis/analysis.py | 28 +++++++------------ .../categories/constraints/default.py | 19 +++++++++++++ src/easydiffraction/io/cif/serialize.py | 2 +- src/easydiffraction/project/project.py | 2 +- src/easydiffraction/project/project_info.py | 5 +++- 6 files changed, 40 insertions(+), 26 deletions(-) diff --git a/docs/dev/plan_display-ux.md b/docs/dev/plan_display-ux.md index d0dbac57..96cfcc87 100644 --- a/docs/dev/plan_display-ux.md +++ b/docs/dev/plan_display-ux.md @@ -135,26 +135,26 @@ each commit, and stage only the files changed for that step. - Render a clear warning or error when a requested include is not available. -- [ ] Move parameter table displays under `project.display.parameters`. +- [x] Move parameter table displays under `project.display.parameters`. - Implement `all()`, `fittable()`, `free()`, `access()`, and `cif_uids()`. - Remove the primary public need for `project.analysis.display`. -- [ ] Move fit displays under `project.display.fit`. +- [x] Move fit displays under `project.display.fit`. - Implement `results()`. - Implement `correlations()`. - Implement `series(param, versus=...)`. -- [ ] Move Bayesian displays under `project.display.posterior`. +- [x] Move Bayesian displays under `project.display.posterior`. - Implement `pairs()`. - Implement `distribution(param)`. - Implement `predictive(expt_name=...)`. -- [ ] Move constraint reporting to +- [x] Move constraint reporting to `project.analysis.constraints.show()`. - Do not add `project.display.constraints()`. -- [ ] Standardize CIF display helpers. +- [x] Standardize CIF display helpers. - Convert `project.analysis.as_cif()` to a read-only `project.analysis.as_cif` property. - Ensure `project.analysis.show_as_cif()` pretty-prints CIF text with diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index 23b9fcf3..36723aee 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from __future__ import annotations + from contextlib import suppress import numpy as np @@ -342,20 +344,7 @@ def parameter_cif_uids(self) -> None: def constraints(self) -> None: """Print a table of all user-defined symbolic constraints.""" - analysis = self._analysis - if not analysis.constraints._items: - log.warning('No constraints defined.') - return - - rows = [[constraint.expression.value] for constraint in analysis.constraints] - - console.paragraph('User defined constraints') - render_table( - columns_headers=['expression'], - columns_alignment=['left'], - columns_data=rows, - ) - console.print(f'Constraints enabled: {analysis.constraints.enabled}') + self._analysis.constraints.show() def fit_results(self) -> None: """ @@ -381,10 +370,7 @@ def fit_results(self) -> None: def as_cif(self) -> None: """Render the analysis section as CIF in console.""" - cif_text: str = self._analysis.as_cif() - paragraph_title: str = 'Analysis ๐Ÿงฎ info as cif' - console.paragraph(paragraph_title) - render_cif(cif_text) + self._analysis.show_as_cif() class Analysis: @@ -900,6 +886,7 @@ def _update_categories( self.constraints_handler.set_constraints(self.constraints) self.constraints_handler.apply() + @property def as_cif(self) -> str: """ Serialize the analysis section to a CIF string. @@ -911,3 +898,8 @@ def as_cif(self) -> str: """ self._update_categories() return analysis_to_cif(self) + + def show_as_cif(self) -> None: + """Pretty-print the analysis section as CIF text.""" + console.paragraph('Analysis info as CIF') + render_cif(self.as_cif) diff --git a/src/easydiffraction/analysis/categories/constraints/default.py b/src/easydiffraction/analysis/categories/constraints/default.py index 63a4264d..fc1d74fd 100644 --- a/src/easydiffraction/analysis/categories/constraints/default.py +++ b/src/easydiffraction/analysis/categories/constraints/default.py @@ -18,6 +18,9 @@ from easydiffraction.core.validation import RegexValidator from easydiffraction.core.variable import StringDescriptor from easydiffraction.io.cif.handler import CifHandler +from easydiffraction.utils.logging import console +from easydiffraction.utils.logging import log +from easydiffraction.utils.utils import render_table class Constraint(CategoryItem): @@ -132,3 +135,19 @@ def create(self, *, expression: str) -> None: item.expression = expression self.add(item) self._enabled = True + + def show(self) -> None: + """Print a table of all user-defined symbolic constraints.""" + if not self._items: + log.warning('No constraints defined.') + return + + rows = [[constraint.expression.value] for constraint in self] + + console.paragraph('User defined constraints') + render_table( + columns_headers=['expression'], + columns_alignment=['left'], + columns_data=rows, + ) + console.print(f'Constraints enabled: {self.enabled}') diff --git a/src/easydiffraction/io/cif/serialize.py b/src/easydiffraction/io/cif/serialize.py index bd74c822..b71e69e2 100644 --- a/src/easydiffraction/io/cif/serialize.py +++ b/src/easydiffraction/io/cif/serialize.py @@ -350,7 +350,7 @@ def project_to_cif(project: object) -> str: if getattr(project, 'experiments', None): parts.append(_as_cif_text(project.experiments)) if getattr(project, 'analysis', None): - parts.append(project.analysis.as_cif()) + parts.append(_as_cif_text(project.analysis)) if getattr(project, 'summary', None): parts.append(project.summary.as_cif()) return '\n\n'.join([p for p in parts if p]) diff --git a/src/easydiffraction/project/project.py b/src/easydiffraction/project/project.py index b9c0c7e0..df1d38b8 100644 --- a/src/easydiffraction/project/project.py +++ b/src/easydiffraction/project/project.py @@ -380,7 +380,7 @@ def save(self) -> None: analysis_dir = self._info.path / 'analysis' analysis_dir.mkdir(parents=True, exist_ok=True) with (analysis_dir / 'analysis.cif').open('w') as f: - f.write(self.analysis.as_cif()) + f.write(self.analysis.as_cif) console.print('โ”œโ”€โ”€ ๐Ÿ“ analysis/') console.print('โ”‚ โ””โ”€โ”€ ๐Ÿ“„ analysis.cif') diff --git a/src/easydiffraction/project/project_info.py b/src/easydiffraction/project/project_info.py index 94247f33..9bdfac7b 100644 --- a/src/easydiffraction/project/project_info.py +++ b/src/easydiffraction/project/project_info.py @@ -2,6 +2,8 @@ # SPDX-License-Identifier: BSD-3-Clause """Project metadata container used by Project.""" +from __future__ import annotations + import datetime import pathlib @@ -121,6 +123,7 @@ def parameters(self) -> None: """List parameters (not implemented).""" # TODO: Consider moving to io.cif.serialize + @property def as_cif(self) -> str: """Export project metadata to CIF.""" return project_info_to_cif(self) @@ -129,6 +132,6 @@ def as_cif(self) -> str: def show_as_cif(self) -> None: """Pretty-print CIF via shared utilities.""" paragraph_title: str = f"Project ๐Ÿ“ฆ '{self.name}' info as CIF" - cif_text: str = self.as_cif() + cif_text: str = self.as_cif console.paragraph(paragraph_title) render_cif(cif_text) From ce52428f02b5ba05224fb71c6ce34e4eb4392090 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Wed, 13 May 2026 23:56:48 +0200 Subject: [PATCH 05/10] Refine display pattern routing --- src/easydiffraction/project/display.py | 214 +++++++++++++++++++------ 1 file changed, 169 insertions(+), 45 deletions(-) diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py index d3f05c38..4b33205f 100644 --- a/src/easydiffraction/project/display.py +++ b/src/easydiffraction/project/display.py @@ -185,15 +185,20 @@ def pattern( ) -> None: """Show a pattern view for one experiment.""" normalized_include = self._normalize_include(include) + statuses = self._pattern_option_statuses(expt_name) if normalized_include == ('auto',): - auto_options = self._pattern_option_statuses(expt_name) - if self._status_by_name(auto_options, 'uncertainty').available: + auto_include = self._auto_include(statuses) + if not auto_include: + msg = self._status_by_name(statuses, 'auto').reason + raise ValueError(msg) + if 'uncertainty' in auto_include: self.posterior.predictive( expt_name=expt_name, style='band', x_min=x_min, x_max=x_max, + show_residual=True if 'residual' in auto_include else None, x=x, ) return @@ -201,28 +206,12 @@ def pattern( expt_name=expt_name, x_min=x_min, x_max=x_max, - include=('measured', 'calculated'), + include=auto_include, x=x, ) return - if normalized_include == ('measured',): - self._project.rendering.plotter.plot_meas( - expt_name=expt_name, - x_min=x_min, - x_max=x_max, - x=x, - ) - return - - if normalized_include == ('calculated',): - self._project.rendering.plotter.plot_calc( - expt_name=expt_name, - x_min=x_min, - x_max=x_max, - x=x, - ) - return + self._validate_requested_include(statuses, normalized_include) if 'uncertainty' in normalized_include: self.posterior.predictive( @@ -291,6 +280,66 @@ def _status_by_name( msg = f'Unknown pattern option: {option_name}.' raise ValueError(msg) + @classmethod + def _auto_include( + cls, + statuses: list[PatternOptionStatus], + ) -> tuple[str, ...]: + """Return the effective include tuple for ``include='auto'``.""" + status_by_name = {status.name: status for status in statuses} + if status_by_name['uncertainty'].available: + include = ['measured', 'calculated', 'uncertainty'] + if status_by_name['background'].available: + include.append('background') + if status_by_name['residual'].available: + include.append('residual') + if status_by_name['bragg'].available: + include.append('bragg') + return tuple(include) + if status_by_name['measured'].available and status_by_name['calculated'].available: + include = ['measured', 'calculated'] + if status_by_name['background'].available: + include.append('background') + if status_by_name['residual'].available: + include.append('residual') + if status_by_name['bragg'].available: + include.append('bragg') + return tuple(include) + if status_by_name['measured'].available: + return ('measured',) + if status_by_name['calculated'].available: + return ('calculated',) + return () + + @classmethod + def _validate_requested_include( + cls, + statuses: list[PatternOptionStatus], + include: tuple[str, ...], + ) -> None: + """Raise a clear error when a requested include is unavailable.""" + status_by_name = {status.name: status for status in statuses} + unavailable = [ + option_name + for option_name in include + if option_name != 'auto' and not status_by_name[option_name].available + ] + if unavailable: + option_name = unavailable[0] + msg = status_by_name[option_name].reason + raise ValueError(msg) + + include_set = set(include) + if 'background' in include_set and not {'measured', 'calculated'}.issubset(include_set): + msg = 'background requires both measured and calculated data in the same view.' + raise ValueError(msg) + if 'bragg' in include_set and not {'measured', 'calculated'}.issubset(include_set): + msg = 'bragg requires both measured and calculated data in the same view.' + raise ValueError(msg) + if 'residual' in include_set and not {'measured', 'calculated'}.issubset(include_set): + msg = 'residual requires both measured and calculated data in the same view.' + raise ValueError(msg) + def _show_point_estimate_pattern( self, *, @@ -301,6 +350,8 @@ def _show_point_estimate_pattern( x: object | None, ) -> None: """Dispatch a point-estimate pattern view to the live plotter.""" + statuses = self._pattern_option_statuses(expt_name) + self._validate_requested_include(statuses, include) include_set = set(include) if include_set == {'measured'}: self._project.rendering.plotter.plot_meas( @@ -343,9 +394,15 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: measured_available = getattr(pattern, 'intensity_meas', None) is not None calculated_available = getattr(pattern, 'intensity_calc', None) is not None - background_available = getattr(pattern, 'intensity_bkg', None) is not None + background_available = ( + measured_available + and calculated_available + and getattr(pattern, 'intensity_bkg', None) is not None + ) bragg_available = ( - sample_form == SampleFormEnum.POWDER.value + measured_available + and calculated_available + and sample_form == SampleFormEnum.POWDER.value and scattering_type == ScatteringTypeEnum.BRAGG.value and getattr(experiment, 'refln', None) is not None ) @@ -355,79 +412,146 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: and calculated_available ) excluded_regions = getattr(experiment, 'excluded_regions', None) - excluded_available = excluded_regions is not None and len(excluded_regions) > 0 - uncertainty_available, uncertainty_reason = self._uncertainty_status() + has_excluded_regions = excluded_regions is not None and len(excluded_regions) > 0 + excluded_available = False + uncertainty_available, uncertainty_reason = self._uncertainty_status( + measured_available=measured_available, + sample_form=sample_form, + scattering_type=scattering_type, + ) - auto_uses_uncertainty = uncertainty_available - auto_measured = measured_available - auto_calculated = calculated_available - auto_background = background_available and calculated_available - auto_residual = residual_available - auto_bragg = bragg_available and measured_available and calculated_available + auto_include = self._auto_include( + [ + PatternOptionStatus( + name='measured', + description=_PATTERN_OPTION_DESCRIPTIONS['measured'], + available=measured_available, + auto_included=False, + reason='', + ), + PatternOptionStatus( + name='calculated', + description=_PATTERN_OPTION_DESCRIPTIONS['calculated'], + available=calculated_available, + auto_included=False, + reason='', + ), + PatternOptionStatus( + name='background', + description=_PATTERN_OPTION_DESCRIPTIONS['background'], + available=background_available, + auto_included=False, + reason='', + ), + PatternOptionStatus( + name='residual', + description=_PATTERN_OPTION_DESCRIPTIONS['residual'], + available=residual_available, + auto_included=False, + reason='', + ), + PatternOptionStatus( + name='bragg', + description=_PATTERN_OPTION_DESCRIPTIONS['bragg'], + available=bragg_available, + auto_included=False, + reason='', + ), + PatternOptionStatus( + name='uncertainty', + description=_PATTERN_OPTION_DESCRIPTIONS['uncertainty'], + available=uncertainty_available, + auto_included=False, + reason='', + ), + ] + ) return [ PatternOptionStatus( name='auto', description=_PATTERN_OPTION_DESCRIPTIONS['auto'], - available=measured_available or calculated_available or uncertainty_available, + available=bool(auto_include), auto_included=True, - reason='' if measured_available or calculated_available or uncertainty_available else ( - 'No measured, calculated, or posterior predictive data is available.' - ), + reason='' if auto_include else 'No supported pattern content is available.', ), PatternOptionStatus( name='measured', description=_PATTERN_OPTION_DESCRIPTIONS['measured'], available=measured_available, - auto_included=auto_measured, + auto_included='measured' in auto_include, reason='' if measured_available else 'Measured intensities are unavailable.', ), PatternOptionStatus( name='calculated', description=_PATTERN_OPTION_DESCRIPTIONS['calculated'], available=calculated_available, - auto_included=auto_calculated, + auto_included='calculated' in auto_include, reason='' if calculated_available else 'Calculated intensities are unavailable.', ), PatternOptionStatus( name='background', description=_PATTERN_OPTION_DESCRIPTIONS['background'], available=background_available, - auto_included=auto_background, - reason='' if background_available else 'Background intensities are unavailable.', + auto_included='background' in auto_include, + reason='' if background_available else ( + 'Background display requires measured and calculated data plus background intensities.' + ), ), PatternOptionStatus( name='residual', description=_PATTERN_OPTION_DESCRIPTIONS['residual'], available=residual_available, - auto_included=auto_residual, - reason='' if residual_available else 'Residuals currently require powder measured and calculated data.', + auto_included='residual' in auto_include, + reason='' if residual_available else ( + 'Residuals currently require powder measured and calculated data.' + ), ), PatternOptionStatus( name='bragg', description=_PATTERN_OPTION_DESCRIPTIONS['bragg'], available=bragg_available, - auto_included=auto_bragg, - reason='' if bragg_available else 'Bragg tick marks require powder Bragg reflection data.', + auto_included='bragg' in auto_include, + reason='' if bragg_available else ( + 'Bragg tick marks require powder Bragg measured and calculated data with reflection rows.' + ), ), PatternOptionStatus( name='excluded', description=_PATTERN_OPTION_DESCRIPTIONS['excluded'], available=excluded_available, auto_included=False, - reason='' if excluded_available else 'No excluded regions are defined for this experiment.', + reason='Excluded-region overlays are not implemented in pattern() yet.' + if has_excluded_regions + else 'No excluded regions are defined for this experiment.', ), PatternOptionStatus( name='uncertainty', description=_PATTERN_OPTION_DESCRIPTIONS['uncertainty'], available=uncertainty_available, - auto_included=auto_uses_uncertainty, + auto_included='uncertainty' in auto_include, reason=uncertainty_reason, ), ] - def _uncertainty_status(self) -> tuple[bool, str]: + def _uncertainty_status( + self, + *, + measured_available: bool, + sample_form: str, + scattering_type: str, + ) -> tuple[bool, str]: """Return whether posterior predictive uncertainty is available.""" + if not measured_available: + return False, 'Uncertainty bands require measured data.' + + supported_sample_form = sample_form == SampleFormEnum.POWDER.value or ( + sample_form == SampleFormEnum.SINGLE_CRYSTAL.value + and scattering_type == ScatteringTypeEnum.BRAGG.value + ) + if not supported_sample_form: + return False, 'Posterior predictive pattern views are unavailable for this experiment type.' + fit_results = getattr(self._project.analysis, 'fit_results', None) if fit_results is None: return False, 'No fit results are available.' From 11346b987dc06616ab09c4e49ab7f8220c3ecebb Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 14 May 2026 00:06:46 +0200 Subject: [PATCH 06/10] Implement state-aware pattern display --- docs/dev/plan_display-ux.md | 2 +- src/easydiffraction/display/plotters/ascii.py | 7 ++ src/easydiffraction/display/plotters/base.py | 4 + .../display/plotters/plotly.py | 33 ++++++ src/easydiffraction/display/plotting.py | 109 ++++++++++++++++++ src/easydiffraction/project/display.py | 64 ++++++++-- 6 files changed, 209 insertions(+), 10 deletions(-) diff --git a/docs/dev/plan_display-ux.md b/docs/dev/plan_display-ux.md index 96cfcc87..bf431fe6 100644 --- a/docs/dev/plan_display-ux.md +++ b/docs/dev/plan_display-ux.md @@ -124,7 +124,7 @@ each commit, and stage only the files changed for that step. - Add `pattern(...)` and `show_pattern_options(...)`. - Add `parameters`, `fit`, and `posterior` namespace objects. -- [ ] Implement `pattern(..., include='auto')`. +- [x] Implement `pattern(..., include='auto')`. - Replace common user-facing calls to `plot_meas`, `plot_calc`, and `plot_meas_vs_calc` with one state-aware method. - Support explicit includes for `measured`, `calculated`, diff --git a/src/easydiffraction/display/plotters/ascii.py b/src/easydiffraction/display/plotters/ascii.py index 4aeb5577..a2ab913e 100644 --- a/src/easydiffraction/display/plotters/ascii.py +++ b/src/easydiffraction/display/plotters/ascii.py @@ -61,6 +61,7 @@ def plot_powder( axes_labels: object, title: str, height: int | None = None, + excluded_ranges: tuple[tuple[float, float], ...] = (), ) -> None: """ Render a line plot for powder diffraction data. @@ -100,6 +101,11 @@ def plot_powder( console.print( f'Displaying data for selected x-range from {x[0]} to {x[-1]} ({len(x)} points)' ) + if excluded_ranges: + formatted_ranges = ', '.join( + f'[{start:,.2f}, {end:,.2f}]' for start, end in excluded_ranges + ) + console.print(f'Excluded regions: {formatted_ranges}') console.print(f'Legend:\n{legend}') padded = '\n'.join(' ' + line for line in chart.splitlines()) @@ -130,6 +136,7 @@ def plot_powder_meas_vs_calc( axes_labels=plot_spec.axes_labels, title=plot_spec.title, height=plot_spec.height, + excluded_ranges=plot_spec.excluded_ranges, ) if plot_spec.predictive_lower_95 is not None and plot_spec.predictive_upper_95 is not None: console.print('Posterior predictive bands are available with the Plotly engine only.') diff --git a/src/easydiffraction/display/plotters/base.py b/src/easydiffraction/display/plotters/base.py index d4be0d67..4cf43d66 100644 --- a/src/easydiffraction/display/plotters/base.py +++ b/src/easydiffraction/display/plotters/base.py @@ -65,6 +65,7 @@ class PowderMeasVsCalcSpec: predictive_draws: np.ndarray | None = None y_calc_name: str | None = None y_calc_line_dash: str | None = None + excluded_ranges: tuple[tuple[float, float], ...] = () class XAxisType(StrEnum): @@ -229,6 +230,7 @@ def plot_powder( axes_labels: object, title: str, height: int | None, + excluded_ranges: tuple[tuple[float, float], ...] = (), ) -> None: """ Render a line plot for powder diffraction data. @@ -250,6 +252,8 @@ def plot_powder( Figure title. height : int | None Backend-specific height (text rows or pixels). + excluded_ranges : tuple[tuple[float, float], ...], default=() + Closed x-intervals to highlight as excluded regions. """ @abstractmethod diff --git a/src/easydiffraction/display/plotters/plotly.py b/src/easydiffraction/display/plotters/plotly.py index cb4914c3..1aef6eb1 100644 --- a/src/easydiffraction/display/plotters/plotly.py +++ b/src/easydiffraction/display/plotters/plotly.py @@ -70,6 +70,7 @@ PREDICTIVE_BAND_COLOR = 'rgba(214, 39, 40, 0.14)' PREDICTIVE_BAND_EDGE_COLOR = 'rgba(214, 39, 40, 0.45)' PREDICTIVE_DRAW_COLOR = 'rgba(140, 140, 140, 0.18)' +EXCLUDED_REGION_FILL_COLOR = 'rgba(120, 120, 120, 0.16)' PREDICTIVE_DRAW_PLOT_CAP = 50 PREDICTIVE_DRAW_ARRAY_NDIM = 2 FIXED_ASPECT_WRAPPER_META_KEY = 'fixed_aspect_wrapper' @@ -1038,6 +1039,7 @@ def plot_powder( axes_labels: object, title: str, height: int | None = None, + excluded_ranges: tuple[tuple[float, float], ...] = (), ) -> None: """ Render a line plot for powder diffraction data. @@ -1075,8 +1077,33 @@ def plot_powder( ) fig = self._get_figure(data, layout) + self._add_excluded_region_vrects(fig=fig, excluded_ranges=excluded_ranges) self._show_figure(fig) + @staticmethod + def _add_excluded_region_vrects( + *, + fig: object, + excluded_ranges: tuple[tuple[float, float], ...], + row: object | None = None, + col: int | None = None, + ) -> None: + """Shade excluded x-ranges on a Plotly figure.""" + for start, end in excluded_ranges: + add_kwargs = { + 'x0': start, + 'x1': end, + 'fillcolor': EXCLUDED_REGION_FILL_COLOR, + 'opacity': 1.0, + 'line_width': 0, + 'layer': 'below', + } + if row is not None: + add_kwargs['row'] = row + if col is not None: + add_kwargs['col'] = col + fig.add_vrect(**add_kwargs) + @staticmethod def _get_bragg_tick_trace( tick_set: BraggTickSet, @@ -1386,6 +1413,12 @@ def plot_powder_meas_vs_calc( hover_data=hover_data, hover_template=hover_template, ) + self._add_excluded_region_vrects( + fig=fig, + excluded_ranges=plot_spec.excluded_ranges, + row='all', + col=1, + ) self._configure_powder_composite_layout(fig=fig, plot_spec=plot_spec, layout=layout) self._configure_powder_composite_axes( fig=fig, diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index 34b0da10..c991960a 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -179,6 +179,7 @@ class _MeasVsCalcPlotOptions: x_min: float | None = None x_max: float | None = None show_residual: bool | None = None + show_excluded: bool = False x: object | None = None @@ -574,6 +575,8 @@ def plot_meas( x_min: float | None = None, x_max: float | None = None, x: object | None = None, + *, + show_excluded: bool = False, ) -> None: """ Plot measured diffraction data for an experiment. @@ -592,12 +595,14 @@ def plot_meas( self._update_project_categories(expt_name) experiment = self._project.experiments[expt_name] self._plot_meas_data( + experiment, intensity_category_for(experiment), expt_name, experiment.type, x_min=x_min, x_max=x_max, x=x, + show_excluded=show_excluded, ) def plot_calc( @@ -606,6 +611,8 @@ def plot_calc( x_min: float | None = None, x_max: float | None = None, x: object | None = None, + *, + show_excluded: bool = False, ) -> None: """ Plot calculated diffraction pattern for an experiment. @@ -624,12 +631,14 @@ def plot_calc( self._update_project_categories(expt_name) experiment = self._project.experiments[expt_name] self._plot_calc_data( + experiment, intensity_category_for(experiment), expt_name, experiment.type, x_min=x_min, x_max=x_max, x=x, + show_excluded=show_excluded, ) def plot_meas_vs_calc( @@ -639,6 +648,7 @@ def plot_meas_vs_calc( x_max: float | None = None, *, show_residual: bool | None = None, + show_excluded: bool = False, x: object | None = None, ) -> None: """ @@ -665,6 +675,7 @@ def plot_meas_vs_calc( x_min=x_min, x_max=x_max, show_residual=show_residual, + show_excluded=show_excluded, x=x, ) self._plot_meas_vs_calc_data( @@ -990,6 +1001,7 @@ def plot_posterior_predictive( x_max: float | None = None, *, show_residual: bool | None = None, + show_excluded: bool = False, x: object | None = None, ) -> None: """ @@ -1040,6 +1052,7 @@ def plot_posterior_predictive( x_min=x_min, x_max=x_max, show_residual=show_residual, + show_excluded=show_excluded, x=x, ) @@ -1221,6 +1234,15 @@ def _plot_non_bragg_posterior_predictive( ctx['x_min'], ctx['x_max'], ) + excluded_ranges = ( + self._excluded_ranges( + experiment=experiment, + x_min=ctx['x_min'], + x_max=ctx['x_max'], + ) + if plot_options.show_excluded + else () + ) axes_labels = self._get_axes_labels(sample_form, scattering_type, x_axis) self._plot_posterior_predictive_summary( @@ -1230,6 +1252,7 @@ def _plot_non_bragg_posterior_predictive( axes_labels=axes_labels, show_band=style in {'band', 'band+draws'}, show_draws=style in {'draws', 'band+draws'}, + excluded_ranges=excluded_ranges, ) @staticmethod @@ -3377,6 +3400,7 @@ def _plot_posterior_predictive_summary( axes_labels: list[str], show_band: bool, show_draws: bool, + excluded_ranges: tuple[tuple[float, float], ...] = (), ) -> None: """Render posterior predictive summaries using Plotly.""" go = __import__('plotly.graph_objects', fromlist=['Figure', 'Scatter']) @@ -3452,6 +3476,15 @@ def _plot_posterior_predictive_summary( legendrank=20, ) ) + for start, end in excluded_ranges: + fig.add_vrect( + x0=start, + x1=end, + fillcolor='rgba(120, 120, 120, 0.16)', + opacity=1.0, + line_width=0, + layer='below', + ) fig.update_layout( title={ 'text': f"Posterior predictive for experiment ๐Ÿ”ฌ '{expt_name}'", @@ -3679,6 +3712,15 @@ def _plot_posterior_predictive_data( x_min=ctx['x_min'], x_max=ctx['x_max'], ) + excluded_ranges = ( + self._excluded_ranges( + experiment=experiment, + x_min=ctx['x_min'], + x_max=ctx['x_max'], + ) + if plot_options.show_excluded + else () + ) plot_spec = PowderMeasVsCalcSpec( x=ctx['x_filtered'], @@ -3697,6 +3739,7 @@ def _plot_posterior_predictive_data( predictive_draws=predictive_draws, y_calc_name=POSTERIOR_POINT_ESTIMATE_TRACE_NAME, y_calc_line_dash=POSTERIOR_POINT_ESTIMATE_LINE_DASH, + excluded_ranges=excluded_ranges, ) self._backend.plot_powder_meas_vs_calc(plot_spec=plot_spec) @@ -4646,12 +4689,14 @@ def _format_correlation_table_dataframe( def _plot_meas_data( self, + experiment: object, pattern: object, expt_name: str, expt_type: object, x_min: object = None, x_max: object = None, x: object = None, + show_excluded: bool = False, ) -> None: """ Plot measured pattern using the current engine. @@ -4689,6 +4734,15 @@ def _plot_meas_data( y_meas = self._filtered_y_array( pattern.intensity_meas, ctx['x_array'], ctx['x_min'], ctx['x_max'] ) + excluded_ranges = ( + self._excluded_ranges( + experiment=experiment, + x_min=ctx['x_min'], + x_max=ctx['x_max'], + ) + if show_excluded + else () + ) self._backend.plot_powder( x=ctx['x_filtered'], @@ -4697,16 +4751,19 @@ def _plot_meas_data( axes_labels=ctx['axes_labels'], title=f"Measured data for experiment ๐Ÿ”ฌ '{expt_name}'", height=self.height, + excluded_ranges=excluded_ranges, ) def _plot_calc_data( self, + experiment: object, pattern: object, expt_name: str, expt_type: object, x_min: object = None, x_max: object = None, x: object = None, + show_excluded: bool = False, ) -> None: """ Plot calculated pattern using the current engine. @@ -4744,6 +4801,15 @@ def _plot_calc_data( y_calc = self._filtered_y_array( pattern.intensity_calc, ctx['x_array'], ctx['x_min'], ctx['x_max'] ) + excluded_ranges = ( + self._excluded_ranges( + experiment=experiment, + x_min=ctx['x_min'], + x_max=ctx['x_max'], + ) + if show_excluded + else () + ) self._backend.plot_powder( x=ctx['x_filtered'], @@ -4752,6 +4818,7 @@ def _plot_calc_data( axes_labels=ctx['axes_labels'], title=f"Calculated data for experiment ๐Ÿ”ฌ '{expt_name}'", height=self.height, + excluded_ranges=excluded_ranges, ) def _plot_meas_vs_calc_data( @@ -4843,6 +4910,15 @@ def _plot_meas_vs_calc_data( y_calc=y_calc, y_bkg=y_bkg, ) + excluded_ranges = ( + self._excluded_ranges( + experiment=experiment, + x_min=ctx['x_min'], + x_max=ctx['x_max'], + ) + if plot_options.show_excluded + else () + ) if sample_form == SampleFormEnum.POWDER and scattering_type == ScatteringTypeEnum.BRAGG: self._plot_powder_bragg_meas_vs_calc( @@ -4852,6 +4928,7 @@ def _plot_meas_vs_calc_data( series=powder_series, plot_options=plot_options, title=title, + excluded_ranges=excluded_ranges, ) return @@ -4863,6 +4940,7 @@ def _plot_meas_vs_calc_data( if plot_options.show_residual is None else plot_options.show_residual, title=title, + excluded_ranges=excluded_ranges, ) def _plot_single_crystal_meas_vs_calc( @@ -4901,6 +4979,7 @@ def _plot_powder_bragg_meas_vs_calc( series: _PowderMeasVsCalcSeries, plot_options: _MeasVsCalcPlotOptions, title: str, + excluded_ranges: tuple[tuple[float, float], ...], ) -> None: """ Render the composite powder Bragg measured-vs-calculated plot. @@ -4930,6 +5009,7 @@ def _plot_powder_bragg_meas_vs_calc( bragg_peaks_height_fraction=DEFAULT_BRAGG_ROW, height=self._composite_plot_height(), y_bkg=series.y_bkg, + excluded_ranges=excluded_ranges, ) self._backend.plot_powder_meas_vs_calc(plot_spec=plot_spec) @@ -4941,6 +5021,7 @@ def _plot_line_meas_vs_calc( *, show_residual: bool, title: str, + excluded_ranges: tuple[tuple[float, float], ...] = (), ) -> None: """ Render the non-composite line version of measured-vs-calculated. @@ -4958,8 +5039,36 @@ def _plot_line_meas_vs_calc( axes_labels=ctx['axes_labels'], title=title, height=self.height, + excluded_ranges=excluded_ranges, ) + @staticmethod + def _excluded_ranges( + *, + experiment: object, + x_min: float | None, + x_max: float | None, + ) -> tuple[tuple[float, float], ...]: + """Return excluded x-ranges clipped to the current view.""" + excluded_regions = getattr(experiment, 'excluded_regions', None) + if excluded_regions is None: + return () + + clipped_ranges: list[tuple[float, float]] = [] + lower_bound = -np.inf if x_min is None else float(x_min) + upper_bound = np.inf if x_max is None else float(x_max) + + for region in excluded_regions: + start = float(region.start.value) + end = float(region.end.value) + clipped_start = max(start, lower_bound) + clipped_end = min(end, upper_bound) + if clipped_start > clipped_end: + continue + clipped_ranges.append((clipped_start, clipped_end)) + + return tuple(clipped_ranges) + @staticmethod def _extract_bragg_tick_sets( experiment: object, diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py index 4b33205f..4ffbd460 100644 --- a/src/easydiffraction/project/display.py +++ b/src/easydiffraction/project/display.py @@ -189,16 +189,19 @@ def pattern( if normalized_include == ('auto',): auto_include = self._auto_include(statuses) + if x is not None: + auto_include = tuple(option for option in auto_include if option != 'excluded') if not auto_include: msg = self._status_by_name(statuses, 'auto').reason raise ValueError(msg) if 'uncertainty' in auto_include: - self.posterior.predictive( + self._project.rendering.plotter.plot_posterior_predictive( expt_name=expt_name, style='band', x_min=x_min, x_max=x_max, show_residual=True if 'residual' in auto_include else None, + show_excluded='excluded' in auto_include, x=x, ) return @@ -212,14 +215,18 @@ def pattern( return self._validate_requested_include(statuses, normalized_include) + if x is not None and 'excluded' in normalized_include: + msg = "Excluded-region overlays currently require the experiment's default x-axis." + raise ValueError(msg) if 'uncertainty' in normalized_include: - self.posterior.predictive( + self._project.rendering.plotter.plot_posterior_predictive( expt_name=expt_name, style='band', x_min=x_min, x_max=x_max, show_residual=True if 'residual' in normalized_include else None, + show_excluded='excluded' in normalized_include, x=x, ) return @@ -295,6 +302,8 @@ def _auto_include( include.append('residual') if status_by_name['bragg'].available: include.append('bragg') + if status_by_name['excluded'].available: + include.append('excluded') return tuple(include) if status_by_name['measured'].available and status_by_name['calculated'].available: include = ['measured', 'calculated'] @@ -304,11 +313,17 @@ def _auto_include( include.append('residual') if status_by_name['bragg'].available: include.append('bragg') + if status_by_name['excluded'].available: + include.append('excluded') return tuple(include) if status_by_name['measured'].available: - return ('measured',) + return ('measured', 'excluded') if status_by_name['excluded'].available else ('measured',) if status_by_name['calculated'].available: - return ('calculated',) + return ( + ('calculated', 'excluded') + if status_by_name['excluded'].available + else ('calculated',) + ) return () @classmethod @@ -339,6 +354,11 @@ def _validate_requested_include( if 'residual' in include_set and not {'measured', 'calculated'}.issubset(include_set): msg = 'residual requires both measured and calculated data in the same view.' raise ValueError(msg) + if 'excluded' in include_set and not include_set.intersection( + {'measured', 'calculated', 'uncertainty'} + ): + msg = 'excluded requires measured, calculated, or uncertainty data in the same view.' + raise ValueError(msg) def _show_point_estimate_pattern( self, @@ -359,6 +379,16 @@ def _show_point_estimate_pattern( x_min=x_min, x_max=x_max, x=x, + show_excluded=False, + ) + return + if include_set == {'measured', 'excluded'}: + self._project.rendering.plotter.plot_meas( + expt_name=expt_name, + x_min=x_min, + x_max=x_max, + x=x, + show_excluded=True, ) return if include_set == {'calculated'}: @@ -367,6 +397,16 @@ def _show_point_estimate_pattern( x_min=x_min, x_max=x_max, x=x, + show_excluded=False, + ) + return + if include_set == {'calculated', 'excluded'}: + self._project.rendering.plotter.plot_calc( + expt_name=expt_name, + x_min=x_min, + x_max=x_max, + x=x, + show_excluded=True, ) return if {'measured', 'calculated'}.issubset(include_set): @@ -375,6 +415,7 @@ def _show_point_estimate_pattern( x_min=x_min, x_max=x_max, show_residual='residual' in include_set, + show_excluded='excluded' in include_set, x=x, ) return @@ -413,7 +454,7 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: ) excluded_regions = getattr(experiment, 'excluded_regions', None) has_excluded_regions = excluded_regions is not None and len(excluded_regions) > 0 - excluded_available = False + excluded_available = has_excluded_regions uncertainty_available, uncertainty_reason = self._uncertainty_status( measured_available=measured_available, sample_form=sample_form, @@ -457,6 +498,13 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: auto_included=False, reason='', ), + PatternOptionStatus( + name='excluded', + description=_PATTERN_OPTION_DESCRIPTIONS['excluded'], + available=excluded_available, + auto_included=False, + reason='', + ), PatternOptionStatus( name='uncertainty', description=_PATTERN_OPTION_DESCRIPTIONS['uncertainty'], @@ -520,10 +568,8 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: name='excluded', description=_PATTERN_OPTION_DESCRIPTIONS['excluded'], available=excluded_available, - auto_included=False, - reason='Excluded-region overlays are not implemented in pattern() yet.' - if has_excluded_regions - else 'No excluded regions are defined for this experiment.', + auto_included='excluded' in auto_include, + reason='' if excluded_available else 'No excluded regions are defined for this experiment.', ), PatternOptionStatus( name='uncertainty', From 08a6b6539e4a2554973799100fdd39839951caea Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 14 May 2026 00:12:50 +0200 Subject: [PATCH 07/10] Update display UX documentation --- docs/dev/architecture.md | 30 +++---- docs/dev/plan_display-ux.md | 2 +- docs/docs/tutorials/ed-1.py | 10 +-- docs/docs/tutorials/ed-10.py | 6 +- docs/docs/tutorials/ed-11.py | 12 +-- docs/docs/tutorials/ed-12.py | 12 +-- docs/docs/tutorials/ed-13.py | 88 +++++++++---------- docs/docs/tutorials/ed-14.py | 12 +-- docs/docs/tutorials/ed-15.py | 14 +-- docs/docs/tutorials/ed-16.py | 14 +-- docs/docs/tutorials/ed-17.py | 34 +++---- docs/docs/tutorials/ed-18.py | 6 +- docs/docs/tutorials/ed-2.py | 18 ++-- docs/docs/tutorials/ed-20.py | 18 ++-- docs/docs/tutorials/ed-21.py | 22 ++--- docs/docs/tutorials/ed-22.py | 20 ++--- docs/docs/tutorials/ed-3.py | 66 +++++++------- docs/docs/tutorials/ed-4.py | 4 +- docs/docs/tutorials/ed-5.py | 14 +-- docs/docs/tutorials/ed-6.py | 38 ++++---- docs/docs/tutorials/ed-7.py | 48 +++++----- docs/docs/tutorials/ed-8.py | 12 +-- docs/docs/tutorials/ed-9.py | 10 +-- .../user-guide/analysis-workflow/analysis.md | 8 +- .../user-guide/analysis-workflow/project.md | 4 +- docs/docs/user-guide/first-steps.md | 12 +-- 26 files changed, 267 insertions(+), 267 deletions(-) diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 48a41161..d6e338b0 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -922,7 +922,7 @@ project_dir/ ``` `project.cif` carries both the `_project.*` metadata and the -`_display.*` engine preferences (`plotter_type`, `tabler_type`), so a +`_rendering.*` engine preferences (`chart_engine`, `table_engine`), so a saved project re-opens with the same display backends. Per-experiment calculator selection (`_calculation.calculator_type`) lives in each experiment file, and fit configuration (`_fit.minimizer_type`, @@ -1064,7 +1064,7 @@ project.experiments['hrpt'].calculation.calculator_type = 'cryspy' project.analysis.fit.minimizer_type = 'lmfit' # Plot before fitting -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True) +project.display.pattern(expt_name='hrpt') # Select free parameters project.structures['lbco'].cell.length_a.free = True @@ -1073,14 +1073,14 @@ project.experiments['hrpt'].instrument.calib_twotheta_offset.free = True project.experiments['hrpt'].background['10'].y.free = True # Inspect free parameters -project.analysis.display.free_params() +project.display.parameters.free() # Fit and show results project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # Plot after fitting -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True) +project.display.pattern(expt_name='hrpt') # Save project.save() @@ -1100,11 +1100,11 @@ project.analysis.fit.minimizer.parallel = 0 project.analysis.fit(random_seed=11) # Runtime-only Bayesian summaries and plots -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() -project.display.plotter.plot_posterior_pairs() -project.display.plotter.plot_param_distribution(param) -project.display.plotter.plot_posterior_predictive(expt_name='hrpt') +project.display.fit.results() +project.display.fit.correlations() +project.display.posterior.pairs() +project.display.posterior.distribution(param) +project.display.posterior.predictive(expt_name='hrpt') ``` ### 8.5 TOF Experiment (tutorial ed-7) @@ -1225,8 +1225,8 @@ that owns calculator selection โ€” `experiment.calculation.calculator_type` and `experiment.calculation.show_calculator_types()` โ€” instead of the selector being exposed at the experiment owner level. The same pattern -applies to `display` on `Project`, which owns `plotter_type` and -`tabler_type` (see ยง9.4.1). +applies to `display` on `Project`, which owns `chart_engine` and +`table_engine` (see ยง9.4.1). **Design decisions:** @@ -1250,7 +1250,7 @@ their intent and ownership differ: | Family | User intent | Examples | CIF | | ---------------------------------- | ------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -| Backend selector | Pick an execution backend | `fit.minimizer_type`, `calculation.calculator_type`, `display.plotter_type` | `_fit.minimizer_type`, `_calculation.calculator_type`, `_display.plotter_type` | +| Backend selector | Pick an execution backend | `fit.minimizer_type`, `calculation.calculator_type`, `display.chart_engine` | `_fit.minimizer_type`, `_calculation.calculator_type`, `_rendering.chart_engine` | | Switchable-category impl. selector | Swap a category implementation | `experiment.background_type`, `experiment.peak_profile_type` | category-owned type tag such as `_peak.profile_type` | | Semantic value selector | Pick a scientific/analysis mode | `fit.mode` | `_fit.mode` | @@ -1272,8 +1272,8 @@ expt.calculation.show_calculator_types() expt.show_extinction_types() project.analysis.fit.show_minimizer_types() project.analysis.fit.show_modes() -project.display.show_plotter_types() -project.display.show_tabler_types() +project.rendering.show_chart_engines() +project.rendering.show_table_engines() ``` Available calculators are filtered by `engine_imported` (whether the diff --git a/docs/dev/plan_display-ux.md b/docs/dev/plan_display-ux.md index bf431fe6..61e205db 100644 --- a/docs/dev/plan_display-ux.md +++ b/docs/dev/plan_display-ux.md @@ -164,7 +164,7 @@ each commit, and stage only the files changed for that step. - Ensure `project.info.show_as_cif()` pretty-prints CIF text with a header. -- [ ] Update docs, tutorials, and architecture text. +- [x] Update docs, tutorials, and architecture text. - Replace old public display examples with the selected API. - Update `docs/dev/architecture.md`. - Update tutorial `.py` files only; regenerate notebooks in Phase 2 if diff --git a/docs/docs/tutorials/ed-1.py b/docs/docs/tutorials/ed-1.py index 0e2a5e55..e6312d4a 100644 --- a/docs/docs/tutorials/ed-1.py +++ b/docs/docs/tutorials/ed-1.py @@ -61,11 +61,11 @@ # %% # Show fit results summary -project.analysis.display.fit_results() +project.display.fit.results() # %% # Show parameter correlations -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% [markdown] # ## Step 5: Perform Analysis (with constraints) @@ -95,11 +95,11 @@ # %% # Show fit results summary -project.analysis.display.fit_results() +project.display.fit.results() # %% # Show parameter correlations -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% # Show defined experiment names @@ -107,4 +107,4 @@ # %% # Plot measured vs. calculated diffraction patterns -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') diff --git a/docs/docs/tutorials/ed-10.py b/docs/docs/tutorials/ed-10.py index 08f33f0c..9fb0b9c6 100644 --- a/docs/docs/tutorials/ed-10.py +++ b/docs/docs/tutorials/ed-10.py @@ -82,11 +82,11 @@ # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations(threshold=0.75) +project.display.fit.results() +project.display.fit.correlations(threshold=0.75) # %% [markdown] # ## Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='pdf', show_residual=True) +project.display.pattern(expt_name='pdf') diff --git a/docs/docs/tutorials/ed-11.py b/docs/docs/tutorials/ed-11.py index a48d2fd6..e4a1ec99 100644 --- a/docs/docs/tutorials/ed-11.py +++ b/docs/docs/tutorials/ed-11.py @@ -21,12 +21,12 @@ # ## Set Plotting Engine # %% -project.display.plotter.show_supported_engines() -project.display.plotter.show_current_engine() +project.rendering.show_chart_engines() +project.rendering.show_config() # %% # Set global plot range for plots -project.display.plotter.x_max = 40 +project.rendering.plotter.x_max = 40 # %% [markdown] # ## Add Structure @@ -94,11 +94,11 @@ # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # ## Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='nomad', show_residual=False) +project.display.pattern(expt_name='nomad', include=('measured', 'calculated')) diff --git a/docs/docs/tutorials/ed-12.py b/docs/docs/tutorials/ed-12.py index 78c722ce..090f948b 100644 --- a/docs/docs/tutorials/ed-12.py +++ b/docs/docs/tutorials/ed-12.py @@ -26,12 +26,12 @@ # %% # Keep the auto-selected engine. Alternatively, you can uncomment the # line below to explicitly set the engine to the required one. -# project.display.plotter.engine = 'plotly' +# project.rendering.chart_engine = 'plotly' # %% # Set global plot range for plots -project.display.plotter.x_min = 2.0 -project.display.plotter.x_max = 30.0 +project.rendering.plotter.x_min = 2.0 +project.rendering.plotter.x_max = 30.0 # %% [markdown] # ## Add Structure @@ -113,11 +113,11 @@ # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # ## Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='xray_pdf') +project.display.pattern(expt_name='xray_pdf') diff --git a/docs/docs/tutorials/ed-13.py b/docs/docs/tutorials/ed-13.py index e89f4288..97abcf1d 100644 --- a/docs/docs/tutorials/ed-13.py +++ b/docs/docs/tutorials/ed-13.py @@ -144,11 +144,11 @@ # [documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#measured-data-category) # for more details about the measured data and its format. # -# To visualize the measured data, we can use the `plot_meas` method of -# the project. +# To visualize the measured data, we can use the `pattern` method of +# the project's `display` facade with `include='measured'`. # %% -project_1.display.plotter.plot_meas(expt_name='sim_si') +project_1.display.pattern(expt_name='sim_si', include='measured') # %% [markdown] # If you zoom in on the highest TOF peak (around 120,000 ฮผs), you will @@ -179,11 +179,11 @@ # %% [markdown] # To visualize the effect of excluding the high TOF region, we can plot -# the measured data again. The excluded region will be omitted from the -# plot and is not used in the fitting process. +# the measured data again. The excluded region will be highlighted on +# the plot and is not used in the fitting process. # %% -project_1.display.plotter.plot_meas(expt_name='sim_si') +project_1.display.pattern(expt_name='sim_si', include=('measured', 'excluded')) # %% [markdown] # #### Set Instrument Parameters @@ -594,7 +594,7 @@ # - show only free parameters of the project. # %% -project_1.analysis.display.free_params() +project_1.display.parameters.free() # %% [markdown] # #### Visualize Diffraction Patterns @@ -603,11 +603,11 @@ # diffraction pattern with the calculated diffraction pattern based on # the initial parameters of the structure and the instrument. This # provides an indication of how well the initial parameters match the -# measured data. The `plot_meas_vs_calc` method of the project allows -# this comparison. +# measured data. The `pattern` method of the project's `display` +# facade allows this comparison. # %% -project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si') +project_1.display.pattern(expt_name='sim_si') # %% [markdown] # #### Run Fitting @@ -622,7 +622,7 @@ # %% project_1.analysis.fit() -project_1.analysis.display.fit_results() +project_1.display.fit.results() # %% [markdown] # #### Check Fit Results @@ -647,7 +647,7 @@ # pattern is now based on the refined parameters. # %% -project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si') +project_1.display.pattern(expt_name='sim_si') # %% [markdown] # #### TOF vs d-spacing @@ -673,12 +673,12 @@ # `quad` terms were not part of the data reduction and are therefore set # to 0 by default. # -# The `plot_meas_vs_calc` method of the project allows us to plot the -# measured and calculated diffraction patterns in the d-spacing axis by -# setting the `d_spacing` parameter to `True`. +# The `pattern` method of the project's `display` facade allows us to +# plot the measured and calculated diffraction patterns in the +# d-spacing axis by setting `x='d_spacing'`. # %% -project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si', x='d_spacing') +project_1.display.pattern(expt_name='sim_si', x='d_spacing') # %% [markdown] # As you can see, the calculated diffraction pattern now matches the @@ -789,12 +789,12 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2.display.plotter.plot_meas(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco', include='measured') project_2.experiments['sim_lbco'].excluded_regions.create(id='1', start=0, end=55000) project_2.experiments['sim_lbco'].excluded_regions.create(id='2', start=105500, end=200000) -project_2.display.plotter.plot_meas(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco', include=('measured', 'excluded')) # %% [markdown] # #### Exercise 2.2: Set Instrument Parameters @@ -1106,19 +1106,19 @@ # **Hint:** # %% [markdown] tags=["dmsc-school-hint"] -# Use the `plot_meas_vs_calc` method of the project to visualize the -# measured and calculated diffraction patterns before fitting. Then, use -# the `fit` method of the `analysis` object of the project to perform -# the fitting process. +# Use the `pattern` method of the project's `display` facade to +# visualize the measured and calculated diffraction patterns before +# fitting. Then, use the `fit` method of the `analysis` object of the +# project to perform the fitting process. # %% [markdown] # **Solution:** # %% tags=["solution", "hide-input"] -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco') project_2.analysis.fit() -project_2.analysis.display.fit_results() +project_2.display.fit.results() # %% [markdown] # #### Exercise 5.3: Find the Misfit in the Fit @@ -1160,7 +1160,7 @@ # peak positions. # %% tags=["solution", "hide-input"] -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco') # %% [markdown] # #### Exercise 5.4: Refine the LBCO Lattice Parameter @@ -1187,9 +1187,9 @@ project_2.structures['lbco'].cell.length_a.free = True project_2.analysis.fit() -project_2.analysis.display.fit_results() +project_2.display.fit.results() -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco') # %% [markdown] # One of the main goals of this study was to refine the lattice @@ -1209,14 +1209,14 @@ # **Hint:** # %% [markdown] tags=["dmsc-school-hint"] -# Use the `plot_meas_vs_calc` method of the project and set the -# `d_spacing` parameter to `True`. +# Use the `pattern` method of the project's `display` facade and set +# `x='d_spacing'`. # %% [markdown] # **Solution:** # %% tags=["solution", "hide-input"] -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing') +project_2.display.pattern(expt_name='sim_lbco', x='d_spacing') # %% [markdown] # #### Exercise 5.6: Refine the Peak Profile Parameters @@ -1233,7 +1233,7 @@ # perfectly describe the peak at about 1.38 ร…, as can be seen below: # %% -project_2.display.plotter.plot_meas_vs_calc( +project_2.display.pattern( expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40 ) @@ -1268,9 +1268,9 @@ project_2.experiments['sim_lbco'].peak.exp_rise_alpha_1.free = True project_2.analysis.fit() -project_2.analysis.display.fit_results() +project_2.display.fit.results() -project_2.display.plotter.plot_meas_vs_calc( +project_2.display.pattern( expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40 ) @@ -1295,7 +1295,7 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2.display.plotter.plot_meas_vs_calc( +project_2.display.pattern( expt_name='sim_lbco', x='d_spacing', x_min=1.53, x_max=1.7 ) @@ -1362,8 +1362,8 @@ # confirm this hypothesis. # %% tags=["solution", "hide-input"] -project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si', x='d_spacing', x_min=1, x_max=1.7) -project_2.display.plotter.plot_meas_vs_calc( +project_1.display.pattern(expt_name='sim_si', x='d_spacing', x_min=1, x_max=1.7) +project_2.display.pattern( expt_name='sim_lbco', x='d_spacing', x_min=1, x_max=1.7 ) @@ -1420,10 +1420,10 @@ # **Hint:** # %% [markdown] tags=["dmsc-school-hint"] -# You can use the `plot_meas_vs_calc` method of the project to visualize -# the patterns. Then, set the `free` attribute of the `scale` parameter -# of the Si phase to `True` to allow the fitting process to adjust the -# scale factor. +# You can use the `pattern` method of the project's `display` facade to +# visualize the patterns. Then, set the `free` attribute of the `scale` +# parameter of the Si phase to `True` to allow the fitting process to +# adjust the scale factor. # %% [markdown] # **Solution:** @@ -1432,7 +1432,7 @@ # Before optimizing the parameters, we can visualize the measured # diffraction pattern and the calculated diffraction pattern based on # the two phases: LBCO and Si. -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco') # As you can see, the calculated pattern is now the sum of both phases, # and Si peaks are visible in the calculated pattern. However, their @@ -1442,14 +1442,14 @@ # Now we can perform the fit with both phases included. project_2.analysis.fit() -project_2.analysis.display.fit_results() +project_2.display.fit.results() # Let's plot the measured diffraction pattern and the calculated # diffraction pattern both for the full range and for a zoomed-in region # around the previously unexplained peak near 95,000 ฮผs. The calculated # pattern will be the sum of the two phases. -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco') -project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco', x_min=88000, x_max=101000) +project_2.display.pattern(expt_name='sim_lbco') +project_2.display.pattern(expt_name='sim_lbco', x_min=88000, x_max=101000) # %% [markdown] # All previously unexplained peaks are now accounted for in the pattern, diff --git a/docs/docs/tutorials/ed-14.py b/docs/docs/tutorials/ed-14.py index c29d3eb8..3ff5f25f 100644 --- a/docs/docs/tutorials/ed-14.py +++ b/docs/docs/tutorials/ed-14.py @@ -85,7 +85,7 @@ # ## Step 4: Perform Analysis I (ADP iso) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='heidi') +project.display.pattern(expt_name='heidi') # %% structure.atom_sites['O1'].fract_x.free = True @@ -110,7 +110,7 @@ # %% # Show fit results summary -project.analysis.display.fit_results() +project.display.fit.results() # %% structure.show_as_cif() @@ -119,7 +119,7 @@ project.experiments.show_names() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='heidi') +project.display.pattern(expt_name='heidi') # %% [markdown] # ## Step 5: Perform Analysis (ADP aniso) @@ -147,13 +147,13 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='heidi') +project.display.pattern(expt_name='heidi') # %% structure.show_as_cif() diff --git a/docs/docs/tutorials/ed-15.py b/docs/docs/tutorials/ed-15.py index c198fc23..659af721 100644 --- a/docs/docs/tutorials/ed-15.py +++ b/docs/docs/tutorials/ed-15.py @@ -67,7 +67,7 @@ # ## Step 4: Perform Analysis I (ADP iso) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='senju') +project.display.pattern(expt_name='senju') # %% experiment.linked_crystal.scale.free = True @@ -86,7 +86,7 @@ # %% # Show fit results summary -project.analysis.display.fit_results() +project.display.fit.results() # %% structure.show_as_cif() @@ -95,7 +95,7 @@ project.experiments.show_names() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='senju') +project.display.pattern(expt_name='senju') # %% [markdown] # ## Step 5: Perform Analysis (ADP aniso) @@ -114,19 +114,19 @@ structure.show_as_cif() # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='senju') +project.display.pattern(expt_name='senju') # %% structure.show_as_cif() diff --git a/docs/docs/tutorials/ed-16.py b/docs/docs/tutorials/ed-16.py index 91d48b1e..bde9af47 100644 --- a/docs/docs/tutorials/ed-16.py +++ b/docs/docs/tutorials/ed-16.py @@ -190,10 +190,10 @@ # #### Plot Measured vs Calculated (Before Fit) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') +project.display.pattern(expt_name='sepd') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='nomad') +project.display.pattern(expt_name='nomad') # %% [markdown] # #### Set Fitting Parameters @@ -231,21 +231,21 @@ # #### Show Free Parameters # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated (After Fit) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') +project.display.pattern(expt_name='sepd') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='nomad') +project.display.pattern(expt_name='nomad') diff --git a/docs/docs/tutorials/ed-17.py b/docs/docs/tutorials/ed-17.py index f929cd47..c1dc3a79 100644 --- a/docs/docs/tutorials/ed-17.py +++ b/docs/docs/tutorials/ed-17.py @@ -280,13 +280,13 @@ # #### Show parameter correlations # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% [markdown] # #### Compare measured and calculated patterns for the first fit. # %% -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +project.display.pattern(expt_name='d20') # %% [markdown] # #### Run Sequential Fitting @@ -329,7 +329,7 @@ def extract_diffrn(file_path): # %% project.apply_params_from_csv(row_index=0) -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +project.display.pattern(expt_name='d20') # %% [markdown] # @@ -337,7 +337,7 @@ def extract_diffrn(file_path): # %% project.apply_params_from_csv(row_index=-1) -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +project.display.pattern(expt_name='d20') # %% [markdown] # #### Plot Parameter Evolution @@ -351,31 +351,31 @@ def extract_diffrn(file_path): # Plot unit cell parameters vs. temperature. # %% -project.display.plotter.plot_param_series(structure.cell.length_a, versus=temperature) -project.display.plotter.plot_param_series(structure.cell.length_b, versus=temperature) -project.display.plotter.plot_param_series(structure.cell.length_c, versus=temperature) +project.display.fit.series(structure.cell.length_a, versus=temperature) +project.display.fit.series(structure.cell.length_b, versus=temperature) +project.display.fit.series(structure.cell.length_c, versus=temperature) # %% [markdown] # Plot isotropic displacement parameters vs. temperature. # %% -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['Co1'].adp_iso, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['Si'].adp_iso, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['O1'].adp_iso, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['O2'].adp_iso, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['O3'].adp_iso, versus=temperature, ) @@ -384,23 +384,23 @@ def extract_diffrn(file_path): # Plot selected fractional coordinates vs. temperature. # %% -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['Co2'].fract_x, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['Co2'].fract_z, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['O1'].fract_z, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['O2'].fract_z, versus=temperature, ) -project.display.plotter.plot_param_series( +project.display.fit.series( structure.atom_sites['O3'].fract_z, versus=temperature, ) diff --git a/docs/docs/tutorials/ed-18.py b/docs/docs/tutorials/ed-18.py index fb44972f..7c4fff9f 100644 --- a/docs/docs/tutorials/ed-18.py +++ b/docs/docs/tutorials/ed-18.py @@ -47,10 +47,10 @@ # ## Show Results # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') diff --git a/docs/docs/tutorials/ed-2.py b/docs/docs/tutorials/ed-2.py index fa4918c0..c2772ba6 100644 --- a/docs/docs/tutorials/ed-2.py +++ b/docs/docs/tutorials/ed-2.py @@ -162,13 +162,13 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% [markdown] # ## Step 5: Perform Analysis (with constraints) @@ -199,13 +199,13 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% [markdown] # ## Step 6: Switch calculator engine @@ -220,10 +220,10 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') diff --git a/docs/docs/tutorials/ed-20.py b/docs/docs/tutorials/ed-20.py index 0ac55fdb..e48e860b 100644 --- a/docs/docs/tutorials/ed-20.py +++ b/docs/docs/tutorials/ed-20.py @@ -245,10 +245,10 @@ # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2') +project.display.pattern(expt_name='expt_s2') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2') +project.display.pattern(expt_name='expt_n2') # %% [markdown] # ## Perform Analysis @@ -268,7 +268,7 @@ # #### Set Free Parameters # %% -project.analysis.display.fittable_params() +project.display.parameters.fittable() # %% ferrite.atom_sites['Fe'].adp_iso.free = True @@ -347,8 +347,8 @@ # Show fit results and parameter correlations. # %% -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated @@ -356,16 +356,16 @@ # Show full range in TOF. # %% -project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2') +project.display.pattern(expt_name='expt_s2') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2') +project.display.pattern(expt_name='expt_n2') # %% [markdown] # Show selected peaks in d-spacing. # %% -project.display.plotter.plot_meas_vs_calc( +project.display.pattern( expt_name='expt_s2', x='d_spacing', x_min=2.08, @@ -373,7 +373,7 @@ ) # %% -project.display.plotter.plot_meas_vs_calc( +project.display.pattern( expt_name='expt_n2', x='d_spacing', x_min=2.08, diff --git a/docs/docs/tutorials/ed-21.py b/docs/docs/tutorials/ed-21.py index d91173c3..e890f55b 100644 --- a/docs/docs/tutorials/ed-21.py +++ b/docs/docs/tutorials/ed-21.py @@ -213,7 +213,7 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # The correlation plot shows how strongly the fitted parameters move @@ -222,10 +222,10 @@ # region. # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% [markdown] # ## Step 5: Prepare for Bayesian Sampling @@ -244,7 +244,7 @@ # Show unset fit bounds before setting them from the local refinement uncertainties. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # Set fit bounds for all free parameters using the default multiplier of @@ -262,7 +262,7 @@ # sampler. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # ## Step 6: Configure and Run DREAM @@ -304,7 +304,7 @@ # statistics. # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # The correlation and posterior-pair plots are complementary: @@ -318,10 +318,10 @@ # keep the grid readable. # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_posterior_pairs() +project.display.posterior.pairs() # %% [markdown] # The one-dimensional posterior distributions below make it easier to @@ -330,7 +330,7 @@ # %% for param in project.free_parameters: - project.display.plotter.plot_param_distribution(param) + project.display.posterior.distribution(param) # %% [markdown] # Finally, the posterior predictive plot propagates the sampled parameter @@ -339,7 +339,7 @@ # model family explains the data in the region of interest. # %% -project.display.plotter.plot_posterior_predictive(expt_name='hrpt') +project.display.posterior.predictive(expt_name='hrpt') # %% [markdown] # A final zoomed measured-vs-calculated plot is useful for checking how @@ -347,7 +347,7 @@ # after the Bayesian run. # %% -project.display.plotter.plot_posterior_predictive( +project.display.posterior.predictive( expt_name='hrpt', x_min=92, x_max=93, diff --git a/docs/docs/tutorials/ed-22.py b/docs/docs/tutorials/ed-22.py index aaa72718..bf0f2e19 100644 --- a/docs/docs/tutorials/ed-22.py +++ b/docs/docs/tutorials/ed-22.py @@ -141,7 +141,7 @@ # estimated uncertainties. # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # The correlation plot shows how strongly the refined parameters move @@ -150,10 +150,10 @@ # intensities. # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_meas_vs_calc(expt_name='heidi') +project.display.pattern(expt_name='heidi') # %% [markdown] # ## Step 5: Prepare for Bayesian Sampling @@ -174,7 +174,7 @@ # uncertainties. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # Set fit bounds for all free parameters using `multiplier=1.5`. In this @@ -192,7 +192,7 @@ # sampler. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # ## Step 6: Configure and Run DREAM @@ -232,7 +232,7 @@ # statistics. # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # The correlation and posterior-pair plots are complementary: @@ -246,10 +246,10 @@ # keep the grid readable. # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% -project.display.plotter.plot_posterior_pairs() +project.display.posterior.pairs() # %% [markdown] # The one-dimensional posterior distributions below make it easier to @@ -258,7 +258,7 @@ # %% for param in project.free_parameters: - project.display.plotter.plot_param_distribution(param) + project.display.posterior.distribution(param) # %% [markdown] # Finally, the posterior predictive plot propagates the sampled @@ -266,4 +266,4 @@ # intensities. # %% -project.display.plotter.plot_posterior_predictive(expt_name='heidi') +project.display.posterior.predictive(expt_name='heidi') diff --git a/docs/docs/tutorials/ed-3.py b/docs/docs/tutorials/ed-3.py index de1f74c6..271b8e8f 100644 --- a/docs/docs/tutorials/ed-3.py +++ b/docs/docs/tutorials/ed-3.py @@ -68,13 +68,13 @@ # Show supported plotting engines. # %% -project.display.plotter.show_supported_engines() +project.rendering.show_chart_engines() # %% [markdown] # Show current plotting configuration. # %% -project.display.plotter.show_config() +project.rendering.show_config() # %% [markdown] # ## Step 2: Define Structure @@ -219,7 +219,7 @@ # #### Show Measured Data # %% -project.display.plotter.plot_meas(expt_name='hrpt') +project.display.pattern(expt_name='hrpt', include='measured') # %% [markdown] # #### Set Instrument @@ -328,16 +328,16 @@ # #### Show Calculated Data # %% -project.display.plotter.plot_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt', include='calculated') # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Show Parameters @@ -345,25 +345,25 @@ # Show all parameters of the project. # %% -# project.analysis.display.all_params() +# project.display.parameters.all() # %% [markdown] # Show all fittable parameters. # %% -project.analysis.display.fittable_params() +project.display.parameters.fittable() # %% [markdown] # Show only free parameters. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # Show how to access parameters in the code. # %% -# project.analysis.display.how_to_access_parameters() +# project.display.parameters.access() # %% [markdown] # #### Set Fit Mode @@ -417,23 +417,23 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Save Project State @@ -456,23 +456,23 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Save Project State @@ -495,23 +495,23 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Save Project State @@ -546,29 +546,29 @@ # Show defined constraints. # %% -project.analysis.display.constraints() +project.analysis.constraints.show() # %% [markdown] # Show free parameters. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Save Project State @@ -605,7 +605,7 @@ # Show defined constraints. # %% -project.analysis.display.constraints() +project.analysis.constraints.show() # %% [markdown] @@ -618,24 +618,24 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41) +project.display.pattern(expt_name='hrpt', x_min=38, x_max=41) # %% [markdown] # #### Save Project State diff --git a/docs/docs/tutorials/ed-4.py b/docs/docs/tutorials/ed-4.py index 8a868277..a0ea3f62 100644 --- a/docs/docs/tutorials/ed-4.py +++ b/docs/docs/tutorials/ed-4.py @@ -315,7 +315,7 @@ # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='npd', x_min=35.5, x_max=38.3) +project.display.pattern(expt_name='npd', x_min=35.5, x_max=38.3) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='xrd', x_min=29.0, x_max=30.4) +project.display.pattern(expt_name='xrd', x_min=29.0, x_max=30.4) diff --git a/docs/docs/tutorials/ed-5.py b/docs/docs/tutorials/ed-5.py index 72e54feb..a82990a3 100644 --- a/docs/docs/tutorials/ed-5.py +++ b/docs/docs/tutorials/ed-5.py @@ -200,10 +200,10 @@ # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +project.display.pattern(expt_name='d20') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='d20', x_min=41, x_max=54) +project.display.pattern(expt_name='d20', x_min=41, x_max=54) # %% [markdown] # #### Set Free Parameters @@ -243,7 +243,7 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Set Constraints @@ -274,19 +274,19 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='d20') +project.display.pattern(expt_name='d20') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='d20', x_min=42, x_max=52) +project.display.pattern(expt_name='d20', x_min=42, x_max=52) # %% [markdown] # ## Summary diff --git a/docs/docs/tutorials/ed-6.py b/docs/docs/tutorials/ed-6.py index 341178e6..14d93515 100644 --- a/docs/docs/tutorials/ed-6.py +++ b/docs/docs/tutorials/ed-6.py @@ -179,10 +179,10 @@ # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51) +project.display.pattern(expt_name='hrpt', x_min=48, x_max=51) # %% [markdown] # ### Perform Fit 1/4 @@ -200,7 +200,7 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting @@ -209,16 +209,16 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51) +project.display.pattern(expt_name='hrpt', x_min=48, x_max=51) # %% [markdown] # ### Perform Fit 2/4 @@ -238,7 +238,7 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting @@ -247,16 +247,16 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51) +project.display.pattern(expt_name='hrpt', x_min=48, x_max=51) # %% [markdown] # ### Perform Fit 3/4 @@ -274,7 +274,7 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting @@ -283,16 +283,16 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51) +project.display.pattern(expt_name='hrpt', x_min=48, x_max=51) # %% [markdown] # ### Perform Fit 4/4 @@ -315,7 +315,7 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting @@ -324,19 +324,19 @@ project.analysis.fit() # %% -project.analysis.display.fit_results() +project.display.fit.results() # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') +project.display.pattern(expt_name='hrpt') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51) +project.display.pattern(expt_name='hrpt', x_min=48, x_max=51) # %% [markdown] # ## Summary diff --git a/docs/docs/tutorials/ed-7.py b/docs/docs/tutorials/ed-7.py index 5305549a..ed3dcdae 100644 --- a/docs/docs/tutorials/ed-7.py +++ b/docs/docs/tutorials/ed-7.py @@ -140,8 +140,8 @@ # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700) +project.display.pattern(expt_name='sepd') +project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700) # %% [markdown] # ### Perform Fit 1/5 @@ -158,23 +158,23 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') +project.display.pattern(expt_name='sepd') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700) +project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700) # %% [markdown] # ### Perform Fit 2/5 @@ -189,23 +189,23 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') +project.display.pattern(expt_name='sepd') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700) +project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700) # %% [markdown] # ### Perform Fit 3/5 @@ -228,23 +228,23 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') +project.display.pattern(expt_name='sepd') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700) +project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700) # %% [markdown] # ### Perform Fit 4/5 @@ -262,32 +262,32 @@ # Show free parameters after selection. # %% -project.analysis.display.free_params() +project.display.parameters.free() # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Show parameter correlations # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd') +project.display.pattern(expt_name='sepd') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700) +project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing') +project.display.pattern(expt_name='sepd', x='d_spacing') # %% [markdown] @@ -334,19 +334,19 @@ # %% project.analysis.fit() -project.analysis.display.fit_results() +project.display.fit.results() # %% [markdown] # #### Show parameter correlations # %% -project.display.plotter.plot_param_correlations() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700) +project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700) # %% -project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing') +project.display.pattern(expt_name='sepd', x='d_spacing') diff --git a/docs/docs/tutorials/ed-8.py b/docs/docs/tutorials/ed-8.py index a9adb7c8..82beedd7 100644 --- a/docs/docs/tutorials/ed-8.py +++ b/docs/docs/tutorials/ed-8.py @@ -333,27 +333,27 @@ # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6') +project.display.pattern(expt_name='wish_5_6') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7') +project.display.pattern(expt_name='wish_4_7') # %% [markdown] # #### Run Fitting # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6') +project.display.pattern(expt_name='wish_5_6') # %% -project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7') +project.display.pattern(expt_name='wish_4_7') # %% [markdown] # ## Summary diff --git a/docs/docs/tutorials/ed-9.py b/docs/docs/tutorials/ed-9.py index c9108b5f..ffae0910 100644 --- a/docs/docs/tutorials/ed-9.py +++ b/docs/docs/tutorials/ed-9.py @@ -227,7 +227,7 @@ # Show measured data as loaded from the file. # %% -project.display.plotter.plot_meas(expt_name='mcstas') +project.display.pattern(expt_name='mcstas', include='measured') # %% [markdown] # Add excluded regions. @@ -246,7 +246,7 @@ # Show measured data after adding excluded regions. # %% -project.display.plotter.plot_meas(expt_name='mcstas') +project.display.pattern(expt_name='mcstas', include=('measured', 'excluded')) # %% [markdown] # Show experiment as CIF. @@ -294,11 +294,11 @@ # %% project.analysis.fit() -project.analysis.display.fit_results() -project.display.plotter.plot_param_correlations() +project.display.fit.results() +project.display.fit.correlations() # %% [markdown] # #### Plot Measured vs Calculated # %% -project.display.plotter.plot_meas_vs_calc(expt_name='mcstas') +project.display.pattern(expt_name='mcstas') diff --git a/docs/docs/user-guide/analysis-workflow/analysis.md b/docs/docs/user-guide/analysis-workflow/analysis.md index bf1d4ee8..b3c9e3d5 100644 --- a/docs/docs/user-guide/analysis-workflow/analysis.md +++ b/docs/docs/user-guide/analysis-workflow/analysis.md @@ -247,11 +247,11 @@ Now, you can inspect the fitted parameters to see how they have changed during the refinement process, select more parameters to be refined, and perform additional fits as needed. -To plot the measured vs calculated data after the fit, you can use the -`plot_meas_vs_calc` method of the `analysis` object: +To plot the measured and calculated data after the fit, you can use the +`pattern` method of the `display` object: ```python -project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True) +project.display.pattern(expt_name='hrpt') ``` ## Constraints @@ -319,7 +319,7 @@ To view the defined constraints, you can use the `show_constraints` method: ```python -project.analysis.display.constraints() +project.analysis.constraints.show() ``` The example of the output is: diff --git a/docs/docs/user-guide/analysis-workflow/project.md b/docs/docs/user-guide/analysis-workflow/project.md index 7959f9f8..5e8e8b41 100644 --- a/docs/docs/user-guide/analysis-workflow/project.md +++ b/docs/docs/user-guide/analysis-workflow/project.md @@ -115,8 +115,8 @@ data_La0.5Ba0.5CoO3 _project.title "La0.5Ba0.5CoO3 from neutron diffraction at HRPT@PSI" _project.description "neutrons, powder, constant wavelength, HRPT@PSI" -_display.plotter_type asciichartpy -_display.tabler_type rich +_display.chart_engine asciichartpy +_display.table_engine rich diff --git a/docs/docs/user-guide/first-steps.md b/docs/docs/user-guide/first-steps.md index b8f69070..cd1de40a 100644 --- a/docs/docs/user-guide/first-steps.md +++ b/docs/docs/user-guide/first-steps.md @@ -125,15 +125,15 @@ project.analysis.fit.show_minimizer_types() EasyDiffraction provides several methods for showing the available parameters grouped in different categories. For example, you can use: -- `project.analysis.display.all_params()` โ€“ to display all available +- `project.display.parameters.all()` โ€“ to display all available parameters for the analysis step. -- `project.analysis.display.fittable_params()` โ€“ to display only the +- `project.display.parameters.fittable()` โ€“ to display only the parameters that can be fitted during the analysis. -- `project.analysis.display.free_params()` โ€“ to display the parameters +- `project.display.parameters.free()` โ€“ to display the parameters that are currently free to be adjusted during the fitting process. Finally, you can use the -`project.analysis.display.how_to_access_parameters()` method to get a +`project.display.parameters.access()` method to get a brief overview of how to access and modify parameters in the analysis step, along with their unique identifiers in the CIF format. This can be particularly useful for users who are new to the EasyDiffraction API or @@ -141,7 +141,7 @@ those who want to quickly understand how to work with parameters in their projects. An example of the output for the -`project.analysis.display.how_to_access_parameters()` method is: +`project.display.parameters.access()` method is: | | Code variable | Unique ID for CIF | | --- | --------------------------------------------------- | -------------------------- | @@ -160,7 +160,7 @@ To see the available plotters, you can use the `display` category on the `Project` instance: ```python -project.display.show_plotter_types() +project.rendering.show_chart_engines() ``` An example of the output is: From bb194e6c87cd050a63f13cbd4c7fa226ddfd001e Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 14 May 2026 12:27:58 +0200 Subject: [PATCH 08/10] Complete display UX verification and test updates --- docs/dev/architecture.md | 8 +- docs/dev/package-structure-full.md | 12 + docs/dev/package-structure-short.md | 5 + docs/dev/plan_display-ux.md | 20 +- docs/docs/tutorials/ed-1.ipynb | 10 +- docs/docs/tutorials/ed-10.ipynb | 6 +- docs/docs/tutorials/ed-11.ipynb | 12 +- docs/docs/tutorials/ed-12.ipynb | 12 +- docs/docs/tutorials/ed-13.ipynb | 98 +++--- docs/docs/tutorials/ed-13.py | 16 +- docs/docs/tutorials/ed-14.ipynb | 12 +- docs/docs/tutorials/ed-15.ipynb | 14 +- docs/docs/tutorials/ed-16.ipynb | 14 +- docs/docs/tutorials/ed-17.ipynb | 34 +- docs/docs/tutorials/ed-18.ipynb | 6 +- docs/docs/tutorials/ed-2.ipynb | 18 +- docs/docs/tutorials/ed-20.ipynb | 18 +- docs/docs/tutorials/ed-21.ipynb | 162 ++++++---- docs/docs/tutorials/ed-22.ipynb | 266 +++++++++++++--- docs/docs/tutorials/ed-3.ipynb | 66 ++-- docs/docs/tutorials/ed-4.ipynb | 4 +- docs/docs/tutorials/ed-5.ipynb | 14 +- docs/docs/tutorials/ed-6.ipynb | 38 +-- docs/docs/tutorials/ed-7.ipynb | 48 +-- docs/docs/tutorials/ed-8.ipynb | 12 +- docs/docs/tutorials/ed-9.ipynb | 10 +- docs/docs/user-guide/first-steps.md | 25 +- src/easydiffraction/analysis/analysis.py | 2 +- src/easydiffraction/display/plotters/ascii.py | 2 + .../display/plotters/plotly.py | 2 + src/easydiffraction/display/plotting.py | 98 +++--- .../project/categories/rendering/__init__.py | 2 +- .../project/categories/rendering/default.py | 2 +- .../project/categories/rendering/factory.py | 2 +- src/easydiffraction/project/display.py | 214 +++++++------ src/easydiffraction/project/project.py | 2 +- .../test_analysis_and_fit_category_support.py | 3 +- .../fitting/test_analysis_display.py | 20 +- .../fitting/test_cli_entrypoints.py | 31 +- tests/integration/fitting/test_plotting.py | 22 +- .../analysis/test_analysis_coverage.py | 4 +- .../easydiffraction/display/test_plotting.py | 65 +++- .../categories/rendering/test_default.py | 55 ++++ .../categories/rendering/test_factory.py | 23 ++ .../easydiffraction/project/test_display.py | 297 ++++++++++++++++++ .../easydiffraction/project/test_project.py | 11 + .../project/test_project_load.py | 10 +- .../project/test_project_save.py | 4 +- tests/unit/easydiffraction/test___main__.py | 31 +- 49 files changed, 1284 insertions(+), 578 deletions(-) create mode 100644 tests/unit/easydiffraction/project/categories/rendering/test_default.py create mode 100644 tests/unit/easydiffraction/project/categories/rendering/test_factory.py create mode 100644 tests/unit/easydiffraction/project/test_display.py diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index d6e338b0..9ac1d4b1 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -1248,11 +1248,11 @@ recognises three distinct selector families. They share a similar `_type` shape so the user can inspect and set them uniformly, but their intent and ownership differ: -| Family | User intent | Examples | CIF | -| ---------------------------------- | ------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| Family | User intent | Examples | CIF | +| ---------------------------------- | ------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | Backend selector | Pick an execution backend | `fit.minimizer_type`, `calculation.calculator_type`, `display.chart_engine` | `_fit.minimizer_type`, `_calculation.calculator_type`, `_rendering.chart_engine` | -| Switchable-category impl. selector | Swap a category implementation | `experiment.background_type`, `experiment.peak_profile_type` | category-owned type tag such as `_peak.profile_type` | -| Semantic value selector | Pick a scientific/analysis mode | `fit.mode` | `_fit.mode` | +| Switchable-category impl. selector | Swap a category implementation | `experiment.background_type`, `experiment.peak_profile_type` | category-owned type tag such as `_peak.profile_type` | +| Semantic value selector | Pick a scientific/analysis mode | `fit.mode` | `_fit.mode` | Backend selectors and semantic value selectors live on a dedicated configuration category (`fit`, `calculation`, `display`). Switchable- diff --git a/docs/dev/package-structure-full.md b/docs/dev/package-structure-full.md index 8083f39b..2bcf3cd2 100644 --- a/docs/dev/package-structure-full.md +++ b/docs/dev/package-structure-full.md @@ -408,8 +408,20 @@ โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿท๏ธ class Display โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ factory.py โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿท๏ธ class DisplayFactory +โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ rendering +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ __init__.py +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ default.py +โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿท๏ธ class Rendering +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ factory.py +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿท๏ธ class RenderingFactory โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ __init__.py โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ __init__.py +โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ display.py +โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿท๏ธ class PatternOptionStatus +โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿท๏ธ class ParameterDisplay +โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿท๏ธ class FitDisplay +โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿท๏ธ class PosteriorDisplay +โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿท๏ธ class ProjectDisplay โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ project.py โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿท๏ธ class Project โ”‚ โ””โ”€โ”€ ๐Ÿ“„ project_info.py diff --git a/docs/dev/package-structure-short.md b/docs/dev/package-structure-short.md index 9ce283eb..9c57c3d3 100644 --- a/docs/dev/package-structure-short.md +++ b/docs/dev/package-structure-short.md @@ -202,8 +202,13 @@ โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ __init__.py โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ default.py โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ factory.py +โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ rendering +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ __init__.py +โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ default.py +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ factory.py โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ __init__.py โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ __init__.py +โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ display.py โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ project.py โ”‚ โ””โ”€โ”€ ๐Ÿ“„ project_info.py โ”œโ”€โ”€ ๐Ÿ“ summary diff --git a/docs/dev/plan_display-ux.md b/docs/dev/plan_display-ux.md index 61e205db..4088ac65 100644 --- a/docs/dev/plan_display-ux.md +++ b/docs/dev/plan_display-ux.md @@ -170,7 +170,7 @@ each commit, and stage only the files changed for that step. - Update tutorial `.py` files only; regenerate notebooks in Phase 2 if required. -- [ ] Stop at the Phase 1 review gate. +- [x] Stop at the Phase 1 review gate. - Summarize changed files and open questions. - Suggest next verification commands. - Wait for user approval before Phase 2. @@ -190,16 +190,16 @@ Update display UX documentation After Phase 1 is reviewed and approved: -- [ ] Add or update unit tests for the rendering category. -- [ ] Add or update unit tests for the display facade namespaces. -- [ ] Add or update plotting integration tests for `pattern(...)`. -- [ ] Add or update analysis display integration tests for parameter and +- [x] Add or update unit tests for the rendering category. +- [x] Add or update unit tests for the display facade namespaces. +- [x] Add or update plotting integration tests for `pattern(...)`. +- [x] Add or update analysis display integration tests for parameter and fit report methods. -- [ ] Regenerate tutorial notebooks if tutorial `.py` files changed. -- [ ] Run formatting and checks. -- [ ] Run unit tests. -- [ ] Run integration tests. -- [ ] Run script tests. +- [x] Regenerate tutorial notebooks if tutorial `.py` files changed. +- [x] Run formatting and checks. +- [x] Run unit tests. +- [x] Run integration tests. +- [x] Run script tests. Verification commands: diff --git a/docs/docs/tutorials/ed-1.ipynb b/docs/docs/tutorials/ed-1.ipynb index 53635a4c..f108a808 100644 --- a/docs/docs/tutorials/ed-1.ipynb +++ b/docs/docs/tutorials/ed-1.ipynb @@ -167,7 +167,7 @@ "outputs": [], "source": [ "# Show fit results summary\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -178,7 +178,7 @@ "outputs": [], "source": [ "# Show parameter correlations\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -234,7 +234,7 @@ "outputs": [], "source": [ "# Show fit results summary\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -245,7 +245,7 @@ "outputs": [], "source": [ "# Show parameter correlations\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -267,7 +267,7 @@ "outputs": [], "source": [ "# Plot measured vs. calculated diffraction patterns\n", - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] } ], diff --git a/docs/docs/tutorials/ed-10.ipynb b/docs/docs/tutorials/ed-10.ipynb index 1e14322b..2b23af88 100644 --- a/docs/docs/tutorials/ed-10.ipynb +++ b/docs/docs/tutorials/ed-10.ipynb @@ -207,8 +207,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations(threshold=0.75)" + "project.display.fit.results()\n", + "project.display.fit.correlations(threshold=0.75)" ] }, { @@ -226,7 +226,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='pdf', show_residual=True)" + "project.display.pattern(expt_name='pdf')" ] } ], diff --git a/docs/docs/tutorials/ed-11.ipynb b/docs/docs/tutorials/ed-11.ipynb index ff9669a2..963894d8 100644 --- a/docs/docs/tutorials/ed-11.ipynb +++ b/docs/docs/tutorials/ed-11.ipynb @@ -82,8 +82,8 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.show_supported_engines()\n", - "project.display.plotter.show_current_engine()" + "project.rendering.show_chart_engines()\n", + "project.rendering.show_config()" ] }, { @@ -94,7 +94,7 @@ "outputs": [], "source": [ "# Set global plot range for plots\n", - "project.display.plotter.x_max = 40" + "project.rendering.plotter.x_max = 40" ] }, { @@ -238,8 +238,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -257,7 +257,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='nomad', show_residual=False)" + "project.display.pattern(expt_name='nomad', include=('measured', 'calculated'))" ] } ], diff --git a/docs/docs/tutorials/ed-12.ipynb b/docs/docs/tutorials/ed-12.ipynb index 77e648e0..176ab147 100644 --- a/docs/docs/tutorials/ed-12.ipynb +++ b/docs/docs/tutorials/ed-12.ipynb @@ -87,7 +87,7 @@ "source": [ "# Keep the auto-selected engine. Alternatively, you can uncomment the\n", "# line below to explicitly set the engine to the required one.\n", - "# project.display.plotter.engine = 'plotly'" + "# project.rendering.chart_engine = 'plotly'" ] }, { @@ -98,8 +98,8 @@ "outputs": [], "source": [ "# Set global plot range for plots\n", - "project.display.plotter.x_min = 2.0\n", - "project.display.plotter.x_max = 30.0" + "project.rendering.plotter.x_min = 2.0\n", + "project.rendering.plotter.x_max = 30.0" ] }, { @@ -278,8 +278,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -297,7 +297,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='xray_pdf')" + "project.display.pattern(expt_name='xray_pdf')" ] } ], diff --git a/docs/docs/tutorials/ed-13.ipynb b/docs/docs/tutorials/ed-13.ipynb index 69eae9aa..aebb420f 100644 --- a/docs/docs/tutorials/ed-13.ipynb +++ b/docs/docs/tutorials/ed-13.ipynb @@ -266,8 +266,8 @@ "[documentation](https://docs.easydiffraction.org/lib/user-guide/analysis-workflow/experiment/#measured-data-category)\n", "for more details about the measured data and its format.\n", "\n", - "To visualize the measured data, we can use the `plot_meas` method of\n", - "the project." + "To visualize the measured data, we can use the `pattern` method of\n", + "the project's `display` facade with `include='measured'`." ] }, { @@ -277,7 +277,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.plotter.plot_meas(expt_name='sim_si')" + "project_1.display.pattern(expt_name='sim_si', include='measured')" ] }, { @@ -330,8 +330,8 @@ "metadata": {}, "source": [ "To visualize the effect of excluding the high TOF region, we can plot\n", - "the measured data again. The excluded region will be omitted from the\n", - "plot and is not used in the fitting process." + "the measured data again. The excluded region will be highlighted on\n", + "the plot and is not used in the fitting process." ] }, { @@ -341,7 +341,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.plotter.plot_meas(expt_name='sim_si')" + "project_1.display.pattern(expt_name='sim_si', include=('measured', 'excluded'))" ] }, { @@ -1002,7 +1002,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.analysis.display.free_params()" + "project_1.display.parameters.free()" ] }, { @@ -1016,8 +1016,8 @@ "diffraction pattern with the calculated diffraction pattern based on\n", "the initial parameters of the structure and the instrument. This\n", "provides an indication of how well the initial parameters match the\n", - "measured data. The `plot_meas_vs_calc` method of the project allows\n", - "this comparison." + "measured data. The `pattern` method of the project's `display`\n", + "facade allows this comparison." ] }, { @@ -1027,7 +1027,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si')" + "project_1.display.pattern(expt_name='sim_si')" ] }, { @@ -1059,7 +1059,7 @@ "outputs": [], "source": [ "project_1.analysis.fit()\n", - "project_1.analysis.display.fit_results()" + "project_1.display.fit.results()" ] }, { @@ -1101,7 +1101,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si')" + "project_1.display.pattern(expt_name='sim_si')" ] }, { @@ -1132,9 +1132,9 @@ "`quad` terms were not part of the data reduction and are therefore set\n", "to 0 by default.\n", "\n", - "The `plot_meas_vs_calc` method of the project allows us to plot the\n", - "measured and calculated diffraction patterns in the d-spacing axis by\n", - "setting the `d_spacing` parameter to `True`." + "The `pattern` method of the project's `display` facade allows us to\n", + "plot the measured and calculated diffraction patterns in the\n", + "d-spacing axis by setting `x='d_spacing'`." ] }, { @@ -1144,7 +1144,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si', x='d_spacing')" + "project_1.display.pattern(expt_name='sim_si', x='d_spacing')" ] }, { @@ -1348,12 +1348,12 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.plotter.plot_meas(expt_name='sim_lbco')\n", + "project_2.display.pattern(expt_name='sim_lbco', include='measured')\n", "\n", "project_2.experiments['sim_lbco'].excluded_regions.create(id='1', start=0, end=55000)\n", "project_2.experiments['sim_lbco'].excluded_regions.create(id='2', start=105500, end=200000)\n", "\n", - "project_2.display.plotter.plot_meas(expt_name='sim_lbco')" + "project_2.display.pattern(expt_name='sim_lbco', include=('measured', 'excluded'))" ] }, { @@ -1938,10 +1938,10 @@ "id": "146", "metadata": {}, "source": [ - "Use the `plot_meas_vs_calc` method of the project to visualize the\n", - "measured and calculated diffraction patterns before fitting. Then, use\n", - "the `fit` method of the `analysis` object of the project to perform\n", - "the fitting process." + "Use the `pattern` method of the project's `display` facade to\n", + "visualize the measured and calculated diffraction patterns before\n", + "fitting. Then, use the `fit` method of the `analysis` object of the\n", + "project to perform the fitting process." ] }, { @@ -1959,10 +1959,10 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco')\n", + "project_2.display.pattern(expt_name='sim_lbco')\n", "\n", "project_2.analysis.fit()\n", - "project_2.analysis.display.fit_results()" + "project_2.display.fit.results()" ] }, { @@ -2036,7 +2036,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco')" + "project_2.display.pattern(expt_name='sim_lbco')" ] }, { @@ -2090,9 +2090,9 @@ "project_2.structures['lbco'].cell.length_a.free = True\n", "\n", "project_2.analysis.fit()\n", - "project_2.analysis.display.fit_results()\n", + "project_2.display.fit.results()\n", "\n", - "project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco')" + "project_2.display.pattern(expt_name='sim_lbco')" ] }, { @@ -2132,8 +2132,8 @@ "id": "163", "metadata": {}, "source": [ - "Use the `plot_meas_vs_calc` method of the project and set the\n", - "`d_spacing` parameter to `True`." + "Use the `pattern` method of the project's `display` facade and set\n", + "`x='d_spacing'`." ] }, { @@ -2151,7 +2151,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco', x='d_spacing')" + "project_2.display.pattern(expt_name='sim_lbco', x='d_spacing')" ] }, { @@ -2180,9 +2180,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.plotter.plot_meas_vs_calc(\n", - " expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40\n", - ")" + "project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40)" ] }, { @@ -2242,11 +2240,9 @@ "project_2.experiments['sim_lbco'].peak.exp_rise_alpha_1.free = True\n", "\n", "project_2.analysis.fit()\n", - "project_2.analysis.display.fit_results()\n", + "project_2.display.fit.results()\n", "\n", - "project_2.display.plotter.plot_meas_vs_calc(\n", - " expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40\n", - ")" + "project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40)" ] }, { @@ -2296,9 +2292,7 @@ "metadata": {}, "outputs": [], "source": [ - "project_2.display.plotter.plot_meas_vs_calc(\n", - " expt_name='sim_lbco', x='d_spacing', x_min=1.53, x_max=1.7\n", - ")" + "project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1.53, x_max=1.7)" ] }, { @@ -2420,10 +2414,8 @@ "metadata": {}, "outputs": [], "source": [ - "project_1.display.plotter.plot_meas_vs_calc(expt_name='sim_si', x='d_spacing', x_min=1, x_max=1.7)\n", - "project_2.display.plotter.plot_meas_vs_calc(\n", - " expt_name='sim_lbco', x='d_spacing', x_min=1, x_max=1.7\n", - ")" + "project_1.display.pattern(expt_name='sim_si', x='d_spacing', x_min=1, x_max=1.7)\n", + "project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1, x_max=1.7)" ] }, { @@ -2520,10 +2512,10 @@ "id": "196", "metadata": {}, "source": [ - "You can use the `plot_meas_vs_calc` method of the project to visualize\n", - "the patterns. Then, set the `free` attribute of the `scale` parameter\n", - "of the Si phase to `True` to allow the fitting process to adjust the\n", - "scale factor." + "You can use the `pattern` method of the project's `display` facade to\n", + "visualize the patterns. Then, set the `free` attribute of the `scale`\n", + "parameter of the Si phase to `True` to allow the fitting process to\n", + "adjust the scale factor." ] }, { @@ -2544,7 +2536,7 @@ "# Before optimizing the parameters, we can visualize the measured\n", "# diffraction pattern and the calculated diffraction pattern based on\n", "# the two phases: LBCO and Si.\n", - "project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco')\n", + "project_2.display.pattern(expt_name='sim_lbco')\n", "\n", "# As you can see, the calculated pattern is now the sum of both phases,\n", "# and Si peaks are visible in the calculated pattern. However, their\n", @@ -2554,14 +2546,14 @@ "\n", "# Now we can perform the fit with both phases included.\n", "project_2.analysis.fit()\n", - "project_2.analysis.display.fit_results()\n", + "project_2.display.fit.results()\n", "\n", "# Let's plot the measured diffraction pattern and the calculated\n", "# diffraction pattern both for the full range and for a zoomed-in region\n", "# around the previously unexplained peak near 95,000 ฮผs. The calculated\n", "# pattern will be the sum of the two phases.\n", - "project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco')\n", - "project_2.display.plotter.plot_meas_vs_calc(expt_name='sim_lbco', x_min=88000, x_max=101000)" + "project_2.display.pattern(expt_name='sim_lbco')\n", + "project_2.display.pattern(expt_name='sim_lbco', x_min=88000, x_max=101000)" ] }, { @@ -2665,7 +2657,7 @@ ], "metadata": { "jupytext": { - "cell_metadata_filter": "title,tags,-all", + "cell_metadata_filter": "tags,title,-all", "main_language": "python", "notebook_metadata_filter": "-all" } diff --git a/docs/docs/tutorials/ed-13.py b/docs/docs/tutorials/ed-13.py index 97abcf1d..66eb2d09 100644 --- a/docs/docs/tutorials/ed-13.py +++ b/docs/docs/tutorials/ed-13.py @@ -1233,9 +1233,7 @@ # perfectly describe the peak at about 1.38 ร…, as can be seen below: # %% -project_2.display.pattern( - expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40 -) +project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40) # %% [markdown] # The peak profile parameters are determined based on both the @@ -1270,9 +1268,7 @@ project_2.analysis.fit() project_2.display.fit.results() -project_2.display.pattern( - expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40 -) +project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1.35, x_max=1.40) # %% [markdown] # #### Exercise 5.7: Find Undefined Features @@ -1295,9 +1291,7 @@ # **Solution:** # %% tags=["solution", "hide-input"] -project_2.display.pattern( - expt_name='sim_lbco', x='d_spacing', x_min=1.53, x_max=1.7 -) +project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1.53, x_max=1.7) # %% [markdown] # #### Exercise 5.8: Identify the Cause of the Unexplained Peaks @@ -1363,9 +1357,7 @@ # %% tags=["solution", "hide-input"] project_1.display.pattern(expt_name='sim_si', x='d_spacing', x_min=1, x_max=1.7) -project_2.display.pattern( - expt_name='sim_lbco', x='d_spacing', x_min=1, x_max=1.7 -) +project_2.display.pattern(expt_name='sim_lbco', x='d_spacing', x_min=1, x_max=1.7) # %% [markdown] # #### Exercise 5.10: Create a Second Structure โ€“ Si as Impurity diff --git a/docs/docs/tutorials/ed-14.ipynb b/docs/docs/tutorials/ed-14.ipynb index 75e6805b..48665fa5 100644 --- a/docs/docs/tutorials/ed-14.ipynb +++ b/docs/docs/tutorials/ed-14.ipynb @@ -254,7 +254,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='heidi')" + "project.display.pattern(expt_name='heidi')" ] }, { @@ -307,7 +307,7 @@ "outputs": [], "source": [ "# Show fit results summary\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -337,7 +337,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='heidi')" + "project.display.pattern(expt_name='heidi')" ] }, { @@ -405,7 +405,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -415,7 +415,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -425,7 +425,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='heidi')" + "project.display.pattern(expt_name='heidi')" ] }, { diff --git a/docs/docs/tutorials/ed-15.ipynb b/docs/docs/tutorials/ed-15.ipynb index d1ece4eb..ab93cd53 100644 --- a/docs/docs/tutorials/ed-15.ipynb +++ b/docs/docs/tutorials/ed-15.ipynb @@ -208,7 +208,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='senju')" + "project.display.pattern(expt_name='senju')" ] }, { @@ -262,7 +262,7 @@ "outputs": [], "source": [ "# Show fit results summary\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -292,7 +292,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='senju')" + "project.display.pattern(expt_name='senju')" ] }, { @@ -344,7 +344,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -364,7 +364,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -374,7 +374,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -384,7 +384,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='senju')" + "project.display.pattern(expt_name='senju')" ] }, { diff --git a/docs/docs/tutorials/ed-16.ipynb b/docs/docs/tutorials/ed-16.ipynb index f7a839da..5da587e9 100644 --- a/docs/docs/tutorials/ed-16.ipynb +++ b/docs/docs/tutorials/ed-16.ipynb @@ -455,7 +455,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')" + "project.display.pattern(expt_name='sepd')" ] }, { @@ -465,7 +465,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='nomad')" + "project.display.pattern(expt_name='nomad')" ] }, { @@ -551,7 +551,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -570,8 +570,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -589,7 +589,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')" + "project.display.pattern(expt_name='sepd')" ] }, { @@ -599,7 +599,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='nomad')" + "project.display.pattern(expt_name='nomad')" ] } ], diff --git a/docs/docs/tutorials/ed-17.ipynb b/docs/docs/tutorials/ed-17.ipynb index d2ee917d..d370ca27 100644 --- a/docs/docs/tutorials/ed-17.ipynb +++ b/docs/docs/tutorials/ed-17.ipynb @@ -569,7 +569,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -587,7 +587,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "project.display.pattern(expt_name='d20')" ] }, { @@ -678,7 +678,7 @@ "outputs": [], "source": [ "project.apply_params_from_csv(row_index=0)\n", - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "project.display.pattern(expt_name='d20')" ] }, { @@ -698,7 +698,7 @@ "outputs": [], "source": [ "project.apply_params_from_csv(row_index=-1)\n", - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "project.display.pattern(expt_name='d20')" ] }, { @@ -736,9 +736,9 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_series(structure.cell.length_a, versus=temperature)\n", - "project.display.plotter.plot_param_series(structure.cell.length_b, versus=temperature)\n", - "project.display.plotter.plot_param_series(structure.cell.length_c, versus=temperature)" + "project.display.fit.series(structure.cell.length_a, versus=temperature)\n", + "project.display.fit.series(structure.cell.length_b, versus=temperature)\n", + "project.display.fit.series(structure.cell.length_c, versus=temperature)" ] }, { @@ -756,23 +756,23 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['Co1'].adp_iso,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['Si'].adp_iso,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['O1'].adp_iso,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['O2'].adp_iso,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['O3'].adp_iso,\n", " versus=temperature,\n", ")" @@ -793,23 +793,23 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['Co2'].fract_x,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['Co2'].fract_z,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['O1'].fract_z,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['O2'].fract_z,\n", " versus=temperature,\n", ")\n", - "project.display.plotter.plot_param_series(\n", + "project.display.fit.series(\n", " structure.atom_sites['O3'].fract_z,\n", " versus=temperature,\n", ")" diff --git a/docs/docs/tutorials/ed-18.ipynb b/docs/docs/tutorials/ed-18.ipynb index 47a25154..c1200010 100644 --- a/docs/docs/tutorials/ed-18.ipynb +++ b/docs/docs/tutorials/ed-18.ipynb @@ -144,7 +144,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -154,7 +154,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -164,7 +164,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] } ], diff --git a/docs/docs/tutorials/ed-2.ipynb b/docs/docs/tutorials/ed-2.ipynb index 4a52b569..927087e9 100644 --- a/docs/docs/tutorials/ed-2.ipynb +++ b/docs/docs/tutorials/ed-2.ipynb @@ -345,7 +345,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -355,7 +355,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -365,7 +365,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -429,7 +429,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -439,7 +439,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -449,7 +449,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -497,7 +497,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -507,7 +507,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -517,7 +517,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] } ], diff --git a/docs/docs/tutorials/ed-20.ipynb b/docs/docs/tutorials/ed-20.ipynb index b9a47a28..4ad0e666 100644 --- a/docs/docs/tutorials/ed-20.ipynb +++ b/docs/docs/tutorials/ed-20.ipynb @@ -515,7 +515,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2')" + "project.display.pattern(expt_name='expt_s2')" ] }, { @@ -525,7 +525,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2')" + "project.display.pattern(expt_name='expt_n2')" ] }, { @@ -576,7 +576,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fittable_params()" + "project.display.parameters.fittable()" ] }, { @@ -731,8 +731,8 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -752,7 +752,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='expt_s2')" + "project.display.pattern(expt_name='expt_s2')" ] }, { @@ -762,7 +762,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='expt_n2')" + "project.display.pattern(expt_name='expt_n2')" ] }, { @@ -780,7 +780,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(\n", + "project.display.pattern(\n", " expt_name='expt_s2',\n", " x='d_spacing',\n", " x_min=2.08,\n", @@ -795,7 +795,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(\n", + "project.display.pattern(\n", " expt_name='expt_n2',\n", " x='d_spacing',\n", " x_min=2.08,\n", diff --git a/docs/docs/tutorials/ed-21.ipynb b/docs/docs/tutorials/ed-21.ipynb index 405bc858..33f77196 100644 --- a/docs/docs/tutorials/ed-21.ipynb +++ b/docs/docs/tutorials/ed-21.ipynb @@ -218,7 +218,9 @@ "id": "14", "metadata": {}, "source": [ - "#### Download the Measured Data" + "Download the measured data from the repository. Alternatively, you\n", + "could use your own data file by providing the path to it instead of\n", + "downloading from the repository." ] }, { @@ -236,7 +238,8 @@ "id": "16", "metadata": {}, "source": [ - "#### Create the Experiment Object" + "Create the experiment object and specify the sample form, beam mode,\n", + "and radiation probe." ] }, { @@ -270,7 +273,25 @@ "id": "19", "metadata": {}, "source": [ - "#### Set Instrument and Peak-Profile Parameters\n", + "Link the structural phase to the experiment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "experiment.linked_phases.create(id='lbco', scale=9.1351)" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "Set instrument and peak profile parameters.\n", "\n", "These values provide the initial instrument description for the local\n", "refinement. Later, a subset of them will be refined." @@ -279,7 +300,7 @@ { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "22", "metadata": {}, "outputs": [], "source": [ @@ -290,7 +311,7 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -302,10 +323,10 @@ }, { "cell_type": "markdown", - "id": "22", + "id": "24", "metadata": {}, "source": [ - "#### Add Background Points and Excluded Regions\n", + "Add background points and excluded regions.\n", "\n", "The line-segment background is defined by a few anchor points. We also\n", "exclude regions that are not intended to contribute to the fit." @@ -314,7 +335,7 @@ { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "25", "metadata": {}, "outputs": [], "source": [ @@ -327,7 +348,7 @@ { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "26", "metadata": {}, "outputs": [], "source": [ @@ -335,24 +356,6 @@ "experiment.excluded_regions.create(id='2', start=100, end=180)" ] }, - { - "cell_type": "markdown", - "id": "25", - "metadata": {}, - "source": [ - "#### Link the Structural Phase to the Experiment" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "26", - "metadata": {}, - "outputs": [], - "source": [ - "experiment.linked_phases.create(id='lbco', scale=9.1351)" - ] - }, { "cell_type": "markdown", "id": "27", @@ -442,7 +445,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -463,7 +466,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -473,7 +476,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -491,8 +494,10 @@ "on the current parameter value and expands them by a chosen multiple of\n", "the reported uncertainty.\n", "\n", - "Default `multiplier` is 8 to give a wide range for the sampler to\n", - "explore, but here we use 3 to speed up the tutorial." + "The default `multiplier` is 4. If the local refinement is very tight,\n", + "or if you expect a broader posterior, increase it explicitly.\n", + "\n", + "Show unset fit bounds before setting them from the local refinement uncertainties." ] }, { @@ -502,23 +507,34 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" + ] + }, + { + "cell_type": "markdown", + "id": "40", + "metadata": {}, + "source": [ + "Set fit bounds for all free parameters using the default multiplier of\n", + "4. In this tutorial that means the posterior pair plot will later\n", + "refer to a `ยฑ4 ร— uncertainty` region in its title. To use a different\n", + "region, pass another value, for example `multiplier=6`." ] }, { "cell_type": "code", "execution_count": null, - "id": "40", + "id": "41", "metadata": {}, "outputs": [], "source": [ "for param in project.free_parameters:\n", - " param.set_fit_bounds_from_uncertainty(multiplier=3.5)" + " param.set_fit_bounds_from_uncertainty()" ] }, { "cell_type": "markdown", - "id": "41", + "id": "42", "metadata": {}, "source": [ "Displaying the free parameters again is a convenient way to confirm\n", @@ -529,16 +545,16 @@ { "cell_type": "code", "execution_count": null, - "id": "42", + "id": "43", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { "cell_type": "markdown", - "id": "43", + "id": "44", "metadata": {}, "source": [ "## Step 6: Configure and Run DREAM\n", @@ -550,20 +566,21 @@ "of steps (`steps`) and often the burn-in (`burn`) as well. When\n", "needed, the DREAM API also lets you tune how chains are initialized\n", "through the `init` setting. Other sampler settings such as `thin` and\n", - "`pop` can be adjusted as well. The current EasyDiffraction default\n", - "also uses `parallel=0`, which tells BUMPS DREAM to use all available\n", - "CPUs for population evaluations.\n", + "`pop` can be adjusted as well. The current EasyDiffraction defaults\n", + "use `steps=3000`, `init='lhs'`, and `parallel=0`, which tells\n", + "BUMPS-DREAM to use all available CPUs for population evaluations.\n", "\n", - "The default `steps` value is 1000, and real analyses often need more\n", - "to achieve good convergence and posterior sampling. Here we use a much\n", - "smaller value to keep the tutorial fast, but this is not recommended\n", - "for production analysis." + "The `burn` setting is auto-resolved when left unset. With the default\n", + "`steps=3000` this gives `burn=600`, but if you override `steps` and\n", + "keep `burn=None`, the effective burn-in is recomputed automatically.\n", + "Here we use a much smaller step count to keep the tutorial fast, but\n", + "this is not recommended for production analysis." ] }, { "cell_type": "code", "execution_count": null, - "id": "44", + "id": "45", "metadata": {}, "outputs": [], "source": [ @@ -573,7 +590,7 @@ { "cell_type": "code", "execution_count": null, - "id": "45", + "id": "46", "metadata": {}, "outputs": [], "source": [ @@ -583,17 +600,17 @@ { "cell_type": "code", "execution_count": null, - "id": "46", + "id": "47", "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer.steps = 100 # lower than the default 1000" + "project.analysis.fit.minimizer.steps = 300 # lower than the default 3000" ] }, { "cell_type": "code", "execution_count": null, - "id": "47", + "id": "48", "metadata": {}, "outputs": [], "source": [ @@ -602,7 +619,7 @@ }, { "cell_type": "markdown", - "id": "48", + "id": "49", "metadata": {}, "source": [ "## Step 7: Inspect Bayesian Results\n", @@ -615,16 +632,16 @@ { "cell_type": "code", "execution_count": null, - "id": "49", + "id": "50", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { "cell_type": "markdown", - "id": "50", + "id": "51", "metadata": {}, "source": [ "The correlation and posterior-pair plots are complementary:\n", @@ -632,32 +649,35 @@ "- `plot_param_correlations` summarizes pairwise structure in a compact\n", " matrix.\n", "- `plot_posterior_pairs` shows marginal densities on the diagonal and\n", - " posterior contours off-diagonal." + " posterior contours off-diagonal. In this tutorial its title also\n", + " reminds you that the display region follows the `ยฑ4 ร— uncertainty`\n", + " bounds defined above, while numeric subplot ranges are omitted to\n", + " keep the grid readable." ] }, { "cell_type": "code", "execution_count": null, - "id": "51", + "id": "52", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { "cell_type": "code", "execution_count": null, - "id": "52", + "id": "53", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_posterior_pairs()" + "project.display.posterior.pairs()" ] }, { "cell_type": "markdown", - "id": "53", + "id": "54", "metadata": {}, "source": [ "The one-dimensional posterior distributions below make it easier to\n", @@ -668,17 +688,17 @@ { "cell_type": "code", "execution_count": null, - "id": "54", + "id": "55", "metadata": {}, "outputs": [], "source": [ "for param in project.free_parameters:\n", - " project.display.plotter.plot_param_distribution(param)" + " project.display.posterior.distribution(param)" ] }, { "cell_type": "markdown", - "id": "55", + "id": "56", "metadata": {}, "source": [ "Finally, the posterior predictive plot propagates the sampled parameter\n", @@ -690,16 +710,16 @@ { "cell_type": "code", "execution_count": null, - "id": "56", + "id": "57", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_posterior_predictive(expt_name='hrpt')" + "project.display.posterior.predictive(expt_name='hrpt')" ] }, { "cell_type": "markdown", - "id": "57", + "id": "58", "metadata": {}, "source": [ "A final zoomed measured-vs-calculated plot is useful for checking how\n", @@ -710,11 +730,15 @@ { "cell_type": "code", "execution_count": null, - "id": "58", + "id": "59", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_posterior_predictive(expt_name='hrpt', x_min=92, x_max=93)" + "project.display.posterior.predictive(\n", + " expt_name='hrpt',\n", + " x_min=92,\n", + " x_max=93,\n", + ")" ] } ], diff --git a/docs/docs/tutorials/ed-22.ipynb b/docs/docs/tutorials/ed-22.ipynb index cc880314..965e4ed8 100644 --- a/docs/docs/tutorials/ed-22.ipynb +++ b/docs/docs/tutorials/ed-22.ipynb @@ -35,7 +35,16 @@ "distribution with BUMPS-DREAM.\n", "\n", "The example uses constant-wavelength neutron single-crystal diffraction data\n", - "for Tb2TiO7 measured on HEiDi at FRM II." + "for Tb2TiO7 measured on HEiDi at FRM II.\n", + "\n", + "The goal is not only to obtain a good fit, but also to answer Bayesian\n", + "questions such as:\n", + "\n", + "- Which parameter values are most probable?\n", + "- How broad are the credible intervals?\n", + "- Which parameters are strongly correlated?\n", + "- How much uncertainty propagates into the calculated reflection\n", + " intensities?" ] }, { @@ -61,7 +70,11 @@ "id": "4", "metadata": {}, "source": [ - "## Step 1: Create a Project Container" + "## Step 1: Create a Project Container\n", + "\n", + "The project object keeps structures, experiments, fit settings, and\n", + "plotting utilities together in a single place. We will build the full\n", + "workflow inside this object." ] }, { @@ -79,7 +92,12 @@ "id": "6", "metadata": {}, "source": [ - "## Step 2: Build the Structural Model" + "## Step 2: Build the Structural Model\n", + "\n", + "For this example we start from a CIF file describing the Tb2TiO7\n", + "pyrochlore structure. Loading the structure from CIF is convenient\n", + "because it preserves a realistic starting\n", + "model without rebuilding the full structure by hand." ] }, { @@ -117,7 +135,11 @@ "id": "10", "metadata": {}, "source": [ - "## Step 3: Define the Diffraction Experiment" + "## Step 3: Define the Diffraction Experiment\n", + "\n", + "Next we download the measured reflection data, create a neutron\n", + "single-crystal experiment, and configure the crystal link,\n", + "wavelength, and extinction model." ] }, { @@ -156,10 +178,18 @@ "experiment = project.experiments['heidi']" ] }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "Link the crystal structure to the experiment and set its scale factor." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "14", + "id": "15", "metadata": {}, "outputs": [], "source": [ @@ -167,10 +197,20 @@ "experiment.linked_crystal.scale = 1.0" ] }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "Set the instrument wavelength and starting extinction parameters.\n", + "These values provide the initial experiment description for the local\n", + "refinement." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "15", + "id": "17", "metadata": {}, "outputs": [], "source": [ @@ -180,7 +220,7 @@ { "cell_type": "code", "execution_count": null, - "id": "16", + "id": "18", "metadata": {}, "outputs": [], "source": [ @@ -190,16 +230,27 @@ }, { "cell_type": "markdown", - "id": "17", + "id": "19", "metadata": {}, "source": [ - "## Step 4: Run an Initial Local Refinement" + "## Step 4: Run an Initial Local Refinement\n", + "\n", + "Before Bayesian sampling, it is useful to run a deterministic fit. This\n", + "gives us:\n", + "\n", + "- a good point estimate near the best-fit region,\n", + "- uncertainties from the local optimizer,\n", + "- a quick check that the model and experiment are configured\n", + " sensibly.\n", + "\n", + "In this tutorial we refine a small set of structural and extinction\n", + "parameters while keeping occupancies fixed." ] }, { "cell_type": "code", "execution_count": null, - "id": "18", + "id": "20", "metadata": {}, "outputs": [], "source": [ @@ -218,7 +269,7 @@ { "cell_type": "code", "execution_count": null, - "id": "19", + "id": "21", "metadata": {}, "outputs": [], "source": [ @@ -226,10 +277,20 @@ "experiment.extinction.radius.free = True" ] }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "We keep using the default LMFIT Levenberg-Marquardt minimizer as a fast local\n", + "optimizer. Its main purpose here is to provide a stable starting point\n", + "and uncertainty estimates for the Bayesian run." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "20", + "id": "23", "metadata": {}, "outputs": [], "source": [ @@ -239,65 +300,111 @@ { "cell_type": "code", "execution_count": null, - "id": "21", + "id": "24", "metadata": {}, "outputs": [], "source": [ "project.analysis.fit()" ] }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "The fit-results display summarizes the locally refined values and their\n", + "estimated uncertainties." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "22", + "id": "26", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "The correlation plot shows how strongly the refined parameters move\n", + "together in the local refinement. The measured-vs-calculated plot shows\n", + "how well the refined crystal model reproduces the measured reflection\n", + "intensities." ] }, { "cell_type": "code", "execution_count": null, - "id": "23", + "id": "28", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { "cell_type": "code", "execution_count": null, - "id": "24", + "id": "29", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='heidi')" + "project.display.pattern(expt_name='heidi')" ] }, { "cell_type": "markdown", - "id": "25", + "id": "30", "metadata": {}, "source": [ - "## Step 5: Prepare for Bayesian Sampling" + "## Step 5: Prepare for Bayesian Sampling\n", + "\n", + "DREAM requires finite bounds for the free parameters. Instead of\n", + "setting them manually, we derive them from the uncertainties estimated\n", + "in the local refinement.\n", + "\n", + "The helper method `set_fit_bounds_from_uncertainty` centers the bounds\n", + "on the current parameter value and expands them by a chosen multiple of\n", + "the reported uncertainty.\n", + "\n", + "The default `multiplier` is 4. In this single-crystal tutorial we use\n", + "a tighter value of `1.5` to keep the sampling window closer to the\n", + "locally refined solution.\n", + "\n", + "Show unset fit bounds before setting them from the local refinement\n", + "uncertainties." ] }, { "cell_type": "code", "execution_count": null, - "id": "26", + "id": "31", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "Set fit bounds for all free parameters using `multiplier=1.5`. In this\n", + "tutorial that means the posterior pair plot will later refer to a\n", + "`ยฑ1.5 ร— uncertainty` region in its title. To widen the sampling window,\n", + "increase the multiplier explicitly." ] }, { "cell_type": "code", "execution_count": null, - "id": "27", + "id": "33", "metadata": {}, "outputs": [], "source": [ @@ -305,28 +412,53 @@ " param.set_fit_bounds_from_uncertainty(multiplier=1.5)" ] }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "Displaying the free parameters again is a convenient way to confirm\n", + "that the fit bounds have been assigned as expected before launching the\n", + "sampler." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "28", + "id": "35", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { "cell_type": "markdown", - "id": "29", + "id": "36", "metadata": {}, "source": [ - "## Step 6: Configure and Run BUMPS-DREAM" + "## Step 6: Configure and Run DREAM\n", + "\n", + "We now switch from the local minimizer to the Bayesian DREAM sampler.\n", + "\n", + "The settings below are intentionally small so the tutorial runs\n", + "quickly. For production analysis you would usually increase the number\n", + "of steps (`steps`) and often the burn-in (`burn`) as well. When\n", + "needed, the DREAM API also lets you tune how chains are initialized\n", + "through the `init` setting. Other sampler settings such as `thin` and\n", + "`pop` can be adjusted as well. The current EasyDiffraction defaults\n", + "use `steps=3000`, `init='lhs'`, and `parallel=0`, which tells\n", + "BUMPS-DREAM to use all available CPUs for population evaluations.\n", + "\n", + "The `burn` setting is auto-resolved when left unset. Here we override\n", + "`steps` with a smaller value to keep the tutorial fast, and the\n", + "effective burn-in is recomputed automatically." ] }, { "cell_type": "code", "execution_count": null, - "id": "30", + "id": "37", "metadata": {}, "outputs": [], "source": [ @@ -336,7 +468,7 @@ { "cell_type": "code", "execution_count": null, - "id": "31", + "id": "38", "metadata": {}, "outputs": [], "source": [ @@ -346,72 +478,120 @@ { "cell_type": "code", "execution_count": null, - "id": "32", + "id": "39", "metadata": {}, "outputs": [], "source": [ - "project.analysis.fit.minimizer.steps = 500" + "project.analysis.fit.minimizer.steps = 500 # lower than the default 3000" ] }, { "cell_type": "code", "execution_count": null, - "id": "33", + "id": "40", "metadata": {}, "outputs": [], "source": [ "project.analysis.fit()" ] }, + { + "cell_type": "markdown", + "id": "41", + "metadata": {}, + "source": [ + "## Step 7: Inspect Bayesian Results\n", + "\n", + "The fit-results display now includes sampler settings, convergence\n", + "diagnostics, committed parameter values, and posterior summary\n", + "statistics." + ] + }, { "cell_type": "code", "execution_count": null, - "id": "34", + "id": "42", "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" + ] + }, + { + "cell_type": "markdown", + "id": "43", + "metadata": {}, + "source": [ + "The correlation and posterior-pair plots are complementary:\n", + "\n", + "- `plot_param_correlations` summarizes pairwise structure in a compact\n", + " matrix.\n", + "- `plot_posterior_pairs` shows marginal densities on the diagonal and\n", + " posterior contours off-diagonal. In this tutorial its title also\n", + " reminds you that the display region follows the `ยฑ1.5 ร— uncertainty`\n", + " bounds defined above, while numeric subplot ranges are omitted to\n", + " keep the grid readable." ] }, { "cell_type": "code", "execution_count": null, - "id": "35", + "id": "44", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { "cell_type": "code", "execution_count": null, - "id": "36", + "id": "45", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_posterior_pairs()" + "project.display.posterior.pairs()" + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "The one-dimensional posterior distributions below make it easier to\n", + "inspect individual parameters in isolation, including asymmetry or\n", + "multimodality." ] }, { "cell_type": "code", "execution_count": null, - "id": "37", + "id": "47", "metadata": {}, "outputs": [], "source": [ "for param in project.free_parameters:\n", - " project.display.plotter.plot_param_distribution(param)" + " project.display.posterior.distribution(param)" + ] + }, + { + "cell_type": "markdown", + "id": "48", + "metadata": {}, + "source": [ + "Finally, the posterior predictive plot propagates the sampled\n", + "parameter uncertainty into the calculated single-crystal reflection\n", + "intensities." ] }, { "cell_type": "code", "execution_count": null, - "id": "38", + "id": "49", "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_posterior_predictive(expt_name='heidi')" + "project.display.posterior.predictive(expt_name='heidi')" ] } ], diff --git a/docs/docs/tutorials/ed-3.ipynb b/docs/docs/tutorials/ed-3.ipynb index f99298a2..03fba514 100644 --- a/docs/docs/tutorials/ed-3.ipynb +++ b/docs/docs/tutorials/ed-3.ipynb @@ -175,7 +175,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.show_supported_engines()" + "project.rendering.show_chart_engines()" ] }, { @@ -193,7 +193,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.show_config()" + "project.rendering.show_config()" ] }, { @@ -492,7 +492,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt', include='measured')" ] }, { @@ -774,7 +774,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt', include='calculated')" ] }, { @@ -792,7 +792,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -802,7 +802,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)" + "project.display.pattern(expt_name='hrpt', x_min=38, x_max=41)" ] }, { @@ -822,7 +822,7 @@ "metadata": {}, "outputs": [], "source": [ - "# project.analysis.display.all_params()" + "# project.display.parameters.all()" ] }, { @@ -840,7 +840,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fittable_params()" + "project.display.parameters.fittable()" ] }, { @@ -858,7 +858,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -876,7 +876,7 @@ "metadata": {}, "outputs": [], "source": [ - "# project.analysis.display.how_to_access_parameters()" + "# project.display.parameters.access()" ] }, { @@ -1014,7 +1014,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -1033,7 +1033,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -1051,7 +1051,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -1061,7 +1061,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)" + "project.display.pattern(expt_name='hrpt', x_min=38, x_max=41)" ] }, { @@ -1120,7 +1120,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -1139,7 +1139,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -1157,7 +1157,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -1167,7 +1167,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)" + "project.display.pattern(expt_name='hrpt', x_min=38, x_max=41)" ] }, { @@ -1226,7 +1226,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -1245,7 +1245,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -1263,7 +1263,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -1273,7 +1273,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)" + "project.display.pattern(expt_name='hrpt', x_min=38, x_max=41)" ] }, { @@ -1356,7 +1356,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.constraints()" + "project.analysis.constraints.show()" ] }, { @@ -1374,7 +1374,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -1393,7 +1393,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -1411,7 +1411,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -1421,7 +1421,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)" + "project.display.pattern(expt_name='hrpt', x_min=38, x_max=41)" ] }, { @@ -1508,7 +1508,7 @@ }, "outputs": [], "source": [ - "project.analysis.display.constraints()" + "project.analysis.constraints.show()" ] }, { @@ -1544,7 +1544,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -1563,8 +1563,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -1582,7 +1582,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -1592,7 +1592,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=38, x_max=41)" + "project.display.pattern(expt_name='hrpt', x_min=38, x_max=41)" ] }, { diff --git a/docs/docs/tutorials/ed-4.ipynb b/docs/docs/tutorials/ed-4.ipynb index 3a77ab8b..bdf0343c 100644 --- a/docs/docs/tutorials/ed-4.ipynb +++ b/docs/docs/tutorials/ed-4.ipynb @@ -687,7 +687,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='npd', x_min=35.5, x_max=38.3)" + "project.display.pattern(expt_name='npd', x_min=35.5, x_max=38.3)" ] }, { @@ -697,7 +697,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='xrd', x_min=29.0, x_max=30.4)" + "project.display.pattern(expt_name='xrd', x_min=29.0, x_max=30.4)" ] } ], diff --git a/docs/docs/tutorials/ed-5.ipynb b/docs/docs/tutorials/ed-5.ipynb index 02ef6045..3fa6294b 100644 --- a/docs/docs/tutorials/ed-5.ipynb +++ b/docs/docs/tutorials/ed-5.ipynb @@ -426,7 +426,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "project.display.pattern(expt_name='d20')" ] }, { @@ -436,7 +436,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='d20', x_min=41, x_max=54)" + "project.display.pattern(expt_name='d20', x_min=41, x_max=54)" ] }, { @@ -507,7 +507,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -582,7 +582,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -592,7 +592,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -610,7 +610,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='d20')" + "project.display.pattern(expt_name='d20')" ] }, { @@ -620,7 +620,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='d20', x_min=42, x_max=52)" + "project.display.pattern(expt_name='d20', x_min=42, x_max=52)" ] }, { diff --git a/docs/docs/tutorials/ed-6.ipynb b/docs/docs/tutorials/ed-6.ipynb index ae8f5a84..3c89362d 100644 --- a/docs/docs/tutorials/ed-6.ipynb +++ b/docs/docs/tutorials/ed-6.ipynb @@ -385,7 +385,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -395,7 +395,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)" + "project.display.pattern(expt_name='hrpt', x_min=48, x_max=51)" ] }, { @@ -437,7 +437,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -465,7 +465,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -483,7 +483,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -493,7 +493,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)" + "project.display.pattern(expt_name='hrpt', x_min=48, x_max=51)" ] }, { @@ -537,7 +537,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -565,7 +565,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -583,7 +583,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -593,7 +593,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)" + "project.display.pattern(expt_name='hrpt', x_min=48, x_max=51)" ] }, { @@ -635,7 +635,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -663,7 +663,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -681,7 +681,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -691,7 +691,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)" + "project.display.pattern(expt_name='hrpt', x_min=48, x_max=51)" ] }, { @@ -738,7 +738,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -766,7 +766,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -776,7 +776,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -794,7 +794,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')" + "project.display.pattern(expt_name='hrpt')" ] }, { @@ -804,7 +804,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=48, x_max=51)" + "project.display.pattern(expt_name='hrpt', x_min=48, x_max=51)" ] }, { diff --git a/docs/docs/tutorials/ed-7.ipynb b/docs/docs/tutorials/ed-7.ipynb index 7a66c6f6..31294b58 100644 --- a/docs/docs/tutorials/ed-7.ipynb +++ b/docs/docs/tutorials/ed-7.ipynb @@ -345,8 +345,8 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')\n", - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)" + "project.display.pattern(expt_name='sepd')\n", + "project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700)" ] }, { @@ -387,7 +387,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -406,7 +406,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -424,7 +424,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')" + "project.display.pattern(expt_name='sepd')" ] }, { @@ -434,7 +434,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)" + "project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700)" ] }, { @@ -473,7 +473,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -492,7 +492,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -510,7 +510,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')" + "project.display.pattern(expt_name='sepd')" ] }, { @@ -520,7 +520,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)" + "project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700)" ] }, { @@ -579,7 +579,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -598,7 +598,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -616,7 +616,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')" + "project.display.pattern(expt_name='sepd')" ] }, { @@ -626,7 +626,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)" + "project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700)" ] }, { @@ -668,7 +668,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.analysis.display.free_params()" + "project.display.parameters.free()" ] }, { @@ -687,7 +687,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -705,7 +705,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -723,7 +723,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd')" + "project.display.pattern(expt_name='sepd')" ] }, { @@ -733,7 +733,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)" + "project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700)" ] }, { @@ -745,7 +745,7 @@ }, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing')" + "project.display.pattern(expt_name='sepd', x='d_spacing')" ] }, { @@ -860,7 +860,7 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()" + "project.display.fit.results()" ] }, { @@ -878,7 +878,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_param_correlations()" + "project.display.fit.correlations()" ] }, { @@ -896,7 +896,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x_min=23200, x_max=23700)" + "project.display.pattern(expt_name='sepd', x_min=23200, x_max=23700)" ] }, { @@ -906,7 +906,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='sepd', x='d_spacing')" + "project.display.pattern(expt_name='sepd', x='d_spacing')" ] } ], diff --git a/docs/docs/tutorials/ed-8.ipynb b/docs/docs/tutorials/ed-8.ipynb index 5df56320..248d4f59 100644 --- a/docs/docs/tutorials/ed-8.ipynb +++ b/docs/docs/tutorials/ed-8.ipynb @@ -630,7 +630,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6')" + "project.display.pattern(expt_name='wish_5_6')" ] }, { @@ -640,7 +640,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7')" + "project.display.pattern(expt_name='wish_4_7')" ] }, { @@ -659,8 +659,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -678,7 +678,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='wish_5_6')" + "project.display.pattern(expt_name='wish_5_6')" ] }, { @@ -688,7 +688,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='wish_4_7')" + "project.display.pattern(expt_name='wish_4_7')" ] }, { diff --git a/docs/docs/tutorials/ed-9.ipynb b/docs/docs/tutorials/ed-9.ipynb index 4c4d7eb5..0bfd0949 100644 --- a/docs/docs/tutorials/ed-9.ipynb +++ b/docs/docs/tutorials/ed-9.ipynb @@ -509,7 +509,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas(expt_name='mcstas')" + "project.display.pattern(expt_name='mcstas', include='measured')" ] }, { @@ -564,7 +564,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas(expt_name='mcstas')" + "project.display.pattern(expt_name='mcstas', include=('measured', 'excluded'))" ] }, { @@ -660,8 +660,8 @@ "outputs": [], "source": [ "project.analysis.fit()\n", - "project.analysis.display.fit_results()\n", - "project.display.plotter.plot_param_correlations()" + "project.display.fit.results()\n", + "project.display.fit.correlations()" ] }, { @@ -679,7 +679,7 @@ "metadata": {}, "outputs": [], "source": [ - "project.display.plotter.plot_meas_vs_calc(expt_name='mcstas')" + "project.display.pattern(expt_name='mcstas')" ] } ], diff --git a/docs/docs/user-guide/first-steps.md b/docs/docs/user-guide/first-steps.md index cd1de40a..66f96e90 100644 --- a/docs/docs/user-guide/first-steps.md +++ b/docs/docs/user-guide/first-steps.md @@ -129,19 +129,18 @@ parameters grouped in different categories. For example, you can use: parameters for the analysis step. - `project.display.parameters.fittable()` โ€“ to display only the parameters that can be fitted during the analysis. -- `project.display.parameters.free()` โ€“ to display the parameters - that are currently free to be adjusted during the fitting process. - -Finally, you can use the -`project.display.parameters.access()` method to get a -brief overview of how to access and modify parameters in the analysis -step, along with their unique identifiers in the CIF format. This can be -particularly useful for users who are new to the EasyDiffraction API or -those who want to quickly understand how to work with parameters in -their projects. - -An example of the output for the -`project.display.parameters.access()` method is: +- `project.display.parameters.free()` โ€“ to display the parameters that + are currently free to be adjusted during the fitting process. + +Finally, you can use the `project.display.parameters.access()` method to +get a brief overview of how to access and modify parameters in the +analysis step, along with their unique identifiers in the CIF format. +This can be particularly useful for users who are new to the +EasyDiffraction API or those who want to quickly understand how to work +with parameters in their projects. + +An example of the output for the `project.display.parameters.access()` +method is: | | Code variable | Unique ID for CIF | | --- | --------------------------------------------------- | -------------------------- | diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index 36723aee..e3c01607 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -104,7 +104,7 @@ class AnalysisDisplay: Accessed via ``analysis.display``. """ - def __init__(self, analysis: 'Analysis') -> None: + def __init__(self, analysis: Analysis) -> None: self._analysis = analysis def _flush_structure_categories(self) -> None: diff --git a/src/easydiffraction/display/plotters/ascii.py b/src/easydiffraction/display/plotters/ascii.py index a2ab913e..c81af0b0 100644 --- a/src/easydiffraction/display/plotters/ascii.py +++ b/src/easydiffraction/display/plotters/ascii.py @@ -84,6 +84,8 @@ def plot_powder( Figure title printed above the chart. height : int | None, default=None Number of text rows to allocate for the chart. + excluded_ranges : tuple[tuple[float, float], ...], default=() + Excluded x-ranges to print below the selected x-range. """ # Intentionally unused; kept for a consistent display API del axes_labels diff --git a/src/easydiffraction/display/plotters/plotly.py b/src/easydiffraction/display/plotters/plotly.py index 1aef6eb1..d41c1f9e 100644 --- a/src/easydiffraction/display/plotters/plotly.py +++ b/src/easydiffraction/display/plotters/plotly.py @@ -1061,6 +1061,8 @@ def plot_powder( Figure title. height : int | None, default=None Ignored; Plotly auto-sizes based on renderer. + excluded_ranges : tuple[tuple[float, float], ...], default=() + Excluded x-ranges to shade on the figure. """ # Intentionally unused; accepted for API compatibility del height diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index c991960a..4c2ca111 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -374,6 +374,18 @@ def _filtered_y_array( mask = (x_array >= lower_bound) & (x_array <= upper_bound) return y_array[mask] + def _filtered_optional_y_array( + self, + y_array: object | None, + x_array: object, + x_min: object, + x_max: object, + ) -> object | None: + """Filter an optional y-array by inclusive x-range limits.""" + if y_array is None: + return None + return self._filtered_y_array(y_array, x_array, x_min, x_max) + @staticmethod def _get_axes_labels( sample_form: object, @@ -591,18 +603,23 @@ def plot_meas( Upper bound for the x-axis range. x : object | None, default=None Optional explicit x-axis data to override stored values. + show_excluded : bool, default=False + Whether to show excluded fitting regions on supported plots. """ self._update_project_categories(expt_name) experiment = self._project.experiments[expt_name] + plot_options = _MeasVsCalcPlotOptions( + x_min=x_min, + x_max=x_max, + show_excluded=show_excluded, + x=x, + ) self._plot_meas_data( experiment, intensity_category_for(experiment), expt_name, experiment.type, - x_min=x_min, - x_max=x_max, - x=x, - show_excluded=show_excluded, + plot_options, ) def plot_calc( @@ -627,18 +644,23 @@ def plot_calc( Upper bound for the x-axis range. x : object | None, default=None Optional explicit x-axis data to override stored values. + show_excluded : bool, default=False + Whether to show excluded fitting regions on supported plots. """ self._update_project_categories(expt_name) experiment = self._project.experiments[expt_name] + plot_options = _MeasVsCalcPlotOptions( + x_min=x_min, + x_max=x_max, + show_excluded=show_excluded, + x=x, + ) self._plot_calc_data( experiment, intensity_category_for(experiment), expt_name, experiment.type, - x_min=x_min, - x_max=x_max, - x=x, - show_excluded=show_excluded, + plot_options, ) def plot_meas_vs_calc( @@ -666,6 +688,8 @@ def plot_meas_vs_calc( When ``None``, powder Bragg plots include the residual by default while other measured-vs-calculated plots keep the historical no-residual default. + show_excluded : bool, default=False + Whether to show excluded fitting regions on supported plots. x : object | None, default=None Optional explicit x-axis data to override stored values. """ @@ -1023,6 +1047,8 @@ def plot_posterior_predictive( show_residual : bool | None, default=None Whether to include the residual row in supported powder composite plots. + show_excluded : bool, default=False + Whether to show excluded fitting regions on supported plots. x : object | None, default=None Optional explicit x-axis data to override stored values. @@ -3660,11 +3686,11 @@ def _plot_posterior_predictive_data( ctx['x_min'], ctx['x_max'], ) - y_bkg_raw = getattr(pattern, 'intensity_bkg', None) - y_bkg = ( - self._filtered_y_array(y_bkg_raw, ctx['x_array'], ctx['x_min'], ctx['x_max']) - if y_bkg_raw is not None - else None + y_bkg = self._filtered_optional_y_array( + getattr(pattern, 'intensity_bkg', None), + ctx['x_array'], + ctx['x_min'], + ctx['x_max'], ) y_calc = self._filtered_y_array( summary.map_prediction, summary.x, ctx['x_min'], ctx['x_max'] @@ -4693,16 +4719,15 @@ def _plot_meas_data( pattern: object, expt_name: str, expt_type: object, - x_min: object = None, - x_max: object = None, - x: object = None, - show_excluded: bool = False, + plot_options: _MeasVsCalcPlotOptions, ) -> None: """ Plot measured pattern using the current engine. Parameters ---------- + experiment : object + Experiment object used for excluded-range extraction. pattern : object Object with x-axis arrays (``two_theta``, ``time_of_flight``, ``d_spacing``) and ``meas`` array. @@ -4710,20 +4735,16 @@ def _plot_meas_data( Experiment name for the title. expt_type : object Experiment type with scattering/beam enums. - x_min : object, default=None - Optional minimum x-axis limit. - x_max : object, default=None - Optional maximum x-axis limit. - x : object, default=None - X-axis type. If ``None``, auto-detected from beam mode. + plot_options : _MeasVsCalcPlotOptions + X-range, excluded-region, and x-axis selection options. """ ctx = self._prepare_powder_context( pattern, expt_name, expt_type, - x_min, - x_max, - x, + plot_options.x_min, + plot_options.x_max, + plot_options.x, ) if ctx is None: return @@ -4740,7 +4761,7 @@ def _plot_meas_data( x_min=ctx['x_min'], x_max=ctx['x_max'], ) - if show_excluded + if plot_options.show_excluded else () ) @@ -4760,16 +4781,15 @@ def _plot_calc_data( pattern: object, expt_name: str, expt_type: object, - x_min: object = None, - x_max: object = None, - x: object = None, - show_excluded: bool = False, + plot_options: _MeasVsCalcPlotOptions, ) -> None: """ Plot calculated pattern using the current engine. Parameters ---------- + experiment : object + Experiment object used for excluded-range extraction. pattern : object Object with x-axis arrays (``two_theta``, ``time_of_flight``, ``d_spacing``) and ``calc`` array. @@ -4777,20 +4797,16 @@ def _plot_calc_data( Experiment name for the title. expt_type : object Experiment type with scattering/beam enums. - x_min : object, default=None - Optional minimum x-axis limit. - x_max : object, default=None - Optional maximum x-axis limit. - x : object, default=None - X-axis type. If ``None``, auto-detected from beam mode. + plot_options : _MeasVsCalcPlotOptions + X-range, excluded-region, and x-axis selection options. """ ctx = self._prepare_powder_context( pattern, expt_name, expt_type, - x_min, - x_max, - x, + plot_options.x_min, + plot_options.x_max, + plot_options.x, ) if ctx is None: return @@ -4807,7 +4823,7 @@ def _plot_calc_data( x_min=ctx['x_min'], x_max=ctx['x_max'], ) - if show_excluded + if plot_options.show_excluded else () ) diff --git a/src/easydiffraction/project/categories/rendering/__init__.py b/src/easydiffraction/project/categories/rendering/__init__.py index 84662b01..3e988915 100644 --- a/src/easydiffraction/project/categories/rendering/__init__.py +++ b/src/easydiffraction/project/categories/rendering/__init__.py @@ -5,4 +5,4 @@ from __future__ import annotations from easydiffraction.project.categories.rendering.default import Rendering -from easydiffraction.project.categories.rendering.factory import RenderingFactory \ No newline at end of file +from easydiffraction.project.categories.rendering.factory import RenderingFactory diff --git a/src/easydiffraction/project/categories/rendering/default.py b/src/easydiffraction/project/categories/rendering/default.py index 3e923366..f8970cb5 100644 --- a/src/easydiffraction/project/categories/rendering/default.py +++ b/src/easydiffraction/project/categories/rendering/default.py @@ -128,4 +128,4 @@ def from_cif(self, block: object, idx: int = 0) -> None: if table_engine == self._tabler.engine: self._table_engine.value = table_engine else: - self.table_engine = table_engine \ No newline at end of file + self.table_engine = table_engine diff --git a/src/easydiffraction/project/categories/rendering/factory.py b/src/easydiffraction/project/categories/rendering/factory.py index acaa03f3..c2bdf7c5 100644 --- a/src/easydiffraction/project/categories/rendering/factory.py +++ b/src/easydiffraction/project/categories/rendering/factory.py @@ -14,4 +14,4 @@ class RenderingFactory(FactoryBase): _default_rules: ClassVar[dict] = { frozenset(): 'default', - } \ No newline at end of file + } diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py index 4ffbd460..20ae149c 100644 --- a/src/easydiffraction/project/display.py +++ b/src/easydiffraction/project/display.py @@ -287,6 +287,19 @@ def _status_by_name( msg = f'Unknown pattern option: {option_name}.' raise ValueError(msg) + @staticmethod + def _with_available_options( + status_by_name: dict[str, PatternOptionStatus], + required: tuple[str, ...], + optional: tuple[str, ...], + ) -> tuple[str, ...]: + """Return required options plus available optional ones.""" + include = list(required) + include.extend( + option_name for option_name in optional if status_by_name[option_name].available + ) + return tuple(include) + @classmethod def _auto_include( cls, @@ -294,35 +307,31 @@ def _auto_include( ) -> tuple[str, ...]: """Return the effective include tuple for ``include='auto'``.""" status_by_name = {status.name: status for status in statuses} + optional_point_estimate = ('background', 'residual', 'bragg', 'excluded') + if status_by_name['uncertainty'].available: - include = ['measured', 'calculated', 'uncertainty'] - if status_by_name['background'].available: - include.append('background') - if status_by_name['residual'].available: - include.append('residual') - if status_by_name['bragg'].available: - include.append('bragg') - if status_by_name['excluded'].available: - include.append('excluded') - return tuple(include) + return cls._with_available_options( + status_by_name, + ('measured', 'calculated', 'uncertainty'), + optional_point_estimate, + ) if status_by_name['measured'].available and status_by_name['calculated'].available: - include = ['measured', 'calculated'] - if status_by_name['background'].available: - include.append('background') - if status_by_name['residual'].available: - include.append('residual') - if status_by_name['bragg'].available: - include.append('bragg') - if status_by_name['excluded'].available: - include.append('excluded') - return tuple(include) + return cls._with_available_options( + status_by_name, + ('measured', 'calculated'), + optional_point_estimate, + ) if status_by_name['measured'].available: - return ('measured', 'excluded') if status_by_name['excluded'].available else ('measured',) + return cls._with_available_options( + status_by_name, + ('measured',), + ('excluded',), + ) if status_by_name['calculated'].available: - return ( - ('calculated', 'excluded') - if status_by_name['excluded'].available - else ('calculated',) + return cls._with_available_options( + status_by_name, + ('calculated',), + ('excluded',), ) return () @@ -332,7 +341,9 @@ def _validate_requested_include( statuses: list[PatternOptionStatus], include: tuple[str, ...], ) -> None: - """Raise a clear error when a requested include is unavailable.""" + """ + Raise a clear error when a requested include is unavailable. + """ status_by_name = {status.name: status for status in statuses} unavailable = [ option_name @@ -354,9 +365,11 @@ def _validate_requested_include( if 'residual' in include_set and not {'measured', 'calculated'}.issubset(include_set): msg = 'residual requires both measured and calculated data in the same view.' raise ValueError(msg) - if 'excluded' in include_set and not include_set.intersection( - {'measured', 'calculated', 'uncertainty'} - ): + if 'excluded' in include_set and not include_set.intersection({ + 'measured', + 'calculated', + 'uncertainty', + }): msg = 'excluded requires measured, calculated, or uncertainty data in the same view.' raise ValueError(msg) @@ -369,7 +382,9 @@ def _show_point_estimate_pattern( include: tuple[str, ...], x: object | None, ) -> None: - """Dispatch a point-estimate pattern view to the live plotter.""" + """ + Dispatch a point-estimate pattern view to the live plotter. + """ statuses = self._pattern_option_statuses(expt_name) self._validate_requested_include(statuses, include) include_set = set(include) @@ -461,59 +476,57 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: scattering_type=scattering_type, ) - auto_include = self._auto_include( - [ - PatternOptionStatus( - name='measured', - description=_PATTERN_OPTION_DESCRIPTIONS['measured'], - available=measured_available, - auto_included=False, - reason='', - ), - PatternOptionStatus( - name='calculated', - description=_PATTERN_OPTION_DESCRIPTIONS['calculated'], - available=calculated_available, - auto_included=False, - reason='', - ), - PatternOptionStatus( - name='background', - description=_PATTERN_OPTION_DESCRIPTIONS['background'], - available=background_available, - auto_included=False, - reason='', - ), - PatternOptionStatus( - name='residual', - description=_PATTERN_OPTION_DESCRIPTIONS['residual'], - available=residual_available, - auto_included=False, - reason='', - ), - PatternOptionStatus( - name='bragg', - description=_PATTERN_OPTION_DESCRIPTIONS['bragg'], - available=bragg_available, - auto_included=False, - reason='', - ), - PatternOptionStatus( - name='excluded', - description=_PATTERN_OPTION_DESCRIPTIONS['excluded'], - available=excluded_available, - auto_included=False, - reason='', - ), - PatternOptionStatus( - name='uncertainty', - description=_PATTERN_OPTION_DESCRIPTIONS['uncertainty'], - available=uncertainty_available, - auto_included=False, - reason='', - ), - ] - ) + auto_include = self._auto_include([ + PatternOptionStatus( + name='measured', + description=_PATTERN_OPTION_DESCRIPTIONS['measured'], + available=measured_available, + auto_included=False, + reason='', + ), + PatternOptionStatus( + name='calculated', + description=_PATTERN_OPTION_DESCRIPTIONS['calculated'], + available=calculated_available, + auto_included=False, + reason='', + ), + PatternOptionStatus( + name='background', + description=_PATTERN_OPTION_DESCRIPTIONS['background'], + available=background_available, + auto_included=False, + reason='', + ), + PatternOptionStatus( + name='residual', + description=_PATTERN_OPTION_DESCRIPTIONS['residual'], + available=residual_available, + auto_included=False, + reason='', + ), + PatternOptionStatus( + name='bragg', + description=_PATTERN_OPTION_DESCRIPTIONS['bragg'], + available=bragg_available, + auto_included=False, + reason='', + ), + PatternOptionStatus( + name='excluded', + description=_PATTERN_OPTION_DESCRIPTIONS['excluded'], + available=excluded_available, + auto_included=False, + reason='', + ), + PatternOptionStatus( + name='uncertainty', + description=_PATTERN_OPTION_DESCRIPTIONS['uncertainty'], + available=uncertainty_available, + auto_included=False, + reason='', + ), + ]) return [ PatternOptionStatus( @@ -542,8 +555,11 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: description=_PATTERN_OPTION_DESCRIPTIONS['background'], available=background_available, auto_included='background' in auto_include, - reason='' if background_available else ( - 'Background display requires measured and calculated data plus background intensities.' + reason='' + if background_available + else ( + 'Background display requires measured and calculated ' + 'data plus background intensities.' ), ), PatternOptionStatus( @@ -551,17 +567,20 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: description=_PATTERN_OPTION_DESCRIPTIONS['residual'], available=residual_available, auto_included='residual' in auto_include, - reason='' if residual_available else ( - 'Residuals currently require powder measured and calculated data.' - ), + reason='' + if residual_available + else ('Residuals currently require powder measured and calculated data.'), ), PatternOptionStatus( name='bragg', description=_PATTERN_OPTION_DESCRIPTIONS['bragg'], available=bragg_available, auto_included='bragg' in auto_include, - reason='' if bragg_available else ( - 'Bragg tick marks require powder Bragg measured and calculated data with reflection rows.' + reason='' + if bragg_available + else ( + 'Bragg tick marks require powder Bragg measured and ' + 'calculated data with reflection rows.' ), ), PatternOptionStatus( @@ -569,7 +588,9 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: description=_PATTERN_OPTION_DESCRIPTIONS['excluded'], available=excluded_available, auto_included='excluded' in auto_include, - reason='' if excluded_available else 'No excluded regions are defined for this experiment.', + reason='' + if excluded_available + else ('No excluded regions are defined for this experiment.'), ), PatternOptionStatus( name='uncertainty', @@ -587,7 +608,9 @@ def _uncertainty_status( sample_form: str, scattering_type: str, ) -> tuple[bool, str]: - """Return whether posterior predictive uncertainty is available.""" + """ + Return whether posterior predictive uncertainty is available. + """ if not measured_available: return False, 'Uncertainty bands require measured data.' @@ -596,7 +619,10 @@ def _uncertainty_status( and scattering_type == ScatteringTypeEnum.BRAGG.value ) if not supported_sample_form: - return False, 'Posterior predictive pattern views are unavailable for this experiment type.' + return ( + False, + ('Posterior predictive pattern views are unavailable for this experiment type.'), + ) fit_results = getattr(self._project.analysis, 'fit_results', None) if fit_results is None: @@ -610,4 +636,4 @@ def _uncertainty_status( if self._project.rendering.chart_engine.value != PlotterEngineEnum.PLOTLY.value: return False, 'Uncertainty bands currently require the Plotly chart engine.' - return True, '' \ No newline at end of file + return True, '' diff --git a/src/easydiffraction/project/project.py b/src/easydiffraction/project/project.py index df1d38b8..3f3ac4fb 100644 --- a/src/easydiffraction/project/project.py +++ b/src/easydiffraction/project/project.py @@ -16,9 +16,9 @@ from easydiffraction.datablocks.structure.collection import Structures from easydiffraction.io.cif.serialize import project_config_to_cif from easydiffraction.io.cif.serialize import project_to_cif -from easydiffraction.project.display import ProjectDisplay from easydiffraction.project.categories.rendering import Rendering from easydiffraction.project.categories.rendering import RenderingFactory +from easydiffraction.project.display import ProjectDisplay from easydiffraction.project.project_info import ProjectInfo from easydiffraction.summary.summary import Summary from easydiffraction.utils.enums import VerbosityEnum diff --git a/tests/integration/fitting/test_analysis_and_fit_category_support.py b/tests/integration/fitting/test_analysis_and_fit_category_support.py index 7a52c63d..0ed65e4f 100644 --- a/tests/integration/fitting/test_analysis_and_fit_category_support.py +++ b/tests/integration/fitting/test_analysis_and_fit_category_support.py @@ -282,6 +282,7 @@ def free_parameters(self): def test_analysis_display_as_cif_and_constraints(monkeypatch, capsys): import easydiffraction.analysis.analysis as analysis_mod + import easydiffraction.analysis.categories.constraints.default as constraints_mod from easydiffraction.analysis.analysis import Analysis analysis = Analysis(project=_make_project()) @@ -302,7 +303,7 @@ class FakeConstraint: analysis.constraints._items = [FakeConstraint()] captured: dict[str, object] = {} - monkeypatch.setattr(analysis_mod, 'render_table', lambda **kwargs: captured.update(kwargs)) + monkeypatch.setattr(constraints_mod, 'render_table', lambda **kwargs: captured.update(kwargs)) analysis.display.constraints() out = capsys.readouterr().out assert 'User defined constraints' in out diff --git a/tests/integration/fitting/test_analysis_display.py b/tests/integration/fitting/test_analysis_display.py index 2f5b0e45..bc549ee4 100644 --- a/tests/integration/fitting/test_analysis_display.py +++ b/tests/integration/fitting/test_analysis_display.py @@ -1,53 +1,53 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Integration tests for Analysis display methods and CIF serialization.""" +"""Integration tests for project display reports and analysis CIF helpers.""" def test_display_all_params(lbco_fitted_project): project = lbco_fitted_project - project.analysis.display.all_params() + project.display.parameters.all() def test_display_fittable_params(lbco_fitted_project): project = lbco_fitted_project - project.analysis.display.fittable_params() + project.display.parameters.fittable() def test_display_free_params(lbco_fitted_project): project = lbco_fitted_project - project.analysis.display.free_params() + project.display.parameters.free() def test_display_how_to_access_parameters(lbco_fitted_project): project = lbco_fitted_project - project.analysis.display.how_to_access_parameters() + project.display.parameters.access() def test_display_parameter_cif_uids(lbco_fitted_project): project = lbco_fitted_project - project.analysis.display.parameter_cif_uids() + project.display.parameters.cif_uids() def test_display_constraints_empty(lbco_fitted_project): project = lbco_fitted_project - project.analysis.display.constraints() + project.analysis.constraints.show() def test_display_fit_results(lbco_fitted_project): project = lbco_fitted_project assert project.analysis.fit_results is not None - project.analysis.display.fit_results() + project.display.fit.results() def test_display_as_cif(lbco_fitted_project): project = lbco_fitted_project - project.analysis.display.as_cif() + project.analysis.show_as_cif() def test_analysis_as_cif(lbco_fitted_project): project = lbco_fitted_project - cif_text = project.analysis.as_cif() + cif_text = project.analysis.as_cif assert isinstance(cif_text, str) assert len(cif_text) > 0 diff --git a/tests/integration/fitting/test_cli_entrypoints.py b/tests/integration/fitting/test_cli_entrypoints.py index 636c0ca4..34cc85ad 100644 --- a/tests/integration/fitting/test_cli_entrypoints.py +++ b/tests/integration/fitting/test_cli_entrypoints.py @@ -92,16 +92,21 @@ def fit_results() -> None: analysis = _analysis() class _display: - class _plotter: + class _fit: @staticmethod - def plot_param_correlations() -> None: - calls.append('PLOT_CORR') + def results() -> None: + calls.append('DISPLAY') @staticmethod - def plot_meas_vs_calc(expt_name: str, *, show_residual: bool = False) -> None: - calls.append(f'PLOT_{expt_name}_{show_residual}') + def correlations() -> None: + calls.append('PLOT_CORR') - plotter = _plotter() + fit = _fit() + + @staticmethod + def pattern(expt_name: str, **kwargs) -> None: + del kwargs + calls.append(f'PLOT_{expt_name}_False') display = _display() @@ -116,7 +121,7 @@ def plot_meas_vs_calc(expt_name: str, *, show_residual: bool = False) -> None: result = runner.invoke(main_mod.app, ['fit', str(project_dir)]) assert result.exit_code == 0 - assert calls == ['FIT', 'DISPLAY', 'PLOT_CORR', 'PLOT_exp1_True'] + assert calls == ['FIT', 'DISPLAY', 'PLOT_CORR', 'PLOT_exp1_False'] def test_cli_fit_dry_clears_path(monkeypatch, tmp_path): @@ -146,16 +151,20 @@ def fit_results() -> None: analysis = _analysis() class _display: - class _plotter: + class _fit: @staticmethod - def plot_param_correlations() -> None: + def results() -> None: return None @staticmethod - def plot_meas_vs_calc(expt_name: str, *, show_residual: bool = False) -> None: + def correlations() -> None: return None - plotter = _plotter() + fit = _fit() + + @staticmethod + def pattern(expt_name: str, **kwargs) -> None: + del expt_name, kwargs display = _display() diff --git a/tests/integration/fitting/test_plotting.py b/tests/integration/fitting/test_plotting.py index 1cb06c41..393e78c3 100644 --- a/tests/integration/fitting/test_plotting.py +++ b/tests/integration/fitting/test_plotting.py @@ -1,29 +1,29 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Integration tests for the Plotter facade on a fitted project.""" +"""Integration tests for ``project.display.pattern``.""" -def test_plot_meas(lbco_fitted_project): +def test_pattern_auto(lbco_fitted_project): project = lbco_fitted_project - project.display.plotter.plot_meas(expt_name='hrpt') + project.display.pattern(expt_name='hrpt') -def test_plot_calc(lbco_fitted_project): +def test_pattern_measured(lbco_fitted_project): project = lbco_fitted_project - project.display.plotter.plot_calc(expt_name='hrpt') + project.display.pattern(expt_name='hrpt', include='measured') -def test_plot_meas_vs_calc(lbco_fitted_project): +def test_pattern_measured_vs_calculated(lbco_fitted_project): project = lbco_fitted_project - project.display.plotter.plot_meas_vs_calc(expt_name='hrpt') + project.display.pattern(expt_name='hrpt', include=('measured', 'calculated')) -def test_plot_meas_with_range(lbco_fitted_project): +def test_pattern_with_range(lbco_fitted_project): project = lbco_fitted_project - project.display.plotter.plot_meas(expt_name='hrpt', x_min=20, x_max=80) + project.display.pattern(expt_name='hrpt', x_min=20, x_max=80) -def test_plot_meas_vs_calc_with_range(lbco_fitted_project): +def test_show_pattern_options(lbco_fitted_project): project = lbco_fitted_project - project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', x_min=20, x_max=80) + project.display.show_pattern_options(expt_name='hrpt') diff --git a/tests/unit/easydiffraction/analysis/test_analysis_coverage.py b/tests/unit/easydiffraction/analysis/test_analysis_coverage.py index 089e1f6b..9ff2926e 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis_coverage.py +++ b/tests/unit/easydiffraction/analysis/test_analysis_coverage.py @@ -72,7 +72,7 @@ def test_empty_constraints_warns(self, capsys): assert 'No constraints' in out def test_constraints_with_items(self, capsys, monkeypatch): - import easydiffraction.analysis.analysis as mod + import easydiffraction.analysis.categories.constraints.default as constraints_mod from easydiffraction.analysis.analysis import Analysis a = Analysis(project=_make_project()) @@ -91,7 +91,7 @@ class FakeConstraint: def fake_render_table(**kwargs): captured.update(kwargs) - monkeypatch.setattr(mod, 'render_table', fake_render_table) + monkeypatch.setattr(constraints_mod, 'render_table', fake_render_table) a.display.constraints() out = capsys.readouterr().out assert 'User defined constraints' in out diff --git a/tests/unit/easydiffraction/display/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py index c0ef1615..74b54776 100644 --- a/tests/unit/easydiffraction/display/test_plotting.py +++ b/tests/unit/easydiffraction/display/test_plotting.py @@ -94,19 +94,43 @@ def __init__(self): p = Plotter() # Error paths (now log errors via console; messages are printed) - p._plot_meas_data(Ptn(two_theta=None, intensity_meas=None), 'E', ExptType()) + p._plot_meas_data( + object(), + Ptn(two_theta=None, intensity_meas=None), + 'E', + ExptType(), + _MeasVsCalcPlotOptions(), + ) out = capsys.readouterr().out assert 'No two_theta data available for experiment E' in out - p._plot_meas_data(Ptn(two_theta=[1], intensity_meas=None), 'E', ExptType()) + p._plot_meas_data( + object(), + Ptn(two_theta=[1], intensity_meas=None), + 'E', + ExptType(), + _MeasVsCalcPlotOptions(), + ) out = capsys.readouterr().out assert 'No measured data available for experiment E' in out - p._plot_calc_data(Ptn(two_theta=None, intensity_calc=None), 'E', ExptType()) + p._plot_calc_data( + object(), + Ptn(two_theta=None, intensity_calc=None), + 'E', + ExptType(), + _MeasVsCalcPlotOptions(), + ) out = capsys.readouterr().out assert 'No two_theta data available for experiment E' in out - p._plot_calc_data(Ptn(two_theta=[1], intensity_calc=None), 'E', ExptType()) + p._plot_calc_data( + object(), + Ptn(two_theta=[1], intensity_calc=None), + 'E', + ExptType(), + _MeasVsCalcPlotOptions(), + ) out = capsys.readouterr().out assert 'No calculated data available for experiment E' in out @@ -154,13 +178,24 @@ def test_plotter_routes_to_ascii_plotter(monkeypatch): from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum from easydiffraction.display.plotting import Plotter + from easydiffraction.display.plotting import _MeasVsCalcPlotOptions called = {} - def fake_plot_powder(self, x, y_series, labels, axes_labels, title, height=None): + def fake_plot_powder( + self, + x, + y_series, + labels, + axes_labels, + title, + height=None, + excluded_ranges=(), + ): called['labels'] = tuple(labels) called['axes'] = tuple(axes_labels) called['title'] = title + called['excluded_ranges'] = excluded_ranges monkeypatch.setattr(ascii_mod.AsciiPlotter, 'plot_powder', fake_plot_powder) @@ -178,9 +213,16 @@ def __init__(self): p = Plotter() p.engine = 'asciichartpy' # ensure AsciiPlotter - p._plot_meas_data(Ptn(), 'E', ExptType()) + p._plot_meas_data( + object(), + Ptn(), + 'E', + ExptType(), + _MeasVsCalcPlotOptions(), + ) assert called['labels'] == ('meas',) assert 'Measured data' in called['title'] + assert called['excluded_ranges'] == () def test_extract_bragg_tick_sets_groups_and_filters(): @@ -742,7 +784,13 @@ class Experiment: plotter._plot_posterior_predictive_data( experiment=Experiment(), expt_name='hrpt', - plot_options=SimpleNamespace(x_min=None, x_max=None, show_residual=None, x=None), + plot_options=SimpleNamespace( + x_min=None, + x_max=None, + show_residual=None, + show_excluded=False, + x=None, + ), x_axis=XAxisType.TWO_THETA, style='band', ) @@ -1105,6 +1153,7 @@ def fake_plot_summary( axes_labels, show_band, show_draws, + excluded_ranges, ): captured['expt_name'] = expt_name captured['summary'] = summary @@ -1112,6 +1161,7 @@ def fake_plot_summary( captured['axes_labels'] = axes_labels captured['show_band'] = show_band captured['show_draws'] = show_draws + captured['excluded_ranges'] = excluded_ranges monkeypatch.setattr(Plotter, '_plot_posterior_predictive_summary', fake_plot_summary) @@ -1125,6 +1175,7 @@ def fake_plot_summary( np.testing.assert_allclose(captured['y_meas'], np.array([20.0])) assert captured['show_band'] is True assert captured['show_draws'] is False + assert captured['excluded_ranges'] == () assert any('ignoring show_residual=True' in warning for warning in warnings) diff --git a/tests/unit/easydiffraction/project/categories/rendering/test_default.py b/tests/unit/easydiffraction/project/categories/rendering/test_default.py new file mode 100644 index 00000000..edb88427 --- /dev/null +++ b/tests/unit/easydiffraction/project/categories/rendering/test_default.py @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import gemmi + + +def test_rendering_defaults(): + from easydiffraction.project.categories.rendering.default import Rendering + + rendering = Rendering() + + assert rendering.type_info.tag == 'default' + assert rendering._identity.category_code == 'rendering' + assert rendering.chart_engine.value == rendering.plotter.engine + assert rendering.table_engine.value == rendering.tabler.engine + + +def test_rendering_plotter_binds_parent(): + from easydiffraction.project.categories.rendering.default import Rendering + + rendering = Rendering() + parent = object() + rendering._parent = parent + + plotter = rendering.plotter + + assert plotter._project is parent + + +def test_rendering_setters_update_engines(): + from easydiffraction.project.categories.rendering.default import Rendering + + rendering = Rendering() + + rendering.chart_engine = 'plotly' + rendering.table_engine = 'rich' + + assert rendering.chart_engine.value == 'plotly' + assert rendering.plotter.engine == 'plotly' + assert rendering.table_engine.value == 'rich' + assert rendering.tabler.engine == 'rich' + + +def test_rendering_from_cif_restores_types(): + from easydiffraction.project.categories.rendering.default import Rendering + + rendering = Rendering() + block = gemmi.cif.read_string( + 'data_test\n_rendering.chart_engine plotly\n_rendering.table_engine rich\n', + ).sole_block() + + rendering.from_cif(block) + + assert rendering.chart_engine.value == 'plotly' + assert rendering.table_engine.value == 'rich' diff --git a/tests/unit/easydiffraction/project/categories/rendering/test_factory.py b/tests/unit/easydiffraction/project/categories/rendering/test_factory.py new file mode 100644 index 00000000..0f2812e5 --- /dev/null +++ b/tests/unit/easydiffraction/project/categories/rendering/test_factory.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import pytest + + +def test_rendering_factory_default_and_create(): + from easydiffraction.project.categories.rendering.default import Rendering + from easydiffraction.project.categories.rendering.factory import RenderingFactory + + assert RenderingFactory.default_tag() == 'default' + assert 'default' in RenderingFactory.supported_tags() + + rendering = RenderingFactory.create('default') + + assert isinstance(rendering, Rendering) + + +def test_rendering_factory_rejects_unknown_tag(): + from easydiffraction.project.categories.rendering.factory import RenderingFactory + + with pytest.raises(ValueError, match=r"Unsupported type: 'missing'"): + RenderingFactory.create('missing') diff --git a/tests/unit/easydiffraction/project/test_display.py b/tests/unit/easydiffraction/project/test_display.py new file mode 100644 index 00000000..59f6d5f8 --- /dev/null +++ b/tests/unit/easydiffraction/project/test_display.py @@ -0,0 +1,297 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Unit tests for project/display.py.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import pytest + +from easydiffraction.project.display import PatternOptionStatus +from easydiffraction.project.display import ProjectDisplay + + +def _make_project_stub() -> tuple[SimpleNamespace, list[tuple[str, tuple, dict]]]: + calls: list[tuple[str, tuple, dict]] = [] + + def record(name: str): + def _recorder(*args, **kwargs): + calls.append((name, args, kwargs)) + + return _recorder + + analysis_display = SimpleNamespace( + all_params=record('all_params'), + fittable_params=record('fittable_params'), + free_params=record('free_params'), + how_to_access_parameters=record('how_to_access_parameters'), + parameter_cif_uids=record('parameter_cif_uids'), + fit_results=record('fit_results'), + ) + plotter = SimpleNamespace( + plot_param_correlations=record('plot_param_correlations'), + plot_param_series=record('plot_param_series'), + plot_posterior_pairs=record('plot_posterior_pairs'), + plot_param_distribution=record('plot_param_distribution'), + plot_posterior_predictive=record('plot_posterior_predictive'), + plot_meas=record('plot_meas'), + plot_calc=record('plot_calc'), + plot_meas_vs_calc=record('plot_meas_vs_calc'), + ) + project = SimpleNamespace( + analysis=SimpleNamespace(display=analysis_display), + rendering=SimpleNamespace(plotter=plotter), + ) + return project, calls + + +def _make_statuses( + *, + measured: bool = False, + calculated: bool = False, + background: bool = False, + residual: bool = False, + bragg: bool = False, + excluded: bool = False, + uncertainty: bool = False, + uncertainty_reason: str = 'Posterior predictive data is unavailable.', +) -> list[PatternOptionStatus]: + return [ + PatternOptionStatus( + name='auto', + description='auto', + available=True, + auto_included=True, + reason='', + ), + PatternOptionStatus( + name='measured', + description='measured', + available=measured, + auto_included=False, + reason='' if measured else 'Measured unavailable', + ), + PatternOptionStatus( + name='calculated', + description='calculated', + available=calculated, + auto_included=False, + reason='' if calculated else 'Calculated unavailable', + ), + PatternOptionStatus( + name='background', + description='background', + available=background, + auto_included=False, + reason='' if background else 'Background unavailable', + ), + PatternOptionStatus( + name='residual', + description='residual', + available=residual, + auto_included=False, + reason='' if residual else 'Residual unavailable', + ), + PatternOptionStatus( + name='bragg', + description='bragg', + available=bragg, + auto_included=False, + reason='' if bragg else 'Bragg unavailable', + ), + PatternOptionStatus( + name='excluded', + description='excluded', + available=excluded, + auto_included=False, + reason='' if excluded else 'Excluded unavailable', + ), + PatternOptionStatus( + name='uncertainty', + description='uncertainty', + available=uncertainty, + auto_included=False, + reason='' if uncertainty else uncertainty_reason, + ), + ] + + +def test_parameter_display_delegates_to_analysis_display(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + + display.parameters.all() + display.parameters.fittable() + display.parameters.free() + display.parameters.access() + display.parameters.cif_uids() + + assert [name for name, _args, _kwargs in calls] == [ + 'all_params', + 'fittable_params', + 'free_params', + 'how_to_access_parameters', + 'parameter_cif_uids', + ] + + +def test_fit_display_delegates_to_analysis_and_rendering(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + + display.fit.results() + display.fit.correlations( + threshold=0.75, + precision=3, + max_parameters=4, + show_diagonal=False, + ) + display.fit.series(param='scale', versus='temperature') + + assert calls[0] == ('fit_results', (), {}) + assert calls[1] == ( + 'plot_param_correlations', + (), + { + 'threshold': 0.75, + 'precision': 3, + 'max_parameters': 4, + 'show_diagonal': False, + }, + ) + assert calls[2] == ( + 'plot_param_series', + (), + {'param': 'scale', 'versus': 'temperature'}, + ) + + +def test_posterior_display_delegates_to_rendering_plotter(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + + display.posterior.pairs(parameters=['a'], threshold=0.5, max_parameters=3) + display.posterior.distribution('a') + display.posterior.predictive( + 'hrpt', + style='draws', + x_min=1.0, + x_max=2.0, + show_residual=True, + x='d_spacing', + ) + + assert calls[0][0] == 'plot_posterior_pairs' + assert calls[0][2]['parameters'] == ['a'] + assert calls[0][2]['threshold'] == 0.5 + assert calls[0][2]['max_parameters'] == 3 + assert calls[1] == ('plot_param_distribution', ('a',), {}) + assert calls[2] == ( + 'plot_posterior_predictive', + (), + { + 'expt_name': 'hrpt', + 'style': 'draws', + 'x_min': 1.0, + 'x_max': 2.0, + 'show_residual': True, + 'x': 'd_spacing', + }, + ) + + +def test_pattern_auto_routes_measured_and_excluded_to_plot_meas(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses( + measured=True, + excluded=True, + ) + + display.pattern('hrpt') + + assert calls == [ + ( + 'plot_meas', + (), + { + 'expt_name': 'hrpt', + 'x_min': None, + 'x_max': None, + 'x': None, + 'show_excluded': True, + }, + ) + ] + + +def test_pattern_uncertainty_routes_to_posterior_predictive(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses( + measured=True, + calculated=True, + residual=True, + excluded=True, + uncertainty=True, + ) + + display.pattern( + 'hrpt', + x_min=1.0, + x_max=2.0, + include=('measured', 'calculated', 'uncertainty', 'residual', 'excluded'), + ) + + assert calls == [ + ( + 'plot_posterior_predictive', + (), + { + 'expt_name': 'hrpt', + 'style': 'band', + 'x_min': 1.0, + 'x_max': 2.0, + 'show_residual': True, + 'show_excluded': True, + 'x': None, + }, + ) + ] + + +def test_pattern_rejects_excluded_with_custom_x(): + project, _calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses( + measured=True, + excluded=True, + ) + + with pytest.raises(ValueError, match='default x-axis'): + display.pattern('hrpt', include=('measured', 'excluded'), x='d_spacing') + + +def test_show_pattern_options_renders_table(monkeypatch): + project, _calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses( + measured=True, + calculated=True, + ) + captured: dict[str, object] = {} + + def fake_render_table(*, columns_headers, columns_alignment, columns_data): + captured['columns_headers'] = columns_headers + captured['columns_alignment'] = columns_alignment + captured['columns_data'] = columns_data + + monkeypatch.setattr('easydiffraction.project.display.render_table', fake_render_table) + + display.show_pattern_options('hrpt') + + assert captured['columns_headers'] == ['Option', 'Description', 'Available', 'Auto', 'Reason'] + assert captured['columns_alignment'] == ['left', 'left', 'center', 'center', 'left'] + assert captured['columns_data'][0][0] == 'auto' + assert captured['columns_data'][1][0] == 'measured' diff --git a/tests/unit/easydiffraction/project/test_project.py b/tests/unit/easydiffraction/project/test_project.py index 090ce540..e0ff677d 100644 --- a/tests/unit/easydiffraction/project/test_project.py +++ b/tests/unit/easydiffraction/project/test_project.py @@ -63,3 +63,14 @@ def test_project_free_params_aggregate_structures_and_experiments(): project._experiments = SimpleNamespace(free_parameters=[experiment_param]) assert project.free_parameters == [structure_param, experiment_param] + + +def test_project_exposes_rendering_and_display_facades(): + from easydiffraction.project.categories.rendering import Rendering + from easydiffraction.project.display import ProjectDisplay + from easydiffraction.project.project import Project + + project = Project() + + assert isinstance(project.rendering, Rendering) + assert isinstance(project.display, ProjectDisplay) diff --git a/tests/unit/easydiffraction/project/test_project_load.py b/tests/unit/easydiffraction/project/test_project_load.py index c788f0a7..44834577 100644 --- a/tests/unit/easydiffraction/project/test_project_load.py +++ b/tests/unit/easydiffraction/project/test_project_load.py @@ -81,16 +81,16 @@ def test_round_trips_fit_mode(self, tmp_path): assert loaded.analysis.fit.mode.value == 'joint' - def test_round_trips_display_configuration(self, tmp_path): + def test_round_trips_rendering_configuration(self, tmp_path): original = Project(name='d1') - original.display.plotter_type = 'asciichartpy' - original.display.tabler_type = 'rich' + original.rendering.chart_engine = 'asciichartpy' + original.rendering.table_engine = 'rich' original.save_as(str(tmp_path / 'proj')) loaded = Project.load(str(tmp_path / 'proj')) - assert loaded.display.plotter_type.value == 'asciichartpy' - assert loaded.display.tabler_type.value == 'rich' + assert loaded.rendering.chart_engine.value == 'asciichartpy' + assert loaded.rendering.table_engine.value == 'rich' def test_round_trips_constraints(self, tmp_path): original = Project(name='c1') diff --git a/tests/unit/easydiffraction/project/test_project_save.py b/tests/unit/easydiffraction/project/test_project_save.py index bf632e11..08292525 100644 --- a/tests/unit/easydiffraction/project/test_project_save.py +++ b/tests/unit/easydiffraction/project/test_project_save.py @@ -24,8 +24,8 @@ def test_project_save_as_writes_core_files(tmp_path, monkeypatch): from easydiffraction.summary.summary import Summary # Monkeypatch as_cif producers to avoid heavy internals - monkeypatch.setattr(ProjectInfo, 'as_cif', lambda self: 'info') - monkeypatch.setattr(Analysis, 'as_cif', lambda self: 'analysis') + monkeypatch.setattr(ProjectInfo, 'as_cif', property(lambda self: 'info')) + monkeypatch.setattr(Analysis, 'as_cif', property(lambda self: 'analysis')) monkeypatch.setattr(Summary, 'as_cif', lambda self: 'summary') p = Project(name='p1') diff --git a/tests/unit/easydiffraction/test___main__.py b/tests/unit/easydiffraction/test___main__.py index 76d150c6..9edd2bc2 100644 --- a/tests/unit/easydiffraction/test___main__.py +++ b/tests/unit/easydiffraction/test___main__.py @@ -95,16 +95,21 @@ def fit_results(): analysis = _analysis() class _display: - class _plotter: + class _fit: @staticmethod - def plot_param_correlations(): - calls.append('PLOT_CORR') + def results(): + calls.append('DISPLAY') @staticmethod - def plot_meas_vs_calc(expt_name, *, show_residual=False): - calls.append(f'PLOT_{expt_name}_{show_residual}') + def correlations(): + calls.append('PLOT_CORR') - plotter = _plotter() + fit = _fit() + + @staticmethod + def pattern(expt_name, **kwargs): + del kwargs + calls.append(f'PLOT_{expt_name}_False') display = _display() @@ -119,7 +124,7 @@ def plot_meas_vs_calc(expt_name, *, show_residual=False): result = runner.invoke(main_mod.app, ['fit', str(proj_dir)]) assert result.exit_code == 0 - assert calls == ['FIT', 'DISPLAY', 'PLOT_CORR', 'PLOT_exp1_True'] + assert calls == ['FIT', 'DISPLAY', 'PLOT_CORR', 'PLOT_exp1_False'] def test_cli_fit_dry_clears_path(monkeypatch, tmp_path): @@ -149,16 +154,20 @@ def fit_results(): analysis = _analysis() class _display: - class _plotter: + class _fit: @staticmethod - def plot_param_correlations(): + def results(): pass @staticmethod - def plot_meas_vs_calc(expt_name, *, show_residual=False): + def correlations(): pass - plotter = _plotter() + fit = _fit() + + @staticmethod + def pattern(expt_name, **kwargs): + del expt_name, kwargs display = _display() From 58921335b1be9a78b23385957de33a305ef8cee0 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 14 May 2026 13:27:37 +0200 Subject: [PATCH 09/10] Address display UX review fixes --- docs/dev/architecture.md | 7 +- docs/docs/tutorials/ed-13.ipynb | 2 +- docs/docs/tutorials/ed-13.py | 2 +- src/easydiffraction/display/plotting.py | 82 +++++++++-- .../project/categories/display/__init__.py | 8 -- .../project/categories/display/default.py | 117 ---------------- .../project/categories/display/factory.py | 17 --- src/easydiffraction/project/display.py | 113 ++++++++++----- .../easydiffraction/display/test_plotting.py | 69 ++++++++++ .../categories/display/test_default.py | 55 -------- .../categories/display/test_factory.py | 23 ---- .../easydiffraction/project/test_display.py | 130 +++++++++++++++++- 12 files changed, 350 insertions(+), 275 deletions(-) delete mode 100644 src/easydiffraction/project/categories/display/__init__.py delete mode 100644 src/easydiffraction/project/categories/display/default.py delete mode 100644 src/easydiffraction/project/categories/display/factory.py delete mode 100644 tests/unit/easydiffraction/project/categories/display/test_default.py delete mode 100644 tests/unit/easydiffraction/project/categories/display/test_factory.py diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 9ac1d4b1..4c464017 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -884,7 +884,8 @@ It owns and coordinates all components: | `project.info` | `ProjectInfo` | Metadata: name, title, description, path | | `project.structures` | `Structures` | Collection of structure datablocks | | `project.experiments` | `Experiments` | Collection of experiment datablocks | -| `project.display` | `Display` | Plot/table engine selection and facades | +| `project.rendering` | `Rendering` | Plot/table engine selection | +| `project.display` | `ProjectDisplay` | Pattern/report facade | | `project.analysis` | `Analysis` | Minimiser, fitting, aliases, constraints | | `project.summary` | `Summary` | Report generation | | `project.verbosity` | `str` | Console output level (full/short/silent) | @@ -1250,12 +1251,12 @@ their intent and ownership differ: | Family | User intent | Examples | CIF | | ---------------------------------- | ------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| Backend selector | Pick an execution backend | `fit.minimizer_type`, `calculation.calculator_type`, `display.chart_engine` | `_fit.minimizer_type`, `_calculation.calculator_type`, `_rendering.chart_engine` | +| Backend selector | Pick an execution backend | `fit.minimizer_type`, `calculation.calculator_type`, `rendering.chart_engine` | `_fit.minimizer_type`, `_calculation.calculator_type`, `_rendering.chart_engine` | | Switchable-category impl. selector | Swap a category implementation | `experiment.background_type`, `experiment.peak_profile_type` | category-owned type tag such as `_peak.profile_type` | | Semantic value selector | Pick a scientific/analysis mode | `fit.mode` | `_fit.mode` | Backend selectors and semantic value selectors live on a dedicated -configuration category (`fit`, `calculation`, `display`). Switchable- +configuration category (`fit`, `calculation`, `rendering`). Switchable- category implementation selectors are owned by the host (typically the experiment) because switching them replaces the category instance, as described in ยง9.3. diff --git a/docs/docs/tutorials/ed-13.ipynb b/docs/docs/tutorials/ed-13.ipynb index aebb420f..616c536c 100644 --- a/docs/docs/tutorials/ed-13.ipynb +++ b/docs/docs/tutorials/ed-13.ipynb @@ -979,7 +979,7 @@ "#### Show Free Parameters\n", "\n", "We can check which parameters are free to be refined by calling the\n", - "`free_params` method of the `analysis.display` object of the project." + "`free` method of the `display.parameters` object of the project." ] }, { diff --git a/docs/docs/tutorials/ed-13.py b/docs/docs/tutorials/ed-13.py index 66eb2d09..436c13b9 100644 --- a/docs/docs/tutorials/ed-13.py +++ b/docs/docs/tutorials/ed-13.py @@ -583,7 +583,7 @@ # #### Show Free Parameters # # We can check which parameters are free to be refined by calling the -# `free_params` method of the `analysis.display` object of the project. +# `free` method of the `display.parameters` object of the project. # %% [markdown] tags=["doc-link"] # ๐Ÿ“– See diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index 4c2ca111..d1072f0a 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -179,6 +179,8 @@ class _MeasVsCalcPlotOptions: x_min: float | None = None x_max: float | None = None show_residual: bool | None = None + show_background: bool | None = None + show_bragg: bool | None = None show_excluded: bool = False x: object | None = None @@ -693,8 +695,6 @@ def plot_meas_vs_calc( x : object | None, default=None Optional explicit x-axis data to override stored values. """ - self._update_project_categories(expt_name) - experiment = self._project.experiments[expt_name] plot_options = _MeasVsCalcPlotOptions( x_min=x_min, x_max=x_max, @@ -702,6 +702,17 @@ def plot_meas_vs_calc( show_excluded=show_excluded, x=x, ) + self._plot_meas_vs_calc_request(expt_name=expt_name, plot_options=plot_options) + + def _plot_meas_vs_calc_request( + self, + *, + expt_name: str, + plot_options: _MeasVsCalcPlotOptions, + ) -> None: + """Render a measured-vs-calculated request from plot options.""" + self._update_project_categories(expt_name) + experiment = self._project.experiments[expt_name] self._plot_meas_vs_calc_data( experiment=experiment, expt_name=expt_name, @@ -1062,6 +1073,28 @@ def plot_posterior_predictive( msg = "style must be 'band', 'draws', or 'band+draws'." raise ValueError(msg) + plot_options = _MeasVsCalcPlotOptions( + x_min=x_min, + x_max=x_max, + show_residual=show_residual, + show_excluded=show_excluded, + x=x, + ) + + self._plot_posterior_predictive_request( + expt_name=expt_name, + style=style, + plot_options=plot_options, + ) + + def _plot_posterior_predictive_request( + self, + *, + expt_name: str, + style: str, + plot_options: _MeasVsCalcPlotOptions, + ) -> None: + """Render a posterior predictive request from plot options.""" if self._project is None: log.warning('Plotter is not attached to a project.') return @@ -1072,14 +1105,9 @@ def plot_posterior_predictive( self._update_project_categories(expt_name) experiment = self._project.experiments[expt_name] - x_axis, _, sample_form, scattering_type, _ = self._resolve_x_axis(experiment.type, x) - - plot_options = _MeasVsCalcPlotOptions( - x_min=x_min, - x_max=x_max, - show_residual=show_residual, - show_excluded=show_excluded, - x=x, + x_axis, _, sample_form, scattering_type, _ = self._resolve_x_axis( + experiment.type, + plot_options.x, ) if sample_form == SampleFormEnum.SINGLE_CRYSTAL: @@ -3692,6 +3720,8 @@ def _plot_posterior_predictive_data( ctx['x_min'], ctx['x_max'], ) + if not self._show_background_enabled(plot_options, background_available=y_bkg is not None): + y_bkg = None y_calc = self._filtered_y_array( summary.map_prediction, summary.x, ctx['x_min'], ctx['x_max'] ) @@ -3728,7 +3758,10 @@ def _plot_posterior_predictive_data( dtype=float, ) - if np.asarray(ctx['x_filtered']).size == 0: + if ( + np.asarray(ctx['x_filtered']).size == 0 + or not self._show_bragg_enabled(plot_options) + ): bragg_tick_sets = () else: bragg_tick_sets = self._extract_bragg_tick_sets( @@ -4920,6 +4953,8 @@ def _plot_meas_vs_calc_data( if y_bkg_raw is not None else None ) + if not self._show_background_enabled(plot_options, background_available=y_bkg is not None): + y_bkg = None powder_series = _PowderMeasVsCalcSeries( y_meas=y_meas, @@ -5002,7 +5037,10 @@ def _plot_powder_bragg_meas_vs_calc( """ show_residual = True if plot_options.show_residual is None else plot_options.show_residual y_resid = series.y_meas - series.y_calc if show_residual else None - if np.asarray(ctx['x_filtered']).size == 0: + if ( + np.asarray(ctx['x_filtered']).size == 0 + or not self._show_bragg_enabled(plot_options) + ): bragg_tick_sets = () else: bragg_tick_sets = self._extract_bragg_tick_sets( @@ -5029,6 +5067,26 @@ def _plot_powder_bragg_meas_vs_calc( ) self._backend.plot_powder_meas_vs_calc(plot_spec=plot_spec) + @staticmethod + def _show_background_enabled( + plot_options: object, + *, + background_available: bool, + ) -> bool: + """Return whether the background curve should be shown.""" + show_background = getattr(plot_options, 'show_background', None) + if show_background is None: + return background_available + return show_background and background_available + + @staticmethod + def _show_bragg_enabled(plot_options: object) -> bool: + """Return whether Bragg reflection rows should be shown.""" + show_bragg = getattr(plot_options, 'show_bragg', None) + if show_bragg is None: + return True + return show_bragg + def _plot_line_meas_vs_calc( self, ctx: dict[str, object], diff --git a/src/easydiffraction/project/categories/display/__init__.py b/src/easydiffraction/project/categories/display/__init__.py deleted file mode 100644 index d0862c2e..00000000 --- a/src/easydiffraction/project/categories/display/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Project display category exports.""" - -from __future__ import annotations - -from easydiffraction.project.categories.display.default import Display -from easydiffraction.project.categories.display.factory import DisplayFactory diff --git a/src/easydiffraction/project/categories/display/default.py b/src/easydiffraction/project/categories/display/default.py deleted file mode 100644 index d8287b09..00000000 --- a/src/easydiffraction/project/categories/display/default.py +++ /dev/null @@ -1,117 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Project display category.""" - -from __future__ import annotations - -from easydiffraction.core.category import CategoryItem -from easydiffraction.core.metadata import TypeInfo -from easydiffraction.core.validation import AttributeSpec -from easydiffraction.core.validation import MembershipValidator -from easydiffraction.core.variable import StringDescriptor -from easydiffraction.display.plotting import Plotter -from easydiffraction.display.plotting import PlotterEngineEnum -from easydiffraction.display.tables import TableEngineEnum -from easydiffraction.display.tables import TableRenderer -from easydiffraction.io.cif.handler import CifHandler -from easydiffraction.io.cif.parse import read_cif_str -from easydiffraction.project.categories.display.factory import DisplayFactory - - -@DisplayFactory.register -class Display(CategoryItem): - """Display engine selection and access for a project.""" - - type_info = TypeInfo( - tag='default', - description='Project display category', - ) - - def __init__(self) -> None: - super().__init__() - - self._plotter = Plotter() - self._tabler = TableRenderer.get() - - self._plotter_type = StringDescriptor( - name='plotter_type', - description='Plot renderer backend type', - value_spec=AttributeSpec( - default=self._plotter.engine, - validator=MembershipValidator( - allowed=[member.value for member in PlotterEngineEnum], - ), - ), - cif_handler=CifHandler(names=['_display.plotter_type']), - ) - self._tabler_type = StringDescriptor( - name='tabler_type', - description='Table renderer backend type', - value_spec=AttributeSpec( - default=self._tabler.engine, - validator=MembershipValidator( - allowed=[member.value for member in TableEngineEnum], - ), - ), - cif_handler=CifHandler(names=['_display.tabler_type']), - ) - - self._identity.category_code = 'display' - - @property - def plotter_type(self) -> StringDescriptor: - """Plot renderer backend type.""" - return self._plotter_type - - @plotter_type.setter - def plotter_type(self, value: str) -> None: - self._plotter.engine = value - self._plotter_type.value = self._plotter.engine - - @property - def tabler_type(self) -> StringDescriptor: - """Table renderer backend type.""" - return self._tabler_type - - @tabler_type.setter - def tabler_type(self, value: str) -> None: - self._tabler.engine = value - self._tabler_type.value = self._tabler.engine - - @property - def plotter(self) -> Plotter: - """Live plotting facade bound to the owning project.""" - parent = getattr(self, '_parent', None) - if parent is not None: - self._plotter._set_project(parent) - return self._plotter - - @property - def tabler(self) -> TableRenderer: - """Live table-rendering facade.""" - return self._tabler - - def show_plotter_types(self) -> None: - """Print supported plot renderer backends.""" - self.plotter.show_supported_engines() - - def show_tabler_types(self) -> None: - """Print supported table renderer backends.""" - self.tabler.show_supported_engines() - - def from_cif(self, block: object, idx: int = 0) -> None: - """Populate this display category from a CIF block.""" - del idx - plotter_type = read_cif_str(block, '_display.plotter_type') - if plotter_type is not None: - if plotter_type == self._plotter.engine: - self._plotter_type.value = plotter_type - else: - self.plotter_type = plotter_type - - tabler_type = read_cif_str(block, '_display.tabler_type') - if tabler_type is not None: - if tabler_type == self._tabler.engine: - self._tabler_type.value = tabler_type - else: - self.tabler_type = tabler_type diff --git a/src/easydiffraction/project/categories/display/factory.py b/src/easydiffraction/project/categories/display/factory.py deleted file mode 100644 index 0d1be80a..00000000 --- a/src/easydiffraction/project/categories/display/factory.py +++ /dev/null @@ -1,17 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause -"""Factory for project display categories.""" - -from __future__ import annotations - -from typing import ClassVar - -from easydiffraction.core.factory import FactoryBase - - -class DisplayFactory(FactoryBase): - """Create project display category instances.""" - - _default_rules: ClassVar[dict] = { - frozenset(): 'default', - } diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py index 20ae149c..c1b5bd67 100644 --- a/src/easydiffraction/project/display.py +++ b/src/easydiffraction/project/display.py @@ -12,6 +12,7 @@ from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum from easydiffraction.display.plotting import PlotterEngineEnum from easydiffraction.display.plotting import PosteriorPairPlotStyleEnum +from easydiffraction.display.plotting import _MeasVsCalcPlotOptions from easydiffraction.utils.utils import render_table if TYPE_CHECKING: @@ -195,14 +196,18 @@ def pattern( msg = self._status_by_name(statuses, 'auto').reason raise ValueError(msg) if 'uncertainty' in auto_include: - self._project.rendering.plotter.plot_posterior_predictive( + self._project.rendering.plotter._plot_posterior_predictive_request( expt_name=expt_name, style='band', - x_min=x_min, - x_max=x_max, - show_residual=True if 'residual' in auto_include else None, - show_excluded='excluded' in auto_include, - x=x, + plot_options=_MeasVsCalcPlotOptions( + x_min=x_min, + x_max=x_max, + show_residual=True if 'residual' in auto_include else None, + show_background='background' in auto_include, + show_bragg='bragg' in auto_include, + show_excluded='excluded' in auto_include, + x=x, + ), ) return self._show_point_estimate_pattern( @@ -210,6 +215,7 @@ def pattern( x_min=x_min, x_max=x_max, include=auto_include, + statuses=statuses, x=x, ) return @@ -220,14 +226,18 @@ def pattern( raise ValueError(msg) if 'uncertainty' in normalized_include: - self._project.rendering.plotter.plot_posterior_predictive( + self._project.rendering.plotter._plot_posterior_predictive_request( expt_name=expt_name, style='band', - x_min=x_min, - x_max=x_max, - show_residual=True if 'residual' in normalized_include else None, - show_excluded='excluded' in normalized_include, - x=x, + plot_options=_MeasVsCalcPlotOptions( + x_min=x_min, + x_max=x_max, + show_residual=True if 'residual' in normalized_include else None, + show_background='background' in normalized_include, + show_bragg='bragg' in normalized_include, + show_excluded='excluded' in normalized_include, + x=x, + ), ) return @@ -236,6 +246,7 @@ def pattern( x_min=x_min, x_max=x_max, include=normalized_include, + statuses=statuses, x=x, ) @@ -380,12 +391,12 @@ def _show_point_estimate_pattern( x_min: float | None, x_max: float | None, include: tuple[str, ...], + statuses: list[PatternOptionStatus], x: object | None, ) -> None: """ Dispatch a point-estimate pattern view to the live plotter. """ - statuses = self._pattern_option_statuses(expt_name) self._validate_requested_include(statuses, include) include_set = set(include) if include_set == {'measured'}: @@ -425,13 +436,17 @@ def _show_point_estimate_pattern( ) return if {'measured', 'calculated'}.issubset(include_set): - self._project.rendering.plotter.plot_meas_vs_calc( + self._project.rendering.plotter._plot_meas_vs_calc_request( expt_name=expt_name, - x_min=x_min, - x_max=x_max, - show_residual='residual' in include_set, - show_excluded='excluded' in include_set, - x=x, + plot_options=_MeasVsCalcPlotOptions( + x_min=x_min, + x_max=x_max, + show_residual='residual' in include_set, + show_background='background' in include_set, + show_bragg='bragg' in include_set, + show_excluded='excluded' in include_set, + x=x, + ), ) return @@ -443,33 +458,40 @@ def _show_point_estimate_pattern( def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: """Return availability details for the requested experiment.""" + self._project.rendering.plotter._update_project_categories(expt_name) experiment = self._project.experiments[expt_name] pattern = intensity_category_for(experiment) sample_form = experiment.type.sample_form.value scattering_type = experiment.type.scattering_type.value + has_valid_linked_phases = self._has_valid_linked_phases(experiment) - measured_available = getattr(pattern, 'intensity_meas', None) is not None - calculated_available = getattr(pattern, 'intensity_calc', None) is not None + measured_available = self._has_nonempty_value(getattr(pattern, 'intensity_meas', None)) + calculated_available = has_valid_linked_phases and self._has_nonempty_value( + getattr(pattern, 'intensity_calc', None) + ) background_available = ( - measured_available + sample_form == SampleFormEnum.POWDER.value + and scattering_type == ScatteringTypeEnum.BRAGG.value + and measured_available and calculated_available - and getattr(pattern, 'intensity_bkg', None) is not None + and self._has_nonempty_value(getattr(experiment, 'background', None)) + and self._has_nonempty_value(getattr(pattern, 'intensity_bkg', None)) ) bragg_available = ( measured_available and calculated_available and sample_form == SampleFormEnum.POWDER.value and scattering_type == ScatteringTypeEnum.BRAGG.value - and getattr(experiment, 'refln', None) is not None + and self._has_nonempty_value(getattr(experiment, 'refln', None)) ) residual_available = ( sample_form == SampleFormEnum.POWDER.value and measured_available and calculated_available ) - excluded_regions = getattr(experiment, 'excluded_regions', None) - has_excluded_regions = excluded_regions is not None and len(excluded_regions) > 0 - excluded_available = has_excluded_regions + has_excluded_regions = self._has_nonempty_value( + getattr(experiment, 'excluded_regions', None) + ) uncertainty_available, uncertainty_reason = self._uncertainty_status( measured_available=measured_available, sample_form=sample_form, @@ -515,7 +537,7 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: PatternOptionStatus( name='excluded', description=_PATTERN_OPTION_DESCRIPTIONS['excluded'], - available=excluded_available, + available=has_excluded_regions, auto_included=False, reason='', ), @@ -558,8 +580,9 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: reason='' if background_available else ( - 'Background display requires measured and calculated ' - 'data plus background intensities.' + 'Background display currently requires powder Bragg ' + 'measured and calculated data plus defined ' + 'background points.' ), ), PatternOptionStatus( @@ -586,10 +609,10 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: PatternOptionStatus( name='excluded', description=_PATTERN_OPTION_DESCRIPTIONS['excluded'], - available=excluded_available, + available=has_excluded_regions, auto_included='excluded' in auto_include, reason='' - if excluded_available + if has_excluded_regions else ('No excluded regions are defined for this experiment.'), ), PatternOptionStatus( @@ -601,6 +624,32 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: ), ] + @staticmethod + def _has_nonempty_value(value: object | None) -> bool: + """Return whether a plotting input has content.""" + if value is None: + return False + + try: + return len(value) > 0 + except TypeError: + return True + + def _has_valid_linked_phases(self, experiment: object) -> bool: + """Return whether the experiment links to a known structure.""" + linked_phases = getattr(experiment, 'linked_phases', None) + if not self._has_nonempty_value(linked_phases): + return False + + structure_names = set(getattr(self._project.structures, 'names', ())) + for linked_phase in linked_phases: + identity = getattr(linked_phase, '_identity', None) + category_entry_name = getattr(identity, 'category_entry_name', None) + if category_entry_name in structure_names: + return True + + return False + def _uncertainty_status( self, *, diff --git a/tests/unit/easydiffraction/display/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py index 74b54776..08540bf0 100644 --- a/tests/unit/easydiffraction/display/test_plotting.py +++ b/tests/unit/easydiffraction/display/test_plotting.py @@ -788,6 +788,8 @@ class Experiment: x_min=None, x_max=None, show_residual=None, + show_background=None, + show_bragg=None, show_excluded=False, x=None, ), @@ -800,6 +802,73 @@ class Experiment: assert plot_spec.y_calc_line_dash == 'dot' +def test_plot_meas_vs_calc_request_respects_background_and_bragg_flags(): + from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum + from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum + from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum + from easydiffraction.display.plotting import Plotter + from easydiffraction.display.plotting import _MeasVsCalcPlotOptions + + captured: dict[str, object] = {} + + class FakeBackend: + def plot_powder_meas_vs_calc(self, *, plot_spec): + captured['plot_spec'] = plot_spec + + class Pattern: + two_theta = np.array([1.0, 2.0, 3.0]) + intensity_meas = np.array([10.0, 12.0, 11.0]) + intensity_calc = np.array([9.0, 11.0, 10.5]) + intensity_bkg = np.array([1.0, 1.0, 1.0]) + + class Refln: + phase_id = np.array(['phase-a']) + two_theta = np.array([2.0]) + index_h = np.array([1]) + index_k = np.array([0]) + index_l = np.array([1]) + f_squared_calc = np.array([50.0]) + f_calc = np.array([7.0]) + + class ExptType: + sample_form = type('SF', (), {'value': SampleFormEnum.POWDER})() + scattering_type = type('S', (), {'value': ScatteringTypeEnum.BRAGG})() + beam_mode = type('B', (), {'value': BeamModeEnum.CONSTANT_WAVELENGTH})() + + class Experiment: + data = Pattern() + type = ExptType() + refln = Refln() + + plotter = Plotter() + plotter._backend = FakeBackend() + plotter._plot_meas_vs_calc_data( + experiment=Experiment(), + expt_name='E1', + plot_options=_MeasVsCalcPlotOptions( + show_background=False, + show_bragg=False, + ), + ) + + plot_spec = captured['plot_spec'] + assert plot_spec.y_bkg is None + assert plot_spec.bragg_tick_sets == () + + plotter._plot_meas_vs_calc_data( + experiment=Experiment(), + expt_name='E1', + plot_options=_MeasVsCalcPlotOptions( + show_background=True, + show_bragg=True, + ), + ) + + plot_spec = captured['plot_spec'] + assert np.allclose(plot_spec.y_bkg, np.array([1.0, 1.0, 1.0])) + assert len(plot_spec.bragg_tick_sets) == 1 + + def test_build_param_distribution_plot_accepts_unique_name_string(): plotter, fit_results, posterior_samples = _make_bayesian_plotter_fixture() unique_name = 'phase.cell.length_a' diff --git a/tests/unit/easydiffraction/project/categories/display/test_default.py b/tests/unit/easydiffraction/project/categories/display/test_default.py deleted file mode 100644 index c43171e3..00000000 --- a/tests/unit/easydiffraction/project/categories/display/test_default.py +++ /dev/null @@ -1,55 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import gemmi - - -def test_display_defaults(): - from easydiffraction.project.categories.display.default import Display - - display = Display() - - assert display.type_info.tag == 'default' - assert display._identity.category_code == 'display' - assert display.plotter_type.value == display.plotter.engine - assert display.tabler_type.value == display.tabler.engine - - -def test_display_plotter_binds_parent(): - from easydiffraction.project.categories.display.default import Display - - display = Display() - parent = object() - display._parent = parent - - plotter = display.plotter - - assert plotter._project is parent - - -def test_display_setters_update_engines(): - from easydiffraction.project.categories.display.default import Display - - display = Display() - - display.plotter_type = 'plotly' - display.tabler_type = 'rich' - - assert display.plotter_type.value == 'plotly' - assert display.plotter.engine == 'plotly' - assert display.tabler_type.value == 'rich' - assert display.tabler.engine == 'rich' - - -def test_display_from_cif_restores_types(): - from easydiffraction.project.categories.display.default import Display - - display = Display() - block = gemmi.cif.read_string( - 'data_test\n_display.plotter_type plotly\n_display.tabler_type rich\n', - ).sole_block() - - display.from_cif(block) - - assert display.plotter_type.value == 'plotly' - assert display.tabler_type.value == 'rich' diff --git a/tests/unit/easydiffraction/project/categories/display/test_factory.py b/tests/unit/easydiffraction/project/categories/display/test_factory.py deleted file mode 100644 index b335a053..00000000 --- a/tests/unit/easydiffraction/project/categories/display/test_factory.py +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-FileCopyrightText: 2026 EasyScience contributors -# SPDX-License-Identifier: BSD-3-Clause - -import pytest - - -def test_display_factory_default_and_create(): - from easydiffraction.project.categories.display.default import Display - from easydiffraction.project.categories.display.factory import DisplayFactory - - assert DisplayFactory.default_tag() == 'default' - assert 'default' in DisplayFactory.supported_tags() - - display = DisplayFactory.create('default') - - assert isinstance(display, Display) - - -def test_display_factory_rejects_unknown_tag(): - from easydiffraction.project.categories.display.factory import DisplayFactory - - with pytest.raises(ValueError, match=r"Unsupported type: 'missing'"): - DisplayFactory.create('missing') diff --git a/tests/unit/easydiffraction/project/test_display.py b/tests/unit/easydiffraction/project/test_display.py index 59f6d5f8..7c804f82 100644 --- a/tests/unit/easydiffraction/project/test_display.py +++ b/tests/unit/easydiffraction/project/test_display.py @@ -8,6 +8,9 @@ import pytest +from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum +from easydiffraction.display.plotting import _MeasVsCalcPlotOptions from easydiffraction.project.display import PatternOptionStatus from easydiffraction.project.display import ProjectDisplay @@ -35,9 +38,11 @@ def _recorder(*args, **kwargs): plot_posterior_pairs=record('plot_posterior_pairs'), plot_param_distribution=record('plot_param_distribution'), plot_posterior_predictive=record('plot_posterior_predictive'), + _plot_posterior_predictive_request=record('_plot_posterior_predictive_request'), plot_meas=record('plot_meas'), plot_calc=record('plot_calc'), plot_meas_vs_calc=record('plot_meas_vs_calc'), + _plot_meas_vs_calc_request=record('_plot_meas_vs_calc_request'), ) project = SimpleNamespace( analysis=SimpleNamespace(display=analysis_display), @@ -246,21 +251,134 @@ def test_pattern_uncertainty_routes_to_posterior_predictive(): assert calls == [ ( - 'plot_posterior_predictive', + '_plot_posterior_predictive_request', (), { 'expt_name': 'hrpt', 'style': 'band', - 'x_min': 1.0, - 'x_max': 2.0, - 'show_residual': True, - 'show_excluded': True, - 'x': None, + 'plot_options': _MeasVsCalcPlotOptions( + x_min=1.0, + x_max=2.0, + show_residual=True, + show_background=False, + show_bragg=False, + show_excluded=True, + x=None, + ), + }, + ) + ] + + +def test_pattern_measured_and_calculated_suppresses_background_and_bragg(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses( + measured=True, + calculated=True, + background=True, + bragg=True, + ) + + display.pattern('hrpt', include=('measured', 'calculated')) + + assert calls == [ + ( + '_plot_meas_vs_calc_request', + (), + { + 'expt_name': 'hrpt', + 'plot_options': _MeasVsCalcPlotOptions( + x_min=None, + x_max=None, + show_residual=False, + show_background=False, + show_bragg=False, + show_excluded=False, + x=None, + ), + }, + ) + ] + + +def test_pattern_measured_and_calculated_can_enable_background_and_bragg(): + project, calls = _make_project_stub() + display = ProjectDisplay(project) + display._pattern_option_statuses = lambda expt_name: _make_statuses( + measured=True, + calculated=True, + background=True, + residual=True, + bragg=True, + excluded=True, + ) + + display.pattern( + 'hrpt', + include=('measured', 'calculated', 'background', 'residual', 'bragg', 'excluded'), + ) + + assert calls == [ + ( + '_plot_meas_vs_calc_request', + (), + { + 'expt_name': 'hrpt', + 'plot_options': _MeasVsCalcPlotOptions( + x_min=None, + x_max=None, + show_residual=True, + show_background=True, + show_bragg=True, + show_excluded=True, + x=None, + ), }, ) ] +def test_pattern_option_statuses_ignore_placeholder_arrays_without_usable_state(monkeypatch): + pattern = SimpleNamespace( + intensity_meas=[1.0, 2.0], + intensity_calc=[0.0, 0.0], + intensity_bkg=[0.0, 0.0], + ) + + experiment = SimpleNamespace( + type=SimpleNamespace( + sample_form=SimpleNamespace(value=SampleFormEnum.POWDER.value), + scattering_type=SimpleNamespace(value=ScatteringTypeEnum.BRAGG.value), + ), + linked_phases=[], + background=[], + refln=[], + excluded_regions=[], + ) + project = SimpleNamespace( + experiments={'hrpt': experiment}, + structures=SimpleNamespace(names=['phase-a']), + analysis=SimpleNamespace(fit_results=None), + rendering=SimpleNamespace( + plotter=SimpleNamespace(_update_project_categories=lambda expt_name: None), + chart_engine=SimpleNamespace(value='plotly'), + ), + ) + display = ProjectDisplay(project) + + monkeypatch.setattr('easydiffraction.project.display.intensity_category_for', lambda expt: pattern) + + statuses = {status.name: status for status in display._pattern_option_statuses('hrpt')} + + assert statuses['measured'].available is True + assert statuses['calculated'].available is False + assert statuses['background'].available is False + assert statuses['bragg'].available is False + assert statuses['measured'].auto_included is True + assert statuses['calculated'].auto_included is False + + def test_pattern_rejects_excluded_with_custom_x(): project, _calls = _make_project_stub() display = ProjectDisplay(project) From 79b53fb62903797a1fb051e8a68f7d3ac76b65dc Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Thu, 14 May 2026 13:58:54 +0200 Subject: [PATCH 10/10] Fix single-crystal display availability checks --- docs/dev/architecture.md | 28 ++++---- docs/dev/package-structure-full.md | 6 -- docs/dev/package-structure-short.md | 4 -- src/easydiffraction/display/plotting.py | 10 +-- src/easydiffraction/project/display.py | 27 ++++---- .../easydiffraction/project/test_display.py | 66 ++++++++++++++++++- 6 files changed, 95 insertions(+), 46 deletions(-) diff --git a/docs/dev/architecture.md b/docs/dev/architecture.md index 4c464017..85fc0ad4 100644 --- a/docs/dev/architecture.md +++ b/docs/dev/architecture.md @@ -879,16 +879,16 @@ project = ed.Project(name='my_project') It owns and coordinates all components: -| Property | Type | Description | -| --------------------- | ------------- | ---------------------------------------- | -| `project.info` | `ProjectInfo` | Metadata: name, title, description, path | -| `project.structures` | `Structures` | Collection of structure datablocks | -| `project.experiments` | `Experiments` | Collection of experiment datablocks | -| `project.rendering` | `Rendering` | Plot/table engine selection | -| `project.display` | `ProjectDisplay` | Pattern/report facade | -| `project.analysis` | `Analysis` | Minimiser, fitting, aliases, constraints | -| `project.summary` | `Summary` | Report generation | -| `project.verbosity` | `str` | Console output level (full/short/silent) | +| Property | Type | Description | +| --------------------- | ---------------- | ---------------------------------------- | +| `project.info` | `ProjectInfo` | Metadata: name, title, description, path | +| `project.structures` | `Structures` | Collection of structure datablocks | +| `project.experiments` | `Experiments` | Collection of experiment datablocks | +| `project.rendering` | `Rendering` | Plot/table engine selection | +| `project.display` | `ProjectDisplay` | Pattern/report facade | +| `project.analysis` | `Analysis` | Minimiser, fitting, aliases, constraints | +| `project.summary` | `Summary` | Report generation | +| `project.verbosity` | `str` | Console output level (full/short/silent) | ### 7.1 Data Flow @@ -1249,11 +1249,11 @@ recognises three distinct selector families. They share a similar `_type` shape so the user can inspect and set them uniformly, but their intent and ownership differ: -| Family | User intent | Examples | CIF | -| ---------------------------------- | ------------------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| Family | User intent | Examples | CIF | +| ---------------------------------- | ------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | Backend selector | Pick an execution backend | `fit.minimizer_type`, `calculation.calculator_type`, `rendering.chart_engine` | `_fit.minimizer_type`, `_calculation.calculator_type`, `_rendering.chart_engine` | -| Switchable-category impl. selector | Swap a category implementation | `experiment.background_type`, `experiment.peak_profile_type` | category-owned type tag such as `_peak.profile_type` | -| Semantic value selector | Pick a scientific/analysis mode | `fit.mode` | `_fit.mode` | +| Switchable-category impl. selector | Swap a category implementation | `experiment.background_type`, `experiment.peak_profile_type` | category-owned type tag such as `_peak.profile_type` | +| Semantic value selector | Pick a scientific/analysis mode | `fit.mode` | `_fit.mode` | Backend selectors and semantic value selectors live on a dedicated configuration category (`fit`, `calculation`, `rendering`). Switchable- diff --git a/docs/dev/package-structure-full.md b/docs/dev/package-structure-full.md index 2bcf3cd2..e71f099d 100644 --- a/docs/dev/package-structure-full.md +++ b/docs/dev/package-structure-full.md @@ -402,12 +402,6 @@ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ ascii.py โ”œโ”€โ”€ ๐Ÿ“ project โ”‚ โ”œโ”€โ”€ ๐Ÿ“ categories -โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ display -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ __init__.py -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ default.py -โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿท๏ธ class Display -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ factory.py -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿท๏ธ class DisplayFactory โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ rendering โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ __init__.py โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ default.py diff --git a/docs/dev/package-structure-short.md b/docs/dev/package-structure-short.md index 9c57c3d3..573142da 100644 --- a/docs/dev/package-structure-short.md +++ b/docs/dev/package-structure-short.md @@ -198,10 +198,6 @@ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ ascii.py โ”œโ”€โ”€ ๐Ÿ“ project โ”‚ โ”œโ”€โ”€ ๐Ÿ“ categories -โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ display -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ __init__.py -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ default.py -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ๐Ÿ“„ factory.py โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“ rendering โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ __init__.py โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ๐Ÿ“„ default.py diff --git a/src/easydiffraction/display/plotting.py b/src/easydiffraction/display/plotting.py index d1072f0a..a258579e 100644 --- a/src/easydiffraction/display/plotting.py +++ b/src/easydiffraction/display/plotting.py @@ -3758,10 +3758,7 @@ def _plot_posterior_predictive_data( dtype=float, ) - if ( - np.asarray(ctx['x_filtered']).size == 0 - or not self._show_bragg_enabled(plot_options) - ): + if np.asarray(ctx['x_filtered']).size == 0 or not self._show_bragg_enabled(plot_options): bragg_tick_sets = () else: bragg_tick_sets = self._extract_bragg_tick_sets( @@ -5037,10 +5034,7 @@ def _plot_powder_bragg_meas_vs_calc( """ show_residual = True if plot_options.show_residual is None else plot_options.show_residual y_resid = series.y_meas - series.y_calc if show_residual else None - if ( - np.asarray(ctx['x_filtered']).size == 0 - or not self._show_bragg_enabled(plot_options) - ): + if np.asarray(ctx['x_filtered']).size == 0 or not self._show_bragg_enabled(plot_options): bragg_tick_sets = () else: bragg_tick_sets = self._extract_bragg_tick_sets( diff --git a/src/easydiffraction/project/display.py b/src/easydiffraction/project/display.py index c1b5bd67..74c510fa 100644 --- a/src/easydiffraction/project/display.py +++ b/src/easydiffraction/project/display.py @@ -463,10 +463,10 @@ def _pattern_option_statuses(self, expt_name: str) -> list[PatternOptionStatus]: pattern = intensity_category_for(experiment) sample_form = experiment.type.sample_form.value scattering_type = experiment.type.scattering_type.value - has_valid_linked_phases = self._has_valid_linked_phases(experiment) + has_linked_structure = self._has_linked_structure_for_calculation(experiment) measured_available = self._has_nonempty_value(getattr(pattern, 'intensity_meas', None)) - calculated_available = has_valid_linked_phases and self._has_nonempty_value( + calculated_available = has_linked_structure and self._has_nonempty_value( getattr(pattern, 'intensity_calc', None) ) background_available = ( @@ -635,20 +635,21 @@ def _has_nonempty_value(value: object | None) -> bool: except TypeError: return True - def _has_valid_linked_phases(self, experiment: object) -> bool: + def _has_linked_structure_for_calculation(self, experiment: object) -> bool: """Return whether the experiment links to a known structure.""" - linked_phases = getattr(experiment, 'linked_phases', None) - if not self._has_nonempty_value(linked_phases): - return False - structure_names = set(getattr(self._project.structures, 'names', ())) - for linked_phase in linked_phases: - identity = getattr(linked_phase, '_identity', None) - category_entry_name = getattr(identity, 'category_entry_name', None) - if category_entry_name in structure_names: - return True - return False + linked_phases = getattr(experiment, 'linked_phases', None) + if self._has_nonempty_value(linked_phases): + for linked_phase in linked_phases: + identity = getattr(linked_phase, '_identity', None) + category_entry_name = getattr(identity, 'category_entry_name', None) + if category_entry_name in structure_names: + return True + + linked_crystal = getattr(experiment, 'linked_crystal', None) + linked_crystal_id = getattr(getattr(linked_crystal, 'id', None), 'value', None) + return linked_crystal_id in structure_names def _uncertainty_status( self, diff --git a/tests/unit/easydiffraction/project/test_display.py b/tests/unit/easydiffraction/project/test_display.py index 7c804f82..46f22296 100644 --- a/tests/unit/easydiffraction/project/test_display.py +++ b/tests/unit/easydiffraction/project/test_display.py @@ -367,7 +367,9 @@ def test_pattern_option_statuses_ignore_placeholder_arrays_without_usable_state( ) display = ProjectDisplay(project) - monkeypatch.setattr('easydiffraction.project.display.intensity_category_for', lambda expt: pattern) + monkeypatch.setattr( + 'easydiffraction.project.display.intensity_category_for', lambda expt: pattern + ) statuses = {status.name: status for status in display._pattern_option_statuses('hrpt')} @@ -379,6 +381,68 @@ def test_pattern_option_statuses_ignore_placeholder_arrays_without_usable_state( assert statuses['calculated'].auto_included is False +def test_pattern_auto_routes_single_crystal_with_calculated_data(monkeypatch): + calls: list[tuple[str, tuple, dict]] = [] + + def record(name: str): + def _recorder(*args, **kwargs): + calls.append((name, args, kwargs)) + + return _recorder + + pattern = SimpleNamespace( + intensity_meas=[10.0, 12.0], + intensity_calc=[9.5, 11.5], + ) + experiment = SimpleNamespace( + type=SimpleNamespace( + sample_form=SimpleNamespace(value=SampleFormEnum.SINGLE_CRYSTAL.value), + scattering_type=SimpleNamespace(value=ScatteringTypeEnum.BRAGG.value), + ), + linked_crystal=SimpleNamespace(id=SimpleNamespace(value='si')), + excluded_regions=[], + ) + project = SimpleNamespace( + experiments={'heidi': experiment}, + structures=SimpleNamespace(names=['si']), + analysis=SimpleNamespace(fit_results=None), + rendering=SimpleNamespace( + plotter=SimpleNamespace( + _update_project_categories=lambda expt_name: None, + _plot_meas_vs_calc_request=record('_plot_meas_vs_calc_request'), + ), + chart_engine=SimpleNamespace(value='plotly'), + ), + ) + display = ProjectDisplay(project) + + monkeypatch.setattr( + 'easydiffraction.project.display.intensity_category_for', + lambda expt: pattern, + ) + + display.pattern('heidi') + + assert calls == [ + ( + '_plot_meas_vs_calc_request', + (), + { + 'expt_name': 'heidi', + 'plot_options': _MeasVsCalcPlotOptions( + x_min=None, + x_max=None, + show_residual=False, + show_background=False, + show_bragg=False, + show_excluded=False, + x=None, + ), + }, + ) + ] + + def test_pattern_rejects_excluded_with_custom_x(): project, _calls = _make_project_stub() display = ProjectDisplay(project)