Skill Tracer
Skills › Scientific Agent Skills › pysam › cram_and_performance.md

cram_and_performance.md

324 lines · skills/pysam/references/cram_and_performance.md at 5f2bc03

Open on GitHub
1# CRAM, Remote I/O, Threads, and Performance
2
3This reference targets pysam 0.24.1, which embeds HTSlib/samtools/bcftools
41.24.
5

CRAM, Remote I/O, Threads, and Performance

This reference targets pysam 0.24.1, which embeds HTSlib/samtools/bcftools 1.24.

Raw lines 1 to 5
6## Pysam 0.24 CRAM Changes
7
8Pysam 0.24 is the first pysam release to wrap HTSlib 1.22 or later. Two
9inherited operational changes matter:
111. New CRAM output defaults to CRAM **3.1**, not 3.0.
122. HTSlib no longer contacts the EBI CRAM reference server by default.
14For compatibility with a consumer that cannot read CRAM 3.1:
16```python
17with pysam.AlignmentFile(
18 "output.cram",
19 "wc",
20 header=header,
21 reference_filename="reference.fa",
22 format_options=["version=3.0"],
23) as output:
24 ...
25```
27In pysam 0.24, `format_options` accepts a list of Python `str` values as
28documented.

Pysam 0.24 CRAM Changes

Pysam 0.24 is the first pysam release to wrap HTSlib 1.22 or later. Two inherited operational changes matter:

  1. New CRAM output defaults to CRAM 3.1, not 3.0.
  2. HTSlib no longer contacts the EBI CRAM reference server by default.

For compatibility with a consumer that cannot read CRAM 3.1:

with pysam.AlignmentFile(
    "output.cram",
    "wc",
    header=header,
    reference_filename="reference.fa",
    format_options=["version=3.0"],
) as output:
    ...

In pysam 0.24, format_options accepts a list of Python str values as documented.

Raw lines 6 to 29
30## Why the Reference Matters
32CRAM commonly stores differences from a reference rather than complete read
33sequences. A matching reference can therefore be required to decode or encode
34records.
36Reference identity is represented by `M5` MD5 values and often `UR` fields in
37`@SQ` header lines. Contig names alone are not sufficient.
39Treat the reference as part of the data provenance:
41- preserve the exact FASTA used to create the CRAM
42- preserve its `.fai`
43- record assembly/version and checksum
44- back up reference cache content when it is the only decoding source
45- verify contig lengths and M5 tags
47Some CRAM files embed all or part of their reference, but do not assume that
48every CRAM is self-contained.

Why the Reference Matters

CRAM commonly stores differences from a reference rather than complete read sequences. A matching reference can therefore be required to decode or encode records.

Reference identity is represented by M5 MD5 values and often UR fields in @SQ header lines. Contig names alone are not sufficient.

Treat the reference as part of the data provenance:

  • preserve the exact FASTA used to create the CRAM
  • preserve its .fai
  • record assembly/version and checksum
  • back up reference cache content when it is the only decoding source
  • verify contig lengths and M5 tags

Some CRAM files embed all or part of their reference, but do not assume that every CRAM is self-contained.

Raw lines 30 to 49
50## Deterministic Local CRAM Access
52Prefer an explicit local FASTA:
54```python
55import pysam
57pysam.faidx("reference.fa")
59with pysam.AlignmentFile(
60 "sample.cram",
61 "rc",
62 reference_filename="reference.fa",
63 require_index=True,
64 threads=4,
65) as cram:
66 for read in cram.fetch("chr1", 1_000, 2_000):
67 ...
68```
70Use the same reference when writing:
72```python
73with pysam.AlignmentFile("input.bam", "rb") as source, pysam.AlignmentFile(
74 "output.cram",
75 "wc",
76 template=source,
77 reference_filename="reference.fa",
78 threads=4,
79) as destination:
80 for read in source.fetch(until_eof=True):
81 destination.write(read)
82```
84Then create a CRAI and read the output back with the same FASTA.

Deterministic Local CRAM Access

Prefer an explicit local FASTA:

import pysam

pysam.faidx("reference.fa")

with pysam.AlignmentFile(
    "sample.cram",
    "rc",
    reference_filename="reference.fa",
    require_index=True,
    threads=4,
) as cram:
    for read in cram.fetch("chr1", 1_000, 2_000):
        ...

Use the same reference when writing:

with pysam.AlignmentFile("input.bam", "rb") as source, pysam.AlignmentFile(
    "output.cram",
    "wc",
    template=source,
    reference_filename="reference.fa",
    threads=4,
) as destination:
    for read in source.fetch(until_eof=True):
        destination.write(read)

Then create a CRAI and read the output back with the same FASTA.

