Skip to content

Mapping

George Muñoz edited this page Jun 3, 2026 · 10 revisions

Mapping (prot2exon map)

prot2exon map projects protein-domain amino-acid coordinates onto a transcript's genomic CDS / UTR / intron structure. You give it a query BED and an index (or GTF), pick how you want the answer shaped with --output, and it writes a set of TSVs and BEDs describing where the domain lives in the genome.

Input: the query BED

prot2exon reads whitespace-separated, BED-like text. Lines starting with # and blank lines are ignored.

# rows with a domain (ENSP or ENST, with or without version)
ENSP00000269305      10    50   AD1     TF1
ENST00000269305.9    10    50   AD1_ENST  TF1   # same answer as the line above

# row without a domain (whole-transcript structure only)
ENSP00000418960
# Required Meaning
1 yes idENSP* (protein) or ENST* (transcript). Versioned or unversioned; the version suffix is stripped on both sides before lookup. RefSeq (NP_*, NM_*) and custom IDs also work as long as the GTF/index has them.
2 no aa_start — 1-based inclusive. Omit (or set to 0) for no-domain mode.
3 no aa_end — 1-based inclusive.
4 no domain_id (used as input_id for tracking through the outputs).
5+ no Ignored — free space for human-readable metadata.

If column 4 is missing but a domain is present, input_id falls back to id:aa_start-aa_end; in no-domain mode it falls back to id. Either way every row stays uniquely identifiable in the outputs.

An ENST resolves to the same intervals as its matching ENSP, so the two produce identical mapping output; the summary's input_id_type column records which form you supplied. A non-coding ENST (no CDS in the GTF) is reported in unmapped_domains.tsv with reason = no_CDS_for_protein. A row that is just an id with no aa range is processed in no-domain (structure-only) mode: the overlap columns are NA and the companion BEDs are empty, but isoform_structure.tsv is still fully populated so the transcript can be plotted as-is.

To build query BEDs from domain databases (InterProScan, UniProt feature tables, HMMER/Pfam), see the prepare helpers on the Python API page. The index that map reads is built with prot2exon index — see Index.

Mapping a domain

Every --output mode describes the same mapping; they are complementary views of where a domain sits in the genome. Pick the one that answers your question, or use the default all to get everything (see Output description).

Coding exons (--output coding)

Answers which CDS exons code the domain? — every CDS exon of the transcript, classified by whether it overlaps the domain.

Output File Contents
domain_cds_segments.tsv one row per CDS exon of the transcript, each with an overlaps_domain column
domain_cds_segments.bed subset: only the CDS exons that code the domain (coding_overlap)
prot2exon map --index human.idx --bed q.bed --out-dir results --output coding

The same in Python:

import prot2exon as p2e
mapper = p2e.Mapper(index="human.idx")
result = mapper.map_batch(
    [{"protein_id": "ENSP00000269305", "aa_start": 102, "aa_end": 292, "domain_id": "TP53_DBD"}],
    output="coding",
)
result.cds_segments   # DataFrame of CDS rows

overlaps_domain is coding_overlap for a CDS exon that codes the domain and no otherwise (NA in no-domain mode); partial-overlap rows also report the coding sub-interval in genomic / CDS-nt / aa space. Full column layout in Output description.

Introns (--output introns)

Answers which introns fall inside the domain's genomic span? — every intron of the transcript, classified by whether it lies within the domain envelope.

Output File Contents
domain_introns.tsv one row per intron of the transcript, each with an overlaps_domain column
domain_introns.bed subset: only the introns inside the domain span (inside_domain_genomic_span)
prot2exon map --index human.idx --bed q.bed --out-dir results --output introns

The same in Python:

result = mapper.map_batch(queries, output="introns")
result.introns        # DataFrame of intron rows

overlaps_domain is inside_domain_genomic_span for an intron between two domain-coding CDS exons, no for an intron elsewhere, and NA in no-domain mode. Full schema in Output description.

Genomic span (--output span)

Answers what is the single genomic envelope of the domain, introns included? — one interval per domain, from its first coding base to its last.

Output File Contents
domain_span_with_introns.bed one row per domain: first → last coding base, spanning any introns in between
prot2exon map --index human.idx --bed q.bed --out-dir results --output span

The same in Python:

result = mapper.map_batch(queries, output="span")
result.span_bed       # DataFrame, one row per domain

See Output description for the BED column meanings.

Also emitting BED12 (--bed12)

