Skip to main content

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:

  1. Project the descriptor vector onto the first n principal components (PCA model precomputed from the CSD and bundled with the package)
  2. Binarize each projection coefficient by its sign: bits[i] = (projection[i] > 0)
  3. Encode each 16-bit chunk as a five-character proquint word (CVCVC pattern 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/

This repository includes a .pre-commit-config.yaml that runs:

  • On commit: ruff check --fix and ruff format for staged Python files.
  • On push: full ruff check, mypy, and pytest.

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)

Source distribution for rinse-descriptor 3.1.0
File Size Uploaded
rinse_descriptor-3.1.0.tar.gz 647.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for rinse-descriptor 3.1.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

3.1.0 This release

2 release files

3.0.0

2 release files

2.0.0

2 release files

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.7

2 release files

1.0.6

2 release files

1.0.5

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page