Raw lines 50 to 85
86## `REF_PATH` and `REF_CACHE`
88Use these HTSlib environment variables only when reference-by-MD5 lookup is
89intentional:
91- `REF_PATH`: colon-separated local paths and optionally URLs used to find a
92 sequence by its M5 checksum
93- `REF_CACHE`: location where retrieved references are cached
95A local cache layout commonly uses:
97```text
98/reference-cache/%2s/%2s/%s
99```
101Do not add a remote endpoint merely to make an error disappear. Network
102reference lookup changes reproducibility, privacy, availability, and cache
103behavior. Pysam 0.24 intentionally inherits HTSlib's removal of implicit EBI
104fetching.
106When remote lookup is required:
108- configure a controlled institutional proxy/cache where possible
109- place a local cache before remote sources
110- ensure downloads are checksum-validated
111- document network and retention behavior
112- avoid credentials in URLs and logs
114An explicit `reference_filename=` is usually clearer for a single known
115assembly.

REF_PATH and REF_CACHE

Use these HTSlib environment variables only when reference-by-MD5 lookup is intentional:

  • REF_PATH: colon-separated local paths and optionally URLs used to find a sequence by its M5 checksum
  • REF_CACHE: location where retrieved references are cached

A local cache layout commonly uses:

/reference-cache/%2s/%2s/%s

Do not add a remote endpoint merely to make an error disappear. Network reference lookup changes reproducibility, privacy, availability, and cache behavior. Pysam 0.24 intentionally inherits HTSlib's removal of implicit EBI fetching.

When remote lookup is required:

  • configure a controlled institutional proxy/cache where possible
  • place a local cache before remote sources
  • ensure downloads are checksum-validated
  • document network and retention behavior
  • avoid credentials in URLs and logs

An explicit reference_filename= is usually clearer for a single known assembly.

Raw lines 86 to 116
117## Convert BAM to CRAM with Wrapped Samtools
119For bulk conversion:
121```python
122import pysam.samtools
124pysam.samtools.view(
125 "-@",
126 "4",
127 "-C",
128 "-T",
129 "reference.fa",
130 "-o",
131 "output.cram",
132 "input.bam",
133 catch_stdout=False,
135pysam.samtools.index(
136 "-@",
137 "4",
138 "output.cram",
139 catch_stdout=False,
141```
143Using `-o` plus `catch_stdout=False` avoids capturing binary CRAM output in
144Python memory.
146Validate:
148```python
149pysam.samtools.quickcheck("-v", "output.cram")
151with pysam.AlignmentFile(
152 "output.cram",
153 "rc",
154 reference_filename="reference.fa",
155) as cram:
156 first = next(cram.fetch(until_eof=True), None)
157```
159`quickcheck` is structural, not a full decode or biological validation.
160Compare record counts and selected records to the source.

Convert BAM to CRAM with Wrapped Samtools

For bulk conversion:

import pysam.samtools

pysam.samtools.view(
    "-@",
    "4",
    "-C",
    "-T",
    "reference.fa",
    "-o",
    "output.cram",
    "input.bam",
    catch_stdout=False,
)
pysam.samtools.index(
    "-@",
    "4",
    "output.cram",
    catch_stdout=False,
)

Using -o plus catch_stdout=False avoids capturing binary CRAM output in Python memory.

Validate:

pysam.samtools.quickcheck("-v", "output.cram")

with pysam.AlignmentFile(
    "output.cram",
    "rc",
    reference_filename="reference.fa",
) as cram:
    first = next(cram.fetch(until_eof=True), None)

quickcheck is structural, not a full decode or biological validation. Compare record counts and selected records to the source.

Raw lines 117 to 161
162## CRAM Failure Diagnosis
164When CRAM open/fetch fails, check in this order:
1661. Confirm the FASTA assembly and contig names.
1672. Confirm `.fai` exists and lengths match the alignment header.
1683. Inspect `@SQ` `M5` and `UR` fields.
1694. Pass `reference_filename=` explicitly.
1705. Confirm CRAI matches the current CRAM.
1716. Try a sequential read to separate index from reference problems.
1727. Capture HTSlib/samtools error messages.
174Do not disable validation or substitute a similarly named assembly.

CRAM Failure Diagnosis

When CRAM open/fetch fails, check in this order:

  1. Confirm the FASTA assembly and contig names.
  2. Confirm .fai exists and lengths match the alignment header.
  3. Inspect @SQ M5 and UR fields.
  4. Pass reference_filename= explicitly.
  5. Confirm CRAI matches the current CRAM.
  6. Try a sequential read to separate index from reference problems.
  7. Capture HTSlib/samtools error messages.

Do not disable validation or substitute a similarly named assembly.

