Skip to content

crispr_analysis_utils.guide_alignment.summary

crispr_analysis_utils.guide_alignment.summary

Per-guide classes, and the guide alignment's output tables.

A guide's class counts the matches whose genomic PAM matches: unique with exactly one, multi with more, no_site with none. A match whose PAM does not is still written, with pam_matches FALSE, and counted nowhere. An entry that duplicates an earlier one's sequence gets that entry's sites.

Coordinates are 0-based and half-open on the reference's forward strand, and the protospacer excludes the PAM and any base 5' of it. The target site is a boundary b 3 bases 5' of the PAM (end - 3 on +, start + 3 on -). A nuclease cuts between b - 1 and b; a dCas9 only sits there, so the name says where the site is, not what happens to it. In the tables, a value that does not apply is an empty field, and booleans are TRUE or FALSE.

UNIQUE module-attribute

UNIQUE = 'unique'

Exactly one perfect site with a matching PAM.

MULTI module-attribute

MULTI = 'multi'

More than one perfect site with a matching PAM.

NO_SITE module-attribute

NO_SITE = 'no_site'

No site with a matching PAM within max_mismatches.

CLASSES module-attribute

CLASSES = (UNIQUE, MULTI, NO_SITE)

Every site class, in report order.

PAM_OFFSET module-attribute

PAM_OFFSET = 3

Bases between the target site and the PAM.

GROUPS module-attribute

GROUPS = (('from the guide table, before anything is aligned', 6), ('about the whole guide, repeated on each of its rows', 5), ("about this row's match; empty when the guide matches nowhere", 7))

The column groups, in order, and how many columns each has.

COLUMNS module-attribute

COLUMNS = {'guide_id': 'The sequence that was aligned, ending in the PAM pattern. Identifies the guide.', 'alias': 'Name for the guide from the input table.', 'accession': "A stable id for the protospacer, the same wherever that target is used. Two guides differing only 5' of the protospacer share it.", 'spacer': "The protospacer: guide_id without the PAM pattern or any base 5' of it.", 'oligo': "The sequence the library ordered, without the PAM. It differs from the spacer when the entry carries a base 5' of the protospacer, such as a G added for U6 transcription.", 'n_oligos': "How many library entries share this spacer. Above one, a feature reference built on the spacer cannot tell them apart, so the counts are a sum: expect roughly that multiple of a guide ordered once. Here the second entry is the same spacer with a 5' G, which no read can be assigned to on its own, because the leader ends in a poly-G run.", 'site_class': 'unique, multi or no_site: how many PAM-bearing places this guide matches.', 'n_matches': 'How many places the protospacer matches, which is how many rows this guide has below.', 'n_target_sites': 'How many of those have a PAM, so Cas9 can act there.', 'orientation': "Whether the sequence reads 5' to 3': forward, reverse_complement, no_pam, no_match, or not_checked.", 'orientation_evidence': 'Only for reverse_complement. distinct: the reversed reading was a different sequence. ambiguous: it was just the reverse complement, which a chance CCN upstream looks identical to.', 'chr': 'Contig of this match.', 'start': 'First base of the protospacer, 0-based.', 'end': 'One past its last base, so the protospacer is [start, end).', 'strand': "Strand the protospacer reads 5' to 3' on.", 'target_site': 'Where Cas9 acts: the boundary 3 bases from the PAM. A nuclease cuts between target_site - 1 and target_site; dCas9 binds without cutting.', 'pam_matches': 'TRUE when the genomic PAM fits the pattern, so Cas9 can act here.', 'mapped_sequence': "What the genome holds here, to compare against guide_id: the protospacer and the real PAM, keeping the 5' base only where the genome has that same base."}

Every column of guide_genome_annotation.tsv, and what it holds.

BED_COLUMNS module-attribute

BED_COLUMNS = {'chr': 'Contig of the match.', 'start': 'First base of the protospacer, 0-based.', 'end': 'One past its last base.', 'name': 'The protospacer, as spacer in the metadata table.', 'score': 'Always 0; BED6 wants the field.', 'strand': "Strand the protospacer reads 5' to 3' on."}

The six fields of guide_mapping.bed, which has no header of its own.

GuideSummary dataclass

One guide's class, site counts and, when unique, its site.

