Live tool: https://maximeremenko.github.io/3DSConvert/
A browser-based converter for 3-D single-crystal diffuse-scattering data. It translates the same intensity grid between the file formats used by RMCProfile, DISCUS, Yell, Meerkat, and Scatty, and reads reduced NeXus volumes from Mantid and NXrefine — entirely client-side. On the way it can process the data (crop, rebin, mask, subtract backgrounds, symmetrize) and compute the 3D-ΔPDF, and it records the steps in the output. No installation, no server, and no upload: files never leave your computer.
| Format | Read | Write | Typical producer |
|---|---|---|---|
| Unified data format (HDF5) | yes | yes | RMCProfile, DiffuseCode/DISCUS |
| Yell 1.0 (HDF5) | yes | yes | DISCUS, Yell, Meerkat |
RMCProfile old text format (.dat, _hkl.dat) |
yes | yes | RMCProfile Diffuse3D |
RMCProfile amplitudes (_aver_amp_calc.dat, _total_amp_calc.dat, _aver_interf_calc.dat) |
yes | no | RMCProfile |
hkl list h k l I σ (.txt) |
yes | yes | Spinteract (_xtal_data_NN.txt), Scatty (scatty_data_01.txt) |
VTK STRUCTURED_POINTS (.vtk, ASCII or binary) |
yes | yes | Scatty, Spinteract, 3DSCalculator; also loads in ParaView |
VTK XML image data (.vti) |
no | yes | ParaView 5.10+ (any grid, through its Direction matrix) |
NumPy (.npz) |
yes | yes | np.load in Python scripts |
3DSCalculator export (.dat, .json) |
yes | no | 3DSCalculator (calculated or loaded intensities) |
| rspace3d volume (HDF5) | yes | no | rspace3d (CrysAlisPro unwarp layers stacked) |
CCP4/MRC map (.mrc) |
no | yes | ChimeraX, Coot, PyMOL (3D-ΔPDF maps) |
NeXus: Mantid MDHistoWorkspace (.nxs) |
yes | no | Mantid SaveMD (CORELLI, TOPAZ, WAND², DEMAND, SXD, ...) |
NeXus: NXdata (.nxs) |
yes | no | NXrefine (APS 6-ID-D, CHESS QM2), other NeXus writers |
|Q| profile (.txt) |
no | yes | shell averages of the (processed) volume |
Any readable format can be converted to any writable one. The input format
is detected automatically from the file content. Direct-space 3D-ΔPDF maps
(u, v, w axes) are read from unified, Yell and .npz files and written to
those, to .vti and to CCP4/MRC; the Q-space text formats cannot hold them,
and a CCP4/MRC map holds only them.
- Unified data format: both the
/scattering/datacompatibility layout and the RMCProfile/DISCUS/entry/datalayout are read; written files contain both. New output follows the current common contract: the mandatory crystal-metadata audit datasets are written under/entry/data, withaudit_conform_dict_name = Disorder unified data,data_type_axes = hkl(uvwfor a 3D-ΔPDF), anddata_type_number = real. Readers accept the olderDisorder scatteringname and infer missing axes metadata as RMCProfile does for legacy files, but reject a structure dictionary or an unsupported axes/number type instead of silently treating it as HKL data. Three-dimensionalQaxes are converted to HKL using the real parent cell. Direct-space 3D-ΔPDF data are read and written with the labels DiffuseCode uses (data_type_axes = uvw,data_type_reciprocal = patterson,data_type_style = single_pdf,data_type_content = 3d-delta-pdf, axesu/v/w); scalar axis types (2theta,r, ...) are refused.data_type_symmetrizedis passed on, and processed output records its recipe in/entry/process(see Processing). Dataset shapes are checked againstdata_dimensionand theh/k/lcoordinate arrays. Files in the transposed/scattering/datalayout of thewrite_diffuse_scattering.pyreference writer (C order[nh,nk,nl], onestep_vectorscolumn per axis) are read when the shape or the NeXush_indices/k_indices/l_indicesattributes tell the order apart. This includes legacy "Disorder scattering 1.0" files (.nx5) that keep the grid only in theh/k/larrays and the cell inunit_cell. Whenhandlhave equal lengths the shape cannot tell the layouts apart, so this converter's layout is assumed, with a log note for files that do not look like RMCProfile or 3DSConvert output. - Yell 1.0: the flat layout with
/data,/lower_limits,/step_sizes, and/unit_celldatasets (the general step vectorsstep_sizes_abs/ord/topare used when present). Files that store the unit metric (unit_cell = 1 1 1 90 90 90) are interpreted as hkl grids in reciprocal lattice units of the parent cell; written files always store the real cell when one is known. 1-D and 2-Ddataare read as grids with one point along the missing axes, andstep_sizeis accepted in place ofstep_sizes. Direct-space files (is_direct = 1, Yell's 3D-ΔPDF maps) are read as u, v, w grids and written back withis_direct = 1. Anis_directvalue that is neither 0 nor 1 is reported as a corrupted flag; set Yell data space to reciprocal space to read such a file anyway. - RMCProfile old text format: an
npoints nsecheader followed byi j k qx qy qz intensityrows. Q is cartesian in 1/Angstrom with the 2*pi convention,q = 2*pi * B * hkl, whereBis the reciprocal basis of the parent cell. Files with several symmetry sections are read using the section 1 coordinates. RMCProfile's*_calc.datoutput addsscale offsetto the header; they are logged and the intensities are kept as stored. Lines are read the way RMCProfile's Fortran reads them: values may be apart by blanks or commas, whatever follows the values a line needs is ignored (a comment afternpoints nsec, an extra column), and a row may wrap over several lines. FortranDexponents are accepted. Grid points missing from a file are read as NaN; when pixel (1,1,1) or its neighbours are among them, the grid geometry is fitted to all rows. On output, NaN or infinite intensities are written as 0.0, which RMCProfile treats as a masked point (points with I = 0 are left out of the fit). The_hkl.datvariant (by its name) has hkl instead of Q coordinates and needs no cell; it can also be written. Output can list symmetry sections: for a chosen Laue group, each row then carries the coordinates of every symmetry-equivalent point (the point itself first) and the header gives their number asnsec. - RMCProfile amplitudes (
*_amp_calc.dat, by name): rows ofi j k, a Q triplet andRe Imper symmetry section, without a header. The value read is |A|² averaged over the sections; when the file holds severalipermutationblocks (a file starting with one is taken as amplitudes whatever its name), the first is used. The interference function (*_interf_calc.dat: the Q triplets, then oneRe Imper point) is read as |F|². - hkl lists: rows of
h k l Iand optionallyσ(Spinteract_xtal_data/_xtal_fit, Scattyscatty_data_01.txt/_sc_list.txt,*_intensity3d_rmc_*.dattables). A first line of column names (H K L Intensity) is skipped. The grid is inferred from the rows (or taken from a Spinteract/Scatty config loaded as the grid config, for rotated grids); coordinates printed with few decimals (a 1/64 step as-1.984 -1.969 ...) go to their nearest grid point. Grid points without a row become NaN. On output, masked voxels are left out, σ comes from the data (1 with a warning when there is none), rows with σ ≤ 1e-8 are left out (Scatty stops on them), and the log shows the matchingORIGIN(Spinteract) orCENTRE(Scatty) config lines. The file is named as the target program expects. - 3DSCalculator exports: the
.dat(# h k l intensity, then rows with l fastest) is read as an hkl list; the.jsongives the grid from itshAxis/kAxis/lAxis, the intensities (l fastest,nullfor NaN) and the parent cell from the metric ofBq(Q = [h,k,l] * Bq), so its c-along-z Cartesian frame does not matter. A.jsondropped on the page is taken as data when it has 3DSCalculator's keys, else as a recipe. - rspace3d (HDF5 with
/data,/H,/K,/L): the volume rspace3d builds from CrysAlisPro unwarp layers, NaN where unmeasured. The cell comes from thecell_*attributes, else fromUBover the wavelength. Unwarp rasters are Cartesian, so for an oblique cell one in-plane index also moves along the other; the shear (fromM_inv, or from the cell) is kept as a sheared hkl grid, and crop on read then does not apply. Data rspace3d symmetrized are labelled so; the radiation is set to x-ray. - NumPy
.npz: an uncompressed archive (read it withnp.load) ofvaluesand, when known,sigma, as C-order arrays[nl, nk, nh](values[l, k, h]),corner,step_vectors(one row per grid axis),unit_cell(a b c alpha beta gamma) andmetadata.json(axes, content, radiation, symmetrization). NaN stays NaN. The pointvalues[i2, i1, i0]sits atcorner + i0*step_vectors[0] + i1*step_vectors[1] + i2*step_vectors[2]. Files written withnp.savezunder these names read back;np.savez_compressedarchives are refused. - VTK XML
.vti:ImageDatawith aDirectionmatrix, so sheared and rotated grids keep their shape in ParaView 5.10 and later. Axes are Cartesian Q in 1/Angstrom (a∥x, as for the text formats) for hkl data with a real cell, Cartesian Angstrom for a 3D-ΔPDF, and r.l.u. (or lattice units) without a cell.values(andsigma) are stored as base64 binary in float32 or float64; NaN is kept. - CCP4/MRC (MRC2014, float32): for direct-space grids along u, v and w,
i.e. a 3D-ΔPDF. The map cell spans the grid, so a voxel is one grid step;
NXSTART... place the origin, which must be a whole number of steps from the grid corner. Empty voxels are written as 0. - VTK:
STRUCTURED_POINTS, ASCII, or binary (big-endian float32) on request. In Scatty (*_sc.vtk) and Spinteract (*_img_NN.vtk) output,ORIGINandSPACINGare cartesian Q in 1/Angstrom (2*pi convention, the same as the old text format); values run x fastest, z slowest. The format stores no axis directions: Scatty and Spinteract writeORIGINas the cartesian corner butSPACINGonly as step lengths along their grid axes, which may be any mutually orthogonal hkl directions (e.g.X_AXIS 6 6 0). Without more information the axes are assumed to lie along cartesian x, y and z. For rotated grids, load the program's config file as the grid config: it gives the exact hkl corner and steps (Scatty:CENTREand half-extent axes with 2p+1 points; Spinteract:ORIGINand full-extent axes with n points, one grid per data set), and the VTK header is cross-checked against it at the chosen cell. 3DSCalculator's "(HKL grid)" export and Scatty's*_sc_supercell.vtkstoreORIGIN/SPACINGin reciprocal-lattice units; they are recognised by their title line and need no cell to read. Writing VTK requires the Q grid to be axis-aligned and ascending, becauseSTRUCTURED_POINTScannot express other grids; NaN or infinite values are written as 0, since legacy VTK readers do not parse NaN. - Mantid MDHistoWorkspace (
SaveMDversion 2):data/signalis read with dimensionD0as the grid abscissa.D0–D2hold bin edges, so the grid points are the bin centres. Only workspaces in the HKL frame are read; each dimension may be a projection, taken from the columns ofexperiment0/logs/W_MATRIX(cross-checked against names like[H,H,0]), which gives a sheared hkl grid. Masked bins and the infinite values left by normalising with zero become NaN, as do bins without events; the log reports how many. The cell comes fromexperiment0/sample/oriented_lattice, and the radiation is set to neutron. Workspaces in the Q (sample frame) are put on hkl axes through their own UB (oriented_lattice/orientation_matrix): hkl = (2π UB)⁻¹ Q, with the sign of Q fromQConvention(−1 for Mantid's default Inelastic convention) — a sheared hkl grid, exact; this is checked on synthetic files only, as no real Q-frame workspace was at hand. A fourth dimension (DeltaE) integrated into one bin is fine. Q (lab frame) workspaces, open 4-D ones, MDEventWorkspaces, event workspaces and raw instrument files are refused with an explanation. - NeXus NXdata: the group named by the
@defaultattributes is read, or another one chosen from the list that appears when a file holds several. Signals and axes are found from the current (@signal,@axes,@AXISNAME_indices) and legacy (signal=1,axes="Qh:Qk:Ql") attributes, in any array order. Axes named h/k/l (Qh,H (r.l.u.), ...) or projections like[H,H,0]give an hkl grid;Qx/Qy/Qzin 1/Angstrom give Cartesian Q, converted to hkl with the UB matrix when the file (sample/ub_matrix) or the Cell card gives one, else with the parent cell (a along x). Axes may hold points or bin edges and must be evenly spaced. Aweightsdataset divides the signal (zero weight becomes NaN). The cell, Laue group, temperature and wavelength are read from the entry's sample and monochromator groups when present. For NXrefine, whose data sit in separate files behind external links, select those files together with the main one: they are placed where the links expect them. - HDF5 compression: the browser HDF5 engine decodes gzip/deflate,
shuffle, szip, fletcher32, n-bit and scale-offset. Datasets compressed
with plugin filters (LZF, Blosc, LZ4, bitshuffle, Zstandard, ...) are
refused with a message that names the filter; re-save such files with
gzip, e.g.
h5repack -f GZIP=4 in.h5 out.h5.
Open index.html in any modern browser — double-clicking the
file works, no web server is required. The app is fully self-contained:
the HDF5 engine (h5wasm, WebAssembly)
is vendored in js/h5wasm.js with the .wasm binary embedded directly in
the script, so nothing is fetched at runtime and the page works from a
file:// URL as well as from any static host. The reading and conversion
run in a Web Worker that the page builds from the loaded scripts, so the
page stays responsive and this also works from file://. If you prefer
serving it locally:
python -m http.server
then browse to http://localhost:8000/.
The same readers, processing and writers run from Node.js 22 or later, for scripts, series and outputs above the browser's 2 GB:
node cli/3dsconvert.js data.nxs --to unified
node cli/3dsconvert.js scan_*.nxs --to npz --out-dir converted --crop h=-6:6,k=-6:6,l=-2:2
node cli/3dsconvert.js data.nxs --to unified --recipe corelli.json --extra background.nxs
node cli/3dsconvert.js data.dat --to vti --structure model.rmc6f
--help lists the options: they are the page's (cell, structure file,
recipe and its extra volumes, crop on read, uncertainties, precision,
layout, gzip, the per-format choices). Several inputs are converted one
after another with the same settings, and a file that fails does not stop
the others; the exit code is 1 if any failed. Text output is streamed to
disk; HDF5 output is built in memory, so its size is limited by the RAM
(a 2.9 GB unified file from a 1.1 GB Mantid workspace took 16 s). Existing
files are kept unless --force is given.
The page is a workspace: four panels on the left (data, unit cell, processing, output), the preview and the log on the right, and a bar at the bottom with the output file name, progress, Cancel, Series…, Process and Download. Process runs the recipe and shows the result in the preview; Download writes the output file, running the recipe first unless Process already did with the same data, cell and recipe. The theme button switches between light and dark (the choice is remembered; by default the page follows the system).
The layout is yours to change, and this browser keeps it: drag the bar between the columns to share the width, and the bar above the log to share the height with the preview (a double-click on a bar resets it). A panel moves by the grip at the left of its title: drag it to another place in either column, or focus the grip and use the arrow keys (up and down in the column, left and right to the other one). The layout button in the top bar swaps the columns or resets everything. In the column that stays in view, the preview takes the height the other panels leave, and the plot grows with it.
- Data: drop files anywhere on the page, or choose them. The format is
detected from the content; the card then shows the grid, its hkl range
and step, the precision and the share of measured voxels, and the
reader's notes. Dropped
.jsonfiles load as recipes (3DSCalculator exports as data),.rmc6ffiles, unified structure files and subhklindexer.h5files as structures, and Scatty or Spinteract configs as the grid config. For NeXus data kept in linked files, select or drop all of them at once; when a NeXus file holds several NXdata groups, pick one from the list. The grid config and the Yell data-space override sit under More input options. - Unit cell: choose where the parent (crystallographic) cell comes
from: the data file, a structure file (
.rmc6f: the supercell divided by its dimensions; a unified structure.h5stores the parent cell directly; a subhklindexer.h5gives its refined cell) or manual entry (a, b, c in Angstrom; alpha, beta, gamma in degrees). The card picks the data file's own cell when it has a real one, and warns when a structure file's cell differs from it. The text and VTK formats store no cell, and some Yell files store only the unit metric; see the table below. - Processing (optional): add steps, start from a preset, or load a saved recipe. Each step opens to show its parameters, can be switched off without losing them, dragged by its icon to another place, and removed; Undo (or Ctrl+Z outside text fields) steps back through changes, and the JSON view edits the whole recipe as text. See Processing below.
- Output: pick the format card, then its options; options a format
does not use are greyed out. The precision (the same as the input,
float64 or float32) applies to HDF5,
.npzand.vti; the unified layout (both/scattering/dataand/entry/data, or/entry/dataonly, which halves the file) and gzip compression to HDF5; the radiation to HDF5 and.npz. The.datoutput takes Q or hkl coordinates and optional symmetry sections, VTK ASCII or binary, and the hkl list its target program. Formats that cannot hold the result (the Q-space text formats for a 3D-ΔPDF, CCP4/MRC for Q-space data) are disabled with the reason. The |Q| profile output writes the mean of the finite voxels in |Q| shells of the chosen width, with the standard error of the mean and the voxel count (Q mean_I sigma_of_mean n_voxels).
Preview: the planes of the grid as read, and after the recipe (Processed, or Process in the bottom bar, runs it once; Download then reuses the result, and a recipe change marks it out of date). A slider moves through the planes, and the box beside it takes a typed position. The colour scale is viridis on a log or linear range, blue–orange and centred on zero for a 3D-ΔPDF, and grey marks voxels without data; a histogram of the slice sits under it. The levels come from the whole volume and stay while you move through the planes; typed levels stay likewise until auto, and per slice scales each plane on its own.
- Custom planes are cut as 3DSCalculator and 3DSView cut them: a normal n you type (in the grid coordinates: [0 0 1] gives the (h k 0) planes, [1 −1 0] the (h h l) ones; for a 3D-ΔPDF they are u, v, w), a point the plane goes through (the middle of the data, a pinned point, or one typed under through), and t, the distance along the unit normal from that point. The slider has 401 positions from corner to corner of the data; a typed t may be anything between. The image axes u and v are orthonormal in the grid coordinates, with v up along the projection of l (else k) and u = v × n, and the pixels are square there, about a voxel each, so an (h h l) plane is √2 wider than high and a (1 1 1) cut through a cube is a regular hexagon. The axis coordinates are the components of the point along u and v; a normal along an axis keeps that axis plane's layout.
- Cartesian (UB) redraws any plane on a square grid in Cartesian space,
Q = 2π UB hkl, so angles and lengths are true (a hexagonal (h k 0) plane
shows its 120°). Without a UB the cell sets the frame, with a along x; a
3D-ΔPDF is drawn in Å. The UB comes from the data file (Mantid's oriented
lattice, NeXus
sample/ub_matrix, rspace3d), from a subhkl structure file, or is typed under Orientation (UB matrix) in the Cell card. - Zoom: the wheel or + and − zoom about the pointer, dragging pans, and 0, a double-click or the 1:1 button shows the whole plane; the zoom stays while the slider moves. The |Q| profile zooms and pans along |Q| the same way.
Pointing at the slice reads out the three coordinates of the point, its value and, in the Cartesian frame, |Q|; a click pins the point, and the other planes then go through it. The slice also works from the keyboard: arrow keys move a cursor, Enter pins it, Page Up and Page Down change the plane, + − 0 zoom. The |Q| profile view plots the shell averages against |Q|, with their standard error, for the data as read or after the recipe.
Text output (.dat, .vtk, .vti, hkl lists, profiles) is streamed directly to disk in
browsers that support the File System Access API (Chromium); elsewhere a
chunked in-memory download is used. The log reports the expected output
size first, and reading, processing and writing show progress and can be
cancelled. The log can be copied or downloaded.
Series… (in the bottom bar) converts several files, one after another, with the page's current cell source, recipe (and its extra volumes), crop and output settings — a temperature or composition series. In Chromium the outputs go into one folder you pick; elsewhere each is downloaded. A file that fails is reported and the series goes on; Cancel stops it. The data loaded in the page stay as they were.
Data files are read without first loading them into memory: HDF5 files
are mounted in the worker and read slab by slab, and text files are parsed
as a stream (an 860 MB, 10-million-row .dat reads in about 5 s). Text
files are parsed once when loaded; changing the cell afterwards needs no
re-read. The limits are the browser's: the volume itself must fit in
memory, and an HDF5 output file is assembled in memory, which Chrome and
Edge cap at about 2 GB; float32, the /entry/data-only layout or
compression reduce it. Text output streamed to disk has no such limit.
Processing holds a few copies of the volume, and the 3D-ΔPDF about 64
bytes per voxel more.
| Input | Cell needed? |
|---|---|
Unified .h5 with a real cell |
no |
Yell .h5 with a real cell |
no |
Yell .h5 with unit metric |
yes (for .dat/.vtk output; passed through for HDF5 output) |
Old text .dat |
yes, always |
_hkl.dat, hkl list |
yes (for Q-frame .dat, .vtk and CCP4/MRC output; .vti then stays in r.l.u.) |
.npz written by 3DSConvert |
no (it stores the cell) |
Q-space .vtk (Scatty, Spinteract) |
yes, always |
r.l.u. .vtk (3DSCalculator, Scatty supercell) |
yes (for .dat/.vtk output; passed through for HDF5 output) |
Mantid .nxs with an oriented lattice |
no |
| NeXus NXdata, hkl axes | no if the file stores a cell; otherwise as for unit metric |
| NeXus NXdata, Cartesian Q axes | yes, unless the file stores a cell |
The Processing panel applies a list of steps to the data before they are written; the input file is never changed. The steps run in the order listed. NaN marks masked or missing voxels throughout, and |Q| is Cartesian in 1/Angstrom with the 2*pi convention, computed with the parent cell.
| Step | What it does |
|---|---|
| Crop | keeps the points inside hkl ranges (grids along h, k, l; use Resample otherwise) |
| Resample | NaN-aware trilinear interpolation onto a new axis-aligned hkl grid |
| Rebin | averages blocks of voxels, skipping NaN |
| Correct the UB | puts the Bragg peaks back on integer hkl: refine finds the peaks near integer positions (half-maximum centroids within a search radius, a robust signal-to-noise cut), fits the linear map (optionally with a shift) that takes them to the integers by least squares with one round of outlier rejection, and resamples the volume on its own grid; matrix does the same from the UB the grid was made with (else the data's) and a corrected UB. The data's UB is corrected too. Run it before any Bragg mask |
| Mask Bragg positions | boxes (half-width in r.l.u.) or spheres (radius in 1/Angstrom) around integer hkl, for P, I, F, C, A, B or R lattices |
| Mask powder rings | voxels within ±w of given |Q| values, or of the powder lines of an aluminium, copper or vanadium sample holder (from its lattice parameter, room temperature by default) |
| Mask |Q| range | voxels outside min ≤ |Q| ≤ max (the beam stop, the corners of the grid) |
| Mask values | values outside a range |
| Fill empty voxels | empty voxels next to data take the mean of their measured neighbours, n layers deep |
| Scale and offset | I × factor + offset; optionally then shifted up by 1.01 × the lowest value, so every value is positive (RMCProfile reads I = 0 as masked) |
| Smooth | Gaussian, σ in voxels; empty voxels neither count nor get filled; uncertainties are dropped |
| Despike | a voxel more than k robust σ (1.4826 × the median absolute deviation) from the median of its 3×3×3 (or 5×5×5) neighbourhood takes that median; k = 0 makes it a median filter. Single-voxel Bragg peaks of calculated patterns count as spikes |
| Normalize by Mantid norms | Σdata/Σnorm − s·Σbkg/Σbkg_norm from MDNorm's component volumes, summed over Laue equivalents if asked; s given, or fitted by least squares of data/norm against bkg/bkg_norm (on real CORELLI data the fit comes out near 0, so check it) |
| Background B(|Q|) | subtracts a constant, linear, a − b·c^|Q| or tabulated function of |Q| (two table rows at one |Q| make a step) |
| Background from |Q| shells | subtracts the minimum or a percentile of each |Q| shell, optionally smoothed over neighbouring shells |
| Background: Laue + thermal | the diffuse scattering of uncorrelated disorder, per formula unit Σ over sites of count × (⟨f²⟩ − ⟨f e^−W⟩²) with 2W = Uiso Q²: the Laue term of species sharing a site and the thermal diffuse scattering the Debye–Waller factors take from the Bragg peaks. The composition is written as sites, e.g. Pb; Mg 0.333 + Nb 0.667; 3*O (occupancies; what is missing to 1 is vacancy), with Uiso for all or per element (0.01, Pb 0.03); X-ray (Waasmaier–Kirfel), neutron (coherent lengths) or electron (Peng) scattering, from 3DSCalculator's tables. It is scaled by least squares to a low percentile of each |Q| shell (with an offset if asked), or by a given factor |
| Remove powder rings | the rings of named materials - sample-environment metals (aluminium, copper, vanadium, niobium, titanium, zirconium, beryllium, ...), ice, calibrants (Si, CeO2, LaB6, ...) or a custom fcc Al 4.05 - are predicted from their structure and fitted to the data, then subtracted at their |Q| from every voxel, so anisotropic diffuse scattering stays; any removes every ring sharper than a cutoff instead. See below |
| Ring filter at the lines | a Fourier filter, on the lines of named materials only, of what a ring removal leaves (or of the rings themselves), over |Q| and the angle to the crystal's rotation axis. See below |
| Combine | subtracts, adds, multiplies or divides by another volume (times a factor), interpolated onto this grid if the grids differ |
| Replace low values | sets values below a threshold to a number or NaN |
| Symmetrize | averages over Laue-equivalent points (-1 to m-3m; hexagonal axes for the trigonal groups), or only fills empty voxels; median takes the median of the equivalents and clip their mean without those more than k robust σ from it, so a spike or a spurious peak in one equivalent drops out. The grid is extended to its symmetric images (a half volume becomes whole) unless that is switched off; a grid that already holds them stays as it is |
| |Q| window | multiplies by a Lorch (sin x / x), Hann or Gaussian window in |Q| up to qmax (by default the largest sphere inside the grid), or keeps a sphere; voxels beyond become empty. Before a 3D-ΔPDF it damps the truncation ripples of the box's edges |
| 3D-ΔPDF | Fourier transform to direct space (below) |
The other volumes that Combine steps use (an empty-can measurement, a calculated pattern, ...) are loaded under Other volumes, in any readable format; a step names its volume by file name.
Recipes. The steps are plain JSON, for example
{ "version": 1, "steps": [
{ "op": "combine", "operation": "subtract", "file": "background.nxs" },
{ "op": "symmetrize", "laue": "m-3m" },
{ "op": "maskBragg", "shape": "box", "size": 0.2 },
{ "op": "deltaPdf" } ] }Recipes can be saved and loaded. Unified output of processed data records
the recipe in /entry/process (an NXprocess group; the JSON and a
readable list of the steps sit in its recipe NXnote), and reading such
a file lists the steps in the log.
Powder rings. For each material the lines come from its structure: every reflection's integrated intensity m|F|²·exp(−2W) (X-ray, neutron or electron form factors, a Debye–Waller B) spread over the sphere of its |Q|, so a line's strength in a reciprocal-space volume is Σ|F|²/(4πQ²), with no Lorentz or polarization factor (for aluminium and neutrons the (111) : (200) : (220) : (311) : (222) lines come out 100 : 55 : 49 : 66 : 20). The library holds sample-environment metals, ice Ih (H₂O, D₂O) and cubic ice, and calibrants, with lattice parameters from COD entries, NIST certificates and CODATA; aluminium can be given its temperature (NIST's expansion fit for 6061-T6), and then the fitted lattice parameter checks the data's |Q| scale, the sample cell.
The data enter as a robust profile, the median of every |Q| bin, with its uncertainty 1.2533 × 1.4826 × MAD/√n, over whole shells and over direction sectors (8 by default). The bins are equal in the coordinate u = asinh(rQ/σ₀)/r, in which a line of width σ(Q) = √(σ₀² + (rQ)²) is one unit wide everywhere: u follows |Q| where the width is constant (monochromatic area detectors) and ln|Q| where it grows in proportion (time of flight). The same Gaussian high-pass (six line widths by default) is taken off the profile and off the model, so a smooth background and broad diffuse features drop out of the fit; applying it to the model too keeps the amplitudes unbiased. The amplitudes follow from non-negative (or, after an empty-can subtraction, signed) Huber-weighted least squares, with one-dimensional searches for each lattice parameter (±1 % by default) and a joint search for σ₀ and r; the profile is rebuilt for the fitted widths when they end at a bound. By default every line has an intensity of its own where it is significant (texture and absorption change the ratios) and a weak one keeps the structure-factor ratio. A material is left out, and logged, when its lines explain under 2 % of the high-passed profile, show no clear lattice match or give fewer than two significant lines. Only the bins near the lines (anywhere in the lattice search) judge the fit, and no uncertainty counts below a quarter of their median: in a symmetrized volume the shells near the origin hold copies of a few values, whose tiny uncertainties would otherwise outweigh the rings (on a CORELLI m-3m volume at 533 K aluminium was not found before). An empty-can subtraction that took off more than the rings leaves them negative, and it can leave one material positive and another negative: the materials the positive fit does not find are tried again, with negative intensities only, on what it leaves, under the same significance and lattice tests; lines of either sign within one material need positive off. The non-negative fit is solved exactly (an active set that takes a line back in whenever it would lower the loss). A sector fits its own amplitudes when they beat one scale on the whole-shell lines by more than their number (Akaike's criterion); the subtraction blends the sectors smoothly by direction, so no seams are left. In binned data a voxel holds the mean over its extent, which widens a line along the direction n by √(Σᵢ(qᵢ·n)²/12) (qᵢ the voxel's steps in Q); this term is on by default and is to be switched off for point-sampled, calculated volumes. A part of the sample environment off the sample position moves its lines with the direction: each sector may shift its lines (by up to 0.2 % by default), kept only when that fits better by more than its cost and by more than a tenth of a line width. Large grains (beryllium windows, annealed copper) leave bright spots on a ring that no smooth model takes off; mask spots then empties the voxels on a line's core more than four robust σ above the median of that line in that sector. On a CORELLI volume in an aluminium can the fit gives a = 4.0494 Å (0.006 % from aluminium at 300 K) and removes 98–102 % of the (111), (200) and (220) ring excess on average over directions; what it leaves varies with direction, which the ring filter below takes off. The Sample-environment rings preset runs both for aluminium and copper.
Both ring steps take protect, the name of a volume loaded under Other
volumes on the same grid (or --extra in the command line): its voxels with
positive values - planes, rods or spots of the sample that cross a ring -
are left out of every estimate and keep their measured values.
Ring filter. In data from a crystal turned about one axis a ring of the sample environment depends on |Q| and on the angle to that axis - a detector pixel sees the ring at one angle to the axis at every turn, so detector edges and gaps, the resolution and the paths through the environment draw bands in that angle, some only a degree or two wide - and only smoothly on the azimuth about it. The eight direction sectors of the fit cannot follow such bands, nor line shapes that are not Gaussian (the asymmetric lines of time of flight). The filter takes what the fit leaves as a function of those coordinates: medians in bins of the angle to the axis (1° by default) and of the fit's coordinate u, band-passed along u by FFT (what is broader than four line widths, and the noise finer than 0.4 of a width, come off), smoothed over the angle (more widely where the bins hold few voxels, near the axis), then the same over coarse cells of angle and azimuth for the Fourier orders up to 4 about the axis; three passes. Optionally (local, off by default), the ring's strength is then fitted locally: a can's ring varies along itself (texture, large grains) more finely than that angular model, but narrow planes or rods of the sample have the same profile across the ring, so the fit can take them too. Over patches of directions holding about 30 voxels each (their angle grows as 1/|Q|), Huber-weighted least squares of the voxels within four widths of a line on a quadratic background and the line's profile give the amplitude left there - the scattering that crosses a ring is broad across it, so the background takes it - shrunk by its significance, held within the ring's height, smoothed over neighbouring patches and interpolated between them. Patches round the sample's reflections at the line's |Q| are not fitted. The axis is found from the rings - a Legendre fit of the line excess over 300 direction cells gives a first axis, refined to where the excess binned by a degree of angle is sharpest (on CORELLI data 0.26° from the vertical goniometer axis of the UB) - or given as h k l. The medians leave out the voxels within 0.3 r.l.u. of the sample's Bragg positions, whose diffuse clouds sit at the rings' |Q| when the lattices are alike (perovskites near 4 Å, aluminium 4.05 Å), and the bins whose circle about the axis the edge of the grid cuts. A line is filtered when what is left on it stands out of the estimate's noise (the same table from every other voxel tells how large that is) and, after a fit, is less than half its ring; otherwise it is left alone and logged, as more is the sample's own scattering. Only the voxels within three line widths of those lines change. Lines, positions and widths come from a ring removal earlier in the recipe, or from a fit of the filter's own (alone it leaves the broad foot of a strong ring, about a third of it: run it after the removal). On the CORELLI volume of PMN-PT in aluminium the scatter of the ring excess over 300 directions, in units of its errors (χ²/n; 1–2 where there are no rings), goes from 78, 25 and 20 at Al (111), (200) and (220) after the fit to 17, 6 and 4 after the filter (5.3, 4.0 and 1.7 with the local strengths; 1.2-1.8 where there are no rings); in the half-integer planes the (111) ring, 0.12 high, leaves less than 0.005. What remains has none of the crystal's symmetry: ring structure that changes with the azimuth faster than the fourth order, and the broad part of lines in the narrow bands at the detector edges, which a high-pass of four line widths cannot tell from the sample's scattering. On a synthetic volume with bands and asymmetric lines the (111) residual falls from 0.036 after the fit to 0.009, against noise of 0.02, with the sample near its Bragg peaks unchanged.
Rings and symmetry. A can's rings are ordered about the rotation axis, not by the crystal's symmetry. Taken off after symmetrizing, they have lost that order (the filter then works as a smooth angular model) and a faint material may go unfound; taken off before, the fit and the filter, being robust, leave the bright spots of large grains, which symmetrizing averages over the 48 copies into a broad ring. On the 533 K CORELLI volume of PMN-PT both orders leave the aluminium lines at χ²/n 1.6-4.8 (3.0-6.3 at the sample's own shells), but symmetrizing first loses the copper, and removing rings first leaves copper (111) at 28, against 2.8 before symmetrizing. A second ring filter after symmetrizing, which takes the lines of the first fit, brings that to 3.0 with aluminium at 1.4-4.4; mask spots in the first removal also helps (19), at some cost to the sample's scattering on the ring cores. Symmetrizing without outliers (mode clip) keeps the spots out of the average - the copper (111) excess of the symmetrized volume as read stays that of the measured one (0.017; 0.032 with average) - and with the second filter no line is left above 4.8 (aluminium 2.1-4.8, copper 4.8 and 3.5, against 2.9-5.1 at the sample's own shells). The preset Rings, then symmetrize runs the fit, the filter and the symmetrization without outliers; the second filter, and the local strengths, are to be added after a look at the result, as they do not help the sample's scattering everywhere. It also serves a volume that was symmetrized already: the filter, working about one axis, breaks the symmetry a little, and the symmetrization puts it back (on Mantid's m-3m volume at 533 K, with the second filter, aluminium 2.1-6.1 and copper 1.7 and 1.4, against 2.7-6.7 and 2.4 and 1.4 with the fit and filter alone). The spots such a volume has already averaged in stay as ring intensity, and a faint material may go unfound. A second ring fit after symmetrizing finds nothing: what is left is no line of the fitted shape. Symmetrized, an empty-can volume has a comb of maxima along each ring, which a subtraction carries into the sample's volume (the streaks along h and k in the half-integer planes of symmetrized, can-subtracted CORELLI volumes).
Recommended order: subtract an empty-can measurement if there is one (both
volumes unsymmetrized, on one scale), then remove the remaining rings and
symmetrize without outliers (the preset Rings, then symmetrize), filter
the rings once more if they still show, then treat the Bragg peaks (mask,
punch and fill) and compute the 3D-ΔPDF. any instead removes every ring sharper
than a cutoff, for phases that are not in the library.
The method takes the angular low-pass of ring filters for images (rings have no angular frequency: Münch et al., Opt. Express 17, 8567 (2009), whose Fourier damping of stripes in sinograms the ring filter applies on the plane of |Q| and the angle to the rotation axis; the azimuthal medians of DIALS, Parkhurst et al., IUCrJ 4, 626 (2017), and of pyFAI) into three dimensions, and fits the lines as ice-ring tools and Le Bail fits do (AUSPEX, Thorn et al., Acta Cryst. D73, 729 (2017)).
3D-ΔPDF. The step computes P(r) = |det V| Σ I(q) exp(2πi q·r) over all grid points q, where the columns of V are the grid steps, so P approximates the Fourier integral whatever the sampling. P is evaluated on the direct grid conjugate to the data: u, v, w in lattice units of the parent cell, centred on the origin, with steps 1/(N·Δh) etc. for an hkl-aligned grid. NaN voxels, such as masked Bragg positions, count as zero, and an optional Tukey taper (0–1) softens truncation ripples. The largest imaginary part is logged and dropped; it vanishes for centrosymmetric data, e.g. after symmetrization. The FFT is wgpuFFT's: on the CPU in float64, or with WebGPU in float32 (errors about 1e-8 of the peak on real data). Grids of up to 2^25 voxels (e.g. 321³) are accepted. The result can be written as unified or Yell data only, since the text formats hold reciprocal-space grids.
As an example, background subtraction, m-3m symmetrization, Bragg masking and the 3D-ΔPDF of a 321 x 321 x 321 CORELLI volume take about 22 s in Chrome, most of it symmetrization; the FFT itself takes 3.4 s on the CPU and about 1 s on a GPU, including transfers.
The Examples/ folder contains one small synthetic dataset (cubic parent
cell a = 5.63 Angstrom, 5 x 5 x 5 grid, hkl from -1 to 1 in steps of 0.5)
written in every format the converter writes, plus a matching structure file:
| File | Demonstrates |
|---|---|
example_unified.h5 |
Unified data format (both /entry/data and /scattering/data layouts) |
example_yell.h5 |
Yell 1.0 HDF5 |
example_diffuse3d.dat |
RMCProfile old text format |
example_scatty.vtk |
Scatty VTK (STRUCTURED_POINTS) |
example_structure.rmc6f |
Parent-cell input: NaCl structure with a 2 x 2 x 2 supercell |
Load any of the data files, add example_structure.rmc6f where a cell is
required, and convert in any direction; the intensity values are identical
in all files, so results are easy to compare.
index.html,css/app.css,js/app.js— the browser app: markup, styles (light and dark themes) and the page logic, which starts the conversion worker.js/worker.js— the conversion worker: file mounting, loading, conversion planning and output, driven by messages from the page.js/converter.js— format readers/writers and the cell/reciprocal-space math. Plain JavaScript with a UMD wrapper, also loadable from Node.js for testing.js/processing.js— the processing steps and recipes (UMD like the converter).js/wgpu_fft_web.js— vendored wgpuFFT browser build (see Third-party code);tools/vendor-wgpu-fft-web.mjsreplaces it with a new build.js/unified_hdf5.js— shared helpers for the unified HDF5 structure/data contract. Not loaded by the converter page itself; this is the canonical copy of a library shared with the companion browser tools (see Provenance).js/h5wasm.js— vendored h5wasm bundle (see Third-party code).cli/3dsconvert.js— the command-line tool: runs the worker core in Node.js, reading input files from disk as needed.Examples/— the example dataset described above.tests/— Node.js regression tests and a browser test (see Validation).
The conversion core was verified against real files from each producer:
- Yell/DISCUS: a DISCUS-written Yell 1.0 file converts to the RMCProfile old
text format reproducing an independently produced reference
.datfor the same dataset at the reference file's own precision. - Scatty: parsing a Scatty
.vtkreproduces the intensities of Scatty's paired*_list.txthkl output exactly, and the recovered hkl grid matches it to the 6-decimal precision of the VTK header. - RMCProfile: HDF5 files written by the converter follow the same dataset
layouts, mandatory audit/type metadata, axis ordering, and fixed-length
string types as RMCProfile's Fortran implementation
(
unified_config/unified_hdf5_io.f90), and round-trip conversions through every format are exact at double precision. - 3D-ΔPDF: the transform is checked against analytic cases (a constant gives an origin peak of N·|det V| and zero at the other grid points; a plane wave gives peaks at ±r0), and in the browser against the same recipe run in Node. On a real 321³ CORELLI volume the browser output matches Node to 1e-16 of the peak with the CPU engine and to 2e-8 with the GPU.
RMCProfile (built with RMC_ENABLE_HDF5=ON) provides Fortran command-line
equivalents for the unified/old-text conversions: unified_to_diffuse3d
and diffuse3d_to_unified.
The regression tests run the converter and the vendored HDF5 engine under Node.js 22 or later, with nothing to install:
npm test
They round-trip the examples through every format and cover the reader
edge cases described under Format notes. A browser test drives
index.html from file:// in headless Chrome or Edge through the
DevTools protocol, feeding files into the page and checking the downloads
(set CHROME to the browser executable if it is not found):
npm run test:e2e
GitHub Actions runs both on every push.
js/h5wasm.js is a vendored copy of
h5wasm 0.10.3 (the HDF5 library
compiled to WebAssembly; this bundle embeds libhdf5 2.0.0). It is
distributed under the NIST and HDF5 license terms reproduced in
js/h5wasm-LICENSE.txt. The only change is a
wrapper: the bundle's code sits inside a function, h5wasmModule(), whose
source text the page uses to start the HDF5 engine in its Web Worker.
js/wgpu_fft_web.js is a build of the browser bindings of
wgpuFFT (Apache-2.0), with the
WebAssembly module embedded, wrapped the same way in wgpuFftWebModule().
It includes Rust crates under the Apache-2.0, MIT and zlib licenses; see
js/wgpu_fft_web-LICENSES.txt.
This repository was extracted, with full git history, from the
MaximEremenko/Utilities
monorepo, where the tool lived at RMCProfileUtilities/Format_Converter.
js/unified_hdf5.js originates in this project and is also vendored into
the companion 3DSCalculator and 3DSView
repositories.
Apache License 2.0 — see LICENSE. Third-party components keep their own licenses as noted above.