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
# Run the demo.py notebook
uv run marimo edit demo.py
Optional CSD API Installation for PCA analysis
To recompute the PCA components used for the dimensionality reduction in the locality-sensitive hash (see Locality-sensitive hashing below), install the CSD Python API and run the scripts in the tools folder.
# Install CSD Python API
uv pip install --extra-index-url https://pip.ccdc.cam.ac.uk/ csd-python-api
# Generate descriptors for CSD structures
uv run tools/compute_csd_hashes.py
# Perform PCA analysis and update components in python/rinse_descriptor/data folder
uv run tools/compute_pca.py
From PyPI
pip install rinse-descriptor
# or
uv add rinse-descriptor
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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rinse_descriptor-3.0.0.tar.gz.
File metadata
- Download URL: rinse_descriptor-3.0.0.tar.gz
- Upload date:
- Size: 646.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.14 {"installer":{"name":"uv","version":"0.12.14","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a027c25837b1e431f39056c050bb421ac4136f7b04d956571d4d1274bab9b350
|
|
| MD5 |
bac57db771695b7ac8e41cbdbd65b1fa
|
|
| BLAKE2b-256 |
1f90139994d7277bb63c6f4922a7b03d134716c2c8239ede0eab573071195e0d
|
File details
Details for the file rinse_descriptor-3.0.0-py3-none-any.whl.
File metadata
- Download URL: rinse_descriptor-3.0.0-py3-none-any.whl
- Upload date:
- Size: 198.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.14 {"installer":{"name":"uv","version":"0.12.14","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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36ded9eef03a3cfb4b3c43d4bc14f05e658d3b86ca424149baf7cac2fd1f5970
|
|
| MD5 |
fef073b86cdb3be8ffffea328db9978e
|
|
| BLAKE2b-256 |
f350b27e8cbd5327facada01c8261b651e644e77605dd2be91902676d1ad509e
|