jqscore 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jqscore-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arthur Zalevsky
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ graft tests
2
+ global-exclude __pycache__ *.py[cod]
jqscore-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: jqscore
3
+ Version: 0.1.0
4
+ Summary: JAX reimplementation of Q-score (github.com/gregdp/mapq)
5
+ Maintainer-email: Arthur Zalevsky <aozalevsky@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/aozalevsky/jqscore
8
+ Project-URL: Repository, https://github.com/aozalevsky/jqscore
9
+ Project-URL: Issues, https://github.com/aozalevsky/jqscore/issues
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: numpy
18
+ Requires-Dist: scipy
19
+ Requires-Dist: jax
20
+ Requires-Dist: gemmi
21
+ Provides-Extra: gpu
22
+ Requires-Dist: jax[cuda13]; extra == "gpu"
23
+ Provides-Extra: benchmark
24
+ Requires-Dist: matplotlib; extra == "benchmark"
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=7.0; extra == "test"
27
+ Dynamic: license-file
28
+
29
+ # jqscore
30
+
31
+ [![Tests](https://github.com/aozalevsky/jqscore/actions/workflows/tests.yml/badge.svg)](https://github.com/aozalevsky/jqscore/actions/workflows/tests.yml)
32
+ [![PyPI](https://img.shields.io/pypi/v/jqscore)](https://pypi.org/project/jqscore/)
33
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)
34
+
35
+ JAX reimplementation of [Q-score](https://github.com/gregdp/mapq) — a per-atom cryo-EM map-to-model fit metric.
36
+
37
+ Q-score measures how well each atom in an atomic model fits into a cryo-EM
38
+ density map by comparing the observed radial density profile around each atom
39
+ to a theoretical Gaussian profile (Pearson correlation).
40
+
41
+ > Pintilie et al., *Nature Methods* 2020. Reference implementation: [gregdp/mapq](https://github.com/gregdp/mapq)
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install jqscore # CPU
47
+ pip install "jqscore[gpu]" # NVIDIA GPU: CUDA 13 build of JAX, driver ≥ 580
48
+ ```
49
+
50
+ With an older driver (≥ 525), use the CUDA 12 build of JAX:
51
+ `pip install jqscore "jax[cuda12]"`. The development version:
52
+ `pip install git+https://github.com/aozalevsky/jqscore`.
53
+
54
+ ## Python API
55
+
56
+ ```python
57
+ import jqscore
58
+
59
+ # From file paths (PDB/mmCIF + MRC/CCP4)
60
+ result = jqscore.run("structure.cif", "map.mrc")
61
+
62
+ # Access results
63
+ print(result.atom_qscores) # per-atom Q-scores (H: nearest heavy atom's in the residue)
64
+ print(jqscore.chain_qscores(result)) # per-chain averages
65
+ print(jqscore.residue_qscores(result)) # per-residue breakdown
66
+
67
+ # Lower-level: supply parsed atoms and map arrays
68
+ atoms = jqscore.read_cif("structure.cif")
69
+ data, origin, voxel_size = jqscore.read_mrc("map.mrc")
70
+ result = jqscore.compute_qscores(atoms, data, origin, voxel_size, sigma=0.4)
71
+ ```
72
+
73
+ ### Computation modes
74
+
75
+ | Mode | What it does |
76
+ |------|--------------|
77
+ | `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. |
78
+ | `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. |
79
+
80
+ ```python
81
+ # Fast mode
82
+ result = jqscore.run("structure.cif", "map.mrc", use_retry=False)
83
+
84
+ # Check JAX backend
85
+ print(jqscore.backend()) # 'cpu', 'gpu', or 'tpu'
86
+ ```
87
+
88
+ Atoms are processed in batches (`batch_size`, default 32768); the map stays on
89
+ the GPU throughout. Lower `batch_size` if the GPU runs out of memory.
90
+
91
+ On an RTX A4000, `jqscore 36ZA.cif emd_76130.map` (11.2 M atoms, 2888
92
+ chains, 800³ map) takes 2 min, half of it reading the 1.2 GB mmCIF, and
93
+ `jqscore-cmd` with the annotated CIF written 2.5 min.
94
+
95
+ ## CLI
96
+
97
+ ```
98
+ jqscore structure.cif map.mrc [options]
99
+
100
+ options:
101
+ --sigma FLOAT Gaussian sigma in Å (default 0.4; use 0.6 for older datasets)
102
+ --num-pts INT Sample points per shell (default 8)
103
+ --to-rad FLOAT Max radial distance in Å
104
+ --d-rad FLOAT Radial shell spacing in Å
105
+ --no-retry Use fast fixed-budget mode
106
+ --max-retries INT Max retry attempts per shell (default 50)
107
+ --n-gen INT Sphere-point budget for --no-retry mode
108
+ --no-mapq-compat Interpolate the map directly instead of replicating
109
+ MapQ's trimmed-map resampling
110
+ --batch-size INT Atoms per batch (default 32768; lower for small GPUs)
111
+ --verbose Print progress and per-atom Q-scores
112
+ --output FILE Write per-residue TSV to file (default: stdout summary)
113
+ ```
114
+
115
+ ## MapQ-compatibility mode
116
+
117
+ 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](https://pypi.org/project/va/) can use jqscore as a drop-in replacement without changes.
118
+
119
+ **How to run:**
120
+
121
+ ```bash
122
+ jqscore-cmd <chimera_root> map=<mapfile> cif=<cif1> [cif=<cif2> ...] [np=<n>] [res=<resolution>] [sigma=<sigma>]
123
+ ```
124
+
125
+ - **Required:** `map=<path>` — path to the MRC/CCP4 density map.
126
+ - **Required:** `cif=<path>` — path to an mmCIF structure; repeat for multiple models.
127
+ - **Optional:** first positional argument is the Chimera root path (accepted but unused; jqscore does not require Chimera).
128
+
129
+ 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.
130
+
131
+ ## Container
132
+
133
+ `container/jqscore.def` builds an Apptainer/Singularity image with the CUDA
134
+ build of JAX (the host needs only an NVIDIA driver, ≥ 580 for CUDA 13).
135
+ From the repository root:
136
+
137
+ ```bash
138
+ apptainer build --fakeroot jqscore.sif container/jqscore.def
139
+ # driver older than 580: --build-arg JAX="jax[cuda12]==0.9.2"
140
+
141
+ apptainer run --nv jqscore.sif structure.cif map.mrc
142
+ apptainer exec --nv jqscore.sif jqscore-cmd /opt/chimera map=map.mrc cif=structure.cif
143
+ ```
144
+
145
+ The build ends by running the test suite on the CPU. `apptainer run-help
146
+ jqscore.sif` lists the commands.
147
+
148
+ ## Benchmarking against MapQ
149
+
150
+ The included benchmark script (needs `pip install "jqscore[benchmark]"` for matplotlib) downloads real PDB+EMDB structures, runs both the original [MapQ](https://github.com/gregdp/mapq) (via Singularity) and jqscore, and produces per-atom scatter plots, a summary comparison, and a markdown report.
151
+
152
+ ```bash
153
+ # Run with default structures (7 entries):
154
+ python -m jqscore.benchmark
155
+
156
+ # Run on specific PDB IDs:
157
+ python -m jqscore.benchmark 9XZK 9UX2
158
+
159
+ # Custom output directory:
160
+ python -m jqscore.benchmark -o my_benchmark
161
+
162
+ # After changing jqscore: re-run it only, keeping MapQ results and times
163
+ python -m jqscore.benchmark --rerun-jqscore
164
+ ```
165
+
166
+ 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.
167
+
168
+ Results are saved to `jqscore_mapq_benchmark/` (plots, output CIFs, and `README.md`); the latest run is in [`docs/jqscore_vs_mapq/`](https://github.com/aozalevsky/jqscore/blob/main/docs/jqscore_vs_mapq/README.md). 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`](https://github.com/aozalevsky/jqscore/blob/main/spec/benchmark.md) for the full specification.
169
+
170
+ ## License
171
+
172
+ MIT
@@ -0,0 +1,144 @@
1
+ # jqscore
2
+
3
+ [![Tests](https://github.com/aozalevsky/jqscore/actions/workflows/tests.yml/badge.svg)](https://github.com/aozalevsky/jqscore/actions/workflows/tests.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/jqscore)](https://pypi.org/project/jqscore/)
5
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)
6
+
7
+ JAX reimplementation of [Q-score](https://github.com/gregdp/mapq) — a per-atom cryo-EM map-to-model fit metric.
8
+
9
+ Q-score measures how well each atom in an atomic model fits into a cryo-EM
10
+ density map by comparing the observed radial density profile around each atom
11
+ to a theoretical Gaussian profile (Pearson correlation).
12
+
13
+ > Pintilie et al., *Nature Methods* 2020. Reference implementation: [gregdp/mapq](https://github.com/gregdp/mapq)
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install jqscore # CPU
19
+ pip install "jqscore[gpu]" # NVIDIA GPU: CUDA 13 build of JAX, driver ≥ 580
20
+ ```
21
+
22
+ With an older driver (≥ 525), use the CUDA 12 build of JAX:
23
+ `pip install jqscore "jax[cuda12]"`. The development version:
24
+ `pip install git+https://github.com/aozalevsky/jqscore`.
25
+
26
+ ## Python API
27
+
28
+ ```python
29
+ import jqscore
30
+
31
+ # From file paths (PDB/mmCIF + MRC/CCP4)
32
+ result = jqscore.run("structure.cif", "map.mrc")
33
+
34
+ # Access results
35
+ print(result.atom_qscores) # per-atom Q-scores (H: nearest heavy atom's in the residue)
36
+ print(jqscore.chain_qscores(result)) # per-chain averages
37
+ print(jqscore.residue_qscores(result)) # per-residue breakdown
38
+
39
+ # Lower-level: supply parsed atoms and map arrays
40
+ atoms = jqscore.read_cif("structure.cif")
41
+ data, origin, voxel_size = jqscore.read_mrc("map.mrc")
42
+ result = jqscore.compute_qscores(atoms, data, origin, voxel_size, sigma=0.4)
43
+ ```
44
+
45
+ ### Computation modes
46
+
47
+ | Mode | What it does |
48
+ |------|--------------|
49
+ | `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. |
50
+ | `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. |
51
+
52
+ ```python
53
+ # Fast mode
54
+ result = jqscore.run("structure.cif", "map.mrc", use_retry=False)
55
+
56
+ # Check JAX backend
57
+ print(jqscore.backend()) # 'cpu', 'gpu', or 'tpu'
58
+ ```
59
+
60
+ Atoms are processed in batches (`batch_size`, default 32768); the map stays on
61
+ the GPU throughout. Lower `batch_size` if the GPU runs out of memory.
62
+
63
+ On an RTX A4000, `jqscore 36ZA.cif emd_76130.map` (11.2 M atoms, 2888
64
+ chains, 800³ map) takes 2 min, half of it reading the 1.2 GB mmCIF, and
65
+ `jqscore-cmd` with the annotated CIF written 2.5 min.
66
+
67
+ ## CLI
68
+
69
+ ```
70
+ jqscore structure.cif map.mrc [options]
71
+
72
+ options:
73
+ --sigma FLOAT Gaussian sigma in Å (default 0.4; use 0.6 for older datasets)
74
+ --num-pts INT Sample points per shell (default 8)
75
+ --to-rad FLOAT Max radial distance in Å
76
+ --d-rad FLOAT Radial shell spacing in Å
77
+ --no-retry Use fast fixed-budget mode
78
+ --max-retries INT Max retry attempts per shell (default 50)
79
+ --n-gen INT Sphere-point budget for --no-retry mode
80
+ --no-mapq-compat Interpolate the map directly instead of replicating
81
+ MapQ's trimmed-map resampling
82
+ --batch-size INT Atoms per batch (default 32768; lower for small GPUs)
83
+ --verbose Print progress and per-atom Q-scores
84
+ --output FILE Write per-residue TSV to file (default: stdout summary)
85
+ ```
86
+
87
+ ## MapQ-compatibility mode
88
+
89
+ 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](https://pypi.org/project/va/) can use jqscore as a drop-in replacement without changes.
90
+
91
+ **How to run:**
92
+
93
+ ```bash
94
+ jqscore-cmd <chimera_root> map=<mapfile> cif=<cif1> [cif=<cif2> ...] [np=<n>] [res=<resolution>] [sigma=<sigma>]
95
+ ```
96
+
97
+ - **Required:** `map=<path>` — path to the MRC/CCP4 density map.
98
+ - **Required:** `cif=<path>` — path to an mmCIF structure; repeat for multiple models.
99
+ - **Optional:** first positional argument is the Chimera root path (accepted but unused; jqscore does not require Chimera).
100
+
101
+ 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.
102
+
103
+ ## Container
104
+
105
+ `container/jqscore.def` builds an Apptainer/Singularity image with the CUDA
106
+ build of JAX (the host needs only an NVIDIA driver, ≥ 580 for CUDA 13).
107
+ From the repository root:
108
+
109
+ ```bash
110
+ apptainer build --fakeroot jqscore.sif container/jqscore.def
111
+ # driver older than 580: --build-arg JAX="jax[cuda12]==0.9.2"
112
+
113
+ apptainer run --nv jqscore.sif structure.cif map.mrc
114
+ apptainer exec --nv jqscore.sif jqscore-cmd /opt/chimera map=map.mrc cif=structure.cif
115
+ ```
116
+
117
+ The build ends by running the test suite on the CPU. `apptainer run-help
118
+ jqscore.sif` lists the commands.
119
+
120
+ ## Benchmarking against MapQ
121
+
122
+ The included benchmark script (needs `pip install "jqscore[benchmark]"` for matplotlib) downloads real PDB+EMDB structures, runs both the original [MapQ](https://github.com/gregdp/mapq) (via Singularity) and jqscore, and produces per-atom scatter plots, a summary comparison, and a markdown report.
123
+
124
+ ```bash
125
+ # Run with default structures (7 entries):
126
+ python -m jqscore.benchmark
127
+
128
+ # Run on specific PDB IDs:
129
+ python -m jqscore.benchmark 9XZK 9UX2
130
+
131
+ # Custom output directory:
132
+ python -m jqscore.benchmark -o my_benchmark
133
+
134
+ # After changing jqscore: re-run it only, keeping MapQ results and times
135
+ python -m jqscore.benchmark --rerun-jqscore
136
+ ```
137
+
138
+ 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.
139
+
140
+ Results are saved to `jqscore_mapq_benchmark/` (plots, output CIFs, and `README.md`); the latest run is in [`docs/jqscore_vs_mapq/`](https://github.com/aozalevsky/jqscore/blob/main/docs/jqscore_vs_mapq/README.md). 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`](https://github.com/aozalevsky/jqscore/blob/main/spec/benchmark.md) for the full specification.
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,34 @@
1
+ """jqscore — JAX-accelerated Q-score (cryo-EM atom-map fit metric)."""
2
+
3
+ from .jqscore import (
4
+ AtomRecord,
5
+ QScoreResult,
6
+ read_mrc,
7
+ read_pdb,
8
+ read_cif,
9
+ compute_qscores,
10
+ run,
11
+ write_qscore_cif,
12
+ residue_qscores,
13
+ chain_qscores,
14
+ backend,
15
+ _cli, # re-exported for the `jqscore` console_scripts entry point
16
+ )
17
+ from .compare import CompareResult, read_qscores, compare_qscores
18
+
19
+ __all__ = [
20
+ "AtomRecord",
21
+ "QScoreResult",
22
+ "read_mrc",
23
+ "read_pdb",
24
+ "read_cif",
25
+ "compute_qscores",
26
+ "run",
27
+ "write_qscore_cif",
28
+ "residue_qscores",
29
+ "chain_qscores",
30
+ "backend",
31
+ "CompareResult",
32
+ "read_qscores",
33
+ "compare_qscores",
34
+ ]