jqscore
JAX reimplementation of Q-score — a per-atom cryo-EM map-to-model fit metric.
Q-score measures how well each atom in an atomic model fits into a cryo-EM density map by comparing the observed radial density profile around each atom to a theoretical Gaussian profile (Pearson correlation).
Pintilie et al., Nature Methods 2020. Reference implementation: gregdp/mapq
Installation
pip install jqscore # CPU
pip install "jqscore[gpu]" # NVIDIA GPU: CUDA 13 build of JAX, driver ≥ 580
With an older driver (≥ 525), use the CUDA 12 build of JAX:
pip install jqscore "jax[cuda12]". The development version:
pip install git+https://github.com/aozalevsky/jqscore.
Python API
import jqscore
# From file paths (PDB/mmCIF + MRC/CCP4)
result = jqscore.run("structure.cif", "map.mrc")
# Access results
print(result.atom_qscores) # per-atom Q-scores (H: nearest heavy atom's in the residue)
print(jqscore.chain_qscores(result)) # per-chain averages
print(jqscore.residue_qscores(result)) # per-residue breakdown
# Lower-level: supply parsed atoms and map arrays
atoms = jqscore.read_cif("structure.cif")
data, origin, voxel_size = jqscore.read_mrc("map.mrc")
result = jqscore.compute_qscores(atoms, data, origin, voxel_size, sigma=0.4)
Computation modes
| Mode | What it does |
|---|---|
use_retry=True (default) |
MapQ's retry loop: up to 50 attempts with 8, 10, 12, … sphere points per shell, until ≥ 8 are clear of other atoms. Matches a float64 reference to ~1e-5, except for about 1 atom in 100,000 with a sample point within ~1e-7 Å of the exclusion cutoff, which float32 cannot decide. |
use_retry=False |
A fixed budget of n_gen points per shell; all clear points count. ~0.03 mean Q-score difference from retry mode. |
# Fast mode
result = jqscore.run("structure.cif", "map.mrc", use_retry=False)
# Check JAX backend
print(jqscore.backend()) # 'cpu', 'gpu', or 'tpu'
Atoms are processed in batches (batch_size, default 32768); the map stays on
the GPU throughout. Lower batch_size if the GPU runs out of memory.
On an RTX A4000, jqscore 36ZA.cif emd_76130.map (11.2 M atoms, 2888
chains, 800³ map) takes 2 min, half of it reading the 1.2 GB mmCIF, and
jqscore-cmd with the annotated CIF written 2.5 min.
CLI
jqscore structure.cif map.mrc [options]
options:
--sigma FLOAT Gaussian sigma in Å (default 0.4; use 0.6 for older datasets)
--num-pts INT Sample points per shell (default 8)
--to-rad FLOAT Max radial distance in Å
--d-rad FLOAT Radial shell spacing in Å
--no-retry Use fast fixed-budget mode
--max-retries INT Max retry attempts per shell (default 50)
--n-gen INT Sphere-point budget for --no-retry mode
--no-mapq-compat Interpolate the map directly instead of replicating
MapQ's trimmed-map resampling
--batch-size INT Atoms per batch (default 32768; lower for small GPUs)
--verbose Print progress and per-atom Q-scores
--output FILE Write per-residue TSV to file (default: stdout summary)
MapQ-compatibility mode
The jqscore-cmd entry point matches the argument style of the original MapQ Chimera plugin’s mapq_cmd.py, so tools like the VA validation package can use jqscore as a drop-in replacement without changes.
How to run:
jqscore-cmd <chimera_root> map=<mapfile> cif=<cif1> [cif=<cif2> ...] [np=<n>] [res=<resolution>] [sigma=<sigma>]
- Required:
map=<path>— path to the MRC/CCP4 density map. - Required:
cif=<path>— path to an mmCIF structure; repeat for multiple models. - Optional: first positional argument is the Chimera root path (accepted but unused; jqscore does not require Chimera).
All parameters other than the map path, structure path(s), and sigma are accepted for compatibility but ignored (e.g. np=, res=, Chimera root). The optional sigma=<float> sets the Gaussian width in Å (default 0.4). Output CIFs with a Q-score column are written to the current working directory as {cif_basename}__Q__{map_basename}.cif, matching the naming expected by VA.
Container
container/jqscore.def builds an Apptainer/Singularity image with the CUDA
build of JAX (the host needs only an NVIDIA driver, ≥ 580 for CUDA 13).
From the repository root:
apptainer build --fakeroot jqscore.sif container/jqscore.def
# driver older than 580: --build-arg JAX="jax[cuda12]==0.9.2"
apptainer run --nv jqscore.sif structure.cif map.mrc
apptainer exec --nv jqscore.sif jqscore-cmd /opt/chimera map=map.mrc cif=structure.cif
The build ends by running the test suite on the CPU. apptainer run-help jqscore.sif lists the commands.
Benchmarking against MapQ
The included benchmark script (needs pip install "jqscore[benchmark]" for matplotlib) downloads real PDB+EMDB structures, runs both the original MapQ (via Singularity) and jqscore, and produces per-atom scatter plots, a summary comparison, and a markdown report.
# Run with default structures (7 entries):
python -m jqscore.benchmark
# Run on specific PDB IDs:
python -m jqscore.benchmark 9XZK 9UX2
# Custom output directory:
python -m jqscore.benchmark -o my_benchmark
# After changing jqscore: re-run it only, keeping MapQ results and times
python -m jqscore.benchmark --rerun-jqscore
The MapQ reference is invoked via the MAPQ_CMD environment variable (expects a Singularity container by default). A full benchmark run with all 7 default structures takes approximately 30 minutes.
Results are saved to jqscore_mapq_benchmark/ (plots, output CIFs, and README.md); the latest run is in docs/jqscore_vs_mapq/. Of the 750,165 non-H atoms of its 7 entries, 15 differ from MapQ by more than 1e-3: 5 in 3J7O where the density around the atom is flat (MapQ gives ~0.1, jqscore 0), and 10 with a sample point within ~1e-7 Å of the exclusion cutoff, a tie that MapQ also breaks differently from a float64 reference in 3 of the 10. See spec/benchmark.md for the full specification.
License
MIT
Metadata
Release files for jqscore 0.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 | |
|---|---|---|---|
| jqscore-0.1.0.tar.gz | 942.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jqscore-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 980.6 kB
Release files / jqscore-0.1.0.tar.gz
| Download URL | jqscore-0.1.0.tar.gz |
|---|---|
| Size | 942.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e886843ef33ad30607574cf3b8681b76bbde93f4d79b5bdf2dcd84fd3ede938f
|
|
BLAKE2b-256 checksum How to use checksums |
1fd33cd248ae9c6e30e545d4eda3208c26a6eda77ffbbe72779eaaeedb8e2a5e
|
| 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 Oct 12, 2026.
Transparency logRelease files / jqscore-0.1.0-py3-none-any.whl
| Download URL | jqscore-0.1.0-py3-none-any.whl |
|---|---|
| Size | 38.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a6f752697a77847684cfdcebc91f09ca27f6e4e66a39232fd26d374e56c6a691
|
|
BLAKE2b-256 checksum How to use checksums |
441e9f261b41ce27fe028243eb6dbd14f0005d82402255b2eef9c2bfe224ad85
|
| 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 Oct 12, 2026.
Transparency log