gain.genomic_resources.gene_models package
Submodules
gain.genomic_resources.gene_models.default_attributes module
Encoding of the atts column of the default gene models format.
The column packs a transcript’s attributes into one field as key:value
pairs joined by ;. Both delimiters, and the escape character itself, are
backslash-escaped inside keys and values, so that an attribute value holding
either delimiter survives a save/load round trip.
- gain.genomic_resources.gene_models.default_attributes.escape_default_attribute(value: str) str[source]
Escape the attribute delimiters in a key or a value.
- gain.genomic_resources.gene_models.default_attributes.format_default_attributes(attributes: dict) str[source]
Pack a transcript’s attributes into the
attscolumn.
- gain.genomic_resources.gene_models.default_attributes.parse_default_attributes(atts: str) dict[str, str][source]
Unpack the
attscolumn into the attributes it holds.
- gain.genomic_resources.gene_models.default_attributes.unescape_default_attribute(value: str) str[source]
Reverse escape_default_attribute.
A backslash not followed by a delimiter or another backslash is literal, so free text that happens to carry one – as NCBI RefSeq notes do – reads back unchanged. A backslash directly in front of one of those characters is always taken as an escape: the column records nothing that would tell an escape apart from a literal backslash there.
gain.genomic_resources.gene_models.gene_models module
- class gain.genomic_resources.gene_models.gene_models.GeneModels(resource: GenomicResource)[source]
Bases:
ResourceConfigValidationMixinManage and query gene model data from genomic resources.
This class provides access to gene models loaded from various file formats (GTF, refFlat, refSeq, CCDS, etc.) and offers efficient querying by gene name or genomic location.
The class maintains three internal data structures: - transcript_models: Dict mapping transcript IDs to TranscriptModel objects - gene_models: Dict mapping gene names to lists of TranscriptModel objects - _tx_index: IntervalTree index for fast location-based queries
- Variables:
resource (GenomicResource) – The genomic resource containing gene models.
config (dict) – Validated configuration from the resource.
reference_genome_id (str | None) – ID of the reference genome.
gene_models (dict[str, list[TranscriptModel]]) – Gene name to transcript models mapping.
transcript_models (dict[str, TranscriptModel]) – Transcript ID to transcript model mapping.
Example
>>> from gain.genomic_resources.gene_models ... .gene_models_factory import build_gene_models_from_file >>> gene_models = build_gene_models_from_file("genes.gtf") >>> gene_models.load() >>> # Query by gene name >>> tp53_transcripts = gene_models.gene_models_by_gene_name("TP53") >>> # Query by location >>> transcripts = gene_models.gene_models_by_location("chr17", 7676592)
Note
The gene models must be loaded using the load() method before queries can be performed. The class is thread-safe for concurrent access.
- close() None[source]
Do nothing: gene models hold no open file and stay loaded.
Here so a gene models resource closes like every other.
- gene_models_by_gene_name(name: str) list[TranscriptModel] | None[source]
Retrieve all transcript models for a specific gene.
- Parameters:
name (str) – The gene name/symbol to search for.
- Returns:
- List of transcript models for the
gene, or None if the gene is not found.
- Return type:
list[TranscriptModel] | None
Example
>>> transcripts = gene_models.gene_models_by_gene_name("BRCA1") >>> if transcripts: ... print(f"BRCA1 has {len(transcripts)} transcript variants")
- gene_models_by_location(chrom: str, pos_begin: int, pos_end: int | None = None) list[TranscriptModel][source]
Retrieve transcripts overlapping a genomic position or region.
This method uses an interval tree index for efficient querying of transcripts by genomic coordinates.
- Parameters:
chrom (str) – The chromosome name (e.g., “chr1”, “17”).
pos_begin (int) – The start position (1-based, inclusive).
pos_end (int | None) – The end position (1-based, inclusive). If None, queries a single position.
- Returns:
List of TranscriptModel objects whose transcript regions overlap the query position/region. Returns empty list if no overlaps found.
- Return type:
list[TranscriptModel]
Example
>>> # Query single position >>> models = gene_models.gene_models_by_location("chr17", 7676592) >>> # Query region >>> models = gene_models.gene_models_by_location( ... "chr17", 7661779, 7687550 ... ) >>> for tm in models: ... print(f"{tm.gene}: {tm.tr_id}")
Note
Positions are swapped automatically if pos_end < pos_begin.
- gene_names() list[str][source]
Get list of all gene names in the loaded gene models.
- Returns:
List of gene names (symbols).
- Return type:
list[str]
Example
>>> gene_models.load() >>> genes = gene_models.gene_names() >>> print(f"Loaded {len(genes)} genes")
- static get_schema() dict[str, Any][source]
The schema a
gene_modelsresource’s config is checked against.The base resource schema plus
filename,format,gene_mappingandchrom_mapping.
- has_chromosome(chrom: str) bool[source]
Check if a chromosome has any gene models.
- Parameters:
chrom (str) – The chromosome name to check.
- Returns:
True if the chromosome has gene models, False otherwise.
- Return type:
bool
Example
>>> if gene_models.has_chromosome("chr1"): ... print("Chromosome 1 has gene annotations")
- is_loaded() bool[source]
Check if gene models have been loaded.
- Returns:
True if load() has been called and completed, False otherwise.
- Return type:
bool
Example
>>> if not gene_models.is_loaded(): ... gene_models.load()
- static join_gene_models(*gene_models: GeneModels) GeneModels[source]
Merge multiple gene models into a single GeneModels object.
This combines transcript models from multiple sources into one unified gene models object.
- Parameters:
*gene_models (GeneModels) – Two or more GeneModels objects to
merge.
- Returns:
New GeneModels object containing all transcripts.
- Return type:
- Raises:
ValueError – If fewer than 2 gene models provided.
Example
>>> gm1 = build_gene_models_from_file("genes1.gtf") >>> gm2 = build_gene_models_from_file("genes2.gtf") >>> merged = GeneModels.join_gene_models(gm1, gm2)
Note
Transcript IDs should be unique across all input gene models.
- load() GeneModels[source]
Load gene models from the genomic resource.
This method parses the gene model file and builds internal indexes for efficient querying. It is thread-safe and will only load once.
- Returns:
Self, for method chaining.
- Return type:
Example
>>> gene_models = build_gene_models_from_file("genes.gtf") >>> gene_models.load() >>> num_transcripts = len(gene_models.transcript_models) >>> print(f"Loaded {num_transcripts} transcripts")
Note
Calling load() multiple times is safe - subsequent calls return immediately if already loaded.
- property resource_id: str
The id of the gene models resource this object wraps.
- gain.genomic_resources.gene_models.gene_models.create_regions_from_genes(gene_models: GeneModels, genes: list[str], regions: list[Region] | None, gene_regions_heuristic_cutoff: int = 20, gene_regions_heuristic_extend: int = 20000) list[Region] | None[source]
Produce a list of regions from given gene symbols.
If given a list of regions, will merge the newly-created regions from the genes with the provided ones.
gain.genomic_resources.gene_models.gene_models_factory module
- gain.genomic_resources.gene_models.gene_models_factory.build_gene_models_from_file(file_name: str, file_format: str | None = None, gene_mapping_file_name: str | None = None, chrom_mapping_file_name: str | None = None) GeneModels[source]
Load gene models from local filesystem.
- gain.genomic_resources.gene_models.gene_models_factory.build_gene_models_from_resource(resource: GenomicResource | None) GeneModels[source]
Load gene models from a genomic resource.
- gain.genomic_resources.gene_models.gene_models_factory.build_gene_models_from_resource_id(resource_id: str, grr: GenomicResourceRepo | None = None) GeneModels[source]
Load gene models from a genomic resource id.
gain.genomic_resources.gene_models.parsers module
- class gain.genomic_resources.gene_models.parsers.ColumnarLayout(accepted_columns: tuple[tuple[str, ...], ...], gene_columns: tuple[str, ...], attribute_columns: tuple[str, ...] = ())[source]
Bases:
objectOne UCSC-derived columnar gene-models layout.
The five layouts gain reads – refFlat, refSeq, CCDS, knownGene and UCSC genePred – are one record loop over one raw read. They differ on three axes and no others, which are the three fields below (gain#941). Everything else the loop does is the same for all of them and lives in parse_columnar_format: the half-open-to-inclusive coordinate shift, suffixing a transcript name into a unique id, the exon read, and update_frames().
gain’s own output format is deliberately not one of these. It is read by column name rather than by position, is already in gain’s coordinates, carries a third exon column, and builds its attributes by parsing a dedicated column rather than by copying whole cells.
- accepted_columns: tuple[tuple[str, ...], ...]
The column lists this format accepts, tried in order. Only genePred has more than one – the ten-column genePred core and the fifteen-column genePredExt. Neither attempt can consume the other’s file, though the two branches of parse_raw rule that out differently: probe_header matches the header against these names and probe_columns counts them.
- attribute_columns: tuple[str, ...] = ()
The columns copied into TranscriptModel.attributes, in this order – attributes are written back out in iteration order, so the order is part of the layout. This is the union over the accepted column lists, not a subset any one file carries: it is how genePred’s two widths share one row, the narrow one carrying none of these five. parse_columnar_format narrows it to the width that actually matched, once, before reading any record.
These are pinned to text on the headerless read as well (gain#973), so naming a column here decides its dtype and not only its presence.
- gene_columns: tuple[str, ...]
Where the gene label comes from, best candidate first. The first column carrying a non-blank cell wins; when none does, the last column named here supplies its cell anyway, blank or absent or not. A one-column rule is therefore just that column, read unconditionally, which is what four of the five layouts want.
This list is also part of what parse_columnar_format hands parse_raw to pin to text, so adding a column here does more than reorder the fallback: it changes the dtype that column is read at (gain#963).
- class gain.genomic_resources.gene_models.parsers.FormatInference(matched: tuple[str, ...], rejected: tuple[tuple[str, str], ...], sampled_rows: int, tie_break: str | None = None)[source]
Bases:
objectWhat trying every supported format against a file prefix established.
A format rejects a file through one of two channels: it raises, or it quietly returns no transcript models. Both end up in rejected, so the ledger has no holes – a reason the reader cannot see is the whole of gain#856.
- property file_format: str | None
The inferred format, or None when the file stays ambiguous.
Exactly one matching format is an inference; a multi-format collision resolved by a content tie-break is one too.
- matched: tuple[str, ...]
- rejected: tuple[tuple[str, str], ...]
- report() str[source]
Render why inference did not settle on a single format.
- sampled_rows: int
- tie_break: str | None = None
- gain.genomic_resources.gene_models.parsers.GTF_CDS_FEATURES = frozenset({'CDS'})
Features that state the coding sequence itself, one record per coding stretch of an exon. Widened into
cdsexactly as the codon records are, and wherever a codon record is missing they are the only statement of the extent there is. For a complete transcript they add nothing: GENCODE and Ensembl exclude the stop codon from theirCDSrecords, so the codon span already covers them. NCBI includes it – folding both sources together answers the same under either convention, so this carries no assumption about flavour.
- gain.genomic_resources.gene_models.parsers.GTF_CODON_FEATURES = frozenset({'start_codon', 'stop_codon'})
Features that delimit the coding sequence. Each record widens its transcript’s
cdsinterval to cover the codon’s span.
- gain.genomic_resources.gene_models.parsers.GTF_EXONLESS_TRANSCRIPT_FEATURES = frozenset({'miRNA', 'pre_miRNA'})
Transcript-level features FlyBase emits with no
exonrecords at all, so admitting them would add hundreds of transcript models carrying no sequence. This is a policy about these two spellings: it skips them up front, before their children are read, which is what lets a child record be reported against a named skipped transcript. An accepted feature that turns out to have noexonchild is a separate matter – it is dropped after the whole file is read (gain#965), so no transcript reaches the models with an empty exon list by either route. Moving one of these intoGTF_TRANSCRIPT_FEATURESshould be deliberate, not a silent behaviour change.
- gain.genomic_resources.gene_models.parsers.GTF_EXON_FEATURES = frozenset({'exon'})
Features that append an exon to their transcript’s model.
- gain.genomic_resources.gene_models.parsers.GTF_IGNORED_FEATURES = frozenset({'3UTR', '5UTR', 'UTR', 'five_prime_utr', 'gene', 'three_prime_utr'})
Features whose records contribute nothing to the models and are skipped outright, before attribute parsing – so an ignored record is not required to carry a
transcript_id(Ensemblgenerecords genuinely lack one).generestates what every transcript-level record already carries, and the UTR spellings are implied by the exons. The exonless biotypes are deliberately not here: their skip runs after attribute parsing, so their children’s errors can name the skipped transcript.
- gain.genomic_resources.gene_models.parsers.GTF_SELENOCYSTEINE_FEATURES = frozenset({'Selenocysteine'})
Features that mark a site within their transcript and contribute nothing to the model. GENCODE emits one
Selenocysteinerecord per recoded UGA codon of a selenoprotein. Taking no measurement from them is a redundancy, not a policy: every such site falls inside aCDSrecord of the same transcript, socdsalready covers it – all 130 records across the 88 selenoproteins of GENCODE v49 comprehensive. Dispatched as child records, so that a record with no parent transcript is reported rather than silently turned into a transcript of its own.
- gain.genomic_resources.gene_models.parsers.GTF_TRANSCRIPT_FEATURES = frozenset({'mRNA', 'ncRNA', 'pseudogene', 'rRNA', 'snRNA', 'snoRNA', 'tRNA', 'transcript'})
Features that introduce a transcript. Ensembl and RefSeq emit the literal
transcript; FlyBase instead names the transcript by its biotype. Every entry here is handled identically – it creates a transcript model keyed bytranscript_id. Supporting a flavour usually takes more than this set: FlyBase also relies on the5UTR/3UTRspellings inGTF_IGNORED_FEATURESand ongene_symbolas its gene label. Check a new file’scut -f3 | sort -uagainst the module’sGTF_*constants, which between them spell out the whole vocabulary the loop dispatches on. Flavour is an intake concern only –serialization.pynormalises back out, always writingtranscriptandgene_name.
- gain.genomic_resources.gene_models.parsers.UCSC_GENEPRED_LAYOUT = ColumnarLayout(accepted_columns=(('name', 'chrom', 'strand', 'txStart', 'txEnd', 'cdsStart', 'cdsEnd', 'exonCount', 'exonStarts', 'exonEnds'), ('name', 'chrom', 'strand', 'txStart', 'txEnd', 'cdsStart', 'cdsEnd', 'exonCount', 'exonStarts', 'exonEnds', 'score', 'name2', 'cdsStartStat', 'cdsEndStat', 'exonFrames')), gene_columns=('name2', 'name'), attribute_columns=('score', 'name2', 'cdsStartStat', 'cdsEndStat', 'exonFrames'))
The only layout accepting two widths, and the only one whose gene label has a fallback: the narrow form has no alternate-name column at all, and the wide form may carry a blank one. UCSC’s own genePred and genePredExt table definitions – the sole specification either width has – are quoted in parse_ucscgenepred_models_format.
- gain.genomic_resources.gene_models.parsers.get_parser(fileformat: str) Callable[[IO, dict[str, str] | None, int | None], dict[str, TranscriptModel] | None] | None[source]
Get gene models parser based on file format.
- gain.genomic_resources.gene_models.parsers.infer_gene_model_parser(infile: IO, file_format: str | None = None) str | None[source]
Infer gene models file format.
- gain.genomic_resources.gene_models.parsers.infer_gene_models_format(infile: IO) FormatInference[source]
Try every supported format against a prefix of infile.
- gain.genomic_resources.gene_models.parsers.load_gene_mapping(resource: GenomicResource) dict[str, str][source]
Load alternative names for genes.
Assume that its first line has two column names
- gain.genomic_resources.gene_models.parsers.load_transcript_models(resource: GenomicResource) dict[str, TranscriptModel][source]
Load gene models.
- gain.genomic_resources.gene_models.parsers.parse_ccds_gene_models_format(infile: IO, gene_mapping: dict[str, str] | None = None, nrows: int | None = None) dict[str, TranscriptModel] | None[source]
Parse CCDS gene models file format.
A CCDS model’s gene and transcript name are the same string; see _REFSEQ_COLUMNS for why this format and refSeq are two entries.
- gain.genomic_resources.gene_models.parsers.parse_columnar_format(layout: ColumnarLayout, infile: IO, gene_mapping: dict[str, str] | None = None, nrows: int | None = None) dict[str, TranscriptModel] | None[source]
Parse a columnar gene-models file against one layout.
Returns
Nonewhen the file matches none of the layout’s accepted column lists – the GeneModelsParser rejection convention, which format inference reads back to say why a format lost.Both of the layout’s per-record rules are resolved against the width that matched before any record is read, rather than re-derived per record. That is not only for speed, though it is worth about 250ns a record over files that run to the hundreds of thousands: which columns a record carries is settled by the match, because parse_raw asserts the frame’s columns are exactly the ones it was given.
- gain.genomic_resources.gene_models.parsers.parse_default_gene_models_format(infile: IO, gene_mapping: dict[str, str] | None = None, nrows: int | None = None) dict[str, TranscriptModel] | None[source]
Parse default gene models file format.
- gain.genomic_resources.gene_models.parsers.parse_gtf_gene_models_format(infile: IO, gene_mapping: dict[str, str] | None = None, nrows: int | None = None) dict[str, TranscriptModel] | None[source]
Parse GTF gene models file format.
- gain.genomic_resources.gene_models.parsers.parse_known_gene_models_format(infile: IO, gene_mapping: dict[str, str] | None = None, nrows: int | None = None) dict[str, TranscriptModel] | None[source]
Parse known gene models file format.
- gain.genomic_resources.gene_models.parsers.parse_raw(infile: IO, expected_columns: list[str], nrows: int | None = None, comment: str | None = None, text_columns: tuple[str, ...] = ()) DataFrame | None[source]
Parse raw gene models data based on expected columns.
Both branches keep a blank cell as the empty string it was. Letting pandas filter it instead made what a cell became a property of its whole column rather than of itself: one blank re-typed the column, so a well-formed record serialized differently depending on whether some other row was blank, and the blank itself reached serialization as the fabricated token
nan(gain#931).What each branch pins differs, and only because of what it costs. The headered branch has always pinned every column to text. The headerless branch pins _IDENTIFYING_COLUMNS, which is what settles the typing gain#929 left here – a chromosome column of bare digits was handed over as the int 17, and a transcript index keyed by that is unreachable by a query for “17”. The two read the same values either way; the pin decides only how much of the frame is object.
text_columnsis how a caller names the rest of what has to reach the model as the file spelled it. A gene label is the second such column, and for the same reason: it keys the gene index, so a gene labelled by a bare digit was unreachable by a lookup for its own name, and which label a record got depended on which branch below recognised the file (gain#963). Only the caller knows which column that is – it differs per layout, and for one of them it is the alternate name with the transcript name behind it. gain’s own output format, which does not come through here, pins its gene column the same way and always has.A layout’s attribute columns are the third, and the first named for something other than a key: they are copied into the model whole and written back out, so leaving them inferred made a record’s attribute a property of which branch read the file. It is the one axis of this where two published resources actually disagreed – refSeq’s
#bin,scoreandexonCountarrive as ints from the headerless file and as text from the headered one (gain#973).Pinning them rewrites no published resource: both refSeq files serialize to the same bytes either way, over their full length. It is not output-neutral in general though, and must not be – a
scoreof007inferred as 7 serializes as7, and gain’s own format, having only text to write an attribute as, leaves nothing downstream able to say what the file first recorded.The GTF reader shares this and names none of these columns, so it keeps the inference its own arithmetic depends on. It does not escape
na_filter: a blank cell reaches it as''too, which is what its blank-attributes guard now decides on.
- gain.genomic_resources.gene_models.parsers.parse_ref_flat_gene_models_format(infile: IO, gene_mapping: dict[str, str] | None = None, nrows: int | None = None) dict[str, TranscriptModel] | None[source]
Parse refFlat gene models file format.
- gain.genomic_resources.gene_models.parsers.parse_ref_seq_gene_models_format(infile: IO, gene_mapping: dict[str, str] | None = None, nrows: int | None = None) dict[str, TranscriptModel] | None[source]
Parse refSeq gene models file format.
- gain.genomic_resources.gene_models.parsers.parse_ucscgenepred_models_format(infile: IO, gene_mapping: dict[str, str] | None = None, nrows: int | None = None) dict[str, TranscriptModel] | None[source]
Parse UCSC gene prediction models file fomrat.
table genePred “A gene prediction.”
( string name; "Name of gene" string chrom; "Chromosome name" char[1] strand; "+ or - for strand" uint txStart; "Transcription start position" uint txEnd; "Transcription end position" uint cdsStart; "Coding region start" uint cdsEnd; "Coding region end" uint exonCount; "Number of exons" uint[exonCount] exonStarts; "Exon start positions" uint[exonCount] exonEnds; "Exon end positions" )
table genePredExt “A gene prediction with some additional info.”
( string name; "Name of gene (usually transcript_id from GTF)" string chrom; "Chromosome name" char[1] strand; "+ or - for strand" uint txStart; "Transcription start position" uint txEnd; "Transcription end position" uint cdsStart; "Coding region start" uint cdsEnd; "Coding region end" uint exonCount; "Number of exons" uint[exonCount] exonStarts; "Exon start positions" uint[exonCount] exonEnds; "Exon end positions" int score; "Score" string name2; "Alternate name (e.g. gene_id from GTF)" string cdsStartStat; "Status of CDS start annotation (none, unknown, incomplete, or complete)" string cdsEndStat; "Status of CDS end annotation (none, unknown, incomplete, or complete)" lstring exonFrames; "Exon frame offsets {0,1,2}" )
- gain.genomic_resources.gene_models.parsers.probe_columns(infile: IO, expected_columns: list[str], comment: str | None = None) bool[source]
Probe gene models file based on expected columns.
- gain.genomic_resources.gene_models.parsers.probe_header(infile: IO, expected_columns: list[str], comment: str | None = None) bool[source]
Probe gene models file header based on expected columns.
- gain.genomic_resources.gene_models.parsers.read_gene_models_tsv(infile: IO, **kwargs: Any) DataFrame[source]
Read a gene-models table, keeping every cell as its own text.
Every read that builds records goes through here, so that
na_filteris off in one place rather than five. pandas otherwise reads a blank cell – and several spellings that are not blank,NAandNULLamong them – as a floatNaN, which reaches serialization as the fabricated tokennanand re-types the column around it (gain#931).probe_header and probe_columns do not: they read one row to recognise a layout and never look at a value, so what a blank cell becomes there cannot reach a record.
That the setting had to be repeated per call site is how a read got missed: the gene mapping kept filtering long after the two model reads stopped, and wrote
naninto the gene column.What each caller pins with
dtypestill differs, and is theirs to decide – the layouts do not agree on which columns are text.
gain.genomic_resources.gene_models.record_cells module
Reading one cell of a columnar gene-models record, and refusing it.
The gene-models parsers all face the same problem: a cell arrives as whatever pandas made of it, and a record built from a cell that cannot be read is worse than no record at all. What a bad cell should produce is one thing – a ValueError naming the record and the column – and it is gathered here rather than repeated in each parser (gain#907, gain#929).
Two shapes of message live here, because a record is named by two of its own columns:
Once the transcript name and chromosome are known, everything else is reported against them:
transcript NM_000546 at chr17 has ....Those two cannot be named that way themselves, so each falls back to the record’s position in the file, plus whichever of the pair is readable:
gene models record 2 at chr17 has a blank name column.
The layouts that use these are near-duplicates of one another, and driving them from a table instead is gain#941; this module is what such a table would call.
- gain.genomic_resources.gene_models.record_cells.QUOTED_TEXT_LIMIT = 60
How much of a cell to quote back when reporting it. Shared with _scan_gtf_attributes, so that the two messages truncate alike.
- gain.genomic_resources.gene_models.record_cells.cell_text(value: Any) str[source]
Render what pandas made of a cell as the text the file held.
Since gain#931 the reads keep a cell as its own text, so what the file said is what arrives: a blank cell is
'', and the spellings pandas would otherwise have taken for missing values –NA,NULLandnanamong them – are the words they are, and the messages built from this can say which one it read.No read in this module produces a
NaNany more – a row that stops short of a column yields''as well, on both the columnar and the GTF paths (measured on pandas 3.0.2). Thepd.isnabranch is kept anyway: it costs one comparison on a path that is already building an error message, parse_coordinate relies on it to tell a missing number from one it should convert, and the supported pandas range is wider than the version this was measured on.valueis annotatedAnyrather thanobjectbecausepd.isnahas no overload for the latter.
- gain.genomic_resources.gene_models.record_cells.parse_coordinate(value: Any, column: str, tr_name: object, chrom: object) int[source]
Read a single coordinate column, naming its record.
The columnar layouts already wrapped these in
int(), which does reject a blank cell – but ascannot convert float NaN to integer, naming neither the record nor the column, and the gain#856 ledger then offers that to the reader as the reason a format was rejected. The default format did not convert at all, so a blank cell became a transcript bound ofNaN(gain#929).A coordinate spelled
100.0is read as100whether it arrives as text or as a number. It used to be only the latter: a column spelled that way throughout was inferred as float on the headerless path andint(100.0)kept it parsing, while the headered path pinned it to text andint("100.0")did not, so the same file parsed or failed depending on which branch recognised it. Since gain#931 reads every columnar cell as text, the text conversion is the one that has to accept both spellings.OverflowErroris caught alongside the rest becauseinfis a coordinate pandas accepts andint()will not take. It reaches here only on the read path that infers a float column, so without it the two paths report the same file differently – and the one that escaped named neither the record nor the column.This runs four times per record on files reaching into the hundreds of thousands, so neither path builds a message until there is one to build. Since gain#931 the text path is the common one – the five UCSC-derived layouts pin every column to a string dtype – and the numeric path is reached by the default format’s four bound columns, which are the ones left to inference.
- gain.genomic_resources.gene_models.record_cells.parse_exon_bounds(rec: dict, tr_name: object, chrom: object) tuple[list[int], list[int]][source]
Read the paired exon-position columns of a columnar record.
Every columnar layout but the default one spells the pair the same way, so they share this rather than repeating the pair of reads and the length check between them.
- gain.genomic_resources.gene_models.record_cells.parse_exon_positions(value: Any, column: str, tr_name: object, chrom: object) list[int][source]
Read a comma-separated coordinate column, naming its record.
pandas delivers a blank cell as a float
NaN, which used to reachstr.stripand escape as anAttributeErrornaming a float (gain#907). Text that is simply not a coordinate list failsintthe same way, and leaves the reader just as stuck, so both are reported here as one thing: this record’s column could not be read.Where a GTF record has to be placed by feature and position – its
transcript_idbeing what tends to be missing – a columnar record is named by the transcript name and chromosome every columnar layout carries in columns of their own.The quoted cell is what pandas made of the column, not the file’s own bytes – see cell_text. The
intfailure stays on the chain, so the offending token survives the truncation.
- gain.genomic_resources.gene_models.record_cells.parse_transcript_bounds(rec: dict, tr_name: object, chrom: object) tuple[tuple[int, int], tuple[int, int]][source]
Read the transcript and coding bounds of a columnar record.
The five UCSC-derived layouts spell these four columns the same way and share the half-open convention that shifts each start by one, so they share this rather than repeating it between them (gain#941).
- gain.genomic_resources.gene_models.record_cells.record_identity(rec: dict, record: int, name_column: str, chrom_column: str) tuple[Any, Any][source]
Read the two columns that say which record a columnar row is.
Both used to be taken as they came. A blank one became a float
NaNin the model: aNaNchromosome keys the transcript index all by itself, so the record is unreachable by every location query, and aNaNtranscript name reaches serialization as the literal tokennan– and is suffixed into a transcript id ofnan_1, an identifier no file ever carried (gain#929).These two are what every other message here names a record by, so they cannot be named that way themselves. Each falls back to the record’s position in the file, plus whichever of the pair is still readable.
Blankness is decided on the text, but what is returned is the cell pandas handed over, untouched – a guard meant only to reject must not re-type a cell that parses. It used to matter which read path handed it over: the headerless one inferred a dtype, so a chromosome column of bare digits came back as the int 17, and a transcript index keyed by that is unreachable by a query for “17”. gain#931 settled that at the read boundary, where it belonged, so both paths now hand over text.
- gain.genomic_resources.gene_models.record_cells.require_cell(value: Any, column: str, tr_name: object, chrom: object) Any[source]
Read a load-bearing text column of an already-identified record.
Blank is refused rather than carried: a strand reaches
update_frames(), so a record without one does not merely hold an odd value – its exon frames come out as if it had a strand, and the output is quietly wrong rather than missing (gain#929).Blankness is decided on the text, but what is returned is the cell pandas handed over, untouched – see record_identity.
- gain.genomic_resources.gene_models.record_cells.require_equal_exon_counts(tr_name: object, chrom: object, **columns: list[int]) None[source]
Refuse a record whose exon columns disagree on how many exons.
This was a bare
assert, which carries no message; the gain#856 ledger renders whatever a parser raised, so what reached the reader as the reason a format was rejected wasAssertionError (no message). Naming the record and the counts costs nothing and is the whole of what the reader needed.The counts themselves are only tallied once they disagree: this runs once per record, and the records that reach it agree.
- gain.genomic_resources.gene_models.record_cells.unparsable(column: str, tr_name: object, chrom: object, text: str) ValueError[source]
Report a cell that a record cannot be built from.
gain.genomic_resources.gene_models.serialization module
- gain.genomic_resources.gene_models.serialization.build_gtf_record(transcript: TranscriptModel, feature: str, start: int, stop: int, attrs: str) tuple[tuple[str, int, int, int], str][source]
Build an indexed GTF format record for a feature.
- gain.genomic_resources.gene_models.serialization.calc_frame_for_gtf_cds_feature(transcript: TranscriptModel, region: BedRegion) int[source]
Calculate frame for the given feature.
- gain.genomic_resources.gene_models.serialization.collect_gtf_cds_regions(strand: str, cds_regions: list[BedRegion]) list[BedRegion][source]
Returns list of all regions that represent the CDS.
- gain.genomic_resources.gene_models.serialization.collect_gtf_start_codon_regions(strand: str, cds_regions: list[BedRegion]) list[BedRegion][source]
Returns list of all regions that represent the start codon.
- gain.genomic_resources.gene_models.serialization.collect_gtf_stop_codon_regions(strand: str, cds_regions: list[BedRegion]) list[BedRegion][source]
Returns list of all regions that represent the stop codon.
- gain.genomic_resources.gene_models.serialization.find_exon_cds_region_for_gtf_cds_feature(transcript: TranscriptModel, region: BedRegion) tuple[Exon, BedRegion][source]
Find exon and CDS region that contains the given feature.
- gain.genomic_resources.gene_models.serialization.gene_models_to_gtf(gene_models: GeneModels, *, sort_by_position: bool = True) StringIO[source]
Output a GTF format string representation.
- gain.genomic_resources.gene_models.serialization.get_exon_number_for(transcript: TranscriptModel, start: int, stop: int) int[source]
Get the exon number for a genomic region.
Returns the exon number (in transcript order) that overlaps the given genomic coordinates.
- Parameters:
start (int) – Start position (1-based).
stop (int) – End position (1-based).
- Returns:
- Exon number (1-based) in transcript orientation.
Returns 0 if no overlapping exon found.
- Return type:
int
Example
>>> # For a region within the second exon of a + strand transcript >>> exon_num = transcript.get_exon_number_for(1000, 1050) >>> print(f"Region is in exon {exon_num}")
Note
Exon numbering is strand-aware: - Positive strand: numbered 5’ to 3’ (exon 1 is first) - Negative strand: numbered 5’ to 3’ (exon 1 is last in genome)
- gain.genomic_resources.gene_models.serialization.gtf_canonical_index(index: tuple[str, int, int, int]) tuple[source]
- gain.genomic_resources.gene_models.serialization.save_as_default_gene_models(gene_models: GeneModels, output_filename: str, *, gzipped: bool = True) None[source]
Save gene models in a file in default file format.
- gain.genomic_resources.gene_models.serialization.transcript_to_gtf(transcript: TranscriptModel) list[tuple[tuple[str, int, int, int], str]][source]
Output an indexed list of GTF-formatted features of a transcript.
gain.genomic_resources.gene_models.to_gpf_gene_models_format module
- gain.genomic_resources.gene_models.to_gpf_gene_models_format.main(argv: list[str] | None = None) None[source]
Convert gene models to default GPF gene models format.
gain.genomic_resources.gene_models.transcript_models module
- class gain.genomic_resources.gene_models.transcript_models.Exon(start: int, stop: int, frame: int | None = None)[source]
Bases:
objectRepresent a single exon within a transcript.
An exon is a segment of a transcript that is retained in the mature RNA after splicing. This class stores the genomic coordinates and codon reading frame of an exon.
- Variables:
start (int) – Genomic start position (1-based, inclusive).
stop (int) – Genomic end position (1-based, inclusive).
frame (int | None) – Codon reading frame (0, 1, or 2) for coding exons, and -1 for a non-coding one – see calc_frames. None means the frame has not been computed yet, not that the exon is non-coding: it is what an exon built without one holds until update_frames fills it in. Serializing a model still holding None is refused rather than guessed.
Example
>>> exon = Exon(start=100, stop=200, frame=0) >>> print(exon.start, exon.stop) 100 200 >>> exon.contains((150, 160)) True
- contains(region: tuple[int, int]) bool[source]
Check if this exon fully contains a genomic region.
- Parameters:
region (tuple[int, int]) – A (start, stop) position tuple to check.
- Returns:
True if the region is fully contained within this exon.
- Return type:
bool
Example
>>> exon = Exon(100, 200) >>> exon.contains((150, 160)) True >>> exon.contains((50, 250)) False
- property frame: int | None
- property start: int
- property stop: int
- class gain.genomic_resources.gene_models.transcript_models.TranscriptModel(gene: str, tr_id: str, tr_name: str, chrom: str, strand: str, *, tx: tuple[int, int], cds: tuple[int, int], exons: list[Exon] | None = None, attributes: dict[str, Any] | None = None)[source]
Bases:
objectRepresent a transcript with all its structural features.
A transcript model contains complete information about a gene transcript, including its genomic location, exon structure, coding regions, and additional attributes from the source annotation.
- Variables:
gene (str) – Gene name/symbol (e.g., “TP53”).
tr_id (str) – Transcript identifier, unique within the gene models.
tr_name (str) – Original transcript name from source annotation.
chrom (str) – Chromosome name (e.g., “chr17”, “17”).
strand (str) – Strand orientation (“+” or “-“).
tx (tuple[int, int]) – Transcript start and end positions (1-based, closed interval).
cds (tuple[int, int]) – Coding sequence start and end positions (1-based, closed interval). For non-coding transcripts, cds[0] >= cds[1].
exons (list[Exon]) – List of Exon objects in genomic order.
attributes (dict[str, Any]) – Additional annotation attributes (e.g., gene_biotype, gene_version).
Example
>>> from gain.genomic_resources.gene_models.transcript_models import \ ... TranscriptModel, Exon >>> tm = TranscriptModel( ... gene="TP53", ... tr_id="ENST00000269305", ... tr_name="TP53-201", ... chrom="17", ... strand="-", ... tx=(7661779, 7687550), ... cds=(7668402, 7687490), ... exons=[Exon(7661779, 7661822), Exon(7668402, 7669690)], ... attributes={"gene_biotype": "protein_coding"}, ... ) >>> print(f"Coding: {tm.is_coding()}") Coding: True >>> regions = tm.cds_regions() >>> print(f"CDS has {len(regions)} regions")
Note
All coordinates use 1-based, closed intervals
CDS includes both start and stop codons
Exons should be in genomic order (not necessarily 5’ to 3’)
- all_regions(ss_extend: int = 0, prom: int = 0) list[BedRegion][source]
Get all transcript regions with optional extensions.
Returns all exonic regions, optionally extending into splice sites and promoter regions.
- Parameters:
ss_extend (int) – Number of bases to extend into splice sites at coding exon boundaries. Default is 0.
prom (int) – Number of bases to extend into promoter region upstream of transcription start. Default is 0.
- Returns:
- List of all transcript regions, potentially
extended based on parameters.
- Return type:
list[BedRegion]
Example
>>> # Basic exonic regions >>> regions = transcript.all_regions() >>> # With splice site extension >>> regions = transcript.all_regions(ss_extend=3) >>> # With promoter region >>> regions = transcript.all_regions(prom=2000)
Note
Promoter extension is strand-aware: extends upstream of the transcription start (before first exon for +, after last for -).
- calc_frames() list[int][source]
Calculate reading frame for each exon.
Computes the codon reading frame (0, 1, or 2) for each exon based on the CDS coordinates and strand orientation.
Frames are computed from the CDS intersected with the exon set, so bases annotated outside the exons – an incomplete stop codon, say – shift no frame.
- Returns:
- Reading frame for each exon. Values are:
0, 1, or 2 for coding exons (bases into current codon)
-1 for non-coding exons or non-coding transcripts
- Return type:
list[int]
Example
>>> frames = transcript.calc_frames() >>> for exon, frame in zip(transcript.exons, frames): ... if frame >= 0: ... print(f"Exon {exon.start}-{exon.stop}: frame {frame}")
Note
Frame calculation is strand-aware and considers exon order. Use update_frames() to set frame attribute on Exon objects.
- cds_len() int[source]
The coding length of the transcript, in base pairs.
The sum of
cds_regions(), so it counts coding sequence only and is 0 for a non-coding transcript.
- cds_regions(ss_extend: int = 0) list[BedRegion][source]
Compute coding sequence (CDS) regions.
Extracts the portions of exons that contain coding sequence, optionally extending into splice sites.
- Parameters:
ss_extend (int) – Number of bases to extend into splice sites at exon boundaries. Default is 0 (no extension).
The CDS is intersected with the exon set first, so the result never leaves an exon. An empty list is returned for a non-coding transcript and for one whose CDS misses its exons entirely.
- Returns:
- List of BedRegion objects representing CDS
segments. Returns empty list for non-coding transcripts.
- Return type:
list[BedRegion]
Example
>>> cds_regions = transcript.cds_regions() >>> for region in cds_regions: ... print(f"{region.chrom}:{region.start}-{region.stop}") >>> # With splice site extension >>> extended = transcript.cds_regions(ss_extend=3)
Note
CDS regions include both start and stop codons where the source annotates them – an incomplete transcript may have neither. Use collect_gtf_cds_regions() from serialization module to exclude the stop codon for GTF format.
- is_coding() bool[source]
Check if this transcript is protein-coding.
The CDS is measured against the exon set, so a transcript whose annotated CDS misses its exons entirely translates nothing and is not coding. This keeps
is_coding()in step withcds_regions(), which callers index into without checking.- Returns:
- True if the transcript has a coding region (CDS),
False for non-coding transcripts.
- Return type:
bool
Example
>>> if transcript.is_coding(): ... cds_len = transcript.cds_len() ... print(f"CDS length: {cds_len}bp")
- test_frames() bool[source]
Verify that exon frames are correctly set.
Compares the frame attribute of each exon with the calculated frame to ensure consistency.
- Returns:
- True if all exon frames match calculated values,
False otherwise.
- Return type:
bool
Example
>>> transcript.update_frames() >>> assert transcript.test_frames()
- total_len() int[source]
The spliced length of the transcript, in base pairs.
The sum of the exon lengths – NOT the genomic span
tx, which includes the introns between them.
- update_frames() None[source]
Update the frame attribute of all exons.
Calculates reading frames using calc_frames() and updates the frame attribute of each Exon object.
Example
>>> transcript.update_frames() >>> for exon in transcript.exons: ... print(f"Exon frame: {exon.frame}")
Note
This modifies the Exon objects in place.
- utr3_len() int[source]
The length of the 3’ untranslated region, in base pairs.
The sum of
utr3_regions(). “3’” is relative to the transcript’s own orientation, so on a-strand transcript this is the lower-coordinate end.
- utr3_regions() list[BedRegion][source]
Get 3’ untranslated region (3’ UTR) segments.
The 3’ UTR extends from the stop codon (translation end) to the transcription end. Strand orientation is considered.
The CDS is intersected with the exon set first, so a CDS overrunning its terminal exon splits the transcript exactly as a flush one would.
- Returns:
- List of 3’ UTR regions. Returns empty list for
non-coding transcripts.
- Return type:
list[BedRegion]
Example
>>> utr3 = transcript.utr3_regions() >>> utr3_length = sum(r.stop - r.start + 1 for r in utr3) >>> print(f"3' UTR: {utr3_length}bp")
Note
For positive strand: regions after CDS end. For negative strand: regions before CDS start.
- utr5_len() int[source]
The length of the 5’ untranslated region, in base pairs.
The sum of
utr5_regions(). “5’” is relative to the transcript’s own orientation, so on a-strand transcript this is the higher-coordinate end.
- utr5_regions() list[BedRegion][source]
Get 5’ untranslated region (5’ UTR) segments.
The 5’ UTR extends from the transcription start to the start codon (translation start). Strand orientation is considered.
The CDS is intersected with the exon set first, so a CDS overrunning its terminal exon splits the transcript exactly as a flush one would.
- Returns:
- List of 5’ UTR regions. Returns empty list for
non-coding transcripts.
- Return type:
list[BedRegion]
Example
>>> utr5 = transcript.utr5_regions() >>> utr5_length = sum(r.stop - r.start + 1 for r in utr5) >>> print(f"5' UTR: {utr5_length}bp")
Note
For positive strand: regions before CDS start. For negative strand: regions after CDS end.