Attributes:

  • guide_id (str) –

    The sequence that was aligned: the input sequence and the PAM.

  • alias (str) –

    The name the guide table gives this entry.

  • spacer (str) –

    The spacer, 5' to 3'.

  • oligo (str) –

    The sequence the library ordered, without the PAM: the spacer plus any base 5' of it.

  • n_oligos (int) –

    How many library entries carry this spacer. More than one means a feature reference built on the spacer merges them, so the counts are a sum and are not comparable with a guide ordered once.

  • mapped_sequence (str | None) –

    The guide's bases that pair with the genome at the unique site, as in Site.mapped_sequence; None unless the class is unique.

  • site_pam (str | None) –

    The genomic PAM of the unique site, in guide orientation; None unless the class is unique.

  • has_perfect_site (bool) –

    Whether the guide has at least one perfect site with a matching PAM.

  • guide_chr (str | None) –

    The unique site's contig; None unless unique.

  • guide_start (int | None) –

    The unique site's protospacer start; None unless unique.

  • guide_end (int | None) –

    The unique site's protospacer end; None unless unique.

  • strand (str | None) –

    The unique site's strand; None unless unique.

  • target_site (int | None) –

    The unique site's target boundary (see the module description); None unless unique.

  • site_class (str) –

    unique, multi or no_site.

  • orientation (str) –

    The orientation verdict, or not_checked.

  • orientation_evidence (str | None) –

    For a reverse_complement guide, whether that reading was a distinct sequence or only the spacer's reverse complement; None otherwise.

  • extra_5p (bool) –

    Whether the guide's site reads carry an added 5' G.

  • min_mismatches (int | None) –

    The fewest spacer mismatches among the sites with a matching PAM; None without any.

  • n_matches (int) –

    Every exact match of the spacer, whether or not a PAM sits beside it. More than one means the spacer occurs more than once in the genome, which site_class does not say: that counts PAM-bearing sites only.

  • n_sites (tuple[int, ...]) –

    Sites with a matching PAM by spacer mismatches: n_sites[m] sites have m, for m from 0 to max_mismatches.

  • input_sequence (str) –

    The sequence as given in the guide table, uppercased.

Source code in src/crispr_analysis_utils/guide_alignment/summary.py
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
@dataclass(frozen=True)
class GuideSummary:
    """One guide's class, site counts and, when unique, its site.

    Attributes
    ----------
    guide_id
        The sequence that was aligned: the input sequence and the PAM.
    alias
        The name the guide table gives this entry.
    spacer
        The spacer, 5' to 3'.
    oligo
        The sequence the library ordered, without the PAM: the spacer plus
        any base 5' of it.
    n_oligos
        How many library entries carry this spacer. More than one means a
        feature reference built on the spacer merges them, so the counts are
        a sum and are not comparable with a guide ordered once.
    mapped_sequence
        The guide's bases that pair with the genome at the unique site, as in
        `Site.mapped_sequence`; None unless the class is ``unique``.
    site_pam
        The genomic PAM of the unique site, in guide orientation; None unless
        the class is ``unique``.
    has_perfect_site
        Whether the guide has at least one perfect site with a matching PAM.
    guide_chr
        The unique site's contig; None unless unique.
    guide_start
        The unique site's protospacer start; None unless unique.
    guide_end
        The unique site's protospacer end; None unless unique.
    strand
        The unique site's strand; None unless unique.
    target_site
        The unique site's target boundary (see the module description); None
        unless unique.
    site_class
        ``unique``, ``multi`` or ``no_site``.
    orientation
        The orientation verdict, or ``not_checked``.
    orientation_evidence
        For a ``reverse_complement`` guide, whether that reading was a
        distinct sequence or only the spacer's reverse complement; None
        otherwise.
    extra_5p
        Whether the guide's site reads carry an added 5' G.
    min_mismatches
        The fewest spacer mismatches among the sites with a matching PAM;
        None without any.
    n_matches
        Every exact match of the spacer, whether or not a PAM sits beside it.
        More than one means the spacer occurs more than once in the genome,
        which `site_class` does not say: that counts PAM-bearing sites only.
    n_sites
        Sites with a matching PAM by spacer mismatches: ``n_sites[m]`` sites
        have ``m``, for ``m`` from 0 to `max_mismatches`.
    input_sequence
        The sequence as given in the guide table, uppercased.
    """

    guide_id: str
    alias: str
    spacer: str
    oligo: str
    n_oligos: int
    mapped_sequence: str | None
    site_pam: str | None
    has_perfect_site: bool
    guide_chr: str | None
    guide_start: int | None
    guide_end: int | None
    strand: str | None
    target_site: int | None
    site_class: str
    orientation: str
    orientation_evidence: str | None
    extra_5p: bool
    min_mismatches: int | None
    n_matches: int
    n_sites: tuple[int, ...]
    input_sequence: str