--bed12 is an add-on flag, not an output mode: it writes one extra file, domain_blocks.bed12, in addition to whatever --output you chose. Use it to get an IGV-ready BED12 alongside a narrower view such as coding. It is a no-op under --output all or --output bed12, which already write the file.

Output File Contents
domain_blocks.bed12 one IGV/UCSC-ready BED12 row per domain — the domain envelope drawn thick, with the coding CDS slices as blocks and the in-domain introns as gaps
prot2exon map --index human.idx --bed q.bed --out-dir results --output coding --bed12

The same in Python:

# The Python results carry the bed12 rows whenever they're produced:
result = mapper.map_batch(queries, output="all")
result.bed12          # DataFrame, one row per domain

Field-by-field meaning in Output description.

Get the transcript structure (--output isoform)

--output isoform writes the plot-ready isoform_structure.tsv: one row per structural feature of the transcript — five_prime_UTR, CDS, three_prime_UTR, and inferred intron rows. CDS exons that the domain only partially covers are split into separate rows (the overlapping and non-overlapping portions), and every row carries a plot_group string for direct colour mapping.

prot2exon map --index human.idx --bed q.bed --out-dir results --output isoform

The same in Python:

result = mapper.map_batch(queries, output="isoform")
result.isoform        # DataFrame, one row per structural feature

This is the table you feed to a renderer; see Plotting for turning it into a transcript diagram. The column layout (including feature_id / feature_part and the plot_group values) is in Output description.

Output description

domain_mapping_summary.tsv is written for every command (one row per input query, mapped or not), and unmapped_domains.tsv appears whenever at least one query fails. The per-feature TSVs and BEDs depend on --output. This section documents every file's schema.

The --output all parameter and what each mode writes

--output selects which feature files are produced; the default is all.

--output Question TSV(s) BED(s)
coding Where are the CDS exons, and which code the domain? domain_cds_segments.tsv domain_cds_segments.bed
introns Where are the introns, and which lie in the domain span? domain_introns.tsv domain_introns.bed
span What is the domain's genomic envelope (introns included)? domain_span_with_introns.bed
isoform How is the whole transcript organised? isoform_structure.tsv
bed12 One IGV-ready BED12 row per domain. domain_blocks.bed12
all (default) Everything above. all 4 TSVs all 4 BEDs

all additionally writes run_metadata.json. The --bed12 flag adds domain_blocks.bed12 to any of the first four modes. Regardless of mode, domain_mapping_summary.tsv is always written and unmapped_domains.tsv is written on any failure.

Coordinate conventions

Output System
*.bed 0-based half-open (BED standard).
*.tsv 1-based inclusive (matches GTF).

NA means not applicable to this row — e.g. CDS-nt fields on a UTR row, or overlap fields in no-domain mode. The BED and the TSV describe the same intervals in different conventions, which is why a coordinate can differ by 1 between them: a CDS at GTF 7676219..7676272 (1-based inclusive, length 54) is BED 7676218..7676272 (0-based half-open, still length 54) — end − start matches, only start shifts.

domain_mapping_summary.tsv

One row per input query, written for every --output mode.

Column Type Meaning
input_id string User-supplied identifier
protein_id string Normalised (version suffix stripped)
transcript_id string Transcript that this protein belongs to
gene_id string Ensembl gene id
gene_name string HGNC-style gene symbol
domain_id string BED column 4 (if any)
chrom, strand Chromosome and strand of the transcript
aa_start, aa_end int / NA Input domain bounds; NA in no-domain mode
domain_length_aa int / NA aa_end − aa_start + 1
domain_length_nt int / NA domain_length_aa × 3
protein_length_aa int Total CDS length / 3 for this protein
domain_genomic_start / _end int / NA Genomic envelope of the domain
n_coding_segments int / NA Number of CDS exon slices the domain spans
fully_mapped bool true if the entire aa range fits inside the CDS
no_domain_mode bool true if the BED row had no aa range
input_id_type ENSP / ENST / NA Which kind of id the user supplied
is_mane_select true / false / NA MANE Select transcript? NA if the GTF lacks tag attributes.
is_ensembl_canonical true / false / NA Ensembl_canonical transcript? NA if the GTF lacks tag attributes.
cds_length_mismatch bool true if sum(CDS_nt) % 3 != 0 (Sec, readthrough, incomplete)
cds_nt_remainder int (0, 1, 2) sum(CDS_nt) % 3
n_coding_exons_touched int / NA Distinct CDS exons the domain overlaps (split CDS rows count once)
n_introns_spanned int / NA Introns between two domain-coding CDS rows
is_single_exon_domain bool / NA n_coding_exons_touched == 1
fraction_domain_in_largest_exon float / NA Max over CDS exons of summed domain_overlap_fraction_of_domain; in [0, 1]
intron_burden_nt int / NA Sum of lengths of the introns counted in n_introns_spanned
status string ok / ok_cds_mismatch / partial / partial_cds_mismatch / structure_only / unmapped reason

