diff --git a/EasyReflectometryApp/Backends/Py/logic/structure.py b/EasyReflectometryApp/Backends/Py/logic/structure.py
index 6645011b..2c0d6f64 100644
--- a/EasyReflectometryApp/Backends/Py/logic/structure.py
+++ b/EasyReflectometryApp/Backends/Py/logic/structure.py
@@ -9,7 +9,16 @@ def flatten(project_lib: ProjectLib) -> tuple[list[dict], list[dict], float]:
"""Flatten the current model's sample into drawable boxes for the Structure view.
Returns (structure, legend, total_thickness):
- - structure: one dict per drawn box, top to bottom (contract in MD/VISUAL_LAYERS_PLAN.md §3.2)
+ - structure: one dict per drawn box, top to bottom, with the keys
+ label layer name, or assembly name for a gradient
+ material material name, or 'front -> back' for a gradient
+ color box color; color_end is the second color of a gradient, else ''
+ sld, isld real/imaginary SLD of the material, preformatted to 2 decimals
+ thickness layer thickness in Angstrom, as a float
+ roughness upper roughness in Angstrom, preformatted to 1 decimal
+ assembly assembly name, and assembly_index/layer_index to address the layer
+ kind 'layer' | 'gradient' | 'superphase' | 'subphase'
+ repetitions n for a collapsed repeating multilayer, else 1
- legend: distinct {label, color} pairs in stack order
- total_thickness: physical total in Angstrom (collapsed repeats counted n times, caps excluded)
"""
diff --git a/INSTALLATION.md b/INSTALLATION.md
index a8bfca08..2c3854c7 100644
--- a/INSTALLATION.md
+++ b/INSTALLATION.md
@@ -5,7 +5,7 @@ To make the installation of EasyReflectometry as easy as possible, we prepare pa
- [Windows](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.4.0/EasyReflectometryApp_v1.4.0_windows-2022.exe)
- [MacOS](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.4.0/EasyReflectometryApp_v1.4.0_macos-14-AppleSilicon.zip) (ARM)
- [Linux](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.4.0/EasyReflectometryApp_v1.4.0_ubuntu-22.04) (built on Ubuntu-22.04)
-- [Linux](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.4.0/EasyReflectometryApp_v1.4.0_ubuntu-24.04) (built on Ubuntu-22.04)
+- [Linux](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.4.0/EasyReflectometryApp_v1.4.0_ubuntu-24.04) (built on Ubuntu-24.04)
If the relevant EasyReflectometry installation does not work on your system, then please try installation from source.
diff --git a/README.md b/README.md
index 1c03b34a..67e51885 100644
--- a/README.md
+++ b/README.md
@@ -31,13 +31,14 @@ Current main features of **EasyReflectometry**:
- Growing support for flexible _item_ types, including chemically consistent models.
- Ability to corefine multiple contrasts of reflectometry data.
- Multiple minimization engines: [lmfit](https://lmfit.github.io/lmfit-py), [bumps](https://github.com/bumps/bumps) and [DFO-LS](https://github.com/numericalalgorithmsgroup/dfols) (including the differential evolution method).
+- Support for magnetic structures: a magnetic scattering length density and in-plane moment angle per layer, refined with [refl1d](https://refl1d.readthedocs.io/en/latest/).
+- Support for polarised reflectometry measurements: import of one file per spin channel, spin-asymmetry views, and simultaneous refinement of all measured spin channels against a single model.
- Interactive HTML and standard PDF report generation.
- Undo/redo for both parameter changes and fitting.
- Saving and loading of projects.
Planned improvements / new functionality for **EasyReflectometry**:
-- Support for magnetic structures and polarised reflectometry measurements.
- Support for mixed model reflectometry datasets.
- Reading of q-dependent resolution from a file.
diff --git a/ROADMAP.md b/ROADMAP.md
index f128fd24..88b3eb44 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -28,8 +28,8 @@ gantt
ESS Start of User Operation :milestone, m3, 2026-11, 2min
section Hard Condensed Matter
- Magnetism support :c1, after m1, 40w
- Spin asymmetry and polarisation analysis :c2, after m2, 30w
+ Magnetism support :done, c1, after m1, 40w
+ Spin asymmetry and polarisation analysis :done, c2, after m2, 30w
section Item Library
# Bilayer item :d1, 2024-01, 8w
@@ -121,7 +121,10 @@ This would involve significant EasyCore work, in addition to the development of
### Magnetism support
-This will begin with enabling the magnetism functionality available in `refl1d`, concurrent to which the graphical user interface will be designed with input from the relevant user communities.
+**Delivered.** Magnetic layers can be defined on the Model page and refined with `refl1d`;
+see the [magnetism tutorial](./docs/src/tutorials/magnetism.md).
+
+This began with enabling the magnetism functionality available in `refl1d`, concurrent to which the graphical user interface will be designed with input from the relevant user communities.
- [Discuss with user community and document requirements for magnetism](https://github.com/easyScience/EasyReflectometryApp/issues/113)
- [Expose magnetisation in EasyReflectometryLib](https://github.com/easyScience/EasyReflectometryLib/issues/21)
@@ -130,6 +133,10 @@ This will begin with enabling the magnetism functionality available in `refl1d`,
### Spin asymmetry and polarisation analysis
+**Delivered.** Polarised experiments are imported one file per spin channel, displayed per channel,
+fitted together and shown as spin asymmetry; see the
+[polarised data tutorial](./docs/src/tutorials/polarized_data.md).
+
Enabling the analysis of difference between different spin flipper state
- [Discuss with user community and document requirements for polarisation analysis](https://github.com/easyScience/EasyReflectometryApp/issues/115)
diff --git a/docs/src/index.md b/docs/src/index.md
index b60a735a..1a2522b5 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -13,6 +13,10 @@ tutorial.md
tutorials/model_def.md
tutorials/load_data.md
tutorials/simple_fitting.md
+tutorials/magnetism.md
+tutorials/polarized_data.md
+tutorials/bayesian.md
tutorials/summary.md
+tutorials/datasets.md
tutorials/settings.md
```
\ No newline at end of file
diff --git a/docs/src/installation.md b/docs/src/installation.md
index ae16701d..edb9069a 100644
--- a/docs/src/installation.md
+++ b/docs/src/installation.md
@@ -1,10 +1,10 @@
# Installation
To make the installation of EasyReflectometry as easy as possible, we prepare packaged releases for three major operating systems:
-- [Windows](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.1.1/EasyReflectometryApp_v1.1.1_windows-2022.exe)
-- [MacOS (Silicon)](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.1.1/EasyReflectometryApp_v1.1.1_macos-14-AppleSilicon.zip)
-- [Linux (built on Ubuntu-24.04)](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.1.1/EasyReflectometryApp_v1.1.1_ubuntu-22.04)
-- [Linux (built on Ubuntu-22.04)](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.1.1/EasyReflectometryApp_v1.1.1_ubuntu-24.04)
+- [Windows](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.4.0/EasyReflectometryApp_v1.4.0_windows-2022.exe)
+- [MacOS (Silicon)](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.4.0/EasyReflectometryApp_v1.4.0_macos-14-AppleSilicon.zip)
+- [Linux (built on Ubuntu-22.04)](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.4.0/EasyReflectometryApp_v1.4.0_ubuntu-22.04)
+- [Linux (built on Ubuntu-24.04)](https://github.com/EasyScience/EasyReflectometryApp/releases/download/v1.4.0/EasyReflectometryApp_v1.4.0_ubuntu-24.04)
If the relevant EasyReflectometry installation does not work on your system, then please try installation from source.
diff --git a/docs/src/tutorials/bayesian.md b/docs/src/tutorials/bayesian.md
new file mode 100644
index 00000000..92c16fde
--- /dev/null
+++ b/docs/src/tutorials/bayesian.md
@@ -0,0 +1,94 @@
+# Bayesian analysis
+
+Next to the classical minimisers the app can sample the posterior distribution of the fitted
+parameters with the BUMPS DREAM sampler. Instead of a single best value per parameter you
+get a distribution: a median, a credible interval, and the correlations between parameters.
+
+## Starting a sampling run
+
+Bayesian sampling is selected like a minimiser. In `Analysis` › `Advanced` ›
+**Minimization method**, pick **BUMPS-DREAM (Bayesian)** - the first entry of the
+**Minimizer** drop-down.
+
+
+
+The settings below the drop-down change with the choice. Instead of the classical
+**Tolerance** and **Max evaluations**, the sampler shows:
+
+| Setting | Meaning |
+|---|---|
+| **Samples** | Total number of samples to draw. |
+| **Burn-in steps** | Initial steps discarded before the chains are recorded. |
+| **Population** | Number of chains walking the parameter space. |
+| **Thinning** | Keep every n-th draw, to reduce autocorrelation. |
+| **Initializer** | How the starting population is spread over the parameter ranges. |
+
+The parameters that are sampled, and the ranges they are sampled in, are the ones ticked
+for fitting in the `Basic controls`, exactly as for a classical fit.
+
+With the Bayesian minimiser selected, the fit button in `Basic controls` reads
+**Start sampling** instead of **Start fitting**. It becomes **Cancel fitting** while a run
+is in progress; cancelling keeps the interface locked until the worker has actually
+stopped, so a superseded run can never write into the parameters of the next one.
+
+## Reading the results
+
+### On the reflectivity chart
+
+When a run finishes, the `Reflectivity` tab of the `Analysis` page gains two extra items,
+with their own legend entries:
+
+- **Posterior median** - the median calculated curve over the retained draws.
+- **95% credible interval** - the band containing 95 % of the posterior predictive curves.
+
+### The Bayesian Posterior tab
+
+The `Analysis` page has a second main tab, **Bayesian Posterior**, holding five views.
+Until a run has finished, it shows *"No Bayesian results available. Run a BUMPS-DREAM
+sampling to see posterior distributions."*
+
+
+
+| View | Shows |
+|---|---|
+| **Marginals** | Marginal posterior distribution of each sampled parameter. |
+| **Corner Plot** | Pairwise parameter correlations together with the marginals. |
+| **Traces** | The MCMC chain traces, for eyeballing mixing and burn-in. |
+| **2D Heatmap** | Joint posterior density of any two chosen parameters - pick them with the **X-axis** and **Y-axis** selectors. |
+| **Diagnostics** | Convergence diagnostics, see below. |
+
+Each view has a **Save** button that writes the plot to disk.
+
+```{note}
+The **Marginals**, **Corner Plot** and **Traces** views are rendered with `plotly`. If it is
+not installed the view says so and gives the install command; the rest of the app is
+unaffected.
+```
+
+### Diagnostics
+
+The **Diagnostics** view reports whether the run can be trusted:
+
+- **Sampling Configuration** - requested samples, burn-in steps, thinning, population
+ (chains), retained draws and number of parameters, as actually used by the run.
+- **Acceptance Rate** of the sampler.
+- **Gelman-Rubin R̂** per parameter. Values close to 1 indicate that the chains have
+ converged on the same distribution.
+
+## When results are discarded
+
+Posterior results describe one specific run of one specific model, so the app clears them -
+the overlays, the plots and the results dialog - as soon as they would become stale:
+
+- when a project is created, loaded or reset,
+- when a classical fit is started,
+- when a new sampling run is started.
+
+## Limitations
+
+- **Polarised experiments cannot be sampled yet.** Fitting them classically is supported,
+ see [polarised data](./polarized_data.md), but a Bayesian run over a polarised experiment
+ is not available.
+- Data files without an uncertainty column are sampled with zero variances, which the
+ sampler reports with a message. The fit is still performed, but the resulting credible
+ intervals should not be read as measurement uncertainties.
diff --git a/docs/src/tutorials/datasets.md b/docs/src/tutorials/datasets.md
new file mode 100644
index 00000000..e33983f4
--- /dev/null
+++ b/docs/src/tutorials/datasets.md
@@ -0,0 +1,33 @@
+# Demo datasets
+
+The repository ships four simulated neutron reflectometry datasets for trying out the
+constraint features described in [material and model setup](./model_def.md). They live in
+[`examples/datasets`](https://github.com/easyScience/EasyReflectometryApp/tree/master/examples/datasets),
+together with a
+[full write-up](https://github.com/easyScience/EasyReflectometryApp/blob/master/examples/datasets/README.md)
+of each demo and the script that regenerates them.
+
+All four are ORSO `.ort` files simulated from a **known structure** with 4 % noise and 5 %
+`dQ/Q` resolution, so every demo has a right answer to compare the fit against. The ground
+truth is recorded in each file's header, and each header also carries the sample structure
+in the ORSO model language, so the files can be opened in two ways:
+
+- **Model** › `Load a sample` › **Load sample from file** builds the layer stack for you,
+ with the true thicknesses, roughnesses and SLDs as starting values. Change those starting
+ values before fitting, so there is something to find.
+- **Experiment** › **Load experiment(s) from file(s)** loads the reflectivity curve, which
+ also works with a hand-built sample.
+
+| Dataset | Demonstrates |
+|---|---|
+| `two_layer_film.ort` | A two-layer film whose total thickness is known (exactly 90 Å) - the thickness budget and the derived total film thickness. |
+| `swapped_layers.ort` | A layer-ordering inequality: started from the swapped guess, the fit only recovers the truth with a `≤` constraint between the two thicknesses. |
+| `ni_ti_multilayer.ort` | A `[Ti / Ni] × 8` repeating multilayer with a Bragg peak that pins the period - the **Constant period Λ** and **Conformal roughness** recipes. |
+| `dppc_monolayer.ort` | A DPPC monolayer at the air/D2O interface - the surfactant recipes (equal head/tail area per molecule, solvent roughness). |
+
+```{note}
+For `ni_ti_multilayer.ort` the loaded stack arrives flattened (8 × [Ti | Ni] becomes 16
+layers in one assembly); rebuild it as a `Repeating Multi-layer` by hand for the
+constant-period demo. For `dppc_monolayer.ort` the loaded stack is the slab equivalent of
+the surfactant; replace it with a `Surfactant layer` assembly for the surfactant recipes.
+```
diff --git a/docs/src/tutorials/load_data.md b/docs/src/tutorials/load_data.md
index dfef8b39..aea40f5c 100644
--- a/docs/src/tutorials/load_data.md
+++ b/docs/src/tutorials/load_data.md
@@ -15,3 +15,8 @@ When data is loaded, it is possible to change instrumental parameters that affec
- **A**: Scale the data by the given value.
- **B**: Set the level where data merges into the experimental background.
- **C**: Instrumental resolution that percentage varies as a function of Q.
+
+## Polarised data
+A measurement that recorded several spin channels is loaded with the second button,
+`Load polarized experiment (file per channel)`, which takes one file per channel and asks
+how to assign them. See [polarised data](./polarized_data.md).
diff --git a/docs/src/tutorials/magnetism.md b/docs/src/tutorials/magnetism.md
new file mode 100644
index 00000000..ddfb3a4b
--- /dev/null
+++ b/docs/src/tutorials/magnetism.md
@@ -0,0 +1,88 @@
+# Magnetic layers
+
+A layer can be given a magnetisation, so that the two neutron spin states see different
+scattering length densities. This page covers the **Model** page controls; loading and
+fitting measured spin channels is described in
+[polarised data](./polarized_data.md).
+
+```{note}
+Magnetic layers can only be calculated by `refl1d`. `refnx` has no magnetism, so the app
+asks to switch the project's calculation engine the first time a layer is made magnetic.
+```
+
+## The Magnetism group
+
+The **Magnetism** group is in the basic controls of the **Model** page, below the layer
+editor. It is titled after the assembly currently selected in the `Layer editor`, for
+example `Magnetism: Multi-layer`, and shows one row per layer of that assembly.
+
+
+
+| Column | Meaning |
+|---|---|
+| **No.** | Position of the layer in the assembly. |
+| **Layer** | Layer name, as in the `Layer editor`. |
+| **ρM/10⁻⁶Å⁻²** | Magnetic scattering length density of the layer. |
+| **θM/°** | In-plane angle of the magnetic moment. |
+| **Magn.** | Makes the layer magnetic. Unticking it removes the magnetisation. |
+
+`ρM` and `θM` are only editable once **Magn.** is ticked.
+
+```{note}
+`θM = 270°` aligns the moment with the guide field, which produces no spin-flip
+scattering. This is the value to start from for a simple saturated film.
+```
+
+### Switching the calculation engine
+
+Ticking **Magn.** while the project uses an engine that cannot model magnetism opens the
+**Switch calculation engine?** dialog. Accepting it makes the layer magnetic *and* switches
+the project to `refl1d` in one step - the page does not change under you.
+
+Switching recalculates the reflectivity and makes any existing fit result stale; the sample
+and the loaded data are untouched. The engine can be switched back once no layer is
+magnetic any more.
+
+The engine itself lives in the **Calculation engine** group of the advanced controls on the
+**Model** page (and in `Analysis` › `Advanced`, see
+[simple fitting](./simple_fitting.md)). Selecting an engine that cannot model magnetism
+while the sample still has magnetic layers is refused, with a message on both pages.
+
+## Fitting ρM and θM
+
+`ρM` and `θM` appear in the `Analysis` parameter table like any other layer parameter,
+named after their assembly and model - for example `Model Fe rho_m`. They come with default
+limits, a fit checkbox and can be used in constraints. The parameter name filter accepts
+`magnetic` as a keyword to show only the magnetic parameters.
+
+## Magnetic depth profiles
+
+Once at least one layer is magnetic, the **Magnetic profile** group appears in the basic
+controls, below **Magnetism**. The same switches are repeated in `Analysis` ›
+`Advanced` › `Plot control`, and the two share one selection.
+
+
+
+- **Show ρ↑ and ρ↓** - adds the spin-up and spin-down potentials
+ `ρ ± ρM·cos(θM − A)` for each magnetic model to the SLD chart, dashed in the model's
+ colour. For non-magnetic layers the two curves collapse onto the nuclear SLD.
+- **Show ρM** - the magnetic SLD profile on its own.
+- **Show θM** - the in-plane moment angle, on its own right-hand axis. `θM` is only defined
+ where there is a moment, so the curve is drawn in pieces rather than joined across the
+ gaps.
+- **Show R↑↑ and R↓↓** - splits each magnetic model's reflectivity into its two
+ non-spin-flip cross-sections on the **Model** page reflectivity chart, dashed in the
+ model's colour with their own legend rows. Off by default.
+
+The y-range of the SLD chart covers every visible curve and grows when a curve is switched
+on, so `ρ + ρM` is never clipped. If no model is magnetic, the chart, its legend and the
+sidebar are unchanged.
+
+```{note}
+For a magnetic sample the plain model curve is **not** an unpolarised average - the
+calculator returns the ↑↑ cross-section - so `R↑↑` is drawn on top of it. The sidebar says
+so as well.
+```
+
+The `Analysis` reflectivity chart is unaffected by this switch: it already draws one
+calculated curve per measured spin channel when the experiment is polarised.
diff --git a/docs/src/tutorials/model_def.md b/docs/src/tutorials/model_def.md
index 23c8effb..61d8495d 100644
--- a/docs/src/tutorials/model_def.md
+++ b/docs/src/tutorials/model_def.md
@@ -43,17 +43,105 @@ By clicking an assembly, the `Layer editor` is specified and changes can be made
- **1**: Choose a material from materials in the `Material Editor`.
- **2**+**3**: Setting the Thickness and Upper Roughness of the material in Angstrom, Å.
+### Magnetism
+A layer can also be given a magnetisation, in the `Magnetism` group below the layer editor.
+This needs the `refl1d` calculation engine and is described in
+[magnetic layers](./magnetism.md).
+
+## Structure view
+The main window has a **Structure** tab next to **Reflectivity** showing a schematic of
+the current model's layer stack: one colored box per layer, ambient medium on top, substrate
+at the bottom. Boxes share a color per material (see the legend), box heights follow layer
+thickness, and repeated multilayers with many repetitions are drawn once with a "× N" badge.
+
+
+
+Hover a box for its material, SLD, thickness and roughness; click it to select that layer in
+the sidebar editor. The view updates immediately when the model changes, including after a fit.
+
+Note the view is a schematic, not a to-scale cross-section: heights are clamped so very thin
+layers stay visible and very thick ones do not crowd out the rest.
+
## Advanced controls
-In the advanced controls, it is possible to apply a specific Q-range of interest and sample constraints on the model.
+In the advanced controls, it is possible to apply a specific Q-range of interest, to choose
+the calculation engine, and to constrain the parameters of the model.

- **A**: Setting min. Q value of interest.
- **B**: Setting max. Q value of interest.
-- **C**: Setting Q-resolution.
-- **D**: Set 1. dependent parameter.
-- **E**: Set 2. dependent parameter.
-- **F**: Set the mathematical operator (+,-,*,/) for **G** to apply to **E**.
-- **G**: Numeric value to apply to **F**.
-- **H**: Set the comparison operators (=,<,>) between **D** and **E**.
-- **I**: Apply the sample constraint to the model.
+- **C**: Setting Q-resolution.
+
+The **Calculation engine** group selects between `refnx` and `refl1d` for this project. The
+same selector is available on the `Analysis` page. Only `refl1d` can model magnetic layers,
+so selecting `refnx` while the sample has magnetic layers is refused - see
+[magnetic layers](./magnetism.md).
+
+Below these sit three constraint groups, from the most specific to the most general.
+
+### Physics constraints
+The `Physics constraints` group applies physically motivated constraints to an assembly with
+one click. The list is per assembly of the current model, and only shows the recipes that
+make sense for that assembly type; a recipe that cannot be applied right now is marked
+`n/a` with the reason, and one that is always in force is marked `always on`.
+
+
+
+| Recipe | Effect |
+|---|---|
+| **Conformal roughness** | Every interface of the assembly shares the roughness of its first layer. |
+| **Conformal thickness** | Every layer of the assembly shares the thickness of its first layer. |
+| **Constant period Λ** | The summed thickness of the layers stays constant: the last layer absorbs whatever the others change by. |
+| **Equal head/tail area per molecule** | The head layer takes the area per molecule of the tail layer (surfactant layers). |
+| **Symmetric head groups** | The back head layer follows the front head layer thickness and area per molecule (bilayers). |
+| **Solvent roughness follows the surfactant** | The roughness of the first layer below the surfactant follows the tail roughness. Needs **Conformal roughness**. |
+| **Mixture fractions sum to 1** | Material mixtures and solvated materials keep their fractions normalised. Always on, not toggleable. |
+
+Each active recipe appears as a single row in the `Active Constraints` table of the
+`Single constraints` group, of type `physics`, counting the parameters it ties together.
+
+### Single constraints
+The `Single constraints` group creates numeric or symbolic relationships between individual
+parameters.
+
+
+
+1. Pick the **dependent parameter** from the drop-down.
+2. Pick the relation: `=`, `≤` or `≥`.
+3. Type the **expression**, for example `np.sqrt(1 / sld_ni) + 4`. Use
+ **Insert parameter alias…** to paste the alias of another parameter rather than typing
+ it, and **Insert total film thickness** to use the read-only sum of all layer thicknesses
+ between superphase and subphase.
+4. Check the **Preview** line, which shows how the constraint will read - and, for an
+ inequality, that numeric literals are interpreted in the dependent parameter's unit.
+5. Press **Add constraint**.
+
+`=` ties the parameter to the expression, so it is no longer free. `≤` and `≥` against other
+parameters become **inequality constraints**, which are enforced as penalties during
+fitting.
+
+```{warning}
+Inequality constraints need a BUMPS minimizer (`Analysis` › `Minimization method`). With
+`lmfit` or `DFO-LS` selected the group shows a warning and fits are refused until the
+minimizer is changed or the inequality is removed. A warning also appears when the current
+parameter values violate an inequality, and again fits will not start until they hold.
+```
+
+Existing constraints are listed in the `Active Constraints` table, with columns `No.`,
+`Type`, `Parameter` and `Expression`. The `Type` column tells the kinds apart - `expr` for a
+plain equality, `≤ ≥` for an inequality, `physics` for a recipe from the group above,
+`bound` for an upper bound and `value` for a fixed value - and each row can be removed
+individually.
+
+### Model constraints
+The `Model constraints` group ties whole models together rather than single parameters:
+select two or more models and press **Constrain models parameters** to constrain all of
+their matching parameters at once. This is the tool for co-refining several contrasts that
+share a structure. The resulting constraints are listed in the `Model Constraints` table
+below the selector and can be removed there.
+
+
+
+### Trying the constraints out
+Four simulated datasets with a known ground truth are provided for exercising these
+features - see [demo datasets](./datasets.md).
diff --git a/docs/src/tutorials/polarized_data.md b/docs/src/tutorials/polarized_data.md
new file mode 100644
index 00000000..dd67bdfc
--- /dev/null
+++ b/docs/src/tutorials/polarized_data.md
@@ -0,0 +1,98 @@
+# Polarised data
+
+A polarised measurement records up to four spin channels of the same sample. The app loads
+them as a single experiment, draws them separately, and fits them together against one
+model.
+
+| Channel | Meaning |
+|---|---|
+| `pp` ↑↑ | non-spin-flip, both incoming and outgoing spin up |
+| `mm` ↓↓ | non-spin-flip, both incoming and outgoing spin down |
+| `pm` ↑↓ | spin-flip |
+| `mp` ↓↑ | spin-flip |
+
+## Loading one file per channel
+
+The **Experimental data** group on the `Experiment` page has a second button,
+**Load polarized experiment (file per channel)**, below the ordinary
+**Load experiment(s) from file(s)**. Select one file per spin channel - the file picker
+allows multiple selection - and the **Assign spin channels** dialog opens.
+
+
+
+Each file gets a channel drop-down, pre-assigned from the ORSO `polarization` header or,
+failing that, from the file name. Adjust anything that came out wrong. Any number of
+channels may be assigned, a single one included, and a file that should be ignored is set
+to **not used**.
+
+The dialog refuses to continue and explains why if a channel is assigned to more than one
+file, if a file is missing, or if nothing at all is assigned.
+
+```{note}
+One resolution function is used for the whole polarised experiment, taken from the first
+assigned channel. Differing per-channel resolution metadata in the other files is ignored;
+the dialog says so.
+```
+
+If the project's calculation engine cannot model magnetism, the dialog also notes that the
+channels will be loaded and displayed but not modelled until the sample has magnetic layers
+and the engine is switched - see [magnetic layers](./magnetism.md).
+
+## Seeing the channels
+
+- The experiment chart draws one measured series, with its error bounds, per **visible**
+ channel, in a fixed palette (↑↑ `pp`, ↑↓ `pm`, ↓↑ `mp`, ↓↓ `mm`), with a per-channel
+ legend.
+- The **Polarization channels** group in the advanced controls of the `Experiment` page
+ toggles which channels are drawn. At least one measured channel always stays visible.
+- Experiment lists mark a polarised experiment with a `⇅N` badge, `N` being the number of
+ measured spin channels.
+- With several experiments selected, each polarised experiment contributes one series per
+ visible channel, using the experiment colour as the hue base.
+
+## Spin asymmetry
+
+The spin asymmetry
+
+```
+SA(q) = (R↑↑ − R↓↓) / (R↑↑ + R↓↓)
+```
+
+is available as a **Spin asymmetry** tab next to **Reflectivity** on the `Experiment` page
+(measured data only) and as a third tab of the lower panel on the `Analysis` page (measured
+data plus the model). Neither tab is shown unless the experiment measured **both**
+non-spin-flip channels; without one, the `Experiment` page has no tab strip at all.
+
+
+
+Error bars are propagated from the channel uncertainties. Two kinds of point are dropped,
+with a note of how many:
+
+- points where `R↑↑ + R↓↓` is not significantly above zero - the background-dominated tail,
+ which would otherwise wreck the axis;
+- points where the two channels do not share the same `q`.
+
+The axis shows the full `[−1, 1]` window and expands only if background-subtracted data go
+outside it.
+
+## Fitting a polarised experiment
+
+Fitting a polarised experiment fits **all** its measured spin channels at once against the
+shared model. Thickness, roughness, nuclear SLD, scale and background are common to every
+channel; `ρM` and `θM` are constrained by all of them. Polarised and ordinary experiments
+can be fitted together in the same refinement.
+
+The analysis and residual charts draw one measured/calculated pair per visible channel,
+each with that channel's cross-section. A channel the model cannot calculate - a spin-flip
+channel on a non-magnetic sample - shows its measured points only and contributes no
+residuals.
+
+In the `Summary` report a polarised experiment contributes one row per measured spin
+channel, named after the channel, and the report figures plot each channel in its channel
+colour.
+
+## Limitations
+
+- One resolution function per polarised experiment, taken from the first assigned channel.
+- Bayesian sampling of polarised experiments is not supported yet; see
+ [Bayesian analysis](./bayesian.md).
diff --git a/docs/src/tutorials/simple_fitting.md b/docs/src/tutorials/simple_fitting.md
index 63cec0c1..67fa0b49 100644
--- a/docs/src/tutorials/simple_fitting.md
+++ b/docs/src/tutorials/simple_fitting.md
@@ -32,3 +32,27 @@ The advanced control is then for changing the calculation engine and setting the
- **B**: Setting the minimisation method for minimising variables.
- **C**: Specifying the tolerance of the set minimiser.
- **D**: Setting the maximum number of iterations(evaluations) for the minimiser to run.
+
+The calculation engine (**A**) is also available on the `Model` page, in its advanced
+controls; both selectors change the same project setting. Note that only `refl1d` can model
+magnetic layers, so a sample with magnetism cannot be calculated with `refnx` - see
+[magnetic layers](./magnetism.md).
+
+The first entry of the minimiser list (**B**), `BUMPS-DREAM (Bayesian)`, does not minimise
+but samples the posterior distribution of the fitted parameters; it replaces **C** and **D**
+with its own sampling settings. See [Bayesian analysis](./bayesian.md).
+
+## Residuals
+The lower panel of the `Analysis` page has three tabs: `SLD`, `Residuals` and, for a
+polarised experiment that measured both non-spin-flip channels, `Spin asymmetry`.
+
+The `Residuals` tab plots the weighted residual `(M-E)/σ` against `q` - the difference
+between the model and the experiment, in units of the measured uncertainty. A good fit
+scatters evenly around zero with no structure left in it; a systematic wave or a drift means
+the model is missing something. The tab has the same legend, hover, zoom, pan and reset
+controls as the other charts, and its q-axis follows the logarithmic setting of the
+reflectivity chart.
+
+With several experiments, or with a polarised experiment, the tab draws one residual curve
+per visible measured series. A spin channel the model cannot calculate contributes no
+residuals.