Skip to main content

symmscreen

Symmetry projectors and modulation signal loss estimators for screening molecular crystals for directional dark matter detection.

Companion package to "The Role of Symmetries in Dark Matter Detector Design" (arXiv:2607.12036, Benjamin Lillard, Jack D. Shergold, and Juri Smirnov). The paper develops a symmetry-projection framework that predicts how much of a molecule's directional dark matter scattering signal survives crystallisation, using only the crystal's space group and the molecule's point-group symmetry and geometry. This package implements exactly that framework (paper Secs. 3-6): the crystal and molecule point-group projectors, the combined survival operator, and the three Lambda estimators for the fractional modulation-signal loss.

This package is intentionally lightweight, and does not compute electronic structure, form factors, scattering rates, or true modulation amplitudes. See SCarFFF and vsdm for software implementing these calculations.

Example notebook

symmscreen_examples.ipynb walks through the package end to end: the survival metrics, point group scans, orientation scans, loading a real CIF, visualising the unit cell, and looking at the projector matrices themselves.

Open In Colab

Installation

pip install symmscreen

Quickstart

The fastest way to get the modulation loss estimate, $\Lambda_\mathrm{coord}$, for a molecular crystal with known coordinates is:

import symmscreen as ss

ss.lambda_coord("your_crystal.cif")

There's a matching one-line function for the quadrupole class of the crystal, and of the molecule itself, too: ss.crystal_quadrupole_class(cif_path), ss.molecule_quadrupole_class(cif_path). If you only have a bare point group symbol (or a space group name/number) rather than a CIF, ss.quadrupole_class(pg_symbol=...) computes the same $\mathsf{Q}_k \in {0, 1, 2, 3, 5}$ directly.

If you want more than one metric for the same crystal, or want to inspect the underlying projector matrices, build a CombinedSurvival object directly instead. This caches the crystal/molecule projectors internally, so nothing is recomputed between calls. Since a CIF gives you the molecule's actual orientation, $\mathcal{T}$, the natural estimators here are the orientation-specific ones, lambda_L and lambda_coord:

from symmscreen import CombinedSurvival

survival = CombinedSurvival.from_cif("your_crystal.cif")

survival.crystal.quadrupole_class      # k in {0, 1, 2, 3, 5}, the quadrupole class of the crystal, Q_k.
survival.molecule.quadrupole_class     # k in {0, 1, 2, 3, 5}, the quadrupole class of the molecule's own symmetry H.
survival.crystal.matrix(l=2)           # Pi_2^(L), the l=2 projector for the crystal symmetry.
survival.molecule.matrix(l=2)          # Pi_2^(H), the l=2 projector for the internal molecule symmetry.
survival.matrix(l=2)                   # C^(2)(T), the combined survival operator.
survival.lambda_L()                    # Lambda^(L)(T), modulation signal loss due to crystallisation alone, for this crystal's specific orientation, T.
survival.lambda_coord()                # Lambda_coord, coordinate-aware estimator of total modulation signal loss.

Real, refined crystallographic coordinates are rarely exactly symmetric, so MoleculeProjector's symmetry-detection tolerance (passed to pymatgen's PointGroupAnalyzer, defaulting to pymatgen's own 0.3 Angstrom) matters. For example, caffeine's mirror plane needs tolerance >= 0.2 to be detected at all; below that its real geometry looks like $C_1$ (no symmetry) and quadrupole_class reports 5 instead of the expected 3. Pass mol_tolerance to override the default — lower it if you want stricter, closer-to-exact symmetry detection instead:

survival = CombinedSurvival.from_cif("your_crystal.cif", mol_tolerance=0.1)

Or work purely from symmetry labels, with no CIF or coordinates at all. Here the relative orientation, $\mathcal{T}$, is generally unknown, so the natural estimators are the analytic $\mathcal{T}$-averaged quantities, lambda_L_avg and lambda_ideal_avg. These have matching one-line functions that take the same symmetry labels rather than a CIF:

import symmscreen as ss

ss.lambda_L_avg(mol_pg_symbol="D6h", crys_pg_symbol="D2h")
ss.lambda_ideal_avg(mol_pg_symbol="D6h", crys_pg_symbol="D2h")   # ~ sqrt(2)/5, matching the benzene example in the paper.

Or build the combined survival operator and access them from there, as well as the matrices:

from symmscreen import CombinedSurvival

survival = CombinedSurvival.from_symmetry(mol_pg_symbol="D6h", crys_pg_symbol="D2h")
survival.matrix(l=2)          # C^(2), combined survival operator at l=2. Defaults to T=I, with T the relative orientation.
survival.lambda_L_avg()       # <Lambda^(L)>_T, loss due to crystallisation alone, averaged over T.
survival.lambda_ideal_avg()   # <Lambda>_T, ~sqrt(2)/5, matching the benzene example in the paper.

from_symmetry also takes an optional relative rotation T (the paper's $\mathcal{T}$), for when you want a specific embedding rather than the $\mathcal{T}$-averaged estimators above:

from symmscreen import CombinedSurvival
from scipy.spatial.transform import Rotation

T = Rotation.from_euler("z", 90, degrees=True).as_matrix()   # e.g. a 90 degree rotation about z.

survival = CombinedSurvival.from_symmetry(mol_pg_symbol="D6h", crys_pg_symbol="D2h", T=T)
survival.matrix(l=2)  # C^(2)(T), combined survival operator at l=2 for a fixed T.
survival.lambda_L()   # Lambda^(L)(T), orientation-specific crystallistion loss, rather than T-averaged.

$\mathcal{T}$ defaults to the identity if omitted.

Getting a CIF

symmscreen does not contain any example structures, as crystal structure databases are generally not freely redistributable. However, experimentally determined molecular crystal structures are available from:

Citation

If you use this package, please cite the accompanying paper:

@article{Lillard:2026jdo,
    author = "Lillard, Benjamin and Shergold, Jack D. and Smirnov, Juri",
    title = "{The Role of Symmetries in Dark Matter Detector Design}",
    eprint = "2607.12036",
    archivePrefix = "arXiv",
    primaryClass = "hep-ph",
    month = "7",
    year = "2026"
}

Metadata

Release files for symmscreen 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for symmscreen 0.2.1
File Size Uploaded
symmscreen-0.2.1.tar.gz 22.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for symmscreen 0.2.1
File Interpreter ABI Platform
symmscreen-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 42.4 kB

Release files / symmscreen-0.2.1.tar.gz

Download URL symmscreen-0.2.1.tar.gz
Size 22.1 kB
Tags Source
SHA-256 checksum
How to use checksums
3a36ddb0e082b7643f3b5117dbb02ed2556cf85142f754eeab34946fb699f50f
BLAKE2b-256 checksum
How to use checksums
c7e60e9db6dd4974b15cbe06dcfc5b4196820c0ec729cd0b24258bc718a2df62
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release files / symmscreen-0.2.1-py3-none-any.whl

Download URL symmscreen-0.2.1-py3-none-any.whl
Size 20.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cdeb81b852e64a96f857795135c151c0e89d5f4d54c33f3b7b8ab75ff6a74fae
BLAKE2b-256 checksum
How to use checksums
09937d2116d1e0c511ab57cae9cda0371aeead358cf57b0eb8ea80799ef3fb6e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.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