RINSE – Reciprocal-space INvariant Spectral Embedding
RINSE computes rotationally invariant descriptors of crystalline materials by projecting the intensity-weighted reciprocal lattice onto a combined radial and angular basis (analogous to SOAP, but working entirely in reciprocal space).
Descriptor formulation
For a crystal, all reflections $\mathbf{G}_{\mathrm{hkl}}$ within a resolution cutoff (default $\sin{\theta}/\lambda \leq 0.35 Å^{-1}$, i.e. $|\mathbf{G}| \leq 0.70 Å^{-1}$) are enumerated. Each reflection is assigned an intensity $\mathrm{I}(\mathbf{G}) = \lvert\mathrm{F}(\mathbf{G})\rvert^{2}$ from the structure factor calculated via cctbx direct summation with X-ray form factors by default. Isotropic and anisotropic displacement parameters are read from the CIF by default. The resolution-dependent intensity envelope is removed at the power-spectrum level by monopole (l = 0) normalisation, which divides each radial level by its spherically-averaged scattering power.
The expansion coefficients are:
$$ A_{nlm} = Σ_\mathbf{G} I(\mathbf{G}) · R_n(|\mathbf{G}|) · Y_l^m(\hat{\mathbf{G}}) $$
and the rotationally invariant power spectrum is:
$$ p_{nl} = Σ_m \lvert A_{nlm}\rvert^{2} $$
Because the intensity field is centrosymmetric if anomalous dispersion is not considered, only even l contributes. By default, RINSE also drops the monopole (l = 0) and quadrupole (l = 2) terms. Default parameters give an 16 × 8 = 128-element descriptor:
| Axis | Values | Count |
|---|---|---|
| Radial (n) | 0, 1, …, 15 | 16 |
| Angular (l) | 4, 6, 8, …, 18 (even only) | 8 |
Installation
From source (requires uv)
# Clone
git clone https://github.com/DuMOCC-Group/rinse-descriptor.git
cd rinse-descriptor
# Install uv (if not already available)
curl -Ls https://astral.sh/uv/install.sh | sh
# Install all dependencies
uv sync
# Open the interactive demo
uv run marimo edit demo.py
From PyPI
pip install rinse-descriptor
# or
uv add rinse-descriptor
Command-line interface
The rinse command computes descriptors directly from crystallographic files.
It accepts CIF, RES/INS, and measured reflection-list (.hkl) inputs.
# One structure; prints a flat 128-value descriptor
rinse mystructure.cif
# Several structures; prints one result per input
rinse structure_1.cif structure_2.res
# Include a deterministic locality-sensitive hash
rinse mystructure.cif --hash --hash-words 3
# Write machine-readable output
rinse mystructure.cif --output-format json
# Save the descriptor beside the input as mystructure.npy
rinse mystructure.cif --output-format npy
Use --help to see all options and their defaults:
rinse --help
rinse --version
By default, the CLI uses X-ray form factors, monopole and L2 normalisation,
and returns a flattened 128-element descriptor. Use --no-flatten to return
the radial-by-angular matrix instead. Common options include:
# Change descriptor resolution and radial basis
rinse mystructure.cif --n-max 12 --l-max 48 --radial-basis lin_gaussian
# Use electron or neutron form factors
rinse mystructure.cif --form-factor electron
# Disable normalisation options
rinse mystructure.cif --no-monopole-normalisation --no-l2
Measured .hkl files require a cell and space group. The cell can be read
from a CIF/RES/INS file or supplied as six comma-separated values:
rinse measured.hkl --cell mystructure.cif
rinse measured.hkl --cell "5.64,5.64,5.64,90,90,90" --space-group "F m -3 m"
Quick start
From a CIF file
from rinse_descriptor import RinseParams, descriptor, descriptor_many
# Single structure → 1-D feature vector
x = descriptor("mystructure.cif")
print(x.shape) # (128,)
# Return the 2-D power-spectrum matrix instead
params = RinseParams(flatten=False)
x_mat = descriptor("mystructure.cif", params=params)
print(x_mat.shape) # (16, 8)
# Batch of structures → (N, 128)
structures = ["structure_1.cif", "structure_2.cif"]
X = descriptor_many(structures)
print(X.shape) # (2, 128)
From a loaded cctbx structure
from rinse_descriptor import descriptor, load_cif
xrs = load_cif("mystructure.cif")
x = descriptor(xrs)
print(x.shape) # (128,)
Custom parameters
from rinse_descriptor import RinseParams, descriptor
params = RinseParams(
n_max=16, # radial shells (n = 0 … 15)
l_max=20, # angular levels (gives l = 4,6,...,18 by default)
radial_scale=0.3, # per-shell scale in Å⁻¹ (resolution cutoff is derived)
radial_basis="cv_gaussian", # or "lin_gaussian"
monopole_normalisation=False, # optional: disable the default monopole (ℓ=0) envelope removal
)
x = descriptor("mystructure.cif", params=params)
Form factors
from rinse_descriptor import descriptor
# Electron scattering factors; descriptor weights are still intensities I = |F|²
x = descriptor("mystructure.cif", form_factor_type="electron")
# Monopole (l=0) normalisation removes the resolution envelope by default.
x_norm = descriptor("mystructure.cif")
Available form_factor_type values: "xray" (default), "electron", "neutron".
Locality-sensitive hashing
RINSE provides a deterministic locality-sensitive hash that converts descriptor vectors into short, pronounceable strings (proquints) for quick similarity comparisons and indexing.
Basic usage
from rinse_descriptor import descriptor, descriptor_hash
x = descriptor("mystructure.cif")
hash_str = descriptor_hash(x)
print(hash_str) # e.g., "lusab"
# Generate longer hashes with more words (each word = 16 bits)
hash_str = descriptor_hash(x, n_words=5)
print(hash_str) # e.g., "lusab-babad-gutih-tugad-mudof"
Algorithm
The hash is computed using PCA-based SimHash:
- Project the descriptor vector onto the first n principal components (PCA model precomputed from the CSD and bundled with the package)
- Binarize each projection coefficient by its sign:
bits[i] = (projection[i] > 0) - Encode each 16-bit chunk as a five-character proquint word (
CVCVCpattern using consonants for 4-bit values and vowels for 2-bit values)
By default, n_words=1 produces a single 5-character word encoding 16 hash bits. Structurally similar crystals produce similar hash bits and thus identical or nearby proquint strings.
Properties
- Deterministic: The same descriptor always produces the same hash
- Locality-sensitive: Similar descriptors map to similar hash bits
- Pronounceable: Uses the proquint encoding (
babab,zuzuz, etc.) for human-readability - PCA-based: Uses learned principal components from the Cambridge Structural Database rather than random projections
Decode hash to bits
from rinse_descriptor import descriptor_hash, hash_to_bits
import numpy as np
hash_str = descriptor_hash(x, n_words=3)
bits = hash_to_bits(hash_str)
print(bits.shape) # (48,) — 3 words × 16 bits/word
print(bits.dtype) # bool
Custom PCA model
To use your own PCA model (e.g., trained on a different dataset):
hash_str = descriptor_hash(x, n_words=5, pca_file="my_pca_components.json")
The JSON file must contain "components" (2-D array) and "mean" (1-D array) keys.
Development
# Run tests
uv run pytest tests/ -v
# Run benchmarks
uv run pytest benchmarks/ --benchmark-only -v
# Lint / format
uv run ruff check python/ tests/
uv run ruff format python/ tests/
uv run mypy python/rinse_descriptor/
Pre-commit hooks (recommended)
This repository includes a .pre-commit-config.yaml that runs:
- On commit:
ruff check --fixandruff formatfor staged Python files. - On push: full
ruff check,mypy, andpytest.
Install and run once:
uv sync --group dev
uv run pre-commit install --hook-type pre-commit --hook-type pre-push
uv run pre-commit run --all-files
Project structure
rinse-descriptor/
├── python/rinse_descriptor/ # Python package
│ ├── __init__.py # Public API: descriptor(), descriptor_many(), descriptor_hash(), hash_to_bits()
│ ├── _crystal.py # CIF loading into cctbx xray.structure objects
│ ├── _structure_factors.py # cctbx structure factor calculation
│ ├── _radial_basis.py # smooth-shell radial bases (index-anchored)
│ ├── _descriptor.py # Power spectrum computation
│ ├── _hash.py # Locality-sensitive hashing via PCA-based SimHash
│ └── data/
│ └── pca_components.json # Precomputed PCA model for hashing
├── tests/ # pytest test suite
├── benchmarks/ # pytest-benchmark suite
├── .github/workflows/ci.yml
└── pyproject.toml
Future: rinse_descriptor.diffraction submodule
The package is designed to support a future rinse_descriptor.diffraction submodule that will provide:
- Indexed diffraction patterns (hkl, d-spacing, intensity)
- Unindexed powder diffraction patterns (2θ or d-spacing profiles)
- Reciprocal-space descriptors beyond the power spectrum (e.g. bispectrum)
The ReflectionList and RinseParams types are designed to be shared between
the core descriptor and the diffraction submodule.
Metadata
Release files for rinse-descriptor 3.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rinse_descriptor-3.1.0.tar.gz | 647.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rinse_descriptor-3.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 846.6 kB
Release files / rinse_descriptor-3.1.0.tar.gz
| Download URL | rinse_descriptor-3.1.0.tar.gz |
|---|---|
| Size | 647.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
038a10a16b2e19942464adcb306f10345757d36a478da8f77e41523662244ac2
|
|
BLAKE2b-256 checksum How to use checksums |
736fd0b88b698dadbedfcfacc570120b8d472549b0e0d45959e9ae05f66af899
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.24 {"installer":{"name":"uv","version":"0.12.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / rinse_descriptor-3.1.0-py3-none-any.whl
| Download URL | rinse_descriptor-3.1.0-py3-none-any.whl |
|---|---|
| Size | 199.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4e509908f6231cf454747cdf7a06f8683337d18eed3521e044256f6922b01f6c
|
|
BLAKE2b-256 checksum How to use checksums |
f5691d4ad8daf5d9ed1e72ae9ec5a684845bb47f59bb010a5e8ee2643b2e9cde
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.24 {"installer":{"name":"uv","version":"0.12.24","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|