summarize

summarize(guides: Sequence[Guide], sites: Mapping[int, Sequence[Site]], orientation: Mapping[int, str] | None = None, *, evidence: Mapping[int, str | None] | None = None, pam: str = 'NGG', max_mismatches: int = 3) -> list[GuideSummary]

Classify every guide from its sites.

Parameters:

  • guides (Sequence[Guide]) –

    The library, as returned by read_guides.

  • sites (Mapping[int, Sequence[Site]]) –

    The kept sites by guide row, as in SiteResult.sites.

  • orientation (Mapping[int, str] | None, default: None ) –

    The orientation verdicts by guide row, as in ScanResult.verdicts; None when the check did not run.

  • pam (str, default: 'NGG' ) –

    The PAM pattern the sites were classified with.

  • max_mismatches (int, default: 3 ) –

    The mismatch limit the sites were kept with.

Returns:

  • list of GuideSummary –

    One per guide, in library order, duplicates included.

Raises:

  • ValueError –

    If the PAM patterns fail validate_pams, max_mismatches is negative, a site has more mismatches than max_mismatches or a PAM class that is neither one of the patterns nor none, a guide is a duplicate of an alias not in the library, or orientation lacks a guide's verdict.

Source code in src/crispr_analysis_utils/guide_alignment/summary.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def summarize(
    guides: Sequence[Guide],
    sites: Mapping[int, Sequence[Site]],
    orientation: Mapping[int, str] | None = None,
    *,
    evidence: Mapping[int, str | None] | None = None,
    pam: str = "NGG",
    max_mismatches: int = 3,
) -> list[GuideSummary]:
    """Classify every guide from its sites.

    Parameters
    ----------
    guides
        The library, as returned by `read_guides`.
    sites
        The kept sites by guide row, as in `SiteResult.sites`.
    orientation
        The orientation verdicts by guide row, as in
        `ScanResult.verdicts`; None when the check did not run.
    pam
        The PAM pattern the sites were classified with.
    max_mismatches
        The mismatch limit the sites were kept with.

    Returns
    -------
    list of GuideSummary
        One per guide, in library order, duplicates included.

    Raises
    ------
    ValueError
        If the PAM patterns fail `validate_pams`, `max_mismatches` is
        negative, a site has more mismatches than `max_mismatches` or a PAM
        class that is neither one of the patterns nor ``none``, a guide is a
        duplicate of an alias not in the library, or `orientation` lacks a
        guide's verdict.
    """
    patterns = validate_pams([pam])
    _check_max_mismatches(max_mismatches)
    primary = patterns[0]
    # Two entries of one spacer are one feature in any reference built on the
    # spacer, so their reads land together. Count them here so the table can
    # say which guides carry a sum.
    per_spacer = Counter(guide.spacer for guide in guides)
    summaries = []
    for guide, source in zip(guides, _source_rows(guides), strict=True):
        n_matches = 0
        n_sites = [0] * (max_mismatches + 1)
        perfect = []
        for site in sites.get(source, ()):
            n_matches += 1
            if site.n_mismatches > max_mismatches:
                raise ValueError(
                    f"A site of guide {site.guide_id!r} has {site.n_mismatches} "
                    f"mismatches, more than max_mismatches={max_mismatches}."
                )
            if site.pam_class == primary:
                n_sites[site.n_mismatches] += 1
                if site.n_mismatches == 0:
                    perfect.append(site)
            elif site.pam_class != NO_PAM_CLASS:
                raise ValueError(
                    f"A site of guide {site.guide_id!r} has PAM class "
                    f"{site.pam_class!r}, which is not one of the PAM patterns "
                    f"{', '.join(patterns)}."
                )

        if orientation is None:
            verdict = NOT_CHECKED
        elif source in orientation:
            verdict = orientation[source]
        else:
            raise ValueError(f"No orientation verdict for guide {guide.alias!r}.")

        if len(perfect) == 1:
            site_class = UNIQUE
        elif perfect:
            site_class = MULTI
        else:
            site_class = NO_SITE
        unique = perfect[0] if site_class == UNIQUE else None

        summaries.append(
            GuideSummary(
                guide_id=guide.guide_id,
                oligo=guide.aligned_sequence,
                n_oligos=per_spacer[guide.spacer],
                alias=guide.alias,
                spacer=guide.spacer,
                mapped_sequence=unique.mapped_sequence if unique else None,
                site_pam=unique.pam if unique else None,
                has_perfect_site=bool(perfect),
                guide_chr=unique.chr if unique else None,
                guide_start=unique.start if unique else None,
                guide_end=unique.end if unique else None,
                strand=unique.strand if unique else None,
                target_site=_target_site(unique) if unique else None,
                site_class=site_class,
                orientation=verdict,
                orientation_evidence=(
                    None if evidence is None else evidence.get(source)
                ),
                extra_5p=guide.extra_5p,
                min_mismatches=next(
                    (count for count, n in enumerate(n_sites) if n), None
                ),
                n_matches=n_matches,
                n_sites=tuple(n_sites),
                input_sequence=guide.input_sequence,
            )
        )
    return summaries