Raw lines 162 to 175
176## Remote HTSlib I/O
178Depending on how the wheel/build was configured, HTSlib can read HTTP(S) and
179other plugin-backed URLs. Random access also needs a reachable index and range
180request support.
182The following URLs are illustrative placeholders, not a deployed endpoint.
183Remote access was checked against upstream contracts, not a production service.
185```python
186with pysam.AlignmentFile(
187 "https://example.org/data/sample.bam",
188 "rb",
189 index_filename="https://example.org/data/sample.bam.bai",
190) as bam:
191 for read in bam.fetch("chr1", 1_000, 2_000):
192 ...
193```
195Remote support is build- and protocol-dependent. Before large analysis:
197- issue one small known region query
198- verify the exact index URL
199- check content versioning/immutability
200- confirm retries, timeout behavior, and credentials outside logs
201- estimate request count from the access pattern
203Many tiny random queries can be slower and more expensive than downloading or
204staging a file once. Never put bearer tokens or secrets directly in committed
205URLs.

Remote HTSlib I/O

Depending on how the wheel/build was configured, HTSlib can read HTTP(S) and other plugin-backed URLs. Random access also needs a reachable index and range request support.

The following URLs are illustrative placeholders, not a deployed endpoint. Remote access was checked against upstream contracts, not a production service.

with pysam.AlignmentFile(
    "https://example.org/data/sample.bam",
    "rb",
    index_filename="https://example.org/data/sample.bam.bai",
) as bam:
    for read in bam.fetch("chr1", 1_000, 2_000):
        ...

Remote support is build- and protocol-dependent. Before large analysis:

  • issue one small known region query
  • verify the exact index URL
  • check content versioning/immutability
  • confirm retries, timeout behavior, and credentials outside logs
  • estimate request count from the access pattern

Many tiny random queries can be slower and more expensive than downloading or staging a file once. Never put bearer tokens or secrets directly in committed URLs.

Raw lines 176 to 206
207## Threads
209`threads=` on `AlignmentFile` and `VariantFile` controls HTSlib
210compression/decompression threads:
212```python
213with pysam.AlignmentFile("sample.bam", "rb", threads=4) as bam:
214 ...
215```
217In 0.24.1, `TabixFile` accepts `threads=` but its open path resets it to 1;
218do not assume threaded tabix decoding. Linux 0.24.1 wheels load networking
219libraries dynamically and restore Debian/Ubuntu S3 support; a wheel install
220alone does not prove those runtime libraries/credentials are present.
222It does **not** parallelize Python filtering, pileup interpretation, or
223statistical analysis. More threads can increase memory and I/O contention, and
224returns diminish when storage or network is the bottleneck.
226`threads > 1` cannot be combined with `ignore_truncation=True`.

Threads

threads= on AlignmentFile and VariantFile controls HTSlib compression/decompression threads:

with pysam.AlignmentFile("sample.bam", "rb", threads=4) as bam:
    ...

In 0.24.1, TabixFile accepts threads= but its open path resets it to 1; do not assume threaded tabix decoding. Linux 0.24.1 wheels load networking libraries dynamically and restore Debian/Ubuntu S3 support; a wheel install alone does not prove those runtime libraries/credentials are present.

It does not parallelize Python filtering, pileup interpretation, or statistical analysis. More threads can increase memory and I/O contention, and returns diminish when storage or network is the bottleneck.

threads > 1 cannot be combined with ignore_truncation=True.

Raw lines 207 to 227
228## Python Thread Safety
230Pysam releases the GIL for many I/O-intensive operations, but not every code
231path has been comprehensively validated for thread safety.
233Avoid sharing one active file handle across threads. Prefer:
235- one independently opened handle per worker, or
236- independent `fetch(..., multiple_iterators=True)` iterators when the
237 overhead is acceptable
239Variant iterators use `fetch(..., reopen=True)`; Tabix and alignment iterators
240use `multiple_iterators=True`. CRAM **pileup** does not implement independent
241iterators via that flag: open separate CRAM handles for concurrent pileups.
243Reopening a remote file for every small iterator can be especially expensive.

Python Thread Safety

Pysam releases the GIL for many I/O-intensive operations, but not every code path has been comprehensively validated for thread safety.

Avoid sharing one active file handle across threads. Prefer:

  • one independently opened handle per worker, or
  • independent fetch(..., multiple_iterators=True) iterators when the overhead is acceptable

Variant iterators use fetch(..., reopen=True); Tabix and alignment iterators use multiple_iterators=True. CRAM pileup does not implement independent iterators via that flag: open separate CRAM handles for concurrent pileups.

Reopening a remote file for every small iterator can be especially expensive.

Raw lines 228 to 244
245## Multiprocessing
247Partition work by independent contigs or nonoverlapping windows, but reopen
248files inside each process:
250```python
251def process_region(bam_path: str, contig: str, start: int, stop: int):
252 with pysam.AlignmentFile(bam_path, "rb", threads=1) as bam:
253 return bam.count(contig, start, stop, read_callback="all")
254```
256Do not pass live pysam handles or proxy records between processes. Balance
257process count and HTSlib `threads`; multiplying both can oversubscribe CPUs.
259When partitioning pileup or coverage, include any necessary boundary context
260and trim results back to the target windows. Reads overlap partition
261boundaries.

