drvizer 0.1.1__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.
Files changed (50) hide show
  1. drvizer-0.1.1/CHANGELOG.md +249 -0
  2. drvizer-0.1.1/LICENSE +21 -0
  3. drvizer-0.1.1/MANIFEST.in +5 -0
  4. drvizer-0.1.1/PKG-INFO +330 -0
  5. drvizer-0.1.1/README.md +297 -0
  6. drvizer-0.1.1/pyproject.toml +49 -0
  7. drvizer-0.1.1/setup.cfg +4 -0
  8. drvizer-0.1.1/setup.py +48 -0
  9. drvizer-0.1.1/src/drvizer/__init__.py +13 -0
  10. drvizer-0.1.1/src/drvizer/_cython_bed.c +10551 -0
  11. drvizer-0.1.1/src/drvizer/_cython_bed.pyx +142 -0
  12. drvizer-0.1.1/src/drvizer/_cython_gtf.c +8326 -0
  13. drvizer-0.1.1/src/drvizer/_cython_gtf.pyx +43 -0
  14. drvizer-0.1.1/src/drvizer/_cython_projection.c +7618 -0
  15. drvizer-0.1.1/src/drvizer/_cython_projection.pyx +43 -0
  16. drvizer-0.1.1/src/drvizer/_parallel.py +108 -0
  17. drvizer-0.1.1/src/drvizer/_track_build.py +224 -0
  18. drvizer-0.1.1/src/drvizer/api.py +1016 -0
  19. drvizer-0.1.1/src/drvizer/bam_parser.py +318 -0
  20. drvizer-0.1.1/src/drvizer/bed_parser.py +368 -0
  21. drvizer-0.1.1/src/drvizer/gtf_parser.py +683 -0
  22. drvizer-0.1.1/src/drvizer/utils.py +183 -0
  23. drvizer-0.1.1/src/drvizer/visualizer.py +784 -0
  24. drvizer-0.1.1/src/drvizer.egg-info/PKG-INFO +330 -0
  25. drvizer-0.1.1/src/drvizer.egg-info/SOURCES.txt +48 -0
  26. drvizer-0.1.1/src/drvizer.egg-info/dependency_links.txt +1 -0
  27. drvizer-0.1.1/src/drvizer.egg-info/requires.txt +9 -0
  28. drvizer-0.1.1/src/drvizer.egg-info/top_level.txt +1 -0
  29. drvizer-0.1.1/tests/test_bam_integration.py +79 -0
  30. drvizer-0.1.1/tests/test_bam_parallel.py +291 -0
  31. drvizer-0.1.1/tests/test_bed_parser_acceleration.py +409 -0
  32. drvizer-0.1.1/tests/test_build_parallel_preparation.py +298 -0
  33. drvizer-0.1.1/tests/test_get_transcript_data_snapshot.py +528 -0
  34. drvizer-0.1.1/tests/test_gtf_parser_acceleration.py +57 -0
  35. drvizer-0.1.1/tests/test_phase_2_2_lru_cache.py +459 -0
  36. drvizer-0.1.1/tests/test_phase_3_bam_kernel.py +175 -0
  37. drvizer-0.1.1/tests/test_phase_3_chunk_degradation.py +78 -0
  38. drvizer-0.1.1/tests/test_phase_3_cpu_guard.py +93 -0
  39. drvizer-0.1.1/tests/test_phase_3_crlf_bed.py +64 -0
  40. drvizer-0.1.1/tests/test_phase_3_error_chain.py +42 -0
  41. drvizer-0.1.1/tests/test_phase_7_degradation_guard.py +98 -0
  42. drvizer-0.1.1/tests/test_phase_7_imap_unordered.py +160 -0
  43. drvizer-0.1.1/tests/test_phase_7_version_and_metadata.py +89 -0
  44. drvizer-0.1.1/tests/test_phase_8_tuple_refactor.py +169 -0
  45. drvizer-0.1.1/tests/test_projection_acceleration.py +107 -0
  46. drvizer-0.1.1/tests/test_projection_invariants.py +178 -0
  47. drvizer-0.1.1/tests/test_split_by_transcript_api.py +860 -0
  48. drvizer-0.1.1/tests/test_split_by_transcript_visualizer.py +1037 -0
  49. drvizer-0.1.1/tests/test_version.py +80 -0
  50. drvizer-0.1.1/tests/test_visualizer_silent_bugs.py +399 -0