fan_out_sites

fan_out_sites(guides: Sequence[Guide], sites: Mapping[int, Sequence[Site]]) -> list[Site]

Every guide's sites, in library order, duplicates included.

Parameters:

  • guides (Sequence[Guide]) –

    The library, as returned by read_guides.

  • sites (Mapping[int, Sequence[Site]]) –

    The kept sites by guide row, as in SiteResult.sites.

Returns:

  • list of Site –

    For each distinct Guide.guide_id in turn, the sites of its row, or of the entry it duplicates, with Site.guide_id set to it. Entries that share a sequence are one guide, so their sites are written once.

Source code in src/crispr_analysis_utils/guide_alignment/summary.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def fan_out_sites(
    guides: Sequence[Guide], sites: Mapping[int, Sequence[Site]]
) -> list[Site]:
    """Every guide's sites, in library order, duplicates included.

    Parameters
    ----------
    guides
        The library, as returned by `read_guides`.
    sites
        The kept sites by guide row, as in `SiteResult.sites`.

    Returns
    -------
    list of Site
        For each distinct `Guide.guide_id` in turn, the sites of its row, or
        of the entry it duplicates, with `Site.guide_id` set to it. Entries
        that share a sequence are one guide, so their sites are written once.
    """
    fanned = []
    seen = set()
    for guide, source in zip(guides, _source_rows(guides), strict=True):
        if guide.guide_id in seen:
            continue
        seen.add(guide.guide_id)
        fanned.extend(
            site
            if site.guide_id == guide.guide_id
            else replace(site, guide_id=guide.guide_id)
            for site in sites.get(source, ())
        )
    return fanned

guide_columns

guide_columns() -> list[str]

The columns of guide_genome_annotation.tsv, in order.

One row per library entry per site, so the guide's own columns repeat on every site it has, and an entry with no site gets one row whose site columns are empty.

Returns:

  • list of str –

    The column names: the guide, then what the whole guide is, then the one site the row is about.

Source code in src/crispr_analysis_utils/guide_alignment/summary.py
367
368
369
370
371
372
373
374
375
376
377
378
379
380
def guide_columns() -> list[str]:
    """The columns of guide_genome_annotation.tsv, in order.

    One row per library entry per site, so the guide's own columns repeat on
    every site it has, and an entry with no site gets one row whose site
    columns are empty.

    Returns
    -------
    list of str
        The column names: the guide, then what the whole guide is, then the
        one site the row is about.
    """
    return list(COLUMNS)

write_guides_tsv

write_guides_tsv(path: str | PathLike[str], guides: Sequence[Guide], summaries: Sequence[GuideSummary], sites: Mapping[int, Sequence[Site]]) -> None

Write guide_genome_annotation.tsv: every entry, a row per site.

Parameters:

  • path (str | PathLike[str]) –

    The file to write.

  • guides (Sequence[Guide]) –

    The library, as returned by read_guides.

  • summaries (Sequence[GuideSummary]) –

    The guides, as returned by summarize, in the same order.

  • sites (Mapping[int, Sequence[Site]]) –

    The sites by guide row, as in ScanResult.sites.

Raises:

  • ValueError –

    If summaries is not one per guide.

Notes

write_guides_bed writes the same sites as a BED.

n_matches is not site_class: it counts every place the protospacer occurs, while multi counts only the matches with a PAM beside them, so a guide can match many places and still bear a PAM at one.