Multiprocessing

Partition work by independent contigs or nonoverlapping windows, but reopen files inside each process:

def process_region(bam_path: str, contig: str, start: int, stop: int):
    with pysam.AlignmentFile(bam_path, "rb", threads=1) as bam:
        return bam.count(contig, start, stop, read_callback="all")

Do not pass live pysam handles or proxy records between processes. Balance process count and HTSlib threads; multiplying both can oversubscribe CPUs.

When partitioning pileup or coverage, include any necessary boundary context and trim results back to the target windows. Reads overlap partition boundaries.

Raw lines 245 to 262
263## Access-Pattern Guidance

Access-Pattern Guidance

Raw lines 263 to 264
265### Prefer sequential scans when
267- every record is needed
268- the file is unindexed
269- remote request overhead dominates
270- aggregate QC is being calculated
272Use `fetch(until_eof=True)` for alignments and normal iteration for variants.

Prefer sequential scans when

  • every record is needed
  • the file is unindexed
  • remote request overhead dominates
  • aggregate QC is being calculated

Use fetch(until_eof=True) for alignments and normal iteration for variants.

Raw lines 265 to 273
274### Prefer indexed queries when
276- only a small fraction of the genome is needed
277- regions are grouped and ordered
278- an index is available and current
280Sort region requests by contig/start to improve locality. Merge heavily
281overlapping windows when duplicate processing is not desired.

Prefer indexed queries when

  • only a small fraction of the genome is needed
  • regions are grouped and ordered
  • an index is available and current

Sort region requests by contig/start to improve locality. Merge heavily overlapping windows when duplicate processing is not desired.

Raw lines 274 to 282
283### Prefer native bulk commands when
285- sorting, merging, indexing, or format conversion
286- normalizing VCF
287- producing standard command outputs
289The wrapped samtools/bcftools implementations are usually faster and more
290tested than equivalent Python loops.

Prefer native bulk commands when

  • sorting, merging, indexing, or format conversion
  • normalizing VCF
  • producing standard command outputs

The wrapped samtools/bcftools implementations are usually faster and more tested than equivalent Python loops.

Raw lines 283 to 291
292## Pileup and Coverage Performance
294- `count()` is appropriate for overlap-record counts.
295- `count_coverage()` efficiently returns dense A/C/G/T arrays for an interval.
296- `pileup()` is needed for base/read-level state but has more Python overhead.
297- The default pileup depth cap is 8000; changing it can materially affect
298 memory and results.
299- Avoid one pileup call per nearby variant; group variants into windows or
300 traverse columns once.
301- Do not materialize all reads or pileup proxies into a list.

Pileup and Coverage Performance

  • count() is appropriate for overlap-record counts.
  • count_coverage() efficiently returns dense A/C/G/T arrays for an interval.
  • pileup() is needed for base/read-level state but has more Python overhead.
  • The default pileup depth cap is 8000; changing it can materially affect memory and results.
  • Avoid one pileup call per nearby variant; group variants into windows or traverse columns once.
  • Do not materialize all reads or pileup proxies into a list.
Raw lines 292 to 302
303## File-Handle and Proxy Lifetimes
305Pysam exposes proxy objects backed by HTSlib memory. Keep the owning file and
306iterator alive while using:
308- `PileupColumn`
309- `PileupRead`
310- `persist=False` FASTX records
312Consume a pileup column before advancing the iterator, because its backing
313buffer changes on advance. Copy primitive values if data must outlive a step.

File-Handle and Proxy Lifetimes

Pysam exposes proxy objects backed by HTSlib memory. Keep the owning file and iterator alive while using:

  • PileupColumn
  • PileupRead
  • persist=False FASTX records

Consume a pileup column before advancing the iterator, because its backing buffer changes on advance. Copy primitive values if data must outlive a step.

Raw lines 303 to 314
315## Reproducibility Checklist
317- pin pysam (`pysam==0.24.1`)
318- record `pysam.__version__` and `pysam.__samtools_version__`
319- record reference FASTA checksum and assembly
320- record CRAM output version
321- record command arguments and filtering thresholds
322- write outputs and indexes atomically or to new paths
323- validate representative regions after writing
324- avoid hidden network reference dependencies

Reproducibility Checklist

  • pin pysam (pysam==0.24.1)
  • record pysam.__version__ and pysam.__samtools_version__
  • record reference FASTA checksum and assembly
  • record CRAM output version
  • record command arguments and filtering thresholds
  • write outputs and indexes atomically or to new paths
  • validate representative regions after writing
  • avoid hidden network reference dependencies
Raw lines 315 to 324