@@ -0,0 +1,249 @@
1
+ # Changelog
2
+
3
+ All notable changes to drVizer are documented here. Versions follow
4
+ [Semantic Versioning](https://semver.org/). The format is based on
5
+ [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
+
7
+ ## [0.1.1] - 2026-09-02
8
+
9
+ ### Added
10
+ - **CI matrix workflow** (`.github/workflows/ci.yml`): ubuntu-latest
11
+ on push to `main` and on `pull_request`, with a Python 3.8 - 3.12
12
+ matrix. Steps include a dedicated Cython build-prerequisites install
13
+ (mirroring `.github/workflows/benchmarks.yml`) so the editable
14
+ install compiles its Cython extensions on every runner, then
15
+ `pytest -q` for the test suite. macOS is intentionally excluded
16
+ from this matrix because Cython compilation on macOS is unvalidated
17
+ in the maintainer's Linux/DSR environment and risks a workflow that
18
+ fails on first push; it can be added in a follow-up phase once the
19
+ Linux matrix is proven stable across several push cycles.
20
+ - **Release workflow** (`.github/workflows/release.yml`): triggered by
21
+ push tags matching `v*`. Three sequential jobs: (1) `build` runs
22
+ `python -m build` and `twine check dist/*`; (2) `test-pypi` always
23
+ uploads the built artifact to TestPyPI as a dry-run / canary
24
+ validation step using the `TESTPYPI_API_TOKEN` secret; (3)
25
+ `publish-pypi` uploads to production PyPI using the
26
+ `PYPI_API_TOKEN` secret, gated on a `pypi` GitHub environment that
27
+ requires manual approval via environment protection rules. **No
28
+ auto-publish on first run.** The upload-artifact / download-artifact
29
+ pair is wired so the `dist/` directory flows from `build` to both
30
+ publish jobs.
31
+ - **Editable install gotcha section** in `CLAUDE.md`: documents that
32
+ switching git branches (especially after manual ref manipulation
33
+ such as `git update-ref refs/heads/main <sha>` or `git symbolic-ref
34
+ HEAD refs/heads/main`) leaves the editable install pointing at the
35
+ old `src/`. Mandated command:
36
+ `/datf/hanxi/software/miniconda3/envs/DRS/bin/pip install
37
+ --force-reinstall --no-deps -e .` after every merge.
38
+
39
+ ### Changed
40
+ - **Tuple-return worker contract** (P1-8, Phase 7 PARTIALLY-FIXED ->
41
+ FIXED; `src/drvizer/_parallel.py`): the
42
+ `_compute_region_coverage_with_path` worker now returns
43
+ `Tuple[str, np.ndarray]` -- `(bam_path, coverage_array)` -- so
44
+ per-bam-path attribution is encoded in the data shape instead of in
45
+ the closure binding that Phase 7 relied on. The master loop in
46
+ `aggregate_region_coverages_parallel` unpacks the tuple:
47
+ `for bam_path, coverage in pool.imap_unordered(...)`. This is a
48
+ **refactor for clarity and test maintainability**, not a
49
+ correctness fix: the Phase 7 closure binding was functionally
50
+ correct (the `bam_path` was bound into the worker closure, the
51
+ error message included the path, and master aggregation was
52
+ order-independent). v0.1.0's "Fixed: imap_unordered attribution
53
+ invariant" entry describes the original closure-binding contract;
54
+ v0.1.1's "Changed: tuple-return worker contract" describes the same
55
+ invariant re-expressed as a data shape. The worker still raises
56
+ `ParallelCoverageError(... from exc)` inside the worker so the
57
+ `__cause__` chain survives the `imap_unordered` boundary intact.
58
+ - **Test mocks updated** to match the new contract: 4 FakePool mocks
59
+ across `tests/test_bam_parallel.py` and
60
+ `tests/test_phase_3_cpu_guard.py` now return `(bam_path,
61
+ coverage_array)` tuples derived from `args[i][0]`, and the Phase 7
62
+ `tests/test_phase_7_imap_unordered.py` order-independence test
63
+ also returns `(bam_path, coverage_array)` tuples in reversed
64
+ arrival order. Net: the worker contract is exercised end-to-end in
65
+ every mock that previously returned a raw ndarray.
66
+
67
+ ### Notes
68
+ - v0.1.0 was tagged locally only (commit `dd33199`) and was **never
69
+ pushed to PyPI**. v0.1.1 is therefore the first PyPI release of
70
+ drVizer without breaking any existing install.
71
+ - No throughput numbers or speedup claims appear in this release.
72
+ The GENCODE + ENCODE + pyGenomeTracks comparison is research-grade
73
+ work that belongs to a separate Stage 3 project, not to v0.1.1.
74
+
75
+ ## [0.1.0] - 2026-09-01
76
+
77
+ ### Added
78
+ - **`pyproject.toml` PEP 621 `[project]` section** (PKG-002): `name=drvizer`,
79
+ `version=0.1.0`, `requires-python>=3.8`, MIT license text, README,
80
+ Python 3.8 - 3.12 classifiers (incl. `Development Status :: 4 - Beta`),
81
+ `dependencies` from `requirements.txt`, optional `[bam]=[pysam]` and
82
+ `[test]=[pytest]` extras, and `project_urls`. Authoritative single
83
+ source of install / metadata truth.
84
+ - **`docs/api.md`**: public API contract document describing `DrViz`
85
+ constructor signature (`adaptive_threshold`, `cache_maxsize`), `plot()`,
86
+ `get_transcript_data()`, `ReusableParser` context manager,
87
+ `ParallelCoverageError` inheritance (`RuntimeError` -> subclass),
88
+ and the `__version__` contract.
89
+ - **Three new test files** (`tests/test_phase_7_*.py`):
90
+ - `test_phase_7_imap_unordered.py` verifies that the existing
91
+ `_compute_region_coverage_with_path` worker binds `bam_path` via
92
+ closure, the `ParallelCoverageError` raised on worker failure
93
+ carries the failing `bam_path` in its message, and the master
94
+ consumer's `imap_unordered` loop pairs `bam_path` to its coverage
95
+ correctly even when results arrive out of order.
96
+ - `test_phase_7_degradation_guard.py` asserts no `RuntimeWarning` is
97
+ emitted when zero chunks degraded and exactly one `RuntimeWarning`
98
+ is emitted with the degradation count in the message when chunks
99
+ degraded.
100
+ - `test_phase_7_version_and_metadata.py` asserts `drvizer.__version__`
101
+ matches `importlib.metadata.version("drvizer")` and both equal
102
+ `"0.1.0"`, and the `pyproject.toml` classifiers include Python
103
+ 3.8 through 3.12 plus `Development Status :: 4 - Beta`.
104
+
105
+ ### Changed
106
+ - **`setup.py` skeleton** (P1-16): `setup.py` is now a thin C-extension
107
+ compile-only wrapper. `extra_compile_args=["-std=c99"]` is preserved on
108
+ every Cython `Extension` block (panel template did not mention this;
109
+ dropping it would silently violate C99 compliance). All metadata
110
+ (name, version, classifiers, install_requires, extras_require,
111
+ project_urls, long_description) was moved to `pyproject.toml [project]`
112
+ so the two sources no longer drift.
113
+ - **`__version__` bumped `1.0.0` -> `0.1.0`** (PKG-002): first public
114
+ release tag.
115
+ - **GTF chunk-degradation warning gate** (P1-3): the end-of-parse
116
+ `RuntimeWarning` in `GTFParser.parse_gtf` is now Python-gated on
117
+ `if self._chunk_parse_degradation:` so clean parses emit no warning.
118
+
119
+ ### Fixed
120
+ - **`imap_unordered` attribution invariant** (P1-8 verification, P0-8
121
+ follow-up): the worker `_compute_region_coverage_with_path` already
122
+ binds `bam_path = args[0]` via closure; the consumer's
123
+ `imap_unordered` loop relies on the per-`bam_path` `ParallelCoverageError`
124
+ message (which now embeds the path) instead of result order. The
125
+ invariant is locked in `tests/test_phase_7_imap_unordered.py`.
126
+
127
+ ### Hygiene
128
+ - **Dead `BEDParser` import removed** from `src/drvizer/api.py:31`
129
+ (OCR-006): `BEDParser` is not referenced anywhere else in `api.py`;
130
+ tests import directly from `drvizer.bed_parser`. Net: zero callers
131
+ depend on the `api.py` re-export.
132
+ - **Dead `defaultdict` import removed** from `src/drvizer/utils.py:10`
133
+ (OCR-005): `defaultdict` is not referenced anywhere else in
134
+ `utils.py`.
135
+ - **Stale `docs/superpowers/` `.gitignore` rule removed** (DOC-001):
136
+ `docs/` does not contain a `superpowers/` directory; the rule was
137
+ leftover from a prior layout. The corresponding `workspace/`
138
+ superpowers material now lives under `/datf/hanxi/software/drVizer/workspace/`.
139
+
140
+ ## [Unreleased] — Phase 2.2 (LRU cache + lifecycle)
141
+
142
+ ### Added
143
+ - **LRU cache in `PreparedDataSource`** (Phase 2.2): an
144
+ `OrderedDict`-backed LRU cache keyed on
145
+ `(track_id, target_id, chrom, start, end)` so repeat
146
+ `get_transcript_data` / `plot` calls skip BED parse and BAM
147
+ coverage work. `cache_maxsize` defaults to 128; configure via
148
+ `DrViz(cache_maxsize=N)`.
149
+ - **`ReusableParser` lifecycle lock** (Phase 2.2): `__enter__`,
150
+ `__exit__`, `close()`, and `clear_cache()` methods. `close()` is
151
+ idempotent and runs the mandated order: `clear_cache` first, then
152
+ `pool.shutdown(wait=True)`, then flip `_is_closed`. The data
153
+ source rejects post-close inserts with `RuntimeError`.
154
+ - **`DrViz` context manager**: `with DrViz() as parser:` returns the
155
+ prepared `ReusableParser` (not the builder) so the cache and pool
156
+ are scoped to the block.
157
+ - **Adaptive `ProcessPool` threshold** (Phase 2.2): the build-stage
158
+ process pool is only opened when the genomic-BED record estimate
159
+ exceeds `adaptive_threshold` (default 20_000). Under the threshold
160
+ the path is pure sequential and no pool is allocated. Configure via
161
+ `DrViz(adaptive_threshold=N)`.
162
+ - **BAM dtype compression**: cached coverage payloads are stored as
163
+ `np.uint32` for `aggregate_method='sum'` and `np.float32` for
164
+ `aggregate_method='mean'`.
165
+ - `benchmarks/bench_cache_vs_no_cache.py`: stdlib-only benchmark
166
+ measuring the cache hit/miss speedup across `small/medium/large`
167
+ BED workloads.
168
+
169
+ ### Changed
170
+ - `DrViz.build()` is now cached: successive calls with the same
171
+ builder state return the same `ReusableParser` instance so the
172
+ LRU cache survives. Any mutating method (`load_gtf`,
173
+ `add_bed_track`, `add_bam_track`) calls `_mark_dirty()` and forces
174
+ a recompile on the next `build()`.
175
+ - `DrViz.plot()` and `DrViz.get_transcript_data()` route through
176
+ `self.build()` instead of constructing a fresh `ReusableParser`
177
+ per call.
178
+ - `drvizer._track_build.prepare_tracks_parallel` accepts
179
+ `adaptive_threshold` and `data_source` keyword arguments; the
180
+ ProcessPool executor is owned by the data source so its lifecycle
181
+ is locked to the `ReusableParser.close()` path.
182
+ - The build-stage outer `ThreadPoolExecutor` wrapper around
183
+ process + serial paths has been removed; the two paths now run
184
+ sequentially within `prepare_tracks_parallel` (process first, then
185
+ serial), and the pool is gated by the threshold.
186
+
187
+ ### Phase 3 — Cython/Python parity & hardening
188
+
189
+ #### Added
190
+ - **`ParallelCoverageError` re-exported from `drvizer` package**:
191
+ callers can now `from drvizer import ParallelCoverageError` for
192
+ typed error handling around parallel coverage failures.
193
+ - **`tests/fixtures/crlf.bed`**: Windows-style CRLF-terminated BED
194
+ fixture (PARSER-011) exercising comment + blank + multi-chrom
195
+ records.
196
+ - **`GTFParser._chunk_parse_degradation` counter**: incremented each
197
+ time the Cython chunk parser falls back to per-row Python parsing
198
+ (P1-3).
199
+ - **Five new test files** (`tests/test_phase_3_*.py`) covering CRLF
200
+ BED parity, GTF chunk degradation, BAM projection kernel
201
+ consistency, `cpu_count()` None guard, and `ParallelCoverageError`
202
+ exception chain. Two additional tests (`test_close_failure_emits_*`,
203
+ `test_get_transcript_data_none_coord_safe`) extend the Phase 2.2
204
+ LRU-cache suite.
205
+
206
+ #### Changed
207
+ - **P1-8** (`_parallel.py`): the `Pool.imap_unordered` worker now
208
+ catches any `Exception`, re-raises as `ParallelCoverageError` chained
209
+ from the worker exception (`raise ... from exc`), and includes the
210
+ failing BAM path in the message. The original traceback is preserved
211
+ on `__cause__` so corrupt BAM / missing `.bai` failures stay
212
+ diagnosable.
213
+ - **P1-3** (`gtf_parser.py`, `_cython_gtf.pyx`): pattern (a) of the
214
+ Cython chunk try/except decision is implemented. The `_process_chunk`
215
+ wrapper calls `_parse_gtf_chunk_impl` inside `try/except`; on
216
+ chunk-level failure it falls back to per-row Python parsing with
217
+ per-row `try/except (ValueError, TypeError)`, increments
218
+ `self._chunk_parse_degradation`, and emits a single
219
+ `RuntimeWarning` at `parse_gtf()` return time. The Cython kernel
220
+ remains a pure best-effort transform with no C-level try/except.
221
+ - **P1-6** (`bam_parser.py`): extracted the
222
+ `_project_aligned_blocks_to_transcript(transcript_id, blocks,
223
+ gtf_parser, target_chrom, region_start, region_len, coverage)`
224
+ pure-numeric kernel; both the single-transcript and the batch
225
+ transcript-coverage paths now route through this single helper so
226
+ spliced-read / int64 / `np.add.at` fixes live in one place.
227
+ - **P1-5** (`_parallel.py`, `_track_build.py`): `cpu_count()` is now
228
+ guarded with `try/except NotImplementedError` plus a
229
+ `isinstance(int)` fallback (and `os.cpu_count() or 1` in
230
+ `_track_build.py`). The original audit's `min(N, None, 32)` →
231
+ `TypeError` failure mode is impossible.
232
+ - **PARSER-011** (`_cython_bed.pyx`, `bed_parser.py`): BED open mode
233
+ unified. Cython path uses `raw_line.rstrip(b'\r\n')`; Python
234
+ fallback opens in `'rb'` and decodes UTF-8 with `errors='replace'`
235
+ before `rstrip('\r\n')` and tab-splitting. CRLF and LF BEDs now
236
+ produce byte-identical records across both paths.
237
+ - **Cleanup B** (`api.py`): the three `try/except Exception: pass`
238
+ blocks in `ReusableParser.close()` and the `__del__` finalizer now
239
+ surface failures as `warnings.warn(..., ResourceWarning,
240
+ stacklevel=2)` instead of silently swallowing them.
241
+ - **`PreparedDataSource._fetch_coverage_in_region` /
242
+ `_fetch_grouped_anno_in_region`** (`api.py`): the cache key now
243
+ preserves `None` for missing coordinates instead of raising
244
+ `TypeError` from `int(None)`; repeated calls with `start=None,
245
+ end=None` are now cache-hits.
246
+
247
+ ## [1.0.0] - prior
248
+
249
+ Pre-Phase 2.2 release.
drvizer-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 drVizer Development Team
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,5 @@
1
+ include src/drvizer/*.pyx
2
+ include LICENSE
3
+ include README.md
4
+ include CHANGELOG.md
5
+ include pyproject.toml
drvizer-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,330 @@
1
+ Metadata-Version: 2.4
2
+ Name: drvizer
3
+ Version: 0.1.1
4
+ Summary: A Python tool for parsing GTF/BED files and visualizing gene transcript structures
5
+ Author-email: x1han <han_xi@gzlab.ac.cn>
6
+ License: MIT
7
+ Project-URL: Bug Reports, https://github.com/x1han/drVizer/issues
8
+ Project-URL: Source, https://github.com/x1han/drVizer
9
+ Project-URL: Documentation, https://github.com/x1han/drVizer#readme
10
+ Keywords: bioinformatics,genomics,transcriptomics,gtf,bed,visualization
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
21
+ Classifier: Topic :: Scientific/Engineering :: Visualization
22
+ Requires-Python: >=3.8
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: pandas>=1.3.0
26
+ Requires-Dist: matplotlib>=3.5.0
27
+ Requires-Dist: numpy>=1.21.0
28
+ Provides-Extra: bam
29
+ Requires-Dist: pysam; extra == "bam"
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest; extra == "test"
32
+ Dynamic: license-file
33
+
34
+ # drVizer
35
+
36
+ `drVizer` is a Python library for building transcript-structure figures from GTF models, BED annotations, and BAM coverage tracks.
37
+
38
+ It is designed for direct RNA sequencing and transcriptomics workflows that need reusable, scriptable, publication-ready matplotlib figures.
39
+
40
+ ## Breaking Fix in v0.1.0
41
+
42
+ - **`ParallelCoverageError` surfaces the BAM path on worker failure** (P1-8, merged in Phase 3): worker exceptions from `compute_region_coverage` are chained via `raise ... from exc` and include the failing `bam_path` in the message so corrupt BAM / missing `.bai` / `KeyError` / `BrokenPipeError` failures stay diagnosable.
43
+ - **GTF chunk-degradation warning now gated on `N > 0`** (P1-3): the end-of-parse `RuntimeWarning` only fires when `_chunk_parse_degradation > 0`. Clean parses emit no warning.
44
+ - **`pyproject.toml` PEP 621 `[project]` section** (PKG-002): `name=drvizer`, `version=0.1.0`, README, MIT, requires-python `>=3.8`, classifiers (Python 3.8 - 3.12 + Development Status 4 Beta), `dependencies` from `requirements.txt`, optional `[bam]` and `[test]` extras.
45
+ - **`setup.py` skeleton** (P1-16): `setup.py` is now a thin C-extension compile-only wrapper; all metadata truth lives in `pyproject.toml [project]`. `extra_compile_args=["-std=c99"]` is preserved on every Cython `Extension`.
46
+ - **Version bump `1.0.0` -> `0.1.0`** (PKG-002): first public release tag.
47
+ - **Hygiene**: dead `BEDParser` import removed from `src/drvizer/api.py`; dead `defaultdict` import removed from `src/drvizer/utils.py`; stale `docs/superpowers/` rule removed from `.gitignore`.
48
+
49
+ ## Features
50
+
51
+ - Build transcript structure figures from one or more GTF files.
52
+ - Overlay BED annotation tracks as interval blocks or numeric score bars.
53
+ - Add BAM-derived coverage tracks with `sum` or `mean` aggregation.
54
+ - Reuse parsed state across many plotting calls through a builder-style API.
55
+ - Project transcript-coordinate BED and BAM inputs back into genomic space.
56
+ - Split transcript-coordinate tracks into transcript-specific subtracks.
57
+ - Share automatic y-axis scaling across matched numeric tracks with `y_axis_group`.
58
+ - Export matplotlib figures for publication workflows.
59
+
60
+ ## Installation
61
+
62
+ Editable install for local development:
63
+
64
+ ```bash
65
+ git clone https://github.com/x1han/drVizer.git
66
+ cd drVizer
67
+ pip install -e .
68
+ ```
69
+
70
+ Regular local install:
71
+
72
+ ```bash
73
+ pip install .
74
+ ```
75
+
76
+ BAM coverage tracks require `pysam`:
77
+
78
+ ```bash
79
+ pip install pysam
80
+ ```
81
+
82
+ ## Quick start
83
+
84
+ The public workflow is centered on `DrViz`:
85
+
86
+ ```python
87
+ from drvizer import DrViz
88
+
89
+ parser = (
90
+ DrViz()
91
+ .load_gtf("genes.gtf")
92
+ .add_bed_track("repeats.bed", label="TE")
93
+ .add_bam_track("reads.bam", label="Coverage")
94
+ .build()
95
+ )
96
+
97
+ fig = parser.plot("TP53", show=False)
98
+ ```
99
+
100
+ `build()` freezes the configured GTF and tracks into a reusable parser, so plotting many genes does not repeat setup work.
101
+
102
+ ### Reusable parser with auto-cleanup (Phase 2.2)
103
+
104
+ `DrViz` is also a context manager; using it as such hands back the
105
+ prepared `ReusableParser` and tears down the cache + lazy
106
+ `ProcessPool` on exit:
107
+
108
+ ```python
109
+ from drvizer import DrViz
110
+
111
+ with DrViz().load_gtf("genes.gtf").add_bed_track("repeats.bed", label="TE") as parser:
112
+ for gene in ("TP53", "BRCA1", "MYC"):
113
+ fig = parser.plot(gene, show=False)
114
+ # cache cleared, pool shut down with wait=True
115
+ ```
116
+
117
+ Cache and pool configuration:
118
+
119
+ - `DrViz(cache_maxsize=N)` — LRU cache capacity (default 128).
120
+ Caches are per-parser-instance; each new `build()` constructs a
121
+ fresh cache.
122
+ - `DrViz(adaptive_threshold=N)` — minimum total BED record count
123
+ that opens a `ProcessPool` during build (default 20_000). Below
124
+ the threshold, build is purely sequential.
125
+
126
+ ## Core API
127
+
128
+ ### `load_gtf(...)`
129
+
130
+ Load one or more GTF files. GTF parsing is required before tracks can be built.
131
+
132
+ ```python
133
+ .load_gtf("genes.gtf")
134
+ .load_gtf(["reference.gtf", "novel_transcripts.gtf"])
135
+ ```
136
+
137
+ | Parameter | Type | Default | Description |
138
+ | --- | --- | --- | --- |
139
+ | `gtf_files` | `str` or `list[str]` | required | Path to one GTF file or ordered list of GTF files. Exon/CDS features are parsed for transcript rendering. Loading a new GTF resets previously added tracks because tracks depend on the active GTF model. |
140
+
141
+ drVizer uses exon/CDS features and supports gene ID, gene name, or transcript ID lookup during plotting.
142
+
143
+ ### `add_bed_track(...)`
144
+
145
+ Add BED-backed annotation tracks.
146
+
147
+ ```python
148
+ .add_bed_track("repeats.bed", label="TE", color="tomato")
149
+ ```
150
+
151
+ | Parameter | Type | Default | Description |
152
+ | --- | --- | --- | --- |
153
+ | `bed_files` | `str` or `list[str]` | required | BED file path or multiple BED files grouped into one logical track. BED3 and wider BED records are supported. |
154
+ | `label` | `str` or `None` | auto `Track_N` | Track label shown on the left side of the figure. Duplicate labels are made unique in registration order. |
155
+ | `color` | `str` or `list[str]` | `"orange"` | Matplotlib color for the track. When multiple BED files are passed, a list gives per-file colors. |
156
+ | `alpha` | `float` or `list[float]` | `0.8` | Track transparency from `0` to `1`. When multiple BED files are passed, a list gives per-file alpha values. |
157
+ | `parser_type` | `"distribution"` or `"score"` | `"distribution"` | Rendering mode. `distribution` draws interval blocks; `score` draws numeric BED scores as bars. |
158
+ | `y_axis_range` | `float` or `None` | `None` | Fixed y-axis maximum for `score` tracks. Takes precedence over automatic scaling and `y_axis_group`. |
159
+ | `y_axis_group` | `str` or `None` | `None` | Shared automatic y-axis scaling group for numeric BED `score` tracks. Invalid for `distribution` tracks. |
160
+ | `transcript_coord` | `bool` | `False` | Treat BED `chrom` field as transcript ID and project transcript coordinates back to genomic coordinates through the loaded GTF. |
161
+ | `layer_order` | `None`, `"ascending"`, or `"descending"` | `"ascending"` | Controls drawing order for layered BED elements. |
162
+ | `split_by_transcript` | `None`, `"nc"`, or `"cn"` | `None` | Split transcript-coordinate BED data into transcript-specific subtracks. Requires `transcript_coord=True`. |
163
+
164
+ Score tracks can share automatic y-axis scaling:
165
+
166
+ ```python
167
+ .add_bed_track("control_m6a.bed", label="Control m6A", parser_type="score", y_axis_group="m6A")
168
+ .add_bed_track("treated_m6a.bed", label="Treated m6A", parser_type="score", y_axis_group="m6A")
169
+ ```
170
+
171
+ ### `add_bam_track(...)`
172
+
173
+ Add BAM-backed coverage tracks.
174
+
175
+ ```python
176
+ .add_bam_track("reads.bam", label="Coverage", color="steelblue")
177
+ ```
178
+
179
+ | Parameter | Type | Default | Description |
180
+ | --- | --- | --- | --- |
181
+ | `bam_files` | `str` or `list[str]` | required | BAM file path or multiple BAM files grouped into one logical coverage track. BAM support requires `pysam`. |
182
+ | `label` | `str` | `"Coverage"` | Track label shown on the left side of the figure. Duplicate labels are made unique in registration order. |
183
+ | `color` | `str` or `list[str]` | `"steelblue"` | Matplotlib color for coverage. When multiple BAM files are rendered as per-file series, a list gives per-file colors. |
184
+ | `alpha` | `float` or `list[float]` | `0.6` | Coverage transparency from `0` to `1`. When multiple BAM files are rendered as per-file series, a list gives per-file alpha values. |
185
+ | `aggregate_method` | `"sum"` or `"mean"` | `"sum"` | Combines multiple BAM files by summed coverage or average coverage. |
186
+ | `y_axis_range` | `float` or `None` | `None` | Fixed coverage y-axis maximum. Takes precedence over automatic scaling and `y_axis_group`. |
187
+ | `y_axis_group` | `str` or `None` | `None` | Shared automatic y-axis scaling group for numeric coverage tracks. |
188
+ | `transcript_coord` | `bool` | `False` | Treat BAM reference names as transcript IDs and project coverage back to genomic coordinates through the loaded GTF. |
189
+ | `layer_order` | `None`, `"ascending"`, or `"descending"` | `"ascending"` | Controls drawing order for per-file coverage series where individual series are rendered. |
190
+ | `split_by_transcript` | `None`, `"nc"`, or `"cn"` | `None` | Split transcript-coordinate BAM coverage into transcript-specific subtracks. Requires `transcript_coord=True`. |
191
+
192
+ Multiple BAM files can be combined:
193
+
194
+ ```python
195
+ .add_bam_track(
196
+ ["sample_a.bam", "sample_b.bam"],
197
+ label="Reads",
198
+ aggregate_method="mean",
199
+ )
200
+ ```
201
+
202
+ Supported aggregation modes:
203
+
204
+ - `aggregate_method="sum"`: sum coverage across BAM files.
205
+ - `aggregate_method="mean"`: average coverage across BAM files.
206
+
207
+ ### `build()` and `plot(...)`
208
+
209
+ Use `build()` when plotting multiple genes from the same inputs:
210
+
211
+ ```python
212
+ parser = (
213
+ DrViz()
214
+ .load_gtf("genes.gtf")
215
+ .add_bed_track("repeats.bed", label="TE")
216
+ .add_bam_track("reads.bam", label="Coverage")
217
+ .build()
218
+ )
219
+
220
+ fig1 = parser.plot("TP53", show=False)
221
+ fig2 = parser.plot("MYC", show=False)
222
+ ```
223
+
224
+ Use one-shot `plot(...)` for quick figures:
225
+
226
+ ```python
227
+ fig = (
228
+ DrViz()
229
+ .load_gtf("genes.gtf")
230
+ .add_bed_track("repeats.bed", label="TE")
231
+ .plot("TP53", show=False)
232
+ )
233
+ ```
234
+
235
+ `ReusableParser.plot(...)` accepts these parameters:
236
+
237
+ | Parameter | Type | Default | Description |
238
+ | --- | --- | --- | --- |
239
+ | `gene` | `str` or `list[str]` | required | Gene ID, gene name, transcript ID, or same-chromosome list of identifiers to plot. |
240
+ | `transcript_to_show` | `str`, `list[str]`, or `None` | `None` | Restrict output to one transcript or selected transcripts from the requested gene. |
241
+ | `output` | `str` or `None` | `None` | Optional output path. When set, figure is saved with matplotlib using tight bounding box and 300 DPI. |
242
+ | `figsize` | `tuple` or `None` | `None` | Explicit final figure size in inches. Overrides automatically computed size. |
243
+ | `figfact` | `tuple` or `None` | `None` | Multiplicative width/height factor applied to automatically computed figure size. Ignored when `figsize` is set. |
244
+ | `show` | `bool` | `True` | Display figure through matplotlib. If `False`, figure is closed after creation but still returned. |
245
+ | `close` | `bool` | `False` | Close figure after showing it. Applies when `show=True`. |
246
+ | `**kwargs` | any | | Forwarded to the visualizer, including transcript sorting and layout options. |
247
+
248
+ ## Transcript-coordinate workflows
249
+
250
+ Set `transcript_coord=True` when BED or BAM records use transcript IDs instead of genomic chromosome names.
251
+
252
+ ```python
253
+ .add_bed_track("mods.transcript.bed", transcript_coord=True)
254
+ .add_bam_track("reads.transcript.bam", transcript_coord=True)
255
+ ```
256
+
257
+ drVizer projects transcript-coordinate intervals and coverage back into genomic plotting space through the loaded GTF model.
258
+
259
+ ## Split transcript tracks
260
+
261
+ Transcript-coordinate BED and BAM tracks can be split by transcript with `split_by_transcript`:
262
+
263
+ ```python
264
+ split_by_transcript="nc" # transcript-major order
265
+ split_by_transcript="cn" # track-major order
266
+ ```
267
+
268
+ Supported modes:
269
+
270
+ - `None`: keep combined track behavior.
271
+ - `"nc"`: transcript-major ordering; each transcript groups its split tracks together.
272
+ - `"cn"`: track-major ordering; each track groups its transcript-specific subtracks together.
273
+
274
+ Split transcript tracks require `transcript_coord=True` and do not support multi-gene plotting.
275
+
276
+ ## Numeric y-axis control
277
+
278
+ Numeric tracks support two y-axis controls:
279
+
280
+ - `y_axis_range`: manually fix the y-axis maximum.
281
+ - `y_axis_group`: share automatic y-axis scaling across numeric tracks with the same group name.
282
+
283
+ `y_axis_range` takes precedence over `y_axis_group`.
284
+
285
+ ```python
286
+ parser = (
287
+ DrViz()
288
+ .load_gtf("genes.gtf")
289
+ .add_bam_track("control.bam", label="Control", y_axis_group="reads")
290
+ .add_bam_track("treated.bam", label="Treated", y_axis_group="reads")
291
+ .add_bed_track("m6a.bed", label="m6A", parser_type="score", y_axis_group="mod_score")
292
+ .build()
293
+ )
294
+ ```
295
+
296
+ ## Output behavior
297
+
298
+ drVizer renders matplotlib figures and returns the generated `Figure` object from `plot(...)`.
299
+
300
+ ```python
301
+ fig = parser.plot("TP53", show=False, output="tp53.pdf")
302
+ fig.set_size_inches((10, 6))
303
+ fig.savefig("tp53.png", dpi=300, bbox_inches="tight")
304
+ ```
305
+
306
+ ## Documentation
307
+
308
+ Detailed docs live in `docs/`:
309
+
310
+ - [Project overview / PDR](docs/project-overview-pdr.md)
311
+ - [Codebase summary](docs/codebase-summary.md)
312
+ - [System architecture](docs/system-architecture.md)
313
+ - [API reference](docs/api-reference.md)
314
+ - [Testing guide](docs/testing-guide.md)
315
+ - [Code standards](docs/code-standards.md)
316
+ - [Changelog](docs/changelog.md)
317
+
318
+ ## Testing
319
+
320
+ Use the project DRS environment for validation:
321
+
322
+ ```bash
323
+ /datf/hanxi/software/miniconda3/envs/DRS/bin/python -m pytest -q
324
+ ```
325
+
326
+ ## Public entry point
327
+
328
+ ```python
329
+ from drvizer import DrViz
330
+ ```