Source code in src/crispr_analysis_utils/guide_alignment/summary.py
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
def write_guides_tsv(
    path: str | os.PathLike[str],
    guides: Sequence[Guide],
    summaries: Sequence[GuideSummary],
    sites: Mapping[int, Sequence[Site]],
) -> None:
    """Write guide_genome_annotation.tsv: every entry, a row per site.

    Parameters
    ----------
    path
        The file to write.
    guides
        The library, as returned by `read_guides`.
    summaries
        The guides, as returned by `summarize`, in the same order.
    sites
        The sites by guide row, as in `ScanResult.sites`.

    Raises
    ------
    ValueError
        If `summaries` is not one per guide.

    Notes
    -----
    `write_guides_bed` writes the same sites as a BED.

    ``n_matches`` is not ``site_class``: it counts every place the protospacer
    occurs, while ``multi`` counts only the matches with a PAM beside them, so
    a guide can match many places and still bear a PAM at one.
    """
    if len(summaries) != len(guides):
        raise ValueError(f"Got {len(summaries)} summaries for {len(guides)} guides.")
    with _open_table(path, guide_columns()) as handle:
        for summary, source in zip(summaries, _source_rows(guides), strict=True):
            head = [
                summary.guide_id,
                summary.alias,
                guide_accession(summary.spacer),
                summary.spacer,
                summary.oligo,
                summary.n_oligos,
                summary.site_class,
                summary.n_matches,
                summary.n_sites[0],
                summary.orientation,
                summary.orientation_evidence,
            ]
            found = sites.get(source, ())
            if not found:
                _write_row(handle, [*head, *[None] * 7])
            for site in found:
                _write_row(
                    handle,
                    [
                        *head,
                        site.chr,
                        site.start,
                        site.end,
                        site.strand,
                        _target_site(site),
                        site.pam_class != NO_PAM_CLASS,
                        site.mapped_sequence,
                    ],
                )

write_guides_bed

write_guides_bed(path: str | PathLike[str], guides: Sequence[Guide], sites: Mapping[int, Sequence[Site]]) -> None

Write guide_mapping.bed: every site a guide could act at.

Parameters:

  • path (str | PathLike[str]) –

    The file to write.

  • guides (Sequence[Guide]) –

    The library, as returned by read_guides.

  • sites (Mapping[int, Sequence[Site]]) –

    The sites by guide row, as in ScanResult.sites.

Notes

BED6 without a header, fields as in BED_COLUMNS, one row per site whose genomic PAM matches, so a spacer with two PAM-bearing sites gets a row for each. The name field is the protospacer, which is the same on every row of a guide that matches more than once. Sorted by contig, then position. A match with no PAM is not somewhere Cas9 can act, so it is left out; guide_genome_annotation.tsv has them all.

Source code in src/crispr_analysis_utils/guide_alignment/summary.py
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
def write_guides_bed(
    path: str | os.PathLike[str],
    guides: Sequence[Guide],
    sites: Mapping[int, Sequence[Site]],
) -> None:
    """Write guide_mapping.bed: every site a guide could act at.

    Parameters
    ----------
    path
        The file to write.
    guides
        The library, as returned by `read_guides`.
    sites
        The sites by guide row, as in `ScanResult.sites`.

    Notes
    -----
    BED6 without a header, fields as in `BED_COLUMNS`, one row per site whose
    genomic PAM matches, so a spacer with two PAM-bearing sites gets a row for
    each. The name field is the protospacer, which is the same on every row
    of a guide that matches more than once. Sorted by contig, then position.
    A match with no PAM
    is not somewhere Cas9 can act, so it is left out;
    guide_genome_annotation.tsv has them all.
    """
    # One row per distinct guide, not per entry: two entries of one sequence
    # would otherwise give byte-identical rows and bedtools would count twice.
    seen: set[str] = set()
    rows = []
    for guide, source in zip(guides, _source_rows(guides), strict=True):
        if guide.guide_id in seen:
            continue
        seen.add(guide.guide_id)
        rows.extend(
            (site.chr, site.start, site.end, guide.spacer, site.strand)
            for site in sites.get(source, ())
            if site.pam_class != NO_PAM_CLASS
        )
    rows.sort()
    with _open_table(path, None) as handle:
        for chrom, start, end, name, strand in rows:
            _write_row(handle, [chrom, start, end, name, 0, strand])

write_columns_yaml

write_columns_yaml(path: str | PathLike[str]) -> None

Write guide_genome_annotation_columns.yaml: what every column holds.

