Skip to content
Merged
227 changes: 227 additions & 0 deletions docs/dev/adr_display-ux.md
Original file line number Diff line number Diff line change
@@ -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.
59 changes: 30 additions & 29 deletions docs/dev/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -879,15 +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.display` | `Display` | Plot/table engine selection and facades |
| `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

Expand Down Expand Up @@ -922,7 +923,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`,
Expand Down Expand Up @@ -1064,7 +1065,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
Expand All @@ -1073,14 +1074,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()
Expand All @@ -1100,11 +1101,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)
Expand Down Expand Up @@ -1225,8 +1226,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:**

Expand All @@ -1248,14 +1249,14 @@ recognises three distinct selector families. They share a similar
`<name>_type` shape so the user can inspect and set them uniformly, but
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` |
| 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` |
| 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` |

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.
Expand All @@ -1272,8 +1273,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
Expand Down
12 changes: 9 additions & 3 deletions docs/dev/package-structure-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,14 +402,20 @@
│ └── 📄 ascii.py
├── 📁 project
│ ├── 📁 categories
│ │ ├── 📁 display
│ │ ├── 📁 rendering
│ │ │ ├── 📄 __init__.py
│ │ │ ├── 📄 default.py
│ │ │ │ └── 🏷️ class Display
│ │ │ │ └── 🏷️ class Rendering
│ │ │ └── 📄 factory.py
│ │ │ └── 🏷️ class DisplayFactory
│ │ │ └── 🏷️ 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
Expand Down
3 changes: 2 additions & 1 deletion docs/dev/package-structure-short.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,13 @@
│ └── 📄 ascii.py
├── 📁 project
│ ├── 📁 categories
│ │ ├── 📁 display
│ │ ├── 📁 rendering
│ │ │ ├── 📄 __init__.py
│ │ │ ├── 📄 default.py
│ │ │ └── 📄 factory.py
│ │ └── 📄 __init__.py
│ ├── 📄 __init__.py
│ ├── 📄 display.py
│ ├── 📄 project.py
│ └── 📄 project_info.py
├── 📁 summary
Expand Down
Loading
Loading