Feature TSVs (domain_cds_segments.tsv, domain_introns.tsv, isoform_structure.tsv)

All three share the same column layout — they differ only in which feature types they include:

File Rows
domain_cds_segments.tsv every CDS row
domain_introns.tsv every intron row
isoform_structure.tsv every 5′UTR / CDS / 3′UTR / intron row

Identity columns: input_id, gene_id, gene_name, transcript_id, protein_id, domain_id.

Location columns:

Column Meaning
chrom Chromosome
strand + or
feature_genomic_start / _end 1-based inclusive
feature_length_nt end − start + 1

Feature-type columns:

Column Meaning
feature_type One of five_prime_UTR, CDS, three_prime_UTR, intron
feature_id Stable across splits. Numbered in translation order: CDS_1 is the most 5′ CDS, then CDS_2, etc. UTRs and introns are numbered separately (five_prime_UTR_1, intron_1, …)
feature_part 1..K when a CDS row was split by partial domain overlap; pieces of the same original CDS share the same feature_id, differ by feature_part. Always 1 for UTR / intron rows
exon_number Source GTF exon_number for UTR / CDS rows; NA for introns

A single GTF CDS exon can be split into multiple rows when the domain only covers part of it. The pieces always share the same feature_id; only feature_part differs. For example, when a domain ends in the middle of CDS_2 (translation order):

feature_id feature_part start end overlaps_domain
CDS_2 1 75278995 75279120 coding_overlap
CDS_2 2 75279121 75279123 no

To re-aggregate the full original CDS, group by (input_id, feature_id). To plot the overlap shape, fill by plot_group and ignore feature_part.

Ordering columns:

Column Meaning
feature_order_genomic 1..N along the chromosome (low → high coord).
feature_order_transcript 1..N in translation direction. Equals feature_order_genomic on + strand; reversed on strand.

For strand genes, plot using feature_genomic_start/end on the X axis (genomic coords), but use feature_order_transcript to interpret biological order (5′ → 3′ of the protein).

CDS-coordinate columns (NA on UTR / intron rows):

Column Meaning
cds_nt_start / cds_nt_end CDS-relative nt offsets (1-based) of this slice's first/last base
aa_start_encoded / aa_end_encoded First / last aa that this slice encodes (1-based)

The mapping is aa = ⌈cds_nt / 3⌉. A 1-nt CDS slice containing only the third base of an aa still reports that aa.

Domain-overlap columns. overlaps_domain is not a yes/no flag — it discriminates coding overlap from intronic overlap inside the domain envelope:

Value Meaning
no The row is outside the domain entirely. UTR rows always carry no
coding_overlap CDS row whose genomic interval overlaps a domain-coding range
inside_domain_genomic_span Intron located between two coding_overlap CDS rows
NA No-domain query (no domain to compare against)

The companion columns are filled only for coding_overlap rows:

Column Meaning
domain_overlap_genomic_start / _end Sub-interval of this row that codes the domain (1-based inclusive)
domain_overlap_cds_nt_start / _end Same overlap projected to CDS-nt (1-based)
domain_overlap_aa_start / _end Same overlap projected to aa
domain_overlap_fraction_of_feature overlap_length / feature_length_nt
domain_overlap_fraction_of_domain overlap_length / domain_length_nt. Sums to 1.0 across all coding_overlap rows of the same domain

Plotting column. plot_group is a single string suitable for direct colour mapping. With a domain: five_prime_UTR / three_prime_UTR, CDS_no_domain (CDS outside the domain), CDS_domain (CDS that encodes the domain), intron (outside the span), intron_domain_span (intron between two CDS_domain rows). In no-domain mode it is just the feature type (five_prime_UTR / CDS / three_prime_UTR / intron).

Companion BEDs

All three are 6-column standard BED (chrom, start_0based, end, name, score=0, strand).

File Rows
domain_cds_segments.bed CDS rows with overlaps_domain == coding_overlap
domain_introns.bed intron rows with overlaps_domain == inside_domain_genomic_span
domain_span_with_introns.bed one row per domain (genomic envelope, introns included)