Parameters:

  • path (str | PathLike[str]) –

    The file to write.

Notes

Generated from COLUMNS and BED_COLUMNS, so it cannot drift from the tables themselves.

Source code in src/crispr_analysis_utils/guide_alignment/summary.py
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
def write_columns_yaml(path: str | os.PathLike[str]) -> None:
    """Write guide_genome_annotation_columns.yaml: what every column holds.

    Parameters
    ----------
    path
        The file to write.

    Notes
    -----
    Generated from `COLUMNS` and `BED_COLUMNS`, so it cannot drift from the
    tables themselves.
    """
    lines = ["# What each column of this folder's outputs holds.", ""]
    lines.append("guide_genome_annotation.tsv:")
    columns = iter(COLUMNS.items())
    for heading, count in GROUPS:
        lines.append(f"  # {heading}")
        for _ in range(count):
            column, description = next(columns)
            lines.append(f'  {column}: "{_one_line(description)}"')
    lines.append("")
    lines.append("guide_mapping.bed:")
    for column, description in BED_COLUMNS.items():
        lines.append(f'  {column}: "{_one_line(description)}"')
    lines.append("")
    _write_text(path, "\n".join(lines))

write_qc_tsv

write_qc_tsv(path: str | PathLike[str], summaries: Sequence[GuideSummary], *, counts: Mapping[str, Mapping[str, int]] | None = None) -> None

Write qc.tsv: the counts worth checking on a run.

Parameters:

  • path (str | PathLike[str]) –

    The file to write.

  • summaries (Sequence[GuideSummary]) –

    The guides, as returned by summarize.

  • counts (Mapping[str, Mapping[str, int]] | None, default: None ) –

    Further counts by section name, such as {"site_search": SiteResult.counts()}.

Raises:

  • ValueError –

    If a summary has an unknown class or verdict, or a counts section reuses a built-in section name.

Notes

Three columns, section, name and value. Section guides (n_entries, n_guides, n_shared_spacers); section class, one row per site class; section orientation, one row per verdict, not_checked included; then each counts section. The class rows and the orientation rows each sum to n_entries. The run's parameters are not here: they belong to the step's metadata file.

Source code in src/crispr_analysis_utils/guide_alignment/summary.py
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
def write_qc_tsv(
    path: str | os.PathLike[str],
    summaries: Sequence[GuideSummary],
    *,
    counts: Mapping[str, Mapping[str, int]] | None = None,
) -> None:
    """Write qc.tsv: the counts worth checking on a run.

    Parameters
    ----------
    path
        The file to write.
    summaries
        The guides, as returned by `summarize`.
    counts
        Further counts by section name, such as
        ``{"site_search": SiteResult.counts()}``.

    Raises
    ------
    ValueError
        If a summary has an unknown class or verdict, or a `counts` section
        reuses a built-in section name.

    Notes
    -----
    Three columns, ``section``, ``name`` and ``value``. Section ``guides``
    (``n_entries``, ``n_guides``, ``n_shared_spacers``); section ``class``,
    one row per site class; section ``orientation``, one row per verdict,
    ``not_checked`` included; then each `counts` section. The class rows and
    the orientation rows each sum to ``n_entries``. The run's parameters are
    not here: they belong to the step's metadata file.
    """
    classes = Counter(s.site_class for s in summaries)
    verdicts = Counter(s.orientation for s in summaries)
    unknown = sorted(set(classes) - set(CLASSES)) + sorted(
        set(verdicts) - set(VERDICTS)
    )
    if unknown:
        raise ValueError(f"Unknown classes or verdicts: {', '.join(unknown)}.")
    clashes = sorted(set(counts or {}) & set(_QC_SECTIONS))
    if clashes:
        raise ValueError(f"counts reuses built-in section names: {', '.join(clashes)}.")

    n_spacers = len({s.spacer for s in summaries})
    rows = [
        ("guides", "n_entries", len(summaries)),
        ("guides", "n_guides", len({s.guide_id for s in summaries})),
        ("guides", "n_shared_spacers", len(summaries) - n_spacers),
        *(("class", name, classes[name]) for name in CLASSES),
        *(("orientation", name, verdicts[name]) for name in VERDICTS),
    ]
    for section, section_counts in (counts or {}).items():
        rows.extend((section, name, value) for name, value in section_counts.items())
    with _open_table(path, ("section", "name", "value")) as handle:
        for row in rows:
            _write_row(handle, row)