figwig 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.
@@ -0,0 +1,35 @@
1
+ # Never distributed -- personal agent instructions and working notes
2
+ CLAUDE.md
3
+ claude_comments/
4
+
5
+ *.swp
6
+ *.swo
7
+ *~
8
+
9
+ *.py[cod]
10
+ __pycache__/
11
+ .cache
12
+ .pytest_cache
13
+ .ruff_cache
14
+ .coverage
15
+ .ipynb_checkpoints/
16
+ .venv/
17
+
18
+ # Setup and build
19
+ docs/_build/
20
+ docs/_*
21
+ *.egg-info/
22
+ dist/
23
+ build/
24
+ MANIFEST
25
+
26
+ # OS-generated files
27
+ .DS_Store
28
+ .Spotlight-V100
29
+ .Trashes
30
+ ehthumbs.db
31
+ Thumbs.db
32
+
33
+ # Model checkpoints and large artifacts
34
+ *.torch
35
+ *.log
figwig-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jacob Schreiber
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.
figwig-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,361 @@
1
+ Metadata-Version: 2.5
2
+ Name: figwig
3
+ Version: 0.1.0
4
+ Summary: A fast, multithreaded reader and writer of bigWig files, into and out of numpy.
5
+ Project-URL: Homepage, https://github.com/jmschrei/figwig
6
+ Project-URL: Repository, https://github.com/jmschrei/figwig
7
+ Project-URL: Issues, https://github.com/jmschrei/figwig/issues
8
+ Project-URL: Documentation, https://figwig.readthedocs.io
9
+ Author-email: Jacob Schreiber <jmschreiber91@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: bigwig,bioinformatics,genomics,machine-learning
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: Microsoft :: Windows
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: numba>=0.58
27
+ Requires-Dist: numpy>=1.23
28
+ Provides-Extra: bam2bw
29
+ Requires-Dist: biopython>=1.73; extra == 'bam2bw'
30
+ Requires-Dist: deflate>=0.9; extra == 'bam2bw'
31
+ Requires-Dist: isal>=1.0; extra == 'bam2bw'
32
+ Requires-Dist: pyfaidx>=0.8; extra == 'bam2bw'
33
+ Requires-Dist: pysam>=0.22; extra == 'bam2bw'
34
+ Requires-Dist: tqdm>=4.60; extra == 'bam2bw'
35
+ Provides-Extra: dev
36
+ Requires-Dist: biopython>=1.73; extra == 'dev'
37
+ Requires-Dist: build>=1.0; extra == 'dev'
38
+ Requires-Dist: deflate>=0.9; extra == 'dev'
39
+ Requires-Dist: isal>=1.0; extra == 'dev'
40
+ Requires-Dist: pybigtools<0.3,>=0.2.5; extra == 'dev'
41
+ Requires-Dist: pybigwig>=0.3.26; (sys_platform != 'win32') and extra == 'dev'
42
+ Requires-Dist: pyfaidx>=0.8; extra == 'dev'
43
+ Requires-Dist: pysam>=0.22; (sys_platform != 'win32') and extra == 'dev'
44
+ Requires-Dist: pytest>=7.0; extra == 'dev'
45
+ Requires-Dist: ruff>=0.4; extra == 'dev'
46
+ Requires-Dist: tqdm>=4.60; extra == 'dev'
47
+ Requires-Dist: twine>=4.0; extra == 'dev'
48
+ Provides-Extra: docs
49
+ Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'docs'
50
+ Requires-Dist: sphinx>=7.0; extra == 'docs'
51
+ Provides-Extra: fast
52
+ Requires-Dist: deflate>=0.9; extra == 'fast'
53
+ Description-Content-Type: text/markdown
54
+
55
+ # figwig
56
+
57
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/figwig?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=ORANGE&left_text=downloads)](https://pepy.tech/projects/figwig) [![Unit Tests](https://github.com/jmschrei/figwig/actions/workflows/python-package.yml/badge.svg)](https://github.com/jmschrei/figwig/actions/workflows/python-package.yml) [![Documentation Status](https://readthedocs.org/projects/figwig/badge/?version=latest)](https://figwig.readthedocs.io/en/latest/?badge=latest)
58
+
59
+ [[docs](https://figwig.readthedocs.io/en/latest/index.html)][[release notes](https://figwig.readthedocs.io/en/latest/whats_new.html)]
60
+
61
+ A fast, multithreaded reader and writer of bigWig files, into and out of
62
+ numpy.
63
+
64
+ Loading training data for a genomics model means reading the signal under tens
65
+ or hundreds of thousands of windows: 1,000 bp around every peak and background
66
+ region of an experiment, say. figwig reads all of them in one call, straight
67
+ into a float32 array, from one bigWig or from several at once, and writes
68
+ bigWigs from the same windows, such as a model's predictions, or from
69
+ intervals. The common readers make one call per window, and each call holds
70
+ Python's GIL, so adding threads does not help them. figwig decompresses and
71
+ compresses with zlib or libdeflate and decodes with numba, all without the
72
+ GIL, so its threads run in parallel.
73
+
74
+ Its command, `figwig bam2bw`, turns SAM/BAM files of reads, or BED/tsv files
75
+ of fragments, into bigWigs of per-base counts. It is
76
+ [bam2bw](https://github.com/jmschrei/bam2bw) with the same arguments, outputs
77
+ and messages, with faster readers. figwig depends only on numpy and numba.
78
+
79
+ | | figwig | fastest other tool |
80
+ |---|---|---|
81
+ | Read 167,750 windows of 1,000 bp from an ATAC-seq bigWig | **0.135 s** on 8 threads | 1.25 s, pybigtools |
82
+ | Write 15.9 million per-base counts | **0.18 s** on 8 threads | 4.20 s, pyBigWig |
83
+ | Write 21.2 million intervals, with zoom levels | **0.55 s** on 8 threads | 5.47 s, pybigtools |
84
+ | Convert a 2.4 GB ATAC-seq BAM to two stranded bigWigs | **4.65 s**, 910 MB peak, at `-p 2` | 58.48 s, 3,039 MB peak, bam2bw 0.5.1 |
85
+
86
+ Measured on a 2x AMD EPYC 9575F with Python 3.13 and numba 0.68. The
87
+ [benchmarks page](https://figwig.readthedocs.io/en/latest/benchmarks.html) has
88
+ the full setup.
89
+
90
+ <details>
91
+ <summary><b>More numbers and plots</b></summary>
92
+
93
+ #### Reading
94
+
95
+ 167,750 windows of 1,000 bp centred on the fold-0 training peaks and negatives
96
+ of ENCODE ATAC-seq experiment ENCSR123WME, read from the ATAC-seq bigWig
97
+ ENCFF830RWF (238 MB) and the DNase-seq bigWig ENCFF989SAK (645 MB) on tmpfs,
98
+ sorted by position, file opening included; median of three runs. The other
99
+ libraries' 8-thread runs split the windows into 8 chunks on a thread pool.
100
+ Every library returned the same values, once pyBigWig's NaN for bases without
101
+ an interval was set to 0.
102
+
103
+ | library | ATAC, 1 thread | ATAC, 8 threads | DNase, 1 thread | DNase, 8 threads |
104
+ |---|---|---|---|---|
105
+ | pybigtools 0.2.5, `values()` per window | 1.25 s | 1.42 s | 1.98 s | 2.21 s |
106
+ | pyBigWig 0.3.26, `values()` per window | 3.95 s | 4.08 s | 4.50 s | 4.64 s |
107
+ | pybbi 0.4.2, `stackup` | 9.61 s | 9.48 s | 10.75 s | 10.63 s |
108
+ | figwig 0.1.0 | 0.81 s | **0.135 s** | 1.85 s | **0.277 s** |
109
+
110
+ #### Writing
111
+
112
+ The plus-strand 5' ends of ENCODE ATAC-seq BAM ENCFF877LRY, as counts at
113
+ 15,875,960 single bases, and the fold-change signal ENCFF932UQM, as 21,212,477
114
+ intervals, written from memory onto tmpfs at compression level 6, opening and
115
+ closing included; median of three runs. pybigtools always writes zoom levels.
116
+
117
+ | writer | counts | counts, zoom levels | signal | signal, zoom levels |
118
+ |---|---|---|---|---|
119
+ | pyBigWig 0.3.26 | 4.20 s | 12.80 s | 7.41 s | 10.48 s |
120
+ | pybigtools 0.2.5 | | 4.45 s | | 5.47 s |
121
+ | figwig, zlib, 1 thread | 4.25 s | 12.09 s | 6.21 s | 8.10 s |
122
+ | figwig, zlib, 8 threads | 0.53 s | 1.63 s | 0.78 s | 1.09 s |
123
+ | figwig, libdeflate, 1 thread | 1.07 s | 4.85 s | 1.80 s | 3.27 s |
124
+ | figwig, libdeflate, 8 threads | **0.18 s** | **0.75 s** | **0.30 s** | **0.55 s** |
125
+
126
+ figwig's files were the size of pyBigWig's. For the counts, its peak memory
127
+ was above pyBigWig's without zoom levels, 554 MB against 354 MB on one thread,
128
+ and below it with them, 739 MB on 8 threads against 1,089 MB.
129
+
130
+ #### figwig bam2bw
131
+
132
+ ENCODE ATAC-seq BAM ENCFF877LRY (2.4 GB) at default flags, and 18.9 million
133
+ scATAC-seq fragments with `-f -u`, from the page cache; figwig's median of
134
+ five runs at `-p 2` and one at `-p 8`, and one run of bam2bw, which reads one
135
+ file in one process whatever `-p` is.
136
+
137
+ | | BAM | fragments | peak memory, BAM | peak memory, fragments |
138
+ |---|---|---|---|---|
139
+ | bam2bw 0.5.1, `-p 2` | 58.48 s | 28.45 s | 3,039 MB | 2,866 MB |
140
+ | figwig bam2bw, `-p 2` | **4.65 s** | **1.73 s** | 910 MB | 1,061 MB |
141
+ | figwig bam2bw, `-p 8` | **1.82 s** | **1.31 s** | 949 MB | 1,180 MB |
142
+
143
+ Both also ran on every one of 591 public BAM, SAM, BED and TSV test files that
144
+ bam2bw takes, from 3 records to a 10 GB ATAC-seq BAM, under 5 to 9 sets of
145
+ flags each, timed once, 16 at a time. In all 3,781 runs that bam2bw
146
+ completed, figwig bam2bw gave the same exit code, messages and decoded
147
+ entries. Where bam2bw took a second or more, figwig bam2bw was a median 3.2
148
+ times faster; where bam2bw took under half a second, figwig bam2bw took a
149
+ median 0.12 s longer, mostly importing numba.
150
+
151
+ ![Wall time of figwig bam2bw against bam2bw 0.5.1, log scales](docs/figures/bam2bw-timings.png)
152
+
153
+ ![Peak and mean memory of figwig bam2bw against bam2bw 0.5.1, log scales](docs/figures/bam2bw-memory.png)
154
+
155
+ The memory runs were made before figwig bam2bw dropped the keys it had
156
+ counted while still reading a BAM. On the 10 GB BAM at `-p 1` its peak is now
157
+ 2.9 GB, against bam2bw's 11.6 GB, and 4.9 GB with `-u -f`, against 20.2 GB.
158
+
159
+ #### Threads
160
+
161
+ The same reads, writes and conversions at 1 to 32 threads, one run at a time,
162
+ median of three. Reading gained up to 32 threads, writing with libdeflate up
163
+ to 8 or 16, and figwig bam2bw up to about 8.
164
+
165
+ ![Wall time against threads for reading, writing and figwig bam2bw, log scales](docs/figures/thread-scaling.png)
166
+
167
+ </details>
168
+
169
+ ## Installation
170
+
171
+ ```bash
172
+ pip install figwig
173
+ ```
174
+
175
+ Or, with [uv](https://docs.astral.sh/uv/):
176
+
177
+ ```bash
178
+ uv add figwig
179
+ ```
180
+
181
+ It needs Python 3.10 or later, numpy 1.23 or later, and numba 0.58 or later.
182
+ Optional extras:
183
+
184
+ - `figwig[fast]` adds the `deflate` package, which makes writing several times
185
+ faster.
186
+ - `figwig[bam2bw]` adds pysam, pyfaidx, biopython, tqdm, isal and deflate,
187
+ which `figwig bam2bw` needs. `figwig bam2bw` runs on Linux and macOS, since
188
+ pysam does not support Windows.
189
+
190
+ ```bash
191
+ pip install "figwig[fast]"
192
+ pip install "figwig[bam2bw]"
193
+ ```
194
+
195
+ ### Development install
196
+
197
+ ```bash
198
+ git clone https://github.com/jmschrei/figwig.git
199
+ cd figwig
200
+ uv sync --extra dev
201
+ uv run pytest
202
+ ```
203
+
204
+ ## Claude Code Skill
205
+
206
+ figwig ships a [Claude Code](https://claude.com/claude-code) skill that
207
+ teaches a coding agent to use figwig in any project: reading windows,
208
+ writing predictions, intervals and counts, converting reads with
209
+ `figwig bam2bw`, the rules a write follows, what each error means, and how
210
+ much threads gain. Install it into `~/.claude/skills/figwig` with:
211
+
212
+ ```bash
213
+ figwig install-skill
214
+ ```
215
+
216
+ After upgrading figwig, run `figwig install-skill --force` to replace the
217
+ installed copy, which otherwise raises `FileExistsError`. `-d DIRECTORY`
218
+ installs into another skills directory, and `--symlink` links to the copy
219
+ inside the installed package instead of copying it.
220
+
221
+ ## Python API
222
+
223
+ The examples read two public ENCODE bigWigs: the ATAC-seq signal of HG02943
224
+ (ENCFF830RWF, 238 MB) and the DNase-seq signal of dorsolateral prefrontal
225
+ cortex (ENCFF989SAK, 645 MB).
226
+
227
+ ```bash
228
+ wget https://www.encodeproject.org/files/ENCFF830RWF/@@download/ENCFF830RWF.bigWig
229
+ wget https://www.encodeproject.org/files/ENCFF989SAK/@@download/ENCFF989SAK.bigWig
230
+ ```
231
+
232
+ #### Reading windows
233
+
234
+ ```python
235
+ import numpy
236
+ from figwig import BigWigReader
237
+
238
+ bw = BigWigReader("ENCFF830RWF.bigWig")
239
+ chroms = numpy.array(["chr1", "chr1", "chr2"])
240
+ starts = numpy.array([1_000_000, 2_500_000, 300_000])
241
+ y = bw.read(chroms, starts, width=1000, n_jobs=8)
242
+ print(y.shape, y.dtype)
243
+ # (3, 1000) float32
244
+ print(y.sum(axis=1))
245
+ # [116. 43. 10.]
246
+ ```
247
+
248
+ Window `j` covers `[starts[j], starts[j] + width)` on `chroms[j]`, 0-based as
249
+ in BED files, and is written into row `j`, whatever order the windows are
250
+ given in. A base that no interval covers is `missing`, 0 unless given, and a
251
+ base past the end of its chromosome is NaN. A `BigWigReader` reads the file's
252
+ index once and keeps it, and can be pickled to DataLoader workers.
253
+
254
+ #### Reading several bigWigs into one array
255
+
256
+ ```python
257
+ import numpy
258
+ from figwig import read_bigwig
259
+
260
+ chroms = numpy.array(["chr1", "chr1", "chr2"])
261
+ starts = numpy.array([1_000_000, 2_500_000, 300_000])
262
+ y = read_bigwig(["ENCFF830RWF.bigWig", "ENCFF989SAK.bigWig"], chroms, starts,
263
+ width=1000)
264
+ print(y.shape)
265
+ # (3, 2, 1000)
266
+ ```
267
+
268
+ Channel `i` holds the values from the `i`-th file, in the
269
+ `(batch, channels, length)` layout that sequence models take, and every
270
+ file's work shares one pool of threads.
271
+
272
+ #### Writing windows
273
+
274
+ ```python
275
+ import numpy
276
+ from figwig import read_bigwig
277
+ from figwig import write_bigwig
278
+
279
+ chrom_sizes = {"chr1": 248_956_422, "chr2": 242_193_529}
280
+ chroms = numpy.array(["chr2", "chr1", "chr1"])
281
+ starts = numpy.array([300_000, 2_500_000, 1_000_000])
282
+ y_hat = numpy.random.default_rng(0).random((3, 2, 1000), dtype=numpy.float32)
283
+
284
+ write_bigwig(["plus.bw", "minus.bw"], chrom_sizes, chroms, starts, y_hat)
285
+ y = read_bigwig(["plus.bw", "minus.bw"], chroms, starts, width=1000)
286
+ print(numpy.array_equal(y, y_hat))
287
+ # True
288
+ ```
289
+
290
+ `write_bigwig` takes what `read_bigwig` gives, such as a model's predictions
291
+ for the same windows. A base whose value is `missing`, 0.0 unless given, or
292
+ NaN is left out of the file, so that reading with the same `missing` gives
293
+ the values back.
294
+
295
+ #### Writing a batch at a time
296
+
297
+ ```python
298
+ from figwig import BigWigReader
299
+ from figwig import BigWigWriter
300
+
301
+ chrom_sizes = {"chr1": 248_956_422, "chr2": 242_193_529}
302
+ with BigWigWriter("example.bw", chrom_sizes) as writer:
303
+ writer.write("chr1", [1000, 2000], [0.5, 2.0], ends=[1500, 2100])
304
+ writer.write("chr1", [5000, 5003, 5004], [[3], [1], [1]])
305
+ writer.write("chr2", [10_000], [[0.0, 1.5, 1.5, 0.0, 2.5]])
306
+
307
+ y = BigWigReader("example.bw").read(["chr1", "chr2"], [5000, 10_000], width=6)
308
+ print(y)
309
+ # [[3. 0. 0. 1. 1. 0. ]
310
+ # [0. 1.5 1.5 0. 2.5 0. ]]
311
+ ```
312
+
313
+ `BigWigWriter` writes intervals (with `ends`), single bases (windows of width
314
+ 1) and windows as they come, so a track larger than memory can be written a
315
+ batch at a time. Each call's items come after the last call's, in the order
316
+ of `chrom_sizes`. The header, index and zoom levels are written when the
317
+ writer is closed.
318
+
319
+ The documentation's [reading](https://figwig.readthedocs.io/en/latest/reading.html)
320
+ and [writing](https://figwig.readthedocs.io/en/latest/writing.html) guides
321
+ cover the rest: values and coordinates, chromosomes a file does not have,
322
+ PyTorch data loaders, binned values, the files figwig refuses and falling back
323
+ to another reader, compression, zoom levels, and files identical to
324
+ pyBigWig's.
325
+
326
+ ## Command line
327
+
328
+ #### figwig bam2bw
329
+
330
+ ```bash
331
+ figwig bam2bw my.bam -s hg38.chrom.sizes -n test-run -p 8 # test-run.+.bw, test-run.-.bw
332
+ figwig bam2bw fragments.tsv.gz -s hg38.chrom.sizes -n test-run -f -u -p 8 # test-run.bw
333
+ ```
334
+
335
+ `figwig bam2bw` is bam2bw 0.5.1 with the same arguments, output files and
336
+ messages, except that `-p` is a number of cores, 1 unless given. By default
337
+ it counts the 5' end of every mapped read at each base, and writes the counts
338
+ of the two strands to two bigWigs; `-u` writes one, `-f` counts both ends of
339
+ each fragment, `-3p` the 3' ends, `-ps`, `-ns`, `-sf` and `-r` shift and scale
340
+ the counts, and `-z` writes zoom levels. A BAM file is inflated by libdeflate
341
+ and walked by a numba kernel on `-p` threads, and a BED/tsv file is scanned
342
+ by a numba kernel; SAM files, `--mate_pairs`, remote files and malformed
343
+ records go through bam2bw's own pysam or line loop, so that their result, or
344
+ their error, is bam2bw's. The bigWigs hold bam2bw's entries but not its bytes,
345
+ since figwig compresses them with libdeflate at level 1. `figwig bam2bw -h`
346
+ lists the arguments, [bam2bw's README](https://github.com/jmschrei/bam2bw)
347
+ has an example of each, and the
348
+ [documentation](https://figwig.readthedocs.io/en/latest/bam2bw.html) says how
349
+ files are read and shared across cores.
350
+
351
+ #### figwig install-skill
352
+
353
+ Installs the Claude Code skill; see [above](#claude-code-skill).
354
+
355
+ ## Origin
356
+
357
+ figwig began as the bigWig reader inside
358
+ [tangermeme](https://github.com/jmschrei/tangermeme)'s `extract_loci`
359
+ (PR #107). It was checked there against pybigtools, bit for bit, on the
360
+ training and validation sets of 92 Cherimoya models and on 3,131 synthetic
361
+ cases.