name is protein_id[_domain_id]_<aa_start>-<aa_end>. No-domain queries contribute zero rows to any companion BED.

domain_blocks.bed12

One BED12 row per domain. The whole feature is drawn thick in IGV; blocks are the CDS slices that code the domain. Empty in no-domain mode.

BED12 field Meaning here
chrom chromosome
chromStart (0-based) first base of the domain genomic envelope
chromEnd last base + 1 of the envelope (so end − start = envelope length, introns included)
name protein_id[_domain_id]_<aa_start>-<aa_end>
score / strand 0 / transcript strand
thickStart / thickEnd equal to chromStart / chromEnd — IGV draws the whole feature thick
itemRgb 255,0,0 (red)
blockCount number of CDS slices that code the domain (coding_overlap rows)
blockSizes / blockStarts comma-separated, in genomic order (starts are offsets from chromStart)

unmapped_domains.tsv

Written only when at least one query failed.

Column Meaning
input_id, protein_id, aa_start, aa_end, domain_id identity (protein_id = NA for non-coding ENST queries)
reason one of: protein_not_in_index, no_CDS_for_protein, domain_beyond_protein_length, no_overlap

run_metadata.json

Written only with --output all. Records tool / version / timestamp_utc, output_kind, annotation_source (the GTF or index used), index_format_version, coordinate_conventions, query_counts ({ total, mapped, unmapped, no_domain_mode }), and cli (the full invocation).

MANE Select / Ensembl Canonical

GENCODE GTFs (since v34) carry tag "MANE_Select" and tag "Ensembl_canonical". The tool surfaces these in is_mane_select / is_ensembl_canonical (true / false / NA) on domain_mapping_summary.tsv and every feature TSV. NA is reserved for GTFs that carry no tag attribute anywhere (typical of base Ensembl) — distinct from false ("tags exist, but not on this transcript"). Filter to MANE Select queries with:

awk -F'\t' 'NR==1 || $20=="true"' domain_mapping_summary.tsv   # col 20 = is_mane_select

CDS-length mismatch (Sec, readthrough, incomplete CDS)

The tool maps every query but flags the rare cases where the CDS isn't a multiple of 3: cds_length_mismatch (true if sum(CDS_nt) % 3 != 0), cds_nt_remainder (0/1/2), and a _cds_mismatch suffix on status. This catches selenoproteins (internal TGA re-coded as Sec, plus a C-terminal extension), stop-codon readthrough, and incomplete cds_start_NF / cds_end_NF transcripts. The aa↔nt math uses ceiling division (aa = ⌈cds_nt / 3⌉), so a domain at the very C-terminus of an incomplete CDS may be clipped by 1–2 aa; the flag is your hint to check.

Codons split across exons

A codon that straddles an exon boundary is mapped correctly in both phases (1+2 and 2+1) — the math is cumulative-nt-based, not exon-by-codon. For a domain on the split aa, both the upstream and downstream CDS slices report a coding_overlap row, the intron between them is marked inside_domain_genomic_span, and the two domain_overlap_fraction_of_domain values (≈1/3 and ≈2/3) sum to 1.0. Two synthetic cases in tests/ exercise this.

Worked example

Input BED:

ENSP00000306245    5    100    RD1    TF2

ENSP00000306245 is a + strand transcript; domain RD1 (aa 5..100) spans the end of one CDS slice and most of the next. The relevant rows of isoform_structure.tsv (selected columns):

feature_type feature_id feature_part feature_genomic_start feature_genomic_end aa_start_encoded aa_end_encoded overlaps_domain plot_group
CDS CDS_1 1 75278983 75278994 1 4 no CDS_no_domain
CDS CDS_2 1 75278995 75279123 5 47 coding_overlap CDS_domain
intron intron_1 1 75279124 75279876 NA NA inside_domain_genomic_span intron_domain_span
CDS CDS_3 1 75279877 75280035 48 100 coding_overlap CDS_domain
CDS CDS_4 1 75280036 75280128 101 131 no CDS_no_domain
three_prime_UTR three_prime_UTR_1 1 75281422 75282230 NA NA no three_prime_UTR

To plot, fill by plot_group; to highlight the domain, take plot_group ∈ {CDS_domain, intron_domain_span}; to get just the domain-coding bases, take domain_cds_segments.bed. The same id on its own line (no aa columns) emits the whole-transcript rows with overlaps_domain = NA.

Clone this wiki locally