Skip to content

Gene annotation

cau guide-annotation places every aligned guide in a gene. Alignment answers where in the genome a guide is; this answers where in a gene: exon, intron or intergenic, whether it falls in a promoter, which gene and transcript, how far it is from the nearest protein-coding TSS, and a few properties of the protospacer itself.

It reads the table cau guide-alignment wrote, and a store built once from GENCODE.

In Claude Code the cau-guide-annotation skill teaches Claude to run it and read the output (Agents and skills).

Quick start

Build the store once:

cau guide-annotation build-db gencode.v43.annotation.gtf.gz \
    --out gencode.v43.protein_coding.parquet

Then annotate:

cau guide-annotation run \
    --alignment results/alignments/guide_genome_annotation.tsv \
    --store gencode.v43.protein_coding.parquet \
    --promoters gencode.v43.protein_coding.TSS1bp.bed6 \
    --outdir results/

Nothing is written outside --outdir, and nothing beside your inputs.

The store

Reading a full GENCODE takes about a minute, mostly because every tag on every row has to be kept: a GENCODE transcript carries several, and a reader that keeps only the last finds 4,290 of the 19,077 MANE Select transcripts. The store is that work done once. It holds the gene, transcript, exon, CDS and UTR rows of the gene types --gene-types kept, is about 13 MB for protein-coding v43, and loads in about a second.

The GTF must be a full annotation. A genes-only GTF has no exon rows, so it cannot say exon, intron or transcript, and build-db refuses it rather than call everything intergenic.

Promoters

--promoters takes a BED of transcription start sites, one per gene, which is usually the file the rest of the lab already uses. The seventh column, if present, is read as the gene id.

Without it, each gene's reference transcript start is used. That is a reasonable default and not the same thing: a TSS file built from a different release, or from a different transcript per gene, will disagree by a few bases or a few kilobases.

--promoter-window (default 1000) is how far either side of a TSS counts as its promoter.

The reference transcript

A gene has many transcripts, so one is chosen for enst, exon_number and the position along the CDS: MANE Select, else Ensembl canonical, else the longest CDS, and the order is deterministic down to the transcript id, so a rebuild chooses the same one.

feature is called against that transcript. exonic_any_transcript records whether any transcript of the gene has an exon there, so a row called intron that is another transcript's exon is still visible.

Three axes, not one

A target site 300 bp into exon 1 is in a gene, in an exon and inside a promoter. One column would have to pick one and hide the others, so there are three: location is genic/intergenic, feature is exon/intron within the reference transcript, and promoter is its own TRUE/FALSE.

With --gene-types at its default, intergenic means "not in a protein-coding gene", not "in no gene at all". A protospacer inside a lncRNA is intergenic, and other_gene_types and other_genes name what it overlaps, so the ambiguity is on the row without it deciding the call.

Every transcript has a promoter

A gene has as many promoters as it has distinct transcript starts, and a guide at an alternative promoter is invisible to a one-TSS-per-gene file. Without --promoters the store supplies a site for every transcript, and promoter_enst, promoter_ensg and promoter_gene name the ones whose window the protospacer overlaps. nearest_tss_gene and nearest_tss_ensg stay singular, because the nearest is one.

What is measured from where

Overlaps are decided on the protospacer, so a guide reaching into an exon counts as exonic. Distances and the CDS position are measured from the target site. Every interval test ignores strand: a guide on one strand targets a gene on either.

Outputs

--outdir gets three folders and nothing loose:

annotations/     guide_gene_annotation.tsv
                 guide_gene_annotation_columns.yaml
run_metadata/    guide_annotation_<UTC timestamp>.json
logs/            guide_annotation.log

guide_gene_annotation_columns.yaml is written from the same definitions the table is, so it is always an accurate data dictionary of the folder it sits in.

guide_gene_annotation.tsv

One row per mapped site per overlapping gene. A site in two genes gets two rows; a site in none gets one row, intergenic, with the gene columns empty. Only sites with a PAM are annotated.

Column Holds
guide_id The aligned sequence. With the four site columns, the key back to guide_genome_annotation.tsv.
chr, start, end, strand The protospacer, 0-based and half-open.
ensg, gene_symbol The gene this row is about, without a version.
enst Its reference transcript, when the protospacer hits one of that transcript's exons.
location genic or intergenic, from whether a gene of an annotated type overlaps.
feature exon or intron inside the reference transcript; empty when intergenic.
exonic_any_transcript TRUE when any transcript of the gene has an exon here.
region CDS or UTR for an exonic row.
exon_number Which exon of the reference transcript, from its 5' end.
cds_position, cds_fraction How far along the CDS the cut falls, in bases and as a fraction.
distance_to_gene_start Signed bases from the anchor to this gene's annotated 5' end, in the gene's orientation. Near zero is the canonical promoter.
sense Whether the protospacer reads on the gene's own strand.
other_gene_types, other_genes Semicolon-separated types and ids of overlapping genes of other types, such as lncRNA. Empty when none.
promoter TRUE within the promoter window of any protein-coding TSS.
promoter_enst, promoter_ensg, promoter_gene Semicolon-separated transcripts whose promoter window the protospacer overlaps, and their genes. Empty when the TSS file carries no transcript id.
distance_to_tss Signed bases from the anchor to the nearest TSS. The anchor is the protospacer midpoint unless --distance-from target_site.
nearest_tss_gene, nearest_tss_ensg Whose TSS that is.
gc_percent, max_polyt_run The protospacer's GC, and its longest T run.

Joining it back

The alias, accession and spacer are not repeated; they are in the alignment table. Join on all five key columns, because a guide with two sites has two rows in each table:

annotation.merge(alignment, on=["guide_id", "chr", "start", "end", "strand"])

What is measured from where

Overlaps are decided on the protospacer, so a guide reaching into an exon counts as exonic. Distances and the CDS position are measured from the cut site, which is where the break falls. Every interval test ignores strand: a guide on one strand targets a gene on either.

run_metadata/guide_annotation_<timestamp>.json

The parameters, the input checksums, the store's own provenance, the counts and the timings. Nothing per-guide lives here.

Cost

A 43,000-site library against protein-coding v43 annotates in about a second, once the store is built.