Skip to content

API reference

Generated from the source. Each entry carries the signature, the argument types and the attributes exactly as the code declares them.

The written pages come first. They cover the same calls in the order you would make them: Assembly, Sequences and regions, Annotations and Aligner indexes for working with a genome; Transcription factors, Motifs, Gene identifiers and Homology for the tables the package ships; and CLI overview for the shell. Come here once you know the name you want.

Each section below is one package's public API: what its __init__.py re-exports, and nothing else. A name you can only reach through a submodule path is internal, and it can move or disappear between releases.

Package What it holds
genome Genome, Region, the sequence types, and the results the common calls return
genome.aligner The STAR and chromap index builders
genome.annotation Registering a GTF and asking an annotation for gene ids
genome.assembly The assembly table, the files on disk, and chimera naming
genome.homology Ensembl Compara ortholog sets
genome.store The errors an assembly directory or a prepared set raises
genome.tf The shipped table linking transcription factors to motifs
genome.tf.motif JASPAR matrices, scanning, and reading hit files
genome.xref Identifier conversion and symbol matching

genome

liulab-genome: handling genomic files (metadata, processing, feature extraction).

AnnotationMetadata dataclass

AnnotationMetadata(
    assembly: str,
    name: str,
    provider: str,
    version: str,
    url: str,
    sha256: str | None = None,
    default: bool = False,
)

One annotation the lab supports for one assembly (one row of the annotation table).

Keyed by assembly plus name — the Registered name the annotation is addressed by everywhere — and carrying enough to register it from that name alone: who publishes it, which release, where to fetch it, and what the unpacked GTF that source yields hashes to. A complete record is also what the registration functions accept in place of the table's own row.

Attributes:

Name Type Description
assembly str

The assembly this annotation belongs to — an annotation belongs to exactly one.

name str

The Registered name, unique within the assembly, e.g. "gencode_v50".

provider str

Who publishes it: "GENCODE", "RefSeq", "WormBase", "UCSC".

version str

The provider's own release identifier, e.g. "v50" or "WS298".

url str

Where the GTF is fetched from.

sha256 str or None

Digest of the unpacked GTF, or None when the row pins none — in which case whatever is fetched is recorded but nothing is compared.

default bool

Whether this is the assembly's Default annotation.

Examples:

>>> record = AnnotationMetadata(
...     assembly="sacCer3",
...     name="ensgene_v101",
...     provider="UCSC",
...     version="ensGene.v101",
...     url="https://hgdownload.soe.ucsc.edu/goldenPath/sacCer3/bigZips/genes/sacCer3.ensGene.gtf.gz",
... )
>>> record.provider
'UCSC'
>>> record.default                   # not the default unless the row says so
False

from_row classmethod

from_row(row: Mapping[str, object]) -> AnnotationMetadata

Build a record from one row of an annotation table.

:meth:AssemblyMetadata.from_row for the annotation table's own columns, and the same rules — with one more: default is a flag column, where a blank cell is the real answer no rather than an unknown.

Parameters:

Name Type Description Default
row mapping of str to object

Column name to cell, as :meth:AssemblyMetadata.from_row takes one. Keys outside :data:ANNOTATION_FIELDS are ignored.

required

Returns:

Type Description
AnnotationMetadata

The record the row spells.

Raises:

Type Description
MetadataRowError

As :meth:AssemblyMetadata.from_row raises it, and additionally for a flag cell spelled a way no row spells one.

Examples:

>>> AnnotationMetadata.from_row(
...     {
...         "assembly": "sacCer3",
...         "name": "ensgene_v101",
...         "provider": "UCSC",
...         "version": "ensGene.v101",
...         "url": "https://example.org/sacCer3.ensGene.gtf.gz",
...         "default": "yes",
...     }
... ).default
True

AnnotationRegistry

AnnotationRegistry(
    assembly_dir: AssemblyDir,
    *,
    chrom_sizes: str | Path | None = None,
    default: str | None = None,
)

One Assembly's annotations, the state each is in, and the acts that add one.

An annotation directory is registered, broken, offered but not begun, or nothing at all, and every useful question about one is a question about that four-way state: what may a caller name, what may it be handed the path of, which is the Default annotation, what does a surface print, what does a name nobody registered earn as an error. This settles all four once, at construction, and answers from that — so the state is assembled in one place rather than wherever it is needed.

Bound to one assembly and carried, never re-derived: the Assembly dir comes in as an :class:~genome.assembly.registration.AssemblyDir, so a registry cannot file an annotation somewhere other than where the caller that built it is looking, and the chrom.sizes every GTF is checked against comes in beside it rather than being guessed from the layout.

Reading is cheap and safe: nothing here is created, fetched or built by asking, an assembly with no directory at all answers emptily, and one broken annotation is reported rather than raised over. Only :meth:register and :meth:register_path write, and both fold what they wrote back in, so the four states stay current without reading the disk again.

Parameters:

Name Type Description Default
assembly_dir AssemblyDir

The assembly this registry is for, and where its gtf/ subtree lives.

required
chrom_sizes str or Path

The assembly's chrom.sizes, whose names every registered GTF's must be among. Defaults to the one the layout names, which is what an assembly prepared in place has; a caller that prepared it elsewhere passes the file it actually wrote. A path that is not there is nothing to check against rather than an error — an annotation may be registered before its assembly is.

None
default str

A Default annotation the caller chose, which wins over the table's flag and need not be registered. See :func:default_annotation for the whole rule.

None

Attributes:

Name Type Description
assembly str

The assembly every annotation here belongs to.

Examples:

>>> registry = AnnotationRegistry.locate("sacCer3", "/tmp/definitely-not-an-assembly")
>>> registry.registered
[]
>>> registry.default                       # the table's flag, registered or not
'ensgene_v101'

registered property

registered: list[str]

The Registered names on this machine, in directory-name order.

What is here, as against :attr:offered, which is what the lab supports, and :attr:broken, which is what is here and cannot be trusted.

broken property

broken: list[BrokenAnnotation]

The annotation directories here that cannot be trusted as finished.

What :attr:registered leaves out, and between the two every directory under gtf/ is accounted for. Each entry says what is wrong and names the one command that repairs it.

offered property

offered: list[AnnotationMetadata]

The annotation table's rows for this assembly, in table order.

What the lab supports, whether or not anyone has registered it. Empty for an assembly the table offers nothing for, which is legal: it is a cross-reference rather than an allow-list.

default property

default: str | None

Name of the Default annotation, or None when nothing decides one.

:func:default_annotation's answer for this assembly, settled when the registry was built. It may name an annotation nobody has registered here — the normal state of a fresh machine — so it is :meth:path that says whether one exists. A default already decided is never displaced by a later registration.

locate classmethod

locate(
    assembly: str,
    cache_dir: str | Path | None = None,
    *,
    default: str | None = None,
) -> AnnotationRegistry

Return the registry for assembly, wherever the layout says its files live.

The assembly-addressed way in, and the only one the CLI has: a name and at most a directory override. :meth:~genome.assembly.registration.AssemblyDir.locate is where that override rule lives, and the chrom.sizes is the one that layout names.

Parameters:

Name Type Description Default
assembly str

The assembly to open the registry of, e.g. "hg38".

required
cache_dir str or Path

An explicit Assembly dir, overriding the Data dir layout.

None
default str

A Default annotation the caller chose.

None

Returns:

Type Description
AnnotationRegistry

Its registry. Nothing is created and nothing is fetched.

Examples:

>>> AnnotationRegistry.locate("hg38", "/tmp/definitely-not-an-assembly").registered
[]

path

path(name: str) -> Path

Return the GTF file path of the annotation registered as name.

Parameters:

Name Type Description Default
name str

The Registered name to resolve.

required

Returns:

Type Description
Path

Path to the placed <name>.gtf.

Raises:

Type Description
AnnotationNotRegisteredError

If nothing of that name is registered here. The four-way state decides what the message says next: the command that registers name when the table offers it, the path-based way in when it does not, and — for a directory of that name that is there and broken — the command that registers it again from scratch, so what is named is a command that runs rather than one that raises in turn.

Examples:

>>> registry = AnnotationRegistry.locate("sacCer3", "/data/genome/sacCer3")
>>> registry.path("ensgene_v101")
PosixPath('/data/genome/sacCer3/gtf/ensgene_v101/ensgene_v101.gtf')

register

register(
    name: str,
    *,
    force: bool = False,
    progressbar: bool = True,
    metadata: AnnotationMetadata | None = None,
    check_chromosomes: bool = True,
    disable_infer_genes: bool = True,
    disable_infer_transcripts: bool = True,
) -> GtfAnnotation

Register the annotation the table lists for this assembly as name.

Naming an annotation is enough: where its GTF comes from and which digest it must match are the curated table's to know. The row's URL is fetched into the working area, the unpacked GTF is checked against the sha256 the row pins — so a GTF that is not the pinned one never reaches the annotation directory — the gffutils database is built, and the record is written last.

Its chromosome names are checked too, against this registry's chrom.sizes and while the GTF is still in the working area: every name the GTF uses must be one the assembly carries, so an Ensembl-spelled GTF registered against a UCSC-spelled assembly fails in seconds rather than after the minutes the database build takes. The reverse is not required — an assembly may carry scaffolds the annotation never mentions. An assembly with no chrom.sizes yet has nothing to check against, and the record says so in details["chromosomes_checked"] — with details["chromosomes_unchecked_because"] saying whether that was for want of a chrom.sizes or because the caller stood the check down.

An annotation that already has a valid record is returned silently: nothing is fetched, nothing is rebuilt and nothing is warned about. A directory that cannot be trusted — files with no record, or a record that disagrees with disk — raises, naming genome annotation register <assembly> <name> --force. That is what force=True is: it skips the question, keeps a GTF whose digest can be shown to be the pinned one, and fetches the source again when it cannot.

Parameters:

Name Type Description Default
name str

The Registered name the table lists, e.g. "gencode_v50".

required
force bool

Register again from scratch, repairing a directory that raises.

False
progressbar bool

Show a download progress bar (requires tqdm).

True
metadata AnnotationMetadata

A complete annotation record to use instead of the curated table's row. Omit it and the row is looked up here.

None
check_chromosomes bool

Check the GTF's chromosome names against the assembly's. Pass False to register an annotation whose mismatch you have inspected and accept; the record then says the check was stood down, rather than merely that it did not run.

True
disable_infer_genes bool

Do not reconstruct gene features from exon lines.

True
disable_infer_transcripts bool

Do not reconstruct transcript features from exon lines.

True

Returns:

Type Description
GtfAnnotation

The registered annotation's name and its two file paths.

Raises:

Type Description
ValueError

If the table lists no annotation name for this assembly; the message lists what it does offer and points at the path-based form for an unlisted GTF.

ChromosomeMismatchError

If the GTF names sequences the assembly does not carry; the message lists them and names the usual cause.

ChecksumMismatchError

If the row pins a sha256 and the unpacked GTF is not it; the message names both digests.

UnfinishedRegistrationError

If the annotation's directory holds files but no record.

RegistrationMismatchError

If its record disagrees with what is on disk.

Examples:

>>> AnnotationRegistry.locate("sacCer3").register(
...     "ensgene_v101"
... )
GtfAnnotation(name='ensgene_v101', ...)

register_path

register_path(
    gtf: str | Path,
    name: str,
    *,
    force: bool = False,
    check_chromosomes: bool = True,
    disable_infer_genes: bool = True,
    disable_infer_transcripts: bool = True,
) -> GtfAnnotation

Register the GTF at gtf under name and build its gffutils database.

The escape hatch for an annotation the curated table does not list — :meth:register is the way in for one it does. A gzipped (.gz) source is decompressed into the registered <name>.gtf; a plain GTF is copied as-is. The digest recorded is of the placed GTF, since an unlisted annotation has no pinned digest to compare against.

Its chromosome names are checked against this registry's chrom.sizes before anything is created, so a GTF that does not line up leaves the annotation directory exactly as it was found. Knowing the assembly is what buys that: the file is found rather than passed, so an unlisted GTF is held to the same check a listed one gets.

Registering something already registered returns it silently, and a directory that cannot be trusted raises naming genome annotation register-gtf <assembly> <gtf> <name> --force, exactly as :meth:register does for a listed one.

Parameters:

Name Type Description Default
gtf str or Path

Path to the source GTF, plain or .gz.

required
name str

The Registered name to address it by, unique within the assembly.

required
force bool

Register again from scratch — the repair for a directory that raises.

False
check_chromosomes bool

Check the GTF's chromosome names against the assembly's. Pass False to register a GTF whose mismatch you have inspected and accept.

True
disable_infer_genes bool

Do not reconstruct gene features from exon lines.

True
disable_infer_transcripts bool

Do not reconstruct transcript features from exon lines.

True

Returns:

Type Description
GtfAnnotation

The registered annotation's name and its two file paths.

Raises:

Type Description
FileNotFoundError

If gtf is not a file.

ChromosomeMismatchError

If the GTF names sequences the assembly does not carry.

RegistrationError

If the annotation's directory cannot be trusted as finished.

Examples:

>>> AnnotationRegistry.locate("sacCer3").register_path(
...     "custom.gtf.gz", "custom"
... )
GtfAnnotation(name='custom', ...)

status

status() -> AnnotationStatus

Report what this assembly's table offers against what is registered here.

Two questions with two answers, joined for one reader: the table's rows say what the lab supports, the disk says what is on this machine, and every row carries which of the two it is. The command behind it is genome annotation list.

A third answer rides along, because this is where a reader would look for it: a directory that cannot be trusted is broken rather than registered. Nothing raises — reporting a broken annotation is the point, and one of them must not cost the rest.

Returns:

Type Description
AnnotationStatus

The assembly, its directory, the Default annotation's name, and one :class:AnnotationStatusRow per name — the offered ones in table order, followed by anything on this disk that no row lists.

Examples:

>>> here = AnnotationRegistry.locate("sacCer3", "/tmp/definitely-not-an-assembly")
>>> here.status().default_annotation
'ensgene_v101'

gene_list

gene_list(
    category: str, name: str | None = None
) -> GeneList

Return the genes one registered annotation puts in category.

The genes come from the Curated gene list shipped for that annotation and never from the GTF's own biotype attribute, which is spelled two ways across four publishers, carries three taxonomies that do not agree, and is absent altogether from some annotations. Nothing here knows a category vocabulary: which categories exist is what the curated list declares.

The annotation must be registered here — it is resolved through :meth:path, so an unregistered name earns the error that names the command registering it. The curated list is then held to the assembly it was curated against, since a name is unique only within its assembly and a list found by name alone is not yet known to be about this reference.

For a Merged annotation the record's merged_from says who contributed, and each contributor's own curated list answers for its own Component: the result carries one source per contributor that declares the category, so a caller counting worm ribosomal RNA can drop the E. coli entry. A contributor that does not declare it is simply absent — a bacterium has no mitochondria, and that is not a failure.

There is no empty answer. An annotation nothing ships a list for, and one whose list does not declare this category, are different facts and each raises an error of its own.

Parameters:

Name Type Description Default
category str

The Gene category to ask for, as the curated list spells it — "rRNA", "Mt_rRNA".

required
name str

The Registered name to ask about. Omitted, this assembly's Default annotation answers.

None

Returns:

Type Description
GeneList

The category, its gene ids, and one :class:GeneListSource per contributing curated list.

Raises:

Type Description
ValueError

If name is omitted and no Default annotation is decided; the message names the argument that chooses one.

AnnotationNotRegisteredError

If nothing of that name is registered here.

NoGeneCategoriesError

If no curated list ships for that annotation — nothing can be asked of it, which is not the same answer as its having no genes in this category.

GeneCategoryNotDeclaredError

If it declares categories and not this one; the message lists the ones it does.

GeneListAssemblyMismatchError

If the curated list found under that name was curated against another assembly, in which case it must not answer here.

Examples:

>>> registry = AnnotationRegistry.locate("ce11")
>>> registry.gene_list("rRNA").gene_ids[:2]
['WBGene00004512', 'WBGene00004513']
>>> [source.component for source in registry.gene_list("rRNA").sources]
[None]

gene_lists

gene_lists(name: str | None = None) -> tuple[GeneList, ...]

Return every Gene category one registered annotation declares.

:meth:gene_list for all of them at once, in the order the curated lists spell them — and for a Merged annotation, each contributor's own order, contributors first-listed first. Everything :meth:gene_list says about resolution, the assembly guard and attribution holds here.

Never an empty tuple. An annotation that declares nothing raises rather than answering emptily, which is the whole distinction this surface exists to keep: a caller that got () could not tell no categories are declared from every category is empty, and no declared category is ever empty.

Parameters:

Name Type Description Default
name str

The Registered name to ask about. Omitted, this assembly's Default annotation answers.

None

Returns:

Type Description
tuple of GeneList

One entry per declared category, in declaration order. Never empty.

Raises:

Type Description
ValueError

If name is omitted and no Default annotation is decided.

AnnotationNotRegisteredError

If nothing of that name is registered here.

NoGeneCategoriesError

If no curated list ships for that annotation.

GeneListAssemblyMismatchError

If a curated list found by name was curated against another assembly.

Examples:

>>> registry = AnnotationRegistry.locate("hg38")
>>> [answer.category for answer in registry.gene_lists()]
['rRNA', 'rRNA_pseudogene', 'Mt_rRNA']

resolve_gene_ids

resolve_gene_ids(
    stems: Iterable[str], name: str | None = None
) -> ResolvedGeneIds

Return the gene ids one registered annotation carries for each Gene id stem.

A stem is a gene id with its version dropped — ENSG00000123456 for ENSG00000123456.7 — which is how every published table keyed by gene arrives, and never how a GENCODE Annotation spells the same gene. This is the crossing: every gene id in the Annotation database is reduced to its own stem, and a stem answers with every gene id that reduced to it. An id carrying no version is its own stem, so an annotation whose ids were never versioned — WormBase's, SGD's — resolves each of its genes to itself and is untouched by an Ensembl-shaped assumption.

Every id, and never a chosen one. One stem naming two gene ids is not a malformed annotation: gencode_v50lift37 has nine such stems, eight of them pseudoautosomal genes carrying a _PAR_Y copy, and a resolver taking the first would hand back the X copy of a Y gene without saying it had chosen. So the answer is a mapping to all of them, ascending.

Nothing is dropped. Stems this annotation carries no gene for come back in :attr:~genome.annotation.stems.ResolvedGeneIds.unresolved, so a caller resolving a few thousand at once can see which of them this annotation does not have rather than counting the answer and wondering.

The annotation must be registered here, and a Merged annotation is read exactly as any other: it has one database of its own, holding both components' gene features under the components' own gene ids — a merge rewrites seqnames and never a gene_id — so a stem naming a gene in each component answers with both, which is the same rule as the pseudoautosomal one and needs no attribution to apply it.

One indexed pass over the database's gene features answers the whole call, however many stems it was handed; nothing reads the GTF, and no annotation is held in memory.

Parameters:

Name Type Description Default
stems iterable of str

The Gene id stems to resolve, in the order they should come back. Repeats are asked once. Pass them all at once: the cost is the pass, not the stem.

required
name str

The Registered name to resolve against. Omitted, this assembly's Default annotation answers.

None

Returns:

Type Description
ResolvedGeneIds

The stems that named gene ids, mapped to every id each names, and the stems that named none.

Raises:

Type Description
ValueError

If name is omitted and no Default annotation is decided; the message names the argument that chooses one.

AnnotationNotRegisteredError

If nothing of that name is registered here.

NoGeneFeaturesError

If its database holds no gene at all — every stem would otherwise resolve to nothing, which is a different fact and must not be reported as this one.

Examples:

>>> registry = AnnotationRegistry.locate("hg19")
>>> answer = registry.resolve_gene_ids(
...     ["ENSG00000182378", "ENSG00000141510"], "gencode_v50lift37"
... )
>>> answer.resolved["ENSG00000182378"]
('ENSG00000182378.14', 'ENSG00000182378.14_PAR_Y')

CuratedGeneListError

Bases: ValueError

A shipped curated gene list cannot be read, so it is not allowed to answer.

A packaging defect and not a caller error: these files ship inside the package, so bad JSON, a missing key, a category with no genes, a gene id in two categories or an annotation field disagreeing with the file name are all faults in what was committed here. A :class:ValueError, because a hand-curated file that says something the format does not is a bad value rather than a broken program.

The message names the file and what is wrong with it, since fixing the file is the only thing anyone can do about it.

Examples:

>>> try:
...     _read_gene_list("{", annotation="mine", origin="mine.curated_gene_list.json")
... except CuratedGeneListError as error:
...     print("mine.curated_gene_list.json" in str(error))
True

GeneCategoryNotDeclaredError

GeneCategoryNotDeclaredError(
    annotation: str,
    assembly: str,
    category: str,
    declared: Iterable[str],
)

Bases: LookupError

The annotation declares categories, and not the one asked for.

The second of the two absences. It is a real answer about a real curated list — the category was not curated for this annotation, which is not the same as its being curated and empty, and no shipped category is ever empty. The message lists the ones it does declare, since those are what a caller may ask for instead.

Parameters:

Name Type Description Default
annotation str

The Registered name that was asked about.

required
assembly str

The Assembly it is registered for.

required
category str

The category that is not declared.

required
declared iterable of str

The categories that are, in the order the curated lists spell them.

required

Attributes:

Name Type Description
annotation str

The name asked about.

assembly str

The assembly it is registered for.

category str

The category nobody declared.

declared tuple of str

The categories that are declared.

Examples:

>>> try:
...     raise GeneCategoryNotDeclaredError("mine", "tiny", "tRNA", ["rRNA"])
... except LookupError as error:
...     print("rRNA" in str(error))
True

GeneList dataclass

GeneList(
    assembly: str,
    annotation: str,
    category: str,
    sources: tuple[GeneListSource, ...],
)

The genes one annotation puts in one Gene category, attributed to their sources.

:meth:AnnotationRegistry.gene_list's answer — what genome annotation gene-list prints and what its --json serializes. There is no empty one: an annotation that declares no categories, and one that declares categories but not this one, each raise a :class:LookupError of their own rather than answering with nothing, so holding one of these means the category was really declared and really has genes in it.

Attributes:

Name Type Description
assembly str

The Assembly asked about.

annotation str

The Registered name asked about — the merged name for a Merged annotation, whose contributors are named in :attr:sources.

category str

The Gene category, as the curated lists spell it.

sources tuple of GeneListSource

One entry per contributing Curated gene list, in contributor order. Never empty. A contributor that does not declare this category is simply absent — a bacterium has no mitochondria, and that is not a failure to report.

Examples:

>>> genes = GeneList(
...     assembly="ce11",
...     annotation="wormbase_ws298",
...     category="rRNA",
...     sources=(
...         GeneListSource(None, "wormbase_ws298", "rRNA genes", "WormBase", ("a", "b")),
...     ),
... )
>>> genes.gene_ids
['a', 'b']
>>> genes.as_json()["category"]
'rRNA'

gene_ids property

gene_ids: list[str]

Every source's gene ids, concatenated in source order — a fresh list each call.

Concatenated and not de-duplicated. A merge rewrites only the seqname and never the gene_id, so two components carrying the same id would be a real ambiguity in the merged annotation, and collapsing it here would hide exactly that — a caller summing over these would silently under-count one of the two. Where that matters, :attr:sources says which contributor each id came from.

as_json

as_json() -> dict[str, Any]

Return this answer as --json serializes it.

Returns:

Type Description
dict

assembly, annotation, category, the concatenated gene_ids, and sources as a list of :meth:GeneListSource.as_json entries. :attr:gene_ids is written out beside the sources it is read from rather than left to the reader: assembling it is where a reader would reach for a set and de-duplicate, which is the one thing this answer must not do.

GeneListAssemblyMismatchError

Bases: CuratedGeneListError

A curated list was asked for by an annotation registered against another assembly.

The list names the Assembly it was curated against, and that is checked rather than assumed: an annotation registered under a name whose curated list belongs to a different reference would otherwise answer with another species' gene ids, and mixing builds is an error and not a warning.

Not one of the two absences: nothing is missing, so a caller catching :class:LookupError for absence does not swallow this.

Examples:

>>> listed = CuratedGeneList("mine", "ce11", {})
>>> try:
...     listed.check_assembly("hg38")
... except GeneListAssemblyMismatchError as error:
...     print("ce11" in str(error) and "hg38" in str(error))
True

NoGeneCategoriesError

NoGeneCategoriesError(
    annotation: str,
    assembly: str,
    curated: Iterable[str],
    *,
    contributors: Iterable[str] = (),
)

Bases: LookupError

Nothing is known about this annotation's categories, so none can be asked of it.

The first of the two absences, and the one a caller must never read as this annotation has no rRNA genes: no curated list ships for it, so the question was not answered rather than answered with nothing. The message says which annotations do declare categories and that answering for this one means shipping a curated list.

A :class:LookupError, so a caller may catch it together with :class:GeneCategoryNotDeclaredError and still act differently on each.

Parameters:

Name Type Description Default
annotation str

The Registered name that was asked about — the merged name for a Merged annotation.

required
assembly str

The Assembly it is registered for.

required
curated iterable of str

The annotations a curated list does ship for.

required
contributors iterable of str

For a Merged annotation, the annotations it merges. Shipping a list under the merged name would fix nothing; it is these that need one.

()

Attributes:

Name Type Description
annotation str

The name asked about.

assembly str

The assembly it is registered for.

curated tuple of str

The annotations that do ship a curated list.

contributors tuple of str

The contributing annotations, empty for anything but a merge.

Examples:

>>> try:
...     raise NoGeneCategoriesError("mine", "tiny", ["gencode_v50"])
... except LookupError as error:
...     print("gencode_v50" in str(error))
True

AmbiguousDefaultAnnotationError

Bases: ValueError

A component carries several annotations and nothing says which one a chimera takes.

A Component contributes its own Default annotation, and a component with several registered and none flagged by the annotation table has no default at all — which is the ordinary, deliberate answer to pick one for me everywhere else in the package, and the one place it cannot stand. Guessing would put a set of gene models into a merged annotation nobody chose, under a name that would look identical to the one the caller meant.

The message names default_gtf=, which is how a caller says which, and it says so of that component rather than of the chimera: the fix is one argument on one component's constructor.

A :class:ValueError because the component handed in is not one this build can use.

Examples:

>>> raise AmbiguousDefaultAnnotationError("ce11 carries 2 annotations ...")
Traceback (most recent call last):
genome.assembly.chimera_build.AmbiguousDefaultAnnotationError: ce11 carries 2 annotations ...

Genome

Genome(
    assembly: str,
    *,
    path_or_url: str | Path | None = None,
    cache_dir: str | Path | None = None,
    progressbar: bool = True,
    metadata: AssemblyMetadata | None = None,
    default_gtf: str | None = None,
)

Bases: AlignerMixin, MotifScanMixin

A reference genome and the operations over it.

Constructing a Genome ensures the assembly's reference files exist locally: the FASTA is fetched from the Source its metadata row pins — UCSC for most assemblies, WormBase or NCBI for others — or, for an assembly no row lists, from a URL derived from the UCSC golden path. Its .fai index, .2bit encoding and chrom.sizes are then prepared, and a completion record is written recording what was done. Everything lands under <LIULAB_DATA>/genome/<assembly>/ (see :func:~genome.assembly.download.assembly_data_dir). A later construction reads that record, confirms every file it claims is present at the size it claims, and opens the .2bit — so it is instant and works offline. Nothing is downloaded twice.

Sequence is read from the .2bit file via py2bit; coordinates are 0-based, half-open throughout.

Parameters:

Name Type Description Default
assembly str

Assembly name, e.g. "sacCer3", "hg38", "mm39". A free-form local key, not necessarily a UCSC one — "ecHT115" is a reference UCSC has never carried. When path_or_url is omitted and no row pins a source, the FASTA is downloaded from UCSC and the name is validated against UCSC first, so a typo fails fast; a pinned source is the source, so there is nothing to guess and that check is skipped. When path_or_url is given, the name only labels the cache directory and files; UCSC is not contacted.

required
path_or_url str or Path

Seed the assembly from your own FASTA instead of downloading from UCSC — either a local file path (copied into the cache) or an http(s)/ftp/sftp URL (fetched with pooch). Gzipped (.gz) sources are decompressed. Useful when UCSC is unreachable (firewall/proxy) or for a custom reference. See :meth:~genome.assembly.download.UCSCGenomeDownloader.fetch_genome_from.

None
cache_dir str or Path

Override the storage directory for this assembly's files. Defaults to the shared per-assembly reference directory.

None
progressbar bool

Show a download progress bar on first fetch (requires tqdm).

True
metadata AssemblyMetadata

A complete metadata record, used instead of the curated table's row for assembly. All-or-nothing: pass a record and every identifier comes from it; omit it and every identifier comes from the table — or is unknown when the table does not list assembly, which is legal, since the table is a cross-reference rather than an allow-list.

None
default_gtf str

Name of the annotation to serve as :attr:default_gtf, overruling the one the annotation table flags. It need not be registered yet — see :attr:default_gtf_path.

None

Attributes:

Name Type Description
assembly str

The assembly name.

files GenomeFiles

Paths to the prepared FASTA and its derived index/companion files.

metadata AssemblyMetadata

The assembly's metadata record, and always a record: the one passed in, else the curated table's row, else — for an assembly the table does not list — one carrying the name with every identifier unknown. Read a field off it (genome.assembly.metadata.species) without asking whether it is there. It is also what says where this assembly's FASTA is fetched from and which checksum it must match. Whether the table lists this assembly at all is a different question, and :func:~genome.assembly.metadata.lookup_assembly's.

Raises:

Type Description
ValueError

If assembly is unknown to UCSC.

RegistrationError

If the assembly's directory holds a registration that cannot be trusted — files with no record (an interrupted run), or a record that disagrees with what is on disk. The message names the file and genome assembly register <assembly> --force, which repairs it. An absent or empty directory is not this: that is a fresh registration.

ToolNotFoundError

If a required native tool (samtools, faToTwoBit, twoBitInfo) is not on PATH.

Examples:

>>> sacCer3 = Genome("sacCer3")
>>> sacCer3.fetch_sequence("chrIV:0-10")
DNA('ACACCACACC')

default_gtf property

default_gtf: str | None

Name of this genome's Default annotation, or None when there is none.

The default_gtf argument if one was given, else the annotation the table flags for this assembly, else the sole registered annotation, else None. It names an annotation that may not be registered here, which on a fresh machine is the normal state and not an error — :attr:default_gtf_path is where it has to exist.

annotations property

annotations: AnnotationRegistry

The assembly's annotations: what is registered, broken, offered, or nothing.

The whole collection surface behind one name. Every question about several annotations is asked of the registry — .registered for what is on this machine, .broken for what is here and cannot be trusted, .offered for what the lab supports, .path(name) for where one is, .register(name) and .register_path(gtf, name) for the two acts that add one. The everyday read is the default annotation, which :attr:default_gtf and :attr:default_gtf_path answer directly.

It is deliberately not a list: a registry settles a four-way state, and iterating or measuring it would silently privilege one of the four. There is no len(), no in, and no iteration — name the set you mean.

Examples:

>>> sacCer3 = Genome("sacCer3")
>>> sacCer3.annotations.registered
['ensgene_v101']
>>> [broken.name for broken in sacCer3.annotations.broken]
[]

default_gtf_path property

default_gtf_path: Path | None

GTF file path of the Default annotation, or None when there is no default.

Where the default stops being an intention and has to exist. :attr:default_gtf may name an annotation nobody has registered on this machine — the table's choice, on a machine that has not fetched it yet, or one a caller named at construction ahead of registering it — and asking for its path is what says so, naming the command that registers it, or the one that repairs it when a broken directory of that name is there. None means no default was decided at all, which is a different answer from one that is not registered.

Raises:

Type Description
AnnotationNotRegisteredError

If the default annotation is not registered here.

Examples:

>>> sacCer3 = Genome("sacCer3")
>>> sacCer3.default_gtf_path
PosixPath('/data/genome/sacCer3/gtf/ensgene_v101/ensgene_v101.gtf')

assembly_dir property

assembly_dir: AssemblyDir

The Assembly dir this genome was opened in, and the layout inside it.

Where everything tied to this assembly lives — its own files, the gtf/ subtree its annotations are filed under, the index/ subtree its indexes are built into. Public because it is what an Index is derived from: an index belongs inside the assembly it indexes, and asking the genome is the only way to get the directory this genome was actually opened in rather than the one the Data dir layout would name for its assembly.

Examples:

>>> sacCer3 = Genome("sacCer3")
>>> sacCer3.assembly_dir.index_dir("chromap")
PosixPath('/data/genome/sacCer3/index/chromap')

fasta_path property

fasta_path: Path

Path to the reference FASTA file.

twobit_path property

twobit_path: Path

Path to the .2bit encoding of the reference.

chrom_sizes_path property

chrom_sizes_path: Path

Path to the chrom.sizes file (<name>\t<length> per sequence).

chrom_sizes property

chrom_sizes: Series

Chromosome lengths as a pandas Series (a defensive copy).

Integer lengths indexed by chromosome name, in reference order.

chromosomes property

chromosomes: list[str]

Chromosome names, in the order the reference declares them.

components property

components: list[str] | None

The Component assembly names this is a Chimera of, or None.

The single test of whether an assembly is a chimera, and the completion record is what answers it — never the metadata table, which lists a chimera as a cross-reference and would answer the same question differently on a machine where the row is stale or absent.

Sorted, which for a chimera is the order its own derived name spells them in. None — not an empty list — for an ordinary assembly: it is not a chimera of nothing, it is not a chimera.

Examples:

>>> chimera = Genome.chimera(Genome("ce11"), Genome("ecHT115"))
>>> chimera.components
['ce11', 'ecHT115']
>>> Genome("ce11").components is None
True

separator property

separator: str | None

The underscore run this Chimera's chromosome names are suffixed with, or None.

Read off the completion record, which is the only honest answer: the separator belongs to one chimera, since a component whose own names carry __ forces a longer run. A caller that assumed the default would split NZ_TINY02__000002.1___tinyEcDub in the wrong place and hand the chromosome to the wrong component. None — not the default — for an assembly that is not a chimera, mirroring :attr:components.

Hand it to :func:~genome.assembly.chimera.split_suffixed to read a name back.

Examples:

>>> chimera = Genome.chimera(Genome("ce11"), Genome("ecHT115"))
>>> chimera.separator
'__'
>>> Genome("ce11").separator is None
True

component_annotations property

component_annotations: dict[str, str | None] | None

What each Component contributed to the Merged annotation, or None.

The Registered name per component, in the sorted-component order :attr:components uses, and None for a component that contributed nothing — a distinction that decides which annotation a per-component count is taken against, so it is read off the record rather than from Genome(component).default_gtf: a component's default now is not necessarily what went into this merge. None — not an empty mapping — for an assembly that is not a chimera, mirroring :attr:components.

Examples:

>>> chimera = Genome.chimera(Genome("ce11"), Genome("ecHT115"))
>>> chimera.component_annotations
{'ce11': 'wormbase_ws298', 'ecHT115': 'refseq_rs_2025_06_26'}
>>> Genome("ce11").component_annotations is None
True

chrom_components property

chrom_components: Series

Which assembly each chromosome came from, as a Series mirroring :attr:chrom_sizes.

Attribution, and total: every chromosome this reference carries gets an answer. For a chimera each name is split at its recorded separator, so the answer is read out of the name itself and no mapping was ever stored; for an assembly that is not a chimera every chromosome maps to that assembly's own name, which is true rather than merely convenient — and it is what leaves :attr:components as the single is-chimera test, since no caller has to read this one to find out.

Returns:

Type Description
Series

Component assembly name per chromosome, indexed and ordered exactly as :attr:chrom_sizes is.

Examples:

>>> chimera = Genome.chimera(Genome("ce11"), Genome("ecHT115"))
>>> chimera.chrom_components["I__ce11"]
'ce11'
>>> Genome("ce11").chrom_components["I"]
'ce11'

chimera classmethod

chimera(
    *components: Genome,
    cache_dir: str | Path | None = None,
    force: bool = False,
) -> Self

Build a Chimera of components and return it open, like any other genome.

A second constructor rather than a second type: the reference it produces is an assembly, so everything an assembly can do — fetch sequence, register an annotation, build an index — it can do, by one code path and not two. Its FASTA is its components' bytes with every chromosome name suffixed by the component it came from; see :mod:genome.assembly.chimera_build for how it is written.

The name is derived, never given — the component names sorted and joined by _, so ce11 and ecHT115 in either order build and reopen the one ce11_ecHT115. A chimera whose record says it finished is opened without rewriting anything; a directory that cannot be trusted raises and names genome assembly register <name> --force, which is what force is.

Nothing is fetched, and no annotation argument is needed or accepted: each component carries its own :attr:default_gtf, so a caller's (assembly, gtf) pairs split at the door and the annotation half travels with the components. Those defaults are merged in the same act, registered under the +-join of their names in sorted-component order — so a built chimera arrives annotated and force repairs the annotation and the FASTA together. A component with no annotation contributes nothing, and components that contribute nothing between them leave the chimera with no annotation rather than an empty one; a component whose default is named but not registered here, or which has several registered and no default, raises before anything is written.

Parameters:

Name Type Description Default
*components Genome

Two or more prepared component assemblies, in any order, each given once. None may itself be a chimera — a Component is always a canonical assembly, so nesting is forbidden by the model rather than deferred.

()
cache_dir str or Path

Override the directory the chimera is built and opened in, exactly as the constructor's cache_dir does for an ordinary assembly. Defaults to the shared per-assembly reference directory, under the derived name.

None
force bool

Build again from scratch — the repair for a directory that raises.

False

Returns:

Type Description
Genome

The chimera, opened under its derived name.

Raises:

Type Description
ChimeraNamingError

If fewer than two components are given, a component repeats, a component's name is not alphanumeric, a component is itself a chimera, or a component's FASTA carries a header that names no sequence for the suffix to ride on.

AnnotationNotRegisteredError

If a component's default annotation is named but not registered here; the message names the command that registers it.

AmbiguousDefaultAnnotationError

If a component carries several annotations and none is its default; the message names default_gtf=.

RegistrationError

If the chimera's directory holds a build that cannot be trusted as finished, or the FASTA just built does not carry the sequences its components predict.

ChromosomeMismatchError

If the merged annotation names a sequence the built FASTA does not carry.

ToolNotFoundError

If samtools, faToTwoBit or twoBitInfo are not on PATH.

Examples:

>>> worm, food = Genome("ce11"), Genome("ecHT115")
>>> chimera = Genome.chimera(worm, food)
>>> chimera.assembly
'ce11_ecHT115'
>>> chimera["I__ce11:0-10"]
DNA('GCCTAAGCCT')
>>> chimera.default_gtf
'wormbase_ws298+refseq_rs_2025_06_26'

gene_list

gene_list(
    category: str, annotation: str | None = None
) -> GeneList

Return the genes one of this genome's annotations puts in category.

The everyday way in to a Gene category: name the category and get the gene ids in it, with one source per contributing Curated gene list so a Chimera's genes stay attributable to the component they came from. Which categories exist is a property of the annotation rather than of this package — ask :meth:gene_lists what this one declares.

Nothing here answers with an empty collection. An annotation no curated list ships for and one whose list does not declare this category raise different errors, because a caller acts differently on nothing can be asked of this annotation and this category was not curated for it — and neither means the annotation has no such genes.

Parameters:

Name Type Description Default
category str

The Gene category, as the curated list spells it — "rRNA".

required
annotation str

The Registered name to ask about. Omitted, :attr:default_gtf answers.

None

Returns:

Type Description
GeneList

The category, its gene ids, and what contributed them.

Raises:

Type Description
ValueError

If annotation is omitted and this genome has no Default annotation.

AnnotationNotRegisteredError

If that annotation is not registered here.

NoGeneCategoriesError

If no curated gene list ships for it.

GeneCategoryNotDeclaredError

If it declares categories and not this one.

Examples:

>>> worm = Genome("ce11")
>>> worm.gene_list("rRNA").category
'rRNA'
>>> len(worm.gene_list("rRNA").gene_ids)
22

gene_lists

gene_lists(
    annotation: str | None = None,
) -> tuple[GeneList, ...]

Return every Gene category one of this genome's annotations declares.

:meth:gene_list for all of them at once, in declaration order — the way to find out what may be asked for, since the categories are the curated list's to declare and differ between annotations. Never an empty tuple: an annotation that declares none raises instead, so declares nothing is never read as declares empty categories.

Parameters:

Name Type Description Default
annotation str

The Registered name to ask about. Omitted, :attr:default_gtf answers.

None

Returns:

Type Description
tuple of genome.annotation.registry.GeneList

One entry per declared category. Never empty.

Raises:

Type Description
ValueError

If annotation is omitted and this genome has no Default annotation.

AnnotationNotRegisteredError

If that annotation is not registered here.

NoGeneCategoriesError

If no curated gene list ships for it.

Examples:

>>> human = Genome("hg38")
>>> [answer.category for answer in human.gene_lists()]
['rRNA', 'rRNA_pseudogene', 'Mt_rRNA']

tf_gene_list

tf_gene_list(
    annotation: str | None = None,
    *,
    include_rejected: bool = False,
) -> TFGeneList

Return the genes a published census judges transcription factors in this genome.

The everyday way to a TF gene list: the census shipped for this assembly's own species, resolved into one registered annotation's gene ids, so the answer joins to a counts matrix with nothing left to normalise. Assessed-positive by default, with the census's DBD family and every judgement it recorded on each gene, and the provenance that says whose verdict it is — nothing here decides what a transcription factor is.

The species is this assembly's, read from its metadata row and never passed in, so asking for another species' factors is not expressible. An assembly whose species has no census, and one nothing names a species for, raise rather than answering with nothing.

Parameters:

Name Type Description Default
annotation str

The Registered name to answer in the gene ids of. Omitted, :attr:default_gtf answers.

None
include_rejected bool

Carry the genes the census assessed and turned down as well, each saying so.

False

Returns:

Type Description
TFGeneList

The genes, the census's provenance, and the stems that resolved to nothing.

Raises:

Type Description
UnknownSpeciesError

If nothing names this assembly's species.

NoTFCensusError

If no census ships for that species; the message names the ones that do.

ValueError

If annotation is omitted and this genome has no Default annotation.

AnnotationNotRegisteredError

If that annotation is not registered here.

NoGeneFeaturesError

If its database holds no gene at all.

Examples:

>>> human = Genome("hg38")
>>> answer = human.tf_gene_list()
>>> len(answer.genes), len(answer.unresolved)
(1638, 1)
>>> print(answer.provenance.attribution())
Lambert et al. 2018 v_1.01 (PMID 29425488) — https://humantfs.ccbr.utoronto.ca/...

tf_cofactor_list

tf_cofactor_list(
    annotation: str | None = None,
) -> TFCofactorList

Return the genes a publisher lists as transcription cofactors in this genome.

The everyday way to a TF cofactor list, and the counterpart of :meth:tf_gene_list in the same shape: the Cofactor table shipped for this assembly's own species, resolved into one registered annotation's gene ids, so the answer joins to a counts matrix with nothing left to normalise. Each entry carries which publisher listed the gene and that publisher's own classification of it — membership is this package's and classification is theirs.

A cofactor recognises no sequence of its own and so has no motif; this says which genes are cofactors, not what they bind, and nothing here ranks one above another.

The species is this assembly's, read from its metadata row and never passed in. An assembly whose species has no cofactor table, and one nothing names a species for, raise rather than answering with nothing. Worm has a table although no TF census covers it, so a worm genome answers here and raises from :meth:tf_gene_list.

Parameters:

Name Type Description Default
annotation str

The Registered name to answer in the gene ids of. Omitted, :attr:default_gtf answers.

None

Returns:

Type Description
TFCofactorList

The cofactors, the publishers' provenance, and the stems that resolved to nothing.

Raises:

Type Description
UnknownSpeciesError

If nothing names this assembly's species.

NoCofactorTableError

If no cofactor table ships for that species; the message names the ones that do.

ValueError

If annotation is omitted and this genome has no Default annotation.

AnnotationNotRegisteredError

If that annotation is not registered here.

NoGeneFeaturesError

If its database holds no gene at all.

Examples:

>>> mouse = Genome("mm39")
>>> answer = mouse.tf_cofactor_list()
>>> len(answer.cofactors), len(answer.unresolved)
(968, 2)
>>> print(answer.provenance.attribution())
AnimalTFDB 4.0 (PMID 36268869) — https://guolab.wchscu.cn/...

__repr__

__repr__() -> str

Return e.g. Genome('sacCer3', 17 sequences).

close

close() -> None

Release the open 2bit file handle (idempotent).

__enter__

__enter__() -> Self

Return self for use as a context manager.

__exit__

__exit__(
    exc_type: type[BaseException] | None,
    exc: BaseException | None,
    tb: TracebackType | None,
) -> None

Close the 2bit handle on context-manager exit.

fetch_sequence

fetch_sequence(region: str | Region) -> DNA

Return the reference sequence for region as a :class:~genome.seq.DNA.

Parameters:

Name Type Description Default
region str or Region

Either a locus string chrom:start-end with 0-based, half-open coordinates (chr1:0-10 is the first ten bases; thousands separators tolerated), a bare chrom for the whole sequence, or a :class:~genome.region.Region. When a Region carries strand "-", the reverse complement is returned.

required

Returns:

Type Description
DNA

The sequence, with soft-masking case preserved. May contain N runs where the reference is unknown.

Raises:

Type Description
ValueError

If region is malformed, names an unknown chromosome, or its coordinates fall outside [0, chromosome length]. Against a chimera a bare chromosome name is one of the unknown ones, and the message names the suffixed spellings that do resolve.

Examples:

>>> genome = Genome("sacCer3")
>>> genome.fetch_sequence("chrIV:0-10")
DNA('ACACCACACC')

__getitem__

__getitem__(region: str | Region) -> DNA

Index by locus string or :class:~genome.region.Region — sugar for :meth:fetch_sequence.

AssemblyMetadata dataclass

AssemblyMetadata(
    assembly_name: str,
    species: str | None,
    ucsc_name: str | None,
    ncbi_name: str | None,
    ncbi_assembly_id: str | None,
    ncbi_taxid: int | None,
    source_url: str | None = None,
    sha256: str | None = None,
    intron_length_cap: int | None = None,
    intron_length_cap_rationale: str | None = None,
)

Identifiers for one reference assembly (one row of the metadata table).

The single declaration of what an assembly's metadata consists of: the table is parsed through these fields, and a complete record is what :class:~genome.assembly.genome.Genome accepts in place of the table's own row.

Only assembly_name is required. Every other column may be left blank, and a blank cell reads back as None rather than as text: the table fills in over time, and a freshly prepared assembly pins its source and digest well before anyone supplies its species, its UCSC and NCBI names or its taxonomy id.

source_url and sha256 are what makes preparing an assembly reproducible. source_url pins where its FASTA is fetched from, so nothing has to be derived or guessed; sha256 pins the digest of the unpacked FASTA that source yields — not of the compressed archive it arrives in, so a copy taken from a mirror or recompressed elsewhere still matches. A row with no digest is unverified rather than wrong.

intron_length_cap is the longest gap a spliced aligner should take for an intron on this assembly, and intron_length_cap_rationale says why that number and not another. It is a deliberately loose round number set by hand, never computed from an annotation — an annotation catalogues the transcripts someone observed, so its longest intron is a floor on what the organism does rather than a ceiling on it. Nothing in this package reads either field: they are curated here so that a consumer choosing aligner parameters reads a fact about the assembly from the same row as its identifiers. A blank cap is an assembly nobody has characterised, which is legal and says no bound has been chosen — the reading that leaves such an assembly aligning exactly as it did before.

A Chimera's row pins neither, and that is deliberate rather than pending: its bytes are not fetched from anywhere, and they are derived by a pure function from components whose own rows are pinned, so it is proven transitively. The table pins what was downloaded; what was derived is pinned by a test, where a change in this package's own concatenation code belongs. Such a row carries its name and nothing else — its identifiers are its components', reachable through :attr:~genome.assembly.genome.Genome.chrom_components — and it exists so that a machine holding none of them can still tell a chimera's name from a local key someone chose.

ucsc_name is blank permanently rather than pending in some rows, for the same reason the assembly id is a local key rather than a UCSC one: the lab supports references UCSC has never carried, and such a row simply has no name in that namespace to give.

Examples:

>>> record = AssemblyMetadata(
...     "hg38", "Homo sapiens", "hg38", "GRCh38", "GCF_000001405.40", 9606
... )
>>> record.species
'Homo sapiens'
>>> record.sha256 is None            # nothing pinned unless the row says so
True

unknown classmethod

unknown(assembly_name: str) -> AssemblyMetadata

Return the record for an assembly the table does not list: the name, and nothing else.

What unlisted looks like as a record rather than as a missing one. Every identifier is genuinely unknown, which a blank cell already means everywhere else, so a caller reads a field and gets None instead of first asking whether there is a record to read it off. It is exactly the line :func:format_table_row emits for an assembly nobody has curated yet.

The name is the local key the caller asked for, since that is the only identifier an unlisted assembly has — the assembly id is a local key and the table is a cross-reference rather than an allow-list. This answers what is known about this assembly; whether the table lists it at all is :func:lookup_assembly's question and stays a separate one.

Parameters:

Name Type Description Default
assembly_name str

The assembly the record is for.

required

Returns:

Type Description
AssemblyMetadata

A record carrying assembly_name with every other field None.

Examples:

>>> record = AssemblyMetadata.unknown("my_ref")
>>> record.assembly_name
'my_ref'
>>> record.species is None and record.sha256 is None
True

from_row classmethod

from_row(row: Mapping[str, object]) -> AssemblyMetadata

Build a record from one row of a metadata table.

The reader half of the register-then-paste flow :func:format_table_row writes: a row is a mapping of column name to cell, which is how the shipped TSV is read and how :func:dataclasses.asdict of a record spells one. Each column is parsed by its own declared type, and a blank cell — empty, absent, or the NaN pandas reads a blank as — means unknown, which only a column that has an unknown takes.

Parameters:

Name Type Description Default
row mapping of str to object

Column name to cell. A cell is the table's own text, but a value already of the column's type is taken as it stands, so a record's fields are a row. Keys outside :data:METADATA_FIELDS are ignored.

required

Returns:

Type Description
AssemblyMetadata

The record the row spells.

Raises:

Type Description
MetadataRowError

If a cell cannot be read as its column's type, or a column that has no unknown is blank. The record is built from parsed cells or not at all, so a caller is handed a whole record or an error naming the column — never a record carrying the columns that happened to come before the bad one.

Examples:

>>> row = {"assembly_name": "sacCer3", "ncbi_taxid": "559292"}
>>> AssemblyMetadata.from_row(row).ncbi_taxid
559292
>>> AssemblyMetadata.from_row(row).species is None   # blank is unknown
True

ToolNotFoundError

Bases: RuntimeError

Raised when an External tool cannot be located on PATH.

The message is the tool's :meth:ExternalTool.install_instructions, so the next action is in the exception rather than somewhere the caller has to go and look.

Examples:

>>> from genome import ToolNotFoundError
>>> try:
...     raise ToolNotFoundError("samtools is not on PATH. Install `samtools`.")
... except ToolNotFoundError as missing:
...     print(missing)
samtools is not on PATH. Install `samtools`.

Region dataclass

Region(chrom: str, start: int, end: int, strand: str = '.')

A single genomic interval in 0-based, half-open coordinates.

Parameters:

Name Type Description Default
chrom str

Chromosome / sequence name.

required
start int

0-based start, inclusive. Must be >= 0.

required
end int

0-based end, exclusive (half-open). Must be >= start.

required
strand str

"+", "-", or "." (unknown). Never silently defaulted to a real strand.

``"."``

Raises:

Type Description
ValueError

If start < 0, end < start, or strand is not one of "+", "-", ".".

Examples:

>>> r = Region("chr1", 0, 10)
>>> len(r), r.length
(10, 10)
>>> Region.from_string("chr2:100-200", strand="-")
Region(chrom='chr2', start=100, end=200, strand='-')

length property

length: int

Number of bases spanned, end - start (alias of len(self)).

__post_init__

__post_init__() -> None

Validate the coordinate and strand invariants.

__len__

__len__() -> int

Return the number of bases spanned, end - start.

__str__

__str__() -> str

Return the 0-based locus string chrom:start-end (as stored).

from_string classmethod

from_string(text: str, *, strand: str = '.') -> Region

Build a :class:Region from a 0-based chrom:start-end string.

Parameters:

Name Type Description Default
text str

A chrom:start-end locus in 0-based half-open coordinates.

required
strand str

Strand to attach (the string itself carries no strand).

``"."``

Raises:

Type Description
ValueError

If text is malformed or carries no coordinates (a bare chromosome cannot be sized without a chromosome-length table).

Examples:

>>> Region.from_string("chr1:0-10")
Region(chrom='chr1', start=0, end=10, strand='.')

DNA

Bases: _Seq

A DNA sequence over the four canonical bases.

Alphabet: A, C, G, T (case-insensitive at construction; case preserved in the stored value).

Examples:

>>> DNA("ATCG")
DNA('ATCG')
>>> DNA("aTcG")[1:3]                      # slicing stays typed
DNA('Tc')
>>> DNA("ATCG").reverse_complement()
DNA('CGAT')
>>> DNA("ATCG").transcribe()
RNA('AUCG')
>>> DNA("GGCC").gc_content
1.0

gc_content property

gc_content: float

Fraction of bases that are G or C (case-insensitive); 0.0 if empty.

Examples:

>>> DNA("GGCC").gc_content
1.0
>>> DNA("ATAT").gc_content
0.0
>>> DNA("aTcG").gc_content
0.5
>>> DNA("").gc_content
0.0

complement

complement() -> DNA

Return the Watson-Crick complement (A↔T, C↔G), case preserved.

Examples:

>>> DNA("ATCG").complement()
DNA('TAGC')
>>> DNA("aTcG").complement()
DNA('tAgC')

reverse_complement

reverse_complement() -> DNA

Return the reverse complement (complement, then reverse), case preserved.

Examples:

>>> DNA("ATCG").reverse_complement()
DNA('CGAT')
>>> DNA("aTcG").reverse_complement()
DNA('CgAt')
>>> DNA("").reverse_complement()
DNA('')

transcribe

transcribe() -> RNA

Transcribe to RNA by replacing T/t with U/u.

Examples:

>>> DNA("ATCG").transcribe()
RNA('AUCG')
>>> DNA("aTcG").transcribe()
RNA('aUcG')

RNA

Bases: _Seq

An RNA sequence over the four canonical bases.

Alphabet: A, C, G, U (case-insensitive at construction; case preserved in the stored value).

Examples:

>>> RNA("AUCG").reverse_complement()
RNA('CGAU')
>>> RNA("AUCG").back_transcribe()
DNA('ATCG')

gc_content property

gc_content: float

Fraction of bases that are G or C (case-insensitive); 0.0 if empty.

Examples:

>>> RNA("GGCC").gc_content
1.0
>>> RNA("").gc_content
0.0

complement

complement() -> RNA

Return the complement, case preserved (A↔U, C↔G).

Examples:

>>> RNA("AUCG").complement()
RNA('UAGC')

reverse_complement

reverse_complement() -> RNA

Return the reverse complement, case preserved.

Examples:

>>> RNA("AUCG").reverse_complement()
RNA('CGAU')

back_transcribe

back_transcribe() -> DNA

Reverse-transcribe to DNA by replacing U/u with T/t.

Examples:

>>> RNA("AUCG").back_transcribe()
DNA('ATCG')
>>> RNA("aUcG").back_transcribe()
DNA('aTcG')

MetadataRowError

Bases: ShippedTableError

A row cannot be read as a record, and the message names the column that refused.

Raised by :func:parse_cell for the curated tables that declare their columns as a frozen dataclass — the Assembly metadata and Annotation metadata tables and the Xref source table — for a cell no column's type can read and for a blank cell in a column that has no unknown. Re-exported from :mod:genome.assembly.metadata, which is where those tables live.

Examples:

>>> from genome.assembly.metadata import AssemblyMetadata
>>> try:
...     AssemblyMetadata.from_row({"assembly_name": "tiny", "ncbi_taxid": "many"})
... except MetadataRowError as error:
...     print("ncbi_taxid" in str(error))
True

ShippedTableError

Bases: ValueError

A Shipped table cannot be read, so it is not allowed to answer.

The base of every table's own error class, so a caller may catch one kind of defect across all of them and still tell a census from a Cofactor table by the class that was raised. A :class:ValueError, because a file that says something the format does not is a bad value rather than a broken program — and never a :class:LookupError, which is what absence is spelled with, so catching an absent table cannot swallow a broken one.

Examples:

>>> from genome.tf.gene import TFGeneTableError
>>> issubclass(TFGeneTableError, ShippedTableError)
True

NoCofactorTableError

NoCofactorTableError(
    assembly: str, species: str, shipped_for: Iterable[str]
)

Bases: LookupError

Nobody has listed this Assembly's species' cofactors, so no table can answer.

:class:NoTFCensusError's counterpart on the cofactor half, and the same kind of fact: nobody has published a cofactor list for this species, which is about the literature and not about the genome, and must never be read as this species has no transcription cofactors. Yeast and E. coli land here today.

Worm does not, although :class:NoTFCensusError raises for the same assembly. A publisher assessed worm cofactors and none has released a worm TF census, so the two halves answer differently for one species — the publishers' shape, and not a defect.

A :class:LookupError, so it may be caught together with :class:UnknownSpeciesError and still told apart, exactly as the census pair is.

Parameters:

Name Type Description Default
assembly str

The Assembly asked about.

required
species str

The species its metadata row names, which is what no table ships for.

required
shipped_for iterable of str

The species a Cofactor table does ship for.

required

Attributes:

Name Type Description
assembly str

The assembly asked about.

species str

Its species.

shipped_for tuple of str

The species that do have a cofactor table.

Examples:

>>> try:
...     raise NoCofactorTableError("sacCer3", "Saccharomyces cerevisiae", ["Mus musculus"])
... except LookupError as error:
...     print("Mus musculus" in str(error))
True

TFCofactorList dataclass

TFCofactorList(
    assembly: str,
    annotation: str,
    species: str,
    provenance: CofactorProvenance,
    cofactors: tuple[TFCofactor, ...],
    unresolved: tuple[str, ...],
)

One Assembly's Transcription cofactors, in its annotation's own gene ids.

:func:resolve_tf_cofactors's answer, and the counterpart of :class:~genome.tf.gene.annotation.TFGeneList in the same shape: the Cofactor table's Gene id stems resolved against one registered annotation, so the ids join to a counts matrix with nothing left to normalise.

Membership is this package's and classification is each publisher's. A table built from two publishers is a union nobody else published, which is why :attr:provenance carries a record per publisher rather than one, and why a source of both on an entry says the two agreed the gene is a cofactor and nothing about how either classified it.

What the table holds and this annotation does not is visible. A stem no gene id here is of comes back in :attr:unresolved rather than being dropped.

There is no empty one, for the reasons an absent table would give: an assembly whose species has no cofactor table, and one nothing names a species for, each raise a :class:LookupError of their own.

Attributes:

Name Type Description
assembly str

The Assembly asked about.

annotation str

The Registered name whose own gene ids these are.

species str

The species the assembly's own metadata row names, which is what selected the table. Never passed in by a caller, so asking for one species' cofactors while holding another species' assembly is not expressible.

provenance CofactorProvenance

Where the table came from: one record per publisher that contributed to it, plus the digest of the shipped bytes. :meth:~genome.tf.cofactor.table.CofactorProvenance.attribution renders the line to print beside anything it answered.

cofactors tuple of TFCofactor

One entry per Gene id stem that named at least one gene id here, in the table's own row order.

unresolved tuple of str

The stems this annotation carries no gene for, in table row order.

Examples:

>>> from genome.tf.cofactor import cofactor_table
>>> answer = TFCofactorList(
...     assembly="mm39",
...     annotation="gencode_vM39",
...     species="Mus musculus",
...     provenance=cofactor_table("Mus musculus").provenance,
...     cofactors=(
...         TFCofactor(
...             "ENSMUSG00000000085",
...             ("ENSMUSG00000000085.16",),
...             "Scmh1",
...             True,
...             "animaltfdb",
...             {},
...         ),
...     ),
...     unresolved=("ENSMUSG00000000275",),
... )
>>> answer.gene_ids
['ENSMUSG00000000085.16']
>>> answer.provenance.sources[0].publisher
'AnimalTFDB'
>>> answer.as_json()["unresolved"]
['ENSMUSG00000000275']

gene_ids property

gene_ids: list[str]

Every gene id, cofactor order then id order — a fresh list each call.

Every id, not one per gene, for the reason :attr:~genome.tf.gene.annotation.TFGeneList.gene_ids gives: flattening is where a reader would take the first id of a stem that names two and lose the other. :attr:cofactors is what says which gene an id came from, and what the publisher said about it.

as_json

as_json() -> dict[str, Any]

Return this answer as --json serializes it.

Returns:

Type Description
dict

assembly, annotation, species, the table's provenance under its own field names with one entry per publisher under sources, cofactors as a list of :meth:TFCofactor.as_json entries, the flattened gene_ids, and unresolved as a list — the keys :class:~genome.tf.gene.annotation.TFGeneList uses, with the entries named for what they are.

NoTFCensusError

NoTFCensusError(
    assembly: str, species: str, censused: Iterable[str]
)

Bases: LookupError

No census has been published for this Assembly's species, so none can answer.

The first of the two absences a TF gene list has, and the one a caller must never read as this species has no transcription factors: nobody has published a census for it, which is a fact about the literature and not about the genome. Worm, yeast and E. coli land here today. The message names the species that do have one, since asking about one of those is the thing a caller can do instead.

A :class:LookupError, so it may be caught together with :class:UnknownSpeciesError and still told apart — exactly as the Curated gene list's two absences are.

Parameters:

Name Type Description Default
assembly str

The Assembly asked about.

required
species str

The species its metadata row names, which is what no census ships for.

required
censused iterable of str

The species a census does ship for.

required

Attributes:

Name Type Description
assembly str

The assembly asked about.

species str

Its species.

censused tuple of str

The species that do have a census.

Examples:

>>> try:
...     raise NoTFCensusError("ce11", "Caenorhabditis elegans", ["Homo sapiens"])
... except LookupError as error:
...     print("Homo sapiens" in str(error))
True

TFGeneList dataclass

TFGeneList(
    assembly: str,
    annotation: str,
    species: str,
    provenance: CensusProvenance,
    genes: tuple[TFGene, ...],
    unresolved: tuple[str, ...],
)

One Assembly's TF genes, in its registered annotation's own gene ids.

:func:resolve_tf_genes's answer, and what a --json surface over it serializes. The census's Gene id stems resolved against one annotation, so the ids join to a counts matrix with nothing left to normalise, and assessed-positive by default: the common case is not 2,765 rows to filter down to 1,639.

Nothing here decides what a transcription factor is. Every verdict is the census's and travels with :attr:provenance, which names the publisher to cite. Two censuses that classify one factor differently are two answers rather than a contradiction, and this says which one is speaking.

What the census holds and this annotation does not is visible. A stem no gene id here is of comes back in :attr:unresolved rather than being dropped, so a caller can count what the crossing cost instead of wondering.

There is no empty one for the reasons an absent census would give: an assembly whose species has no census, and one nothing names a species for, each raise a :class:LookupError of their own.

Attributes:

Name Type Description
assembly str

The Assembly asked about.

annotation str

The Registered name whose own gene ids these are.

species str

The species the assembly's own metadata row names, which is what selected the census. Never passed in by a caller, so asking for one species' transcription factors while holding another species' assembly is not expressible.

provenance CensusProvenance

Where the census came from: publisher, version, PubMed id, source URL and digest. :meth:~genome.tf.gene.census.CensusProvenance.attribution renders the line to print beside anything it answered.

genes tuple of TFGene

One entry per Gene id stem that named at least one gene id here, in the census's own row order.

unresolved tuple of str

The stems this annotation carries no gene for, in census row order.

Examples:

>>> from genome.tf.gene import tf_gene_table
>>> answer = TFGeneList(
...     assembly="hg38",
...     annotation="gencode_v50",
...     species="Homo sapiens",
...     provenance=tf_gene_table("Homo sapiens").provenance,
...     genes=(
...         TFGene("ENSG00000137203", ("ENSG00000137203.12",), "TFAP2A", True, "AP-2", {}),
...     ),
...     unresolved=("ENSG00000214717",),
... )
>>> answer.gene_ids
['ENSG00000137203.12']
>>> answer.provenance.publisher
'Lambert et al. 2018'
>>> answer.as_json()["unresolved"]
['ENSG00000214717']

gene_ids property

gene_ids: list[str]

Every gene id, gene order then id order — a fresh list each call.

Every id, not one per gene, for the reason :attr:~genome.annotation.stems.ResolvedGeneIds.gene_ids gives: flattening is where a reader would take the first id of a stem that names two and lose the other. :attr:genes is what says which gene an id came from, and what the census said about it.

as_json

as_json() -> dict[str, Any]

Return this answer as --json serializes it.

Returns:

Type Description
dict

assembly, annotation, species, the census's provenance under its own field names, genes as a list of :meth:TFGene.as_json entries, the flattened gene_ids, and unresolved as a list. The ids are written out beside the genes they are read from for the reason :attr:gene_ids gives.

UnknownSpeciesError

UnknownSpeciesError(
    assembly: str,
    shipped_for: Iterable[str],
    *,
    shipped_table: str,
)

Bases: LookupError

Nothing says what species this Assembly is, so no shipped table can be chosen.

The second absence both gene-keyed halves have, and a different fact from :class:NoTFCensusError and :class:NoCofactorTableError: the question was not has anyone published for this species but which species is this, and nothing answered it. Two ways in, and neither is a mistake — a Chimera is more than one species by construction and nothing published answers for one, and an assembly the curated table does not list carries no species at all, which is the ordinary state of a free-form local key.

The species is read from the assembly's own metadata and never passed in, so this is what a caller gets instead of quietly being handed another species' answer.

One class for both halves because the fact is one fact: only what a caller can ask about instead differs, and shipped_table is what says which of them was asked for.

Parameters:

Name Type Description Default
assembly str

The Assembly whose species nothing names.

required
shipped_for iterable of str

The species the table asked for does ship for.

required
shipped_table str

What could not be chosen, named in the message — "TF census" or "cofactor table". Keyword-only and required, so a message never says census over an answer that was about cofactors.

required

Attributes:

Name Type Description
assembly str

The assembly asked about.

shipped_for tuple of str

The species the table asked for does ship for.

shipped_table str

What could not be chosen.

Examples:

>>> try:
...     raise UnknownSpeciesError(
...         "ce11_ecHT115", ["Homo sapiens"], shipped_table="TF census"
...     )
... except LookupError as error:
...     print("Homo sapiens" in str(error))
True

genome.aligner

Aligner abstractions and the :class:AlignerMixin for :class:~genome.assembly.genome.Genome.

Aligner

Aligner(
    genome: Genome, *, tool: ExternalTool | None = None
)

Bases: ABC

Base class for an external aligner that can build a genome index.

Subclasses set the class attributes :attr:name (the lowercase identifier used in the index path), :attr:binary (the executable on PATH) and :attr:_flag_separator (how its long options join words), and implement :meth:index, :attr:_artifact and :attr:_build_arguments. An :meth:index hands :meth:_build a way to compose its command line; :meth:_build owns the sequence every build shares and composes only when a build is going to run.

An aligner is given its External tool rather than making one, and constructing it runs nothing at all: the binary is located, and its version asked for, the first time either is needed. A binary that is not installed raises :class:~genome.external.ToolNotFoundError carrying the install instructions, at the point the build would have started rather than at construction.

Parameters:

Name Type Description Default
genome Genome

The genome whose reference FASTA will be indexed.

required
tool ExternalTool

The tool to drive. Defaults to :attr:binary as installed on this machine; pass one to bind the build to a particular executable, or to a recording stand-in.

None

assembly property

assembly: str

The assembly name of the bound genome.

version property

version: str

The installed aligner version, asked of the binary on first use.

index_dir property

index_dir: Path

Directory holding this aligner's index for the assembly.

<assembly dir>/index/<name>/ — inside the Assembly dir the bound genome was opened in, asked of that genome rather than re-derived from the Data dir. The two agree under the ordinary layout and part company for a genome opened somewhere of its own, where re-deriving would put the index beside a different assembly's files and read a completion record that is not there.

index_path property

index_path: Path

The built index file or prefix this aligner consumes.

The exact flavour (a directory, a file, or a path prefix) is decided by the subclass via :attr:_artifact. Reading this property asserts that the build finished, and the completion record in :attr:index_dir is what says so — never the presence of an index file, which is exactly what that record exists to distrust.

Returns:

Type Description
Path

The path to hand to the aligner's own command line.

Raises:

Type Description
IndexNotBuiltError

If nothing has been built here yet.

UnfinishedRegistrationError

If the directory holds index files but no record — a build that was interrupted before it finished.

RegistrationMismatchError

If the record disagrees with what is on disk, naming every file that differs and how, or if the assembly was re-registered after this index was built, naming both digests.

install_instructions

install_instructions() -> str

Return how to install this aligner — its tool's own text.

Every aligner owes one, and it is the same text the :class:~genome.external.ToolNotFoundError carries, so a caller reading either gets the command to run.

Returns:

Type Description
str

What to run, naming the bioconda package.

Examples:

>>> from genome.aligner.star import STAR
>>> print(STAR(genome, gtf="gencode_v50").install_instructions())
STAR is not installed. Add it to the project environment with:
    pixi add star            # channels: conda-forge, bioconda
...

index abstractmethod

index(*, overwrite: bool = False, **kwargs: Any) -> Path

Build the genome index and return :attr:index_path.

IndexNotBuiltError

Bases: RuntimeError

No index has ever been built where one was asked for.

Distinct from the two states :mod:genome.store.completion calls broken: nothing on disk is damaged or half-written, there is simply no index there yet. The message names the call that builds one, so the gap is self-explaining.

Chromap

Chromap(
    genome: Genome, *, tool: ExternalTool | None = None
)

Bases: Aligner

Chromap aligner index builder.

A chromap index is a single minimizer table built from the reference FASTA alone; it carries no annotation, so — unlike :class:~genome.aligner.star.STAR — there is exactly one index per assembly. The index is written to .../index/chromap/chromap.index and :attr:index_path returns that file (chromap consumes it via -x/--index).

Parameters:

Name Type Description Default
genome Genome

The genome whose reference FASTA will be indexed.

required
tool ExternalTool

As :class:~genome.aligner.aligner.Aligner.

None

index

index(
    *,
    kmer: int | None = None,
    window: int | None = None,
    overwrite: bool = False,
    **kwargs: Any,
) -> Path

Build the chromap index for the bound assembly and return :attr:index_path.

Output goes to <LIULAB_DATA>/genome/<assembly>/index/chromap/chromap.index. An index whose completion record says it finished is reused unless overwrite=True; a directory holding index files that no record vouches for raises rather than being silently rebuilt. chromap needs only the reference FASTA — no gene annotation — so one index serves every use of the assembly.

Only the two minimizer knobs are named below. Any other --build-index option may be passed as a keyword argument using chromap's flag name with underscores for hyphens (e.g. min_frag_length=30 -> --min-frag-length 30); for their meaning see chromap --help.

Parameters:

Name Type Description Default
kmer int

-k/--kmer: minimizer k-mer length. chromap's own default is used when omitted.

None
window int

-w/--window: minimizer window size. chromap's own default is used when omitted.

None
overwrite bool

Rebuild even if a finished index already exists, and rebuild over a directory that cannot be trusted rather than raising on it.

False
**kwargs Any

Extra --build-index options forwarded verbatim as chromap flags.

{}

Returns:

Type Description
Path

The built index file (also available as :attr:index_path).

Raises:

Type Description
RegistrationError

If the index directory holds files without a record, a record that disagrees with them, or a record pinning a different assembly digest than the one registered now. Pass overwrite=True to rebuild.

RuntimeError

If chromap exits non-zero.

AlignerMixin

Build aligner genome indexes for a :class:~genome.assembly.genome.Genome.

Each build_<aligner>_index method instantiates the corresponding :class:~genome.aligner.aligner.Aligner. Constructing one runs nothing: the binary is located and asked its version on first use, so a missing aligner raises when a build is asked for and not before. Index files land under <assembly dir>/index/<aligner>/ — inside the Assembly dir the genome was opened in.

build_star_index

build_star_index(
    gtf: str | None = None,
    *,
    tool: ExternalTool | None = None,
    **kwargs: Any,
) -> Path

Build a STAR genome index for this assembly against annotation gtf.

A thin entry point onto :meth:genome.aligner.star.STAR.index; the remaining keyword arguments are forwarded there (see its docstring for the exposed options and how to pass arbitrary STAR flags).

Parameters:

Name Type Description Default
gtf str

Name of a GTF annotation registered on this genome (see :meth:~genome.annotation.registry.AnnotationRegistry.register_path). Its path is resolved via :meth:~genome.annotation.registry.AnnotationRegistry.path and passed to STAR; the index is written to a per-annotation directory index/star_<gtf>/, so different annotations build independent indexes. Omitted, this genome's Default annotation (:attr:~genome.assembly.genome.Genome.default_gtf) is what the index is built against and what names its directory — which is the everyday call, since a chimera and any assembly the table flags one for already carry a default.

None
tool ExternalTool

The tool to drive, forwarded to the aligner — the same seam :class:~genome.aligner.aligner.Aligner offers, reachable from here so that arriving through a :class:~genome.assembly.genome.Genome does not close it.

None
**kwargs Any

Forwarded to :meth:genome.aligner.star.STAR.index.

{}

Returns:

Type Description
Path

The built STAR genome directory.

Raises:

Type Description
ValueError

If gtf is omitted and this genome has no Default annotation to fall back on. STAR's index is built against one annotation and cannot be built against none, so the message names both ways to supply one.

Examples:

>>> from genome import Genome
>>> sacCer3 = Genome("sacCer3")
>>> sacCer3.default_gtf
'ensgene_v101'
>>> sacCer3.build_star_index(threads=8).name           # the default
'star_ensgene_v101'
>>> sacCer3.build_star_index("refseq_2023").name       # ...or name one
'star_refseq_2023'

build_chromap_index

build_chromap_index(
    *, tool: ExternalTool | None = None, **kwargs: Any
) -> Path

Build a chromap genome index for this assembly.

A thin entry point onto :meth:genome.aligner.chromap.Chromap.index; the keyword arguments are forwarded there (see its docstring for the exposed options and how to pass arbitrary chromap flags). Unlike :meth:build_star_index, chromap needs no gene annotation at all — not even a default one — so there is no gtf argument and one index serves the whole assembly. The index is written to index/chromap/chromap.index.

Parameters:

Name Type Description Default
tool ExternalTool

As :meth:build_star_index.

None
**kwargs Any

Forwarded to :meth:genome.aligner.chromap.Chromap.index.

{}

Returns:

Type Description
Path

The built chromap index file.

get_index

get_index(
    aligner: str,
    *,
    tool: ExternalTool | None = None,
    **kwargs: Any,
) -> Path

Return the path of an already-built index, for use in aligner commands.

Locates the index a prior build_<aligner>_index produced and returns the file or prefix to hand to the aligner's own command line (e.g. STAR's --genomeDir). Nothing is built here: the index's completion record is read, and anything short of a finished index that agrees with what is on disk raises, naming the call that puts it right.

The aligner-specific selectors that identify which index are passed as keyword arguments, mirroring the matching build_* method — for STAR, the annotation gtf key that named the index.

Parameters:

Name Type Description Default
aligner str

Aligner identifier, case-insensitive (e.g. "star").

required
tool ExternalTool

As :meth:build_star_index. Spelled out rather than left to **kwargs so it cannot be read as one of the selectors below. Nothing is run here, so passing one changes only which executable a later build would drive.

None
**kwargs Any

Aligner-specific selectors forwarded to the aligner constructor to pin down the index. STAR requires gtf.

{}

Returns:

Type Description
Path

The built index file or prefix, ready to drop into the aligner command.

Raises:

Type Description
ValueError

If aligner is not a known aligner.

IndexNotBuiltError

If no index has been built yet — build it first with the corresponding build_<aligner>_index method.

RegistrationError

If the index directory holds files without a completion record, or a record that disagrees with them; rebuild it with overwrite=True.

get_star_index

get_star_index(gtf: str) -> Path

Return the path of the STAR genomeDir built for annotation gtf.

Convenience wrapper over :meth:get_index for STAR — the returned directory is what STAR's --genomeDir expects at mapping time.

Parameters:

Name Type Description Default
gtf str

Name of the GTF annotation the index was built against (the same key passed to :meth:build_star_index).

required

Returns:

Type Description
Path

The STAR genome directory.

Raises:

Type Description
IndexNotBuiltError

If no STAR index has been built yet for gtf — build it first with :meth:build_star_index.

RegistrationError

If that index directory cannot be trusted; see :meth:get_index.

get_chromap_index

get_chromap_index() -> Path

Return the path of the chromap index built for this assembly.

Convenience wrapper over :meth:get_index for chromap — the returned file is what chromap's -x/--index expects at mapping time. A chromap index carries no annotation, so no selector is needed.

Returns:

Type Description
Path

The chromap index file.

Raises:

Type Description
IndexNotBuiltError

If no chromap index has been built yet for this assembly — build it first with :meth:build_chromap_index.

RegistrationError

If that index directory cannot be trusted; see :meth:get_index.

STAR

STAR(
    genome: Genome,
    *,
    gtf: str,
    tool: ExternalTool | None = None,
)

Bases: Aligner

STAR aligner index builder.

A STAR index is splice-junction-aware: it is built against one gene annotation, so each annotation gets its own genomeDir. The bound GTF key selects that annotation (its path resolved via :meth:~genome.annotation.registry.AnnotationRegistry.path) and names the index directory star_<gtf_key>. STAR's index is the genomeDir directory itself, so :attr:index_path returns :attr:index_dir.

Parameters:

Name Type Description Default
genome Genome

The genome whose reference FASTA will be indexed.

required
gtf str

Name of a GTF annotation registered on genome (see :meth:~genome.annotation.registry.AnnotationRegistry.register_path).

required
tool ExternalTool

As :class:~genome.aligner.aligner.Aligner.

None

index_dir property

index_dir: Path

Per-annotation genome directory .../index/star_<gtf_key>/.

index

index(
    *,
    sjdb_overhang: int = 100,
    threads: int = 1,
    overwrite: bool = False,
    **kwargs: Any,
) -> Path

Build the STAR genome index for the bound assembly and annotation.

Output goes to <LIULAB_DATA>/genome/<assembly>/index/star_<gtf_key>/. An index whose completion record says it finished is reused unless overwrite=True; a directory holding index files that no record vouches for raises rather than being silently rebuilt. The annotation GTF is resolved from the bound gtf key via :meth:~genome.annotation.registry.AnnotationRegistry.path and passed to STAR as --sjdbGTFfile for splice-junction-aware indexing.

Only the most commonly tuned options are named below. Any other STAR genomeGenerate option may be passed as a keyword argument using its STAR name without the leading -- (e.g. genomeSAindexNbases=11); for the meaning of those options see the STAR manual / STAR --help. Two of them are sized from the assembly rather than left to STAR's defaults — the suffix-array index size genomeSAindexNbases, from the total sequence length, and the genome-storage bin size genomeChrBinNbits, from the mean sequence length and the read length sjdb_overhang implies. Passing either one yourself wins.

Parameters:

Name Type Description Default
sjdb_overhang int

--sjdbOverhang: ideally read_length - 1, which is also where the computed genomeChrBinNbits reads the read length back out of.

100
threads int

--runThreadN: number of threads to build with.

1
overwrite bool

Rebuild even if a finished index already exists, and rebuild over a directory that cannot be trusted rather than raising on it.

False
**kwargs Any

Extra genomeGenerate options forwarded verbatim as STAR flags.

{}

Returns:

Type Description
Path

The genome directory (also available as :attr:index_path).

Raises:

Type Description
RegistrationError

If the index directory holds files without a record, a record that disagrees with them, or a record pinning a different assembly digest than the one registered now. Pass overwrite=True to rebuild.

RuntimeError

If STAR exits non-zero.

genome.annotation

Annotations — a GTF filed under one Assembly, and everything asked of it after.

I/O boundary package. A reference assembly may carry several gene annotations (GENCODE, RefSeq, WormBase, …). Each is registered under a Registered name and lives in its own directory beside the assembly's sequence files::

<LIULAB_DATA>/genome/<assembly>/gtf/<name>/
    <name>.gtf          # the annotation, kept decompressed
    <name>.db           # the gffutils SQLite database built from it
    .completion.json    # the record saying all of that finished
    .work/              # the disposable working area a fetch downloads into

:class:~genome.annotation.registry.AnnotationRegistry is the way in, and it is one class with one interface. Bound once to one assembly — its name, its Assembly dir and its chrom.sizes — it holds every annotation that assembly has and answers everything about them. Its implementation is spread over four modules and it calls across them:

  • :mod:~genome.annotation.registration puts an annotation on disk: the fetch, the placement, the Chromosome check, the repair-command strings, the Completion marker, the Merged annotation a Chimera build derives, and the two registrars addressed by assembly name.
  • :mod:~genome.annotation.registry holds the class itself, the three scans and the Default annotation rule it settles at construction, and the by-assembly-name questions genome annotation list, genome annotation gene-list and genome annotation gene-categories ask.
  • :mod:~genome.annotation.stems resolves a Gene id stem against an annotation's own gene ids — the seam the Xref, Orthology and TF contexts all cross, and the only one.
  • :mod:~genome.annotation.database is the gffutils adapter: the build, and the read that yields gene ids a row at a time. Nothing else in the package imports the library.

Two more modules sit beside those four because they are about the same thing and nothing else: :mod:~genome.annotation.metadata is the curated table of what the lab supports for each assembly, keyed by assembly plus Registered name, and :mod:~genome.annotation.curated reads the shipped Curated gene list — which of an annotation's genes are in a hand-curated category.

Nothing here knows what a caller's gene ids are a list of. Which species selects a shipped file, and what a row of one says, are facts this package has no stake in, so it holds no import of any of them and gains no method when a fourth topic arrives.

What a registration answers with is re-exported here beside what produces it, so a caller holding one imports it from the package rather than from the module that happens to define it.

Examples:

>>> from pathlib import Path
>>> from genome.annotation import annotation_dir
>>> annotation_dir(Path("/data/genome/sacCer3"), "ensgene_v101").name
'ensgene_v101'

CuratedGeneListError

Bases: ValueError

A shipped curated gene list cannot be read, so it is not allowed to answer.

A packaging defect and not a caller error: these files ship inside the package, so bad JSON, a missing key, a category with no genes, a gene id in two categories or an annotation field disagreeing with the file name are all faults in what was committed here. A :class:ValueError, because a hand-curated file that says something the format does not is a bad value rather than a broken program.

The message names the file and what is wrong with it, since fixing the file is the only thing anyone can do about it.

Examples:

>>> try:
...     _read_gene_list("{", annotation="mine", origin="mine.curated_gene_list.json")
... except CuratedGeneListError as error:
...     print("mine.curated_gene_list.json" in str(error))
True

GeneCategoryNotDeclaredError

GeneCategoryNotDeclaredError(
    annotation: str,
    assembly: str,
    category: str,
    declared: Iterable[str],
)

Bases: LookupError

The annotation declares categories, and not the one asked for.

The second of the two absences. It is a real answer about a real curated list — the category was not curated for this annotation, which is not the same as its being curated and empty, and no shipped category is ever empty. The message lists the ones it does declare, since those are what a caller may ask for instead.

Parameters:

Name Type Description Default
annotation str

The Registered name that was asked about.

required
assembly str

The Assembly it is registered for.

required
category str

The category that is not declared.

required
declared iterable of str

The categories that are, in the order the curated lists spell them.

required

Attributes:

Name Type Description
annotation str

The name asked about.

assembly str

The assembly it is registered for.

category str

The category nobody declared.

declared tuple of str

The categories that are declared.

Examples:

>>> try:
...     raise GeneCategoryNotDeclaredError("mine", "tiny", "tRNA", ["rRNA"])
... except LookupError as error:
...     print("rRNA" in str(error))
True

GeneListAssemblyMismatchError

Bases: CuratedGeneListError

A curated list was asked for by an annotation registered against another assembly.

The list names the Assembly it was curated against, and that is checked rather than assumed: an annotation registered under a name whose curated list belongs to a different reference would otherwise answer with another species' gene ids, and mixing builds is an error and not a warning.

Not one of the two absences: nothing is missing, so a caller catching :class:LookupError for absence does not swallow this.

Examples:

>>> listed = CuratedGeneList("mine", "ce11", {})
>>> try:
...     listed.check_assembly("hg38")
... except GeneListAssemblyMismatchError as error:
...     print("ce11" in str(error) and "hg38" in str(error))
True

NoGeneCategoriesError

NoGeneCategoriesError(
    annotation: str,
    assembly: str,
    curated: Iterable[str],
    *,
    contributors: Iterable[str] = (),
)

Bases: LookupError

Nothing is known about this annotation's categories, so none can be asked of it.

The first of the two absences, and the one a caller must never read as this annotation has no rRNA genes: no curated list ships for it, so the question was not answered rather than answered with nothing. The message says which annotations do declare categories and that answering for this one means shipping a curated list.

A :class:LookupError, so a caller may catch it together with :class:GeneCategoryNotDeclaredError and still act differently on each.

Parameters:

Name Type Description Default
annotation str

The Registered name that was asked about — the merged name for a Merged annotation.

required
assembly str

The Assembly it is registered for.

required
curated iterable of str

The annotations a curated list does ship for.

required
contributors iterable of str

For a Merged annotation, the annotations it merges. Shipping a list under the merged name would fix nothing; it is these that need one.

()

Attributes:

Name Type Description
annotation str

The name asked about.

assembly str

The assembly it is registered for.

curated tuple of str

The annotations that do ship a curated list.

contributors tuple of str

The contributing annotations, empty for anything but a merge.

Examples:

>>> try:
...     raise NoGeneCategoriesError("mine", "tiny", ["gencode_v50"])
... except LookupError as error:
...     print("gencode_v50" in str(error))
True

AnnotationMetadata dataclass

AnnotationMetadata(
    assembly: str,
    name: str,
    provider: str,
    version: str,
    url: str,
    sha256: str | None = None,
    default: bool = False,
)

One annotation the lab supports for one assembly (one row of the annotation table).

Keyed by assembly plus name — the Registered name the annotation is addressed by everywhere — and carrying enough to register it from that name alone: who publishes it, which release, where to fetch it, and what the unpacked GTF that source yields hashes to. A complete record is also what the registration functions accept in place of the table's own row.

Attributes:

Name Type Description
assembly str

The assembly this annotation belongs to — an annotation belongs to exactly one.

name str

The Registered name, unique within the assembly, e.g. "gencode_v50".

provider str

Who publishes it: "GENCODE", "RefSeq", "WormBase", "UCSC".

version str

The provider's own release identifier, e.g. "v50" or "WS298".

url str

Where the GTF is fetched from.

sha256 str or None

Digest of the unpacked GTF, or None when the row pins none — in which case whatever is fetched is recorded but nothing is compared.

default bool

Whether this is the assembly's Default annotation.

Examples:

>>> record = AnnotationMetadata(
...     assembly="sacCer3",
...     name="ensgene_v101",
...     provider="UCSC",
...     version="ensGene.v101",
...     url="https://hgdownload.soe.ucsc.edu/goldenPath/sacCer3/bigZips/genes/sacCer3.ensGene.gtf.gz",
... )
>>> record.provider
'UCSC'
>>> record.default                   # not the default unless the row says so
False

from_row classmethod

from_row(row: Mapping[str, object]) -> AnnotationMetadata

Build a record from one row of an annotation table.

:meth:AssemblyMetadata.from_row for the annotation table's own columns, and the same rules — with one more: default is a flag column, where a blank cell is the real answer no rather than an unknown.

Parameters:

Name Type Description Default
row mapping of str to object

Column name to cell, as :meth:AssemblyMetadata.from_row takes one. Keys outside :data:ANNOTATION_FIELDS are ignored.

required

Returns:

Type Description
AnnotationMetadata

The record the row spells.

Raises:

Type Description
MetadataRowError

As :meth:AssemblyMetadata.from_row raises it, and additionally for a flag cell spelled a way no row spells one.

Examples:

>>> AnnotationMetadata.from_row(
...     {
...         "assembly": "sacCer3",
...         "name": "ensgene_v101",
...         "provider": "UCSC",
...         "version": "ensGene.v101",
...         "url": "https://example.org/sacCer3.ensGene.gtf.gz",
...         "default": "yes",
...     }
... ).default
True

ChromosomeMismatchError

ChromosomeMismatchError(
    name: str, missing: Iterable[str], known: Iterable[str]
)

Bases: ValueError

A GTF names Chromosomes its assembly does not carry, so the two do not line up.

Registering it would build an annotation where nothing matches: every feature would sit on a sequence the assembly has never heard of, and every query over it would answer nothing while looking perfectly healthy. The usual cause is a spelling difference rather than a wrong file, so the message says which one out loud and names the argument that registers it anyway.

The check behind it is strict in one direction only. An assembly carrying scaffolds the annotation never mentions is normal and is not this.

Parameters:

Name Type Description Default
name str

The Registered name the annotation was being registered under.

required
missing iterable of str

Every name the GTF uses that the assembly's chrom.sizes does not list.

required
known iterable of str

The names the assembly does carry, for the message to contrast against.

required

Attributes:

Name Type Description
name str

The registered name.

missing tuple of str

Every offending name, sorted — the message lists at most ten of them and counts the rest, this is the whole set.

known tuple of str

The names the assembly carries, as they were passed in.

Examples:

>>> raise ChromosomeMismatchError("gencode_v44", ["1", "2"], ["chr1", "chr2"])
Traceback (most recent call last):
genome.annotation.registration.ChromosomeMismatchError: the GTF for 'gencode_v44' ...

GtfAnnotation dataclass

GtfAnnotation(name: str, gtf: Path, db: Path)

A registered GTF annotation: its name and the on-disk GTF + database paths.

MergeSource dataclass

MergeSource(component: str, annotation: str, gtf: Path)

One component's contribution to a Merged annotation.

What :func:register_merged_gtf needs about a single component: whose sequences the features sit on, which of that component's annotations was taken, and where its GTF is. The component name is not decoration — it is the suffix every seqname the merge writes carries, so the merged features land on the chimera's own chromosome names.

Attributes:

Name Type Description
component str

The Component assembly name, alphanumeric.

annotation str

The Registered name of that component's contributing annotation.

gtf Path

That annotation's placed GTF, read one line at a time and never in full.

Examples:

>>> from pathlib import Path
>>> MergeSource("ce11", "wormbase_ws298", Path("/data/ce11.gtf")).component
'ce11'

RegisteredAnnotation dataclass

RegisteredAnnotation(
    assembly: str, directory: Path, record: CompletionRecord
)

What registering one annotation produced: its record, and where that landed.

:func:register_annotation's answer and :func:register_gtf's — what genome annotation register and genome annotation register-gtf print, and what their --json serializes. A :class:GtfAnnotation says where an annotation's two files are; this says what the run that wrote them did, which is the Completion marker itself, carried whole. Every question a surface then asks — the digest, the source, the files claimed, whether the chromosome names were actually checked — is answered from that one record rather than by reading the directory again.

Attributes:

Name Type Description
assembly str

The Assembly the annotation belongs to. It is not in the record, which names the annotation rather than what it annotates.

directory Path

The annotation's own directory, <assembly dir>/gtf/<name>/.

record CompletionRecord

The record the registration wrote, read back.

Examples:

>>> from pathlib import Path
>>> from genome.store.completion import CompletionRecord
>>> registered = RegisteredAnnotation(
...     assembly="hg38",
...     directory=Path("/data/genome/hg38/gtf/gencode_v50"),
...     record=CompletionRecord(
...         kind="annotation",
...         name="gencode_v50",
...         files={"gencode_v50.gtf": 12, "gencode_v50.db": 34},
...         source_url="https://example.org/gencode_v50.gtf.gz",
...         sha256="1a2b3c",
...         tool_versions={},
...         package_version="2026.8.0",
...         completed_at="2026-08-12T09:00:00+00:00",
...         details={"chromosomes_checked": True},
...     ),
... )
>>> registered.name, registered.file_names
('gencode_v50', ['gencode_v50.db', 'gencode_v50.gtf'])
>>> print(registered.chromosome_check)
chromosomes checked — every name the GTF uses is one the assembly carries

name property

name: str

The Registered name it is addressed by — the record's own name.

source_url property

source_url: str | None

The URL fetched, or the path a GTF was handed over at; None for a merge.

sha256 property

sha256: str | None

Digest of the placed GTF, or None when none was computed.

file_names property

file_names: list[str]

Every file the record claims, sorted — a fresh list each call.

chromosome_check property

chromosome_check: str

The one line saying what the chromosome-name check settled for this annotation.

:func:chromosome_check_summary over the record this registration wrote, so the surface that prints it never reads the record's own keys. Always a sentence: silence would read as a pass.

as_json

as_json() -> dict[str, Any]

Return this registration as --json serializes it.

The record's own fields under the record's own names, then the assembly it belongs to and the directory it landed in — the two facts a record does not hold about itself. The names are the ones written on disk and are never respelled here.

Returns:

Type Description
dict

The record's fields, followed by assembly and directory.

AnnotationNotRegisteredError

AnnotationNotRegisteredError(
    assembly: str,
    name: str,
    registered: Iterable[str],
    offered: Iterable[str],
    *,
    broken: BrokenAnnotation | None = None,
)

Bases: KeyError

No annotation of that name is registered here, so there is no path to hand back.

Routinely not a mistake. An assembly's Default annotation comes from the curated table, and on a fresh machine the table's choice is exactly what nobody has registered yet — so a :class:~genome.assembly.genome.Genome opens with that default named and only asking for its path raises, naming the command that closes the gap. The other way in is a name nothing knows, and the message then says what the table does offer and how to register a GTF it does not list.

A third way in is a directory that is there and cannot be trusted, which is not registered either. The next action is then neither of the above — registering it plainly would itself raise and demand --force — so broken carries what :func:list_broken_annotations found and the message quotes its repair, which is a command that runs as it stands.

A :class:KeyError, because that is what asking a registry for a name it does not hold has always been.

Parameters:

Name Type Description Default
assembly str

The assembly the annotation was asked for.

required
name str

The Registered name that is not registered.

required
registered iterable of str

The names that are registered on this machine.

required
offered iterable of str

The names the annotation table offers for this assembly.

required
broken BrokenAnnotation

The broken registration filed under name, when that is why there is no path to hand back.

None

Attributes:

Name Type Description
assembly str

The assembly asked about.

name str

The name that is not registered.

registered tuple of str

The registered names, as they were passed in.

offered tuple of str

The offered names, as they were passed in.

broken BrokenAnnotation or None

The broken registration, or None when nothing of that name is on disk.

Examples:

>>> raise AnnotationNotRegisteredError("hg38", "gencode_v50", [], ["gencode_v50"])
Traceback (most recent call last):
genome.annotation.registry.AnnotationNotRegisteredError: "no annotation ...

AnnotationRegistry

AnnotationRegistry(
    assembly_dir: AssemblyDir,
    *,
    chrom_sizes: str | Path | None = None,
    default: str | None = None,
)

One Assembly's annotations, the state each is in, and the acts that add one.

An annotation directory is registered, broken, offered but not begun, or nothing at all, and every useful question about one is a question about that four-way state: what may a caller name, what may it be handed the path of, which is the Default annotation, what does a surface print, what does a name nobody registered earn as an error. This settles all four once, at construction, and answers from that — so the state is assembled in one place rather than wherever it is needed.

Bound to one assembly and carried, never re-derived: the Assembly dir comes in as an :class:~genome.assembly.registration.AssemblyDir, so a registry cannot file an annotation somewhere other than where the caller that built it is looking, and the chrom.sizes every GTF is checked against comes in beside it rather than being guessed from the layout.

Reading is cheap and safe: nothing here is created, fetched or built by asking, an assembly with no directory at all answers emptily, and one broken annotation is reported rather than raised over. Only :meth:register and :meth:register_path write, and both fold what they wrote back in, so the four states stay current without reading the disk again.

Parameters:

Name Type Description Default
assembly_dir AssemblyDir

The assembly this registry is for, and where its gtf/ subtree lives.

required
chrom_sizes str or Path

The assembly's chrom.sizes, whose names every registered GTF's must be among. Defaults to the one the layout names, which is what an assembly prepared in place has; a caller that prepared it elsewhere passes the file it actually wrote. A path that is not there is nothing to check against rather than an error — an annotation may be registered before its assembly is.

None
default str

A Default annotation the caller chose, which wins over the table's flag and need not be registered. See :func:default_annotation for the whole rule.

None

Attributes:

Name Type Description
assembly str

The assembly every annotation here belongs to.

Examples:

>>> registry = AnnotationRegistry.locate("sacCer3", "/tmp/definitely-not-an-assembly")
>>> registry.registered
[]
>>> registry.default                       # the table's flag, registered or not
'ensgene_v101'

registered property

registered: list[str]

The Registered names on this machine, in directory-name order.

What is here, as against :attr:offered, which is what the lab supports, and :attr:broken, which is what is here and cannot be trusted.

broken property

broken: list[BrokenAnnotation]

The annotation directories here that cannot be trusted as finished.

What :attr:registered leaves out, and between the two every directory under gtf/ is accounted for. Each entry says what is wrong and names the one command that repairs it.

offered property

offered: list[AnnotationMetadata]

The annotation table's rows for this assembly, in table order.

What the lab supports, whether or not anyone has registered it. Empty for an assembly the table offers nothing for, which is legal: it is a cross-reference rather than an allow-list.

default property

default: str | None

Name of the Default annotation, or None when nothing decides one.

:func:default_annotation's answer for this assembly, settled when the registry was built. It may name an annotation nobody has registered here — the normal state of a fresh machine — so it is :meth:path that says whether one exists. A default already decided is never displaced by a later registration.

locate classmethod

locate(
    assembly: str,
    cache_dir: str | Path | None = None,
    *,
    default: str | None = None,
) -> AnnotationRegistry

Return the registry for assembly, wherever the layout says its files live.

The assembly-addressed way in, and the only one the CLI has: a name and at most a directory override. :meth:~genome.assembly.registration.AssemblyDir.locate is where that override rule lives, and the chrom.sizes is the one that layout names.

Parameters:

Name Type Description Default
assembly str

The assembly to open the registry of, e.g. "hg38".

required
cache_dir str or Path

An explicit Assembly dir, overriding the Data dir layout.

None
default str

A Default annotation the caller chose.

None

Returns:

Type Description
AnnotationRegistry

Its registry. Nothing is created and nothing is fetched.

Examples:

>>> AnnotationRegistry.locate("hg38", "/tmp/definitely-not-an-assembly").registered
[]

path

path(name: str) -> Path

Return the GTF file path of the annotation registered as name.

Parameters:

Name Type Description Default
name str

The Registered name to resolve.

required

Returns:

Type Description
Path

Path to the placed <name>.gtf.

Raises:

Type Description
AnnotationNotRegisteredError

If nothing of that name is registered here. The four-way state decides what the message says next: the command that registers name when the table offers it, the path-based way in when it does not, and — for a directory of that name that is there and broken — the command that registers it again from scratch, so what is named is a command that runs rather than one that raises in turn.

Examples:

>>> registry = AnnotationRegistry.locate("sacCer3", "/data/genome/sacCer3")
>>> registry.path("ensgene_v101")
PosixPath('/data/genome/sacCer3/gtf/ensgene_v101/ensgene_v101.gtf')

register

register(
    name: str,
    *,
    force: bool = False,
    progressbar: bool = True,
    metadata: AnnotationMetadata | None = None,
    check_chromosomes: bool = True,
    disable_infer_genes: bool = True,
    disable_infer_transcripts: bool = True,
) -> GtfAnnotation

Register the annotation the table lists for this assembly as name.

Naming an annotation is enough: where its GTF comes from and which digest it must match are the curated table's to know. The row's URL is fetched into the working area, the unpacked GTF is checked against the sha256 the row pins — so a GTF that is not the pinned one never reaches the annotation directory — the gffutils database is built, and the record is written last.

Its chromosome names are checked too, against this registry's chrom.sizes and while the GTF is still in the working area: every name the GTF uses must be one the assembly carries, so an Ensembl-spelled GTF registered against a UCSC-spelled assembly fails in seconds rather than after the minutes the database build takes. The reverse is not required — an assembly may carry scaffolds the annotation never mentions. An assembly with no chrom.sizes yet has nothing to check against, and the record says so in details["chromosomes_checked"] — with details["chromosomes_unchecked_because"] saying whether that was for want of a chrom.sizes or because the caller stood the check down.

An annotation that already has a valid record is returned silently: nothing is fetched, nothing is rebuilt and nothing is warned about. A directory that cannot be trusted — files with no record, or a record that disagrees with disk — raises, naming genome annotation register <assembly> <name> --force. That is what force=True is: it skips the question, keeps a GTF whose digest can be shown to be the pinned one, and fetches the source again when it cannot.

Parameters:

Name Type Description Default
name str

The Registered name the table lists, e.g. "gencode_v50".

required
force bool

Register again from scratch, repairing a directory that raises.

False
progressbar bool

Show a download progress bar (requires tqdm).

True
metadata AnnotationMetadata

A complete annotation record to use instead of the curated table's row. Omit it and the row is looked up here.

None
check_chromosomes bool

Check the GTF's chromosome names against the assembly's. Pass False to register an annotation whose mismatch you have inspected and accept; the record then says the check was stood down, rather than merely that it did not run.

True
disable_infer_genes bool

Do not reconstruct gene features from exon lines.

True
disable_infer_transcripts bool

Do not reconstruct transcript features from exon lines.

True

Returns:

Type Description
GtfAnnotation

The registered annotation's name and its two file paths.

Raises:

Type Description
ValueError

If the table lists no annotation name for this assembly; the message lists what it does offer and points at the path-based form for an unlisted GTF.

ChromosomeMismatchError

If the GTF names sequences the assembly does not carry; the message lists them and names the usual cause.

ChecksumMismatchError

If the row pins a sha256 and the unpacked GTF is not it; the message names both digests.

UnfinishedRegistrationError

If the annotation's directory holds files but no record.

RegistrationMismatchError

If its record disagrees with what is on disk.

Examples:

>>> AnnotationRegistry.locate("sacCer3").register(
...     "ensgene_v101"
... )
GtfAnnotation(name='ensgene_v101', ...)

register_path

register_path(
    gtf: str | Path,
    name: str,
    *,
    force: bool = False,
    check_chromosomes: bool = True,
    disable_infer_genes: bool = True,
    disable_infer_transcripts: bool = True,
) -> GtfAnnotation

Register the GTF at gtf under name and build its gffutils database.

The escape hatch for an annotation the curated table does not list — :meth:register is the way in for one it does. A gzipped (.gz) source is decompressed into the registered <name>.gtf; a plain GTF is copied as-is. The digest recorded is of the placed GTF, since an unlisted annotation has no pinned digest to compare against.

Its chromosome names are checked against this registry's chrom.sizes before anything is created, so a GTF that does not line up leaves the annotation directory exactly as it was found. Knowing the assembly is what buys that: the file is found rather than passed, so an unlisted GTF is held to the same check a listed one gets.

Registering something already registered returns it silently, and a directory that cannot be trusted raises naming genome annotation register-gtf <assembly> <gtf> <name> --force, exactly as :meth:register does for a listed one.

Parameters:

Name Type Description Default
gtf str or Path

Path to the source GTF, plain or .gz.

required
name str

The Registered name to address it by, unique within the assembly.

required
force bool

Register again from scratch — the repair for a directory that raises.

False
check_chromosomes bool

Check the GTF's chromosome names against the assembly's. Pass False to register a GTF whose mismatch you have inspected and accept.

True
disable_infer_genes bool

Do not reconstruct gene features from exon lines.

True
disable_infer_transcripts bool

Do not reconstruct transcript features from exon lines.

True

Returns:

Type Description
GtfAnnotation

The registered annotation's name and its two file paths.

Raises:

Type Description
FileNotFoundError

If gtf is not a file.

ChromosomeMismatchError

If the GTF names sequences the assembly does not carry.

RegistrationError

If the annotation's directory cannot be trusted as finished.

Examples:

>>> AnnotationRegistry.locate("sacCer3").register_path(
...     "custom.gtf.gz", "custom"
... )
GtfAnnotation(name='custom', ...)

status

status() -> AnnotationStatus

Report what this assembly's table offers against what is registered here.

Two questions with two answers, joined for one reader: the table's rows say what the lab supports, the disk says what is on this machine, and every row carries which of the two it is. The command behind it is genome annotation list.

A third answer rides along, because this is where a reader would look for it: a directory that cannot be trusted is broken rather than registered. Nothing raises — reporting a broken annotation is the point, and one of them must not cost the rest.

Returns:

Type Description
AnnotationStatus

The assembly, its directory, the Default annotation's name, and one :class:AnnotationStatusRow per name — the offered ones in table order, followed by anything on this disk that no row lists.

Examples:

>>> here = AnnotationRegistry.locate("sacCer3", "/tmp/definitely-not-an-assembly")
>>> here.status().default_annotation
'ensgene_v101'

gene_list

gene_list(
    category: str, name: str | None = None
) -> GeneList

Return the genes one registered annotation puts in category.

The genes come from the Curated gene list shipped for that annotation and never from the GTF's own biotype attribute, which is spelled two ways across four publishers, carries three taxonomies that do not agree, and is absent altogether from some annotations. Nothing here knows a category vocabulary: which categories exist is what the curated list declares.

The annotation must be registered here — it is resolved through :meth:path, so an unregistered name earns the error that names the command registering it. The curated list is then held to the assembly it was curated against, since a name is unique only within its assembly and a list found by name alone is not yet known to be about this reference.

For a Merged annotation the record's merged_from says who contributed, and each contributor's own curated list answers for its own Component: the result carries one source per contributor that declares the category, so a caller counting worm ribosomal RNA can drop the E. coli entry. A contributor that does not declare it is simply absent — a bacterium has no mitochondria, and that is not a failure.

There is no empty answer. An annotation nothing ships a list for, and one whose list does not declare this category, are different facts and each raises an error of its own.

Parameters:

Name Type Description Default
category str

The Gene category to ask for, as the curated list spells it — "rRNA", "Mt_rRNA".

required
name str

The Registered name to ask about. Omitted, this assembly's Default annotation answers.

None

Returns:

Type Description
GeneList

The category, its gene ids, and one :class:GeneListSource per contributing curated list.

Raises:

Type Description
ValueError

If name is omitted and no Default annotation is decided; the message names the argument that chooses one.

AnnotationNotRegisteredError

If nothing of that name is registered here.

NoGeneCategoriesError

If no curated list ships for that annotation — nothing can be asked of it, which is not the same answer as its having no genes in this category.

GeneCategoryNotDeclaredError

If it declares categories and not this one; the message lists the ones it does.

GeneListAssemblyMismatchError

If the curated list found under that name was curated against another assembly, in which case it must not answer here.

Examples:

>>> registry = AnnotationRegistry.locate("ce11")
>>> registry.gene_list("rRNA").gene_ids[:2]
['WBGene00004512', 'WBGene00004513']
>>> [source.component for source in registry.gene_list("rRNA").sources]
[None]

gene_lists

gene_lists(name: str | None = None) -> tuple[GeneList, ...]

Return every Gene category one registered annotation declares.

:meth:gene_list for all of them at once, in the order the curated lists spell them — and for a Merged annotation, each contributor's own order, contributors first-listed first. Everything :meth:gene_list says about resolution, the assembly guard and attribution holds here.

Never an empty tuple. An annotation that declares nothing raises rather than answering emptily, which is the whole distinction this surface exists to keep: a caller that got () could not tell no categories are declared from every category is empty, and no declared category is ever empty.

Parameters:

Name Type Description Default
name str

The Registered name to ask about. Omitted, this assembly's Default annotation answers.

None

Returns:

Type Description
tuple of GeneList

One entry per declared category, in declaration order. Never empty.

Raises:

Type Description
ValueError

If name is omitted and no Default annotation is decided.

AnnotationNotRegisteredError

If nothing of that name is registered here.

NoGeneCategoriesError

If no curated list ships for that annotation.

GeneListAssemblyMismatchError

If a curated list found by name was curated against another assembly.

Examples:

>>> registry = AnnotationRegistry.locate("hg38")
>>> [answer.category for answer in registry.gene_lists()]
['rRNA', 'rRNA_pseudogene', 'Mt_rRNA']

resolve_gene_ids

resolve_gene_ids(
    stems: Iterable[str], name: str | None = None
) -> ResolvedGeneIds

Return the gene ids one registered annotation carries for each Gene id stem.

A stem is a gene id with its version dropped — ENSG00000123456 for ENSG00000123456.7 — which is how every published table keyed by gene arrives, and never how a GENCODE Annotation spells the same gene. This is the crossing: every gene id in the Annotation database is reduced to its own stem, and a stem answers with every gene id that reduced to it. An id carrying no version is its own stem, so an annotation whose ids were never versioned — WormBase's, SGD's — resolves each of its genes to itself and is untouched by an Ensembl-shaped assumption.

Every id, and never a chosen one. One stem naming two gene ids is not a malformed annotation: gencode_v50lift37 has nine such stems, eight of them pseudoautosomal genes carrying a _PAR_Y copy, and a resolver taking the first would hand back the X copy of a Y gene without saying it had chosen. So the answer is a mapping to all of them, ascending.

Nothing is dropped. Stems this annotation carries no gene for come back in :attr:~genome.annotation.stems.ResolvedGeneIds.unresolved, so a caller resolving a few thousand at once can see which of them this annotation does not have rather than counting the answer and wondering.

The annotation must be registered here, and a Merged annotation is read exactly as any other: it has one database of its own, holding both components' gene features under the components' own gene ids — a merge rewrites seqnames and never a gene_id — so a stem naming a gene in each component answers with both, which is the same rule as the pseudoautosomal one and needs no attribution to apply it.

One indexed pass over the database's gene features answers the whole call, however many stems it was handed; nothing reads the GTF, and no annotation is held in memory.

Parameters:

Name Type Description Default
stems iterable of str

The Gene id stems to resolve, in the order they should come back. Repeats are asked once. Pass them all at once: the cost is the pass, not the stem.

required
name str

The Registered name to resolve against. Omitted, this assembly's Default annotation answers.

None

Returns:

Type Description
ResolvedGeneIds

The stems that named gene ids, mapped to every id each names, and the stems that named none.

Raises:

Type Description
ValueError

If name is omitted and no Default annotation is decided; the message names the argument that chooses one.

AnnotationNotRegisteredError

If nothing of that name is registered here.

NoGeneFeaturesError

If its database holds no gene at all — every stem would otherwise resolve to nothing, which is a different fact and must not be reported as this one.

Examples:

>>> registry = AnnotationRegistry.locate("hg19")
>>> answer = registry.resolve_gene_ids(
...     ["ENSG00000182378", "ENSG00000141510"], "gencode_v50lift37"
... )
>>> answer.resolved["ENSG00000182378"]
('ENSG00000182378.14', 'ENSG00000182378.14_PAR_Y')

AnnotationStatus dataclass

AnnotationStatus(
    assembly: str,
    directory: Path,
    default_annotation: str | None,
    annotations: tuple[AnnotationStatusRow, ...],
)

What one assembly's table offers, set against what is registered on this machine.

:meth:AnnotationRegistry.status's answer, and what genome annotation list prints. Two questions joined for one reader, with a third riding along because this is where anyone would look for it: a directory that cannot be trusted is broken rather than registered, and reporting one is the point — nothing here raises.

Attributes:

Name Type Description
assembly str

The Assembly reported on.

directory Path

Its Assembly dir, whether or not anything is there.

default_annotation str or None

The Default annotation's name, or None when nothing decides one. It may name one nobody has registered here, which is a fresh machine's ordinary state.

annotations tuple of AnnotationStatusRow

One row per name: the offered ones in table order, then anything on this disk that no row lists.

Examples:

>>> from pathlib import Path
>>> status = AnnotationStatus(
...     assembly="hg38",
...     directory=Path("/data/genome/hg38"),
...     default_annotation=None,
...     annotations=(),
... )
>>> status.default_row is None
True
>>> status.default_summary
'default: (none)'
>>> status.as_json()["directory"]
'/data/genome/hg38'

default_row property

default_row: AnnotationStatusRow | None

The Default annotation's own row, or None when no row is about it.

None covers both of the ways that happens, and a caller wanting to tell them apart reads :attr:default_annotation beside this: nothing decided a default, or one is decided and the table lists it under a name this disk knows nothing about.

default_summary property

default_summary: str

The closing sentence naming the Default annotation, and how to get it.

Four answers, and three of them tell the reader what to do next: nothing decided a default; one is decided and registered, which needs no advice; one is decided and broken, which is repaired by the command its own row already carries; or one is decided and absent, which is the ordinary state of a fresh machine and is registered by :func:annotation_register_command. Both commands come off an interface rather than being assembled here, so the two halves of this sentence cannot drift apart.

Returns:

Type Description
str

One line, beginning default: — the whole of what genome annotation list prints last.

as_json

as_json() -> dict[str, Any]

Return this report as --json serializes it.

Returns:

Type Description
dict

assembly, the directory as text, the default_annotation name, and annotations as a list of :meth:AnnotationStatusRow.as_json rows.

AnnotationStatusRow dataclass

AnnotationStatusRow(
    name: str,
    offered: bool,
    registered: bool,
    broken: bool,
    default: bool,
    provider: str | None,
    version: str | None,
    url: str | None,
    sha256: str | None,
    path: str | None,
    problem: str | None,
    repair: str | None,
)

One annotation, in whichever of its states it is: offered, registered, broken.

One shape for all of them, so a reader never has to ask which fields a row has — a name the table does not list carries the table's columns as None, and one nothing is wrong with carries the broken columns as None. :attr:registered and :attr:broken are never both true: a registration nothing vouches for is not one, and :attr:state is that invariant said in one word.

Attributes:

Name Type Description
name str

The Registered name this row is about.

offered bool

Whether the annotation table lists it for this assembly.

registered bool

Whether a record here vouches for it.

broken bool

Whether its directory is here and cannot be trusted.

default bool

The table's own default flag, False for a name no row lists.

provider str or None

Who publishes it, from the table's row; None for an unlisted one.

version str or None

The provider's release identifier; None for an unlisted one.

url str or None

Where the table says its GTF is fetched from; None for an unlisted one.

sha256 str or None

The digest the table pins; None when it pins none, and for an unlisted one.

path str or None

The registered GTF's path, or None when it is not registered here.

problem str or None

What is wrong, when :attr:broken; None otherwise.

repair str or None

The command that registers it again from scratch, when :attr:broken.

Examples:

>>> row = AnnotationStatusRow(
...     name="gencode_v50",
...     offered=True,
...     registered=False,
...     broken=False,
...     default=True,
...     provider="GENCODE",
...     version="v50",
...     url="https://example.org/gencode_v50.gtf.gz",
...     sha256=None,
...     path=None,
...     problem=None,
...     repair=None,
... )
>>> row.as_json()["offered"]
True
>>> row.state
'offered, not registered'

state property

state: str

Which of its states this row is in, in the words a surface prints.

broken first, because the three fields it is read from are not independent: a broken annotation is not registered — no record vouches for it — so answering with the absence of one would be true and useless, and it is the state that needs acting on. A row nothing offers is one this disk holds and the table does not, so the only thing left to say about it is that.

Returns:

Type Description
str

One of broken, registered, not offered, registered, or offered, not registered.

as_json

as_json() -> dict[str, Any]

Return this row as --json serializes it: every attribute above, in order.

:attr:state is not among them: it is read from :attr:broken, :attr:offered and :attr:registered, which are all here, so writing it out too would be a second spelling of the same rule for a reader to disagree with.

Returns:

Type Description
dict

The row's fields, under their own names.

BrokenAnnotation dataclass

BrokenAnnotation(
    name: str, directory: Path, problem: str, repair: str
)

An annotation directory that is there and cannot be trusted as finished.

What :func:list_annotations leaves out, said out loud. It is not a :class:GtfAnnotation and carries no file paths, because the whole point is that nothing vouches for the files: what it carries instead is why it cannot be trusted and the one command that makes it trustworthy again.

Attributes:

Name Type Description
name str

The Registered name its directory is filed under.

directory Path

The annotation directory, whatever state it is in.

problem str

What is wrong, in full — which files disagree or which are there with no record — ending in the repair below. This is :func:~genome.store.completion.check_registration's own message, so re-registering the annotation says exactly what listing it says.

repair str

The command that registers it again from scratch.

Examples:

>>> from pathlib import Path
>>> broken = BrokenAnnotation(
...     name="mine",
...     directory=Path("/data/genome/hg38/gtf/mine"),
...     problem="... holds files but no .completion.json ...",
...     repair="genome annotation register-gtf hg38 /tmp/mine.gtf mine --force",
... )
>>> broken.repair
'genome annotation register-gtf hg38 /tmp/mine.gtf mine --force'

GeneList dataclass

GeneList(
    assembly: str,
    annotation: str,
    category: str,
    sources: tuple[GeneListSource, ...],
)

The genes one annotation puts in one Gene category, attributed to their sources.

:meth:AnnotationRegistry.gene_list's answer — what genome annotation gene-list prints and what its --json serializes. There is no empty one: an annotation that declares no categories, and one that declares categories but not this one, each raise a :class:LookupError of their own rather than answering with nothing, so holding one of these means the category was really declared and really has genes in it.

Attributes:

Name Type Description
assembly str

The Assembly asked about.

annotation str

The Registered name asked about — the merged name for a Merged annotation, whose contributors are named in :attr:sources.

category str

The Gene category, as the curated lists spell it.

sources tuple of GeneListSource

One entry per contributing Curated gene list, in contributor order. Never empty. A contributor that does not declare this category is simply absent — a bacterium has no mitochondria, and that is not a failure to report.

Examples:

>>> genes = GeneList(
...     assembly="ce11",
...     annotation="wormbase_ws298",
...     category="rRNA",
...     sources=(
...         GeneListSource(None, "wormbase_ws298", "rRNA genes", "WormBase", ("a", "b")),
...     ),
... )
>>> genes.gene_ids
['a', 'b']
>>> genes.as_json()["category"]
'rRNA'

gene_ids property

gene_ids: list[str]

Every source's gene ids, concatenated in source order — a fresh list each call.

Concatenated and not de-duplicated. A merge rewrites only the seqname and never the gene_id, so two components carrying the same id would be a real ambiguity in the merged annotation, and collapsing it here would hide exactly that — a caller summing over these would silently under-count one of the two. Where that matters, :attr:sources says which contributor each id came from.

as_json

as_json() -> dict[str, Any]

Return this answer as --json serializes it.

Returns:

Type Description
dict

assembly, annotation, category, the concatenated gene_ids, and sources as a list of :meth:GeneListSource.as_json entries. :attr:gene_ids is written out beside the sources it is read from rather than left to the reader: assembling it is where a reader would reach for a set and de-duplicate, which is the one thing this answer must not do.

GeneListSource dataclass

GeneListSource(
    component: str | None,
    annotation: str,
    description: str,
    source: str,
    gene_ids: tuple[str, ...],
)

One Curated gene list that contributed to an answer, and what it contributed.

What makes a Merged annotation's genes attributable: one of these per contributing annotation, so a caller counting worm ribosomal RNA can drop the E. coli entry rather than being handed one number it cannot take apart. An annotation that is not a merge has exactly one, whose component is None.

description and source travel with the ids rather than being looked up separately, because they are what says whether these ids mean what the caller's metric needs: two annotations spelling a category the same way need not have curated it the same way.

Attributes:

Name Type Description
component str or None

The Component assembly whose genes these are, for a contributor to a Merged annotation; None for anything else.

annotation str

The Registered name of the contributing annotation.

description str

What membership in this category means for that annotation.

source str

Where that membership came from, and the caveats on using it.

gene_ids tuple of str

The gene ids it contributed, in the order its curated list lists them.

Examples:

>>> source = GeneListSource(
...     component="ce11",
...     annotation="wormbase_ws298",
...     description="the mature ribosomal RNA genes",
...     source="WormBase WS298 gene_biotype",
...     gene_ids=("WBGene00004512", "WBGene00004513"),
... )
>>> source.as_json()["component"]
'ce11'
>>> len(source.gene_ids)
2

as_json

as_json() -> dict[str, Any]

Return this contribution as --json serializes it: every attribute, in order.

Returns:

Type Description
dict

The fields above, under their own names, with gene_ids as a list.

NoGeneFeaturesError

NoGeneFeaturesError(annotation: str, assembly: str)

Bases: LookupError

An annotation's database holds no gene at all, so no gene id can be resolved.

The absence a caller must never read as this annotation carries none of my genes. A GTF that declares only exons registers as exons alone — :meth:~genome.annotation.registry.AnnotationRegistry.register leaves Feature inference off, and rightly, since it is the library's slow path and the publishers who matter declare their genes — so an annotation like that would answer every stem with not found while looking perfectly healthy. It says so instead, and names the argument that rebuilds it with the genes in.

A :class:LookupError, as the other absences on this surface are.

Parameters:

Name Type Description Default
annotation str

The Registered name that was asked about.

required
assembly str

The Assembly it is registered for.

required

Attributes:

Name Type Description
annotation str

The name asked about.

assembly str

The assembly it is registered for.

Examples:

>>> try:
...     raise NoGeneFeaturesError("mine", "tiny")
... except LookupError as error:
...     print("--infer-genes" in str(error))
True

ResolvedGeneIds dataclass

ResolvedGeneIds(
    assembly: str,
    annotation: str,
    resolved: Mapping[str, tuple[str, ...]],
    unresolved: tuple[str, ...],
)

The gene ids one Annotation carries for the Gene id stems it was asked about.

:meth:~genome.annotation.registry.AnnotationRegistry.resolve_gene_ids's answer, and the one result type two contexts share: it is defined beside the call that returns it, and :mod:genome.tf.gene, :mod:genome.tf.cofactor and :mod:genome.homology.annotation import it from here. A stem is a gene id with its version dropped, and inside one annotation it may name more than one gene id — nine do in gencode_v50lift37, eight of them pseudoautosomal-Y — so every stem answers with all of them and nothing here picks one. Two stems never name the same gene id, since an id has exactly one stem.

What was asked about and is not there rides back on the answer. A caller holding a few thousand stems gets the ones this annotation carries no gene for in :attr:unresolved rather than a shorter list than it passed, so what the thing it was holding contains and this annotation does not is visible instead of dropped.

Attributes:

Name Type Description
assembly str

The Assembly asked about.

annotation str

The Registered name whose own gene ids these are.

resolved mapping of str to tuple of str

Every stem that named at least one gene id, in the order the stems were asked about, to the ids it names, in ascending order. No value is ever an empty tuple — a stem that named nothing is in :attr:unresolved instead.

unresolved tuple of str

The stems no gene id in the annotation is of, in the order they were asked about.

Examples:

>>> answer = ResolvedGeneIds(
...     assembly="hg19",
...     annotation="gencode_v50lift37",
...     resolved={
...         "ENSG00000182378": ("ENSG00000182378.14", "ENSG00000182378.14_PAR_Y"),
...         "ENSG00000141510": ("ENSG00000141510.18",),
...     },
...     unresolved=("ENSG00000288541",),
... )
>>> answer.gene_ids
['ENSG00000182378.14', 'ENSG00000182378.14_PAR_Y', 'ENSG00000141510.18']
>>> answer.as_json()["unresolved"]
['ENSG00000288541']

gene_ids property

gene_ids: list[str]

Every gene id resolved, stem order and then id order — a fresh list each call.

Every id, not one per stem. Flattening is exactly where a reader would take the first id of each stem and lose the second, which is the pseudoautosomal gene this answer's shape exists to keep; :attr:resolved is what says which stem an id came from.

as_json

as_json() -> dict[str, Any]

Return this answer as --json serializes it.

Returns:

Type Description
dict

assembly, annotation, resolved as a mapping of stem to a list of gene ids, unresolved as a list, and the flattened gene_ids. The last is written out beside the mapping it is read from for the reason :attr:gene_ids gives: a reader assembling it is a reader who might take one id per stem.

annotation_dir

annotation_dir(assembly_dir: Path, name: str) -> Path

Return the directory holding the annotation registered as name.

annotation_register_command

annotation_register_command(
    assembly: str, name: str
) -> str

Return the command that registers the annotation name for assembly.

The one spelling of it. Errors quote it, the repair adds --force to it, and :attr:~genome.annotation.registry.AnnotationStatus.default_summary names it for a Default annotation nobody has fetched yet — so a renamed command is renamed once.

Parameters:

Name Type Description Default
assembly str

The Assembly the annotation belongs to, e.g. "hg38".

required
name str

The Registered name to address it by.

required

Returns:

Type Description
str

A shell command, unquoted and unfenced — the caller decides how to set it.

Examples:

>>> annotation_register_command("hg38", "gencode_v50")
'genome annotation register hg38 gencode_v50'

chromosome_check_summary

chromosome_check_summary(details: Mapping[str, Any]) -> str

Return the one line a surface prints about an annotation's chromosome-name check.

Four states, four sentences, and one of them is always returned: the check ran and passed; it had nothing to run against, and registering the assembly is what fixes that; the caller stood it down, which is not something to advise about; or the record does not say which, and none of the three may be claimed. Silence is not a fifth state — a surface that prints nothing about the check reads as one that passed.

details is a registration record's details; a caller holding what a registration answered with asks :attr:RegisteredAnnotation.chromosome_check instead and never spells the two fields. Those are chromosomes_checked — the check ran and the GTF's names were all the assembly's — and chromosomes_unchecked_because, which says which of the two reasons it did not, and is None when it did.

A record written before the second field existed carries a bare chromosomes_checked: false that was written for either reason, and nothing on disk says which. It reads as unknown rather than as either one, and rather than raising: the reason is a fact that was never gathered, which is what an absent entry in tool_versions means too.

Parameters:

Name Type Description Default
details mapping of str to object

A registration record's details. Anything else it holds is ignored, and a mapping holding neither field reads as unknown.

required

Returns:

Type Description
str

One sentence, with no trailing punctuation and no leading indent — the caller decides how to set it.

Examples:

>>> chromosome_check_summary({"chromosomes_checked": True})
'chromosomes checked — every name the GTF uses is one the assembly carries'
>>> print(chromosome_check_summary({"chromosomes_unchecked_because": "caller-override"}))
chromosomes not checked — the check was stood down, so the record does not vouch for the names

discard_merged_annotation

discard_merged_annotation(
    assembly_dir: Path, name: str
) -> bool

Remove the Merged annotation registered as name, when that is what it is.

The other half of a chimera build owning its annotation. The merged name is the +-join of the contributing annotations' names, so a rebuild whose contributing set changed registers the merge under a new name — and the previous one, which nothing else will ever write again, would otherwise stay registered beside it. Two derived annotations with nothing to choose between them is a chimera whose Default annotation is suddenly none, which is how an annotated chimera comes back from a legitimate repair with none at all. So the build removes what it no longer owns, and :meth:~genome.assembly.chimera_build.ChimeraBuilder.build_genome is the only caller.

Owning it is proved, not assumed: only a directory whose record carries the merged_from marker a merge writes is removed, so an annotation a caller registered by hand — and a directory nothing vouches for — is left exactly where it is, whatever it is called.

Parameters:

Name Type Description Default
assembly_dir Path

The chimera's Assembly dir, which the annotation is filed under.

required
name str

The Registered name to remove, as the previous build's completion record names it.

required

Returns:

Type Description
bool

Whether an annotation was removed. False for a name nothing is registered under, and for one whose record does not show a merge wrote it.

Examples:

>>> from pathlib import Path
>>> discard_merged_annotation(Path("/tmp/definitely-not-an-assembly"), "a+b")
False

register_annotation

register_annotation(
    assembly: str,
    name: str,
    *,
    force: bool = False,
    cache_dir: str | Path | None = None,
    progressbar: bool = True,
    metadata: AnnotationMetadata | None = None,
    check_chromosomes: bool = True,
    disable_infer_genes: bool = True,
    disable_infer_transcripts: bool = True,
) -> RegisteredAnnotation

Register name for assembly and return the record of what that did.

:meth:~genome.annotation.registry.AnnotationRegistry.register addressed by assembly name, and answering with the record rather than the paths — the call genome annotation register makes, and the one a script makes when it wants to serialize what happened. An annotation that is already registered is returned from its record without fetching anything. :func:register_gtf is the same shape for a GTF the table does not list.

Parameters:

Name Type Description Default
assembly str

The assembly the annotation belongs to, e.g. "hg38".

required
name str

The Registered name the table lists, e.g. "gencode_v50".

required
force bool

Register again from scratch, repairing a directory that raises.

False
cache_dir str or Path

Override which assembly directory the annotation is filed under. Defaults to :func:assembly_data_dir(assembly) <genome.assembly.download.assembly_data_dir>.

None
progressbar bool

Show a download progress bar (requires tqdm).

True
metadata AnnotationMetadata

A complete annotation record to use instead of the curated table's row.

None
check_chromosomes bool

Check the GTF's chromosome names against the assembly's. Pass False to register an annotation whose mismatch you have inspected and accept; the record then says the check was stood down, rather than merely that it did not run.

True
disable_infer_genes bool

Do not reconstruct gene features from exon lines.

True
disable_infer_transcripts bool

Do not reconstruct transcript features from exon lines.

True

Returns:

Type Description
RegisteredAnnotation

The completion record the run wrote — files, source_url, sha256, details, completed_at and the rest — with the assembly it belongs to and the directory it lives in. :meth:RegisteredAnnotation.as_json serializes it.

Raises:

Type Description
ValueError

If the table lists no annotation name for assembly.

ChromosomeMismatchError

If the GTF names sequences the assembly does not carry.

RegistrationError

If the directory holds a build that cannot be trusted as finished, or (with force) if the run somehow left no record behind.

ChecksumMismatchError

If the row pins a sha256 and the unpacked GTF is not it.

Examples:

>>> register_annotation("sacCer3", "ensgene_v101")
RegisteredAnnotation(assembly='sacCer3', directory=PosixPath('...'), record=...)

register_gtf

register_gtf(
    assembly: str,
    gtf: str | Path,
    name: str,
    *,
    force: bool = False,
    cache_dir: str | Path | None = None,
    check_chromosomes: bool = True,
    disable_infer_genes: bool = True,
    disable_infer_transcripts: bool = True,
) -> RegisteredAnnotation

Register the GTF at gtf for assembly and return the record of what that did.

:meth:~genome.annotation.registry.AnnotationRegistry.register_path addressed by assembly name, and answering with the record rather than the paths — the call genome annotation register-gtf makes, and the way a script registers an annotation the curated table does not list and then serializes what happened. :func:register_annotation is the same shape for one the table does list.

Naming the assembly is what lets its chrom.sizes be found rather than passed, so an unlisted GTF has its chromosome names checked by default, exactly as a listed one does — and it is what says which reference these gene models are for. An assembly that is not prepared yet has no chrom.sizes to check against, and the record then says the names went unchecked — and that it was for want of that file, not because anyone stood the check down — rather than claiming they passed.

Parameters:

Name Type Description Default
assembly str

The assembly the annotation belongs to, e.g. "hg38". Never inferred from the GTF: it says which reference these gene models are for.

required
gtf str or Path

Path to the source GTF, plain or .gz.

required
name str

The Registered name to address it by, unique within the assembly.

required
force bool

Register again from scratch, repairing a directory that raises.

False
cache_dir str or Path

Override which assembly directory the annotation is filed under. Defaults to :func:assembly_data_dir(assembly) <genome.assembly.download.assembly_data_dir>.

None
check_chromosomes bool

Check the GTF's chromosome names against the assembly's. Pass False to register a GTF whose mismatch you have inspected and accept; the record then says the check was stood down, rather than merely that it did not run.

True
disable_infer_genes bool

Do not reconstruct gene features from exon lines.

True
disable_infer_transcripts bool

Do not reconstruct transcript features from exon lines.

True

Returns:

Type Description
RegisteredAnnotation

The completion record the run wrote, with the assembly and the directory it lives in, exactly as :func:register_annotation returns them. The source_url is the path the GTF was taken from.

Raises:

Type Description
FileNotFoundError

If gtf is not a file.

ChromosomeMismatchError

If the GTF names sequences the assembly does not carry.

RegistrationError

If the directory holds a build that cannot be trusted as finished, or (with force) if the run somehow left no record behind.

Examples:

>>> register_gtf(
...     "sacCer3", "custom.gtf.gz", "custom"
... )
RegisteredAnnotation(assembly='sacCer3', directory=PosixPath('...'), record=...)

register_merged_gtf

register_merged_gtf(
    assembly_dir: Path,
    name: str,
    sources: Sequence[MergeSource],
    *,
    separator: str,
    chrom_sizes: str | Path,
    disable_infer_genes: bool = True,
    disable_infer_transcripts: bool = True,
) -> GtfAnnotation

Write the Merged annotation of sources under name and build its database.

The annotation half of a Chimera build, called from :meth:~genome.assembly.chimera_build.ChimeraBuilder.build_genome and from nowhere else. Each source's GTF is streamed a line at a time into one file whose seqnames carry the component suffix the chimera's FASTA already carries, which is then placed, checked, built and recorded exactly as any other annotation is.

No coordinate is converted. Only the first column of each data line is rewritten; every byte after the first tab — including both position fields, which are 1-based and inclusive as GTF has them — is copied through untouched. The features are the components' own features on the components' own sequences, under a new spelling of the sequence name and nothing else.

Comment lines are dropped, all of them. A #!genome-build pragma names the single assembly its file was built for, and several of those concatenated would each be false about the chimera; the ordinary # comment beside them describes a file that no longer exists as such. Nothing else is dropped: a line carrying a tab is a data line and survives, unsorted and in component order.

The chromosome-name check is not optional here and has no argument that stands it down. Everything else in the build derives the chimera's names twice — once for the FASTA and once for this — and the check is the one place those two answers are set against each other, so a merge that misspelled a name raises :class:ChromosomeMismatchError rather than registering an annotation that queries empty.

Nothing on disk is adopted: unlike the other ways in, this one never asks whether the annotation is already registered. It is written by the build that owns it, every time that build runs, which is what makes a stale database impossible to hand back — the name is derived from the contributing annotations, so it changes when they do, but it cannot say which components contributed and would otherwise be reusable under a meaning it no longer has.

Parameters:

Name Type Description Default
assembly_dir Path

The chimera's Assembly dir, which this annotation is filed under.

required
name str

The Registered name to write it as — derived by the caller from the contributing annotations' names.

required
sources sequence of MergeSource

One entry per contributing component, in the order their sequences are written. Must not be empty: no contributors means no annotation, which the caller decides rather than registering an empty one.

required
separator str

The run of underscores this chimera's chromosome names carry, as :func:~genome.assembly.chimera.derive_separator gave it.

required
chrom_sizes str or Path

The chimera's chrom.sizes, whose names every merged seqname must be among.

required
disable_infer_genes bool

Do not reconstruct gene features from exon lines.

True
disable_infer_transcripts bool

Do not reconstruct transcript features from exon lines.

True

Returns:

Type Description
GtfAnnotation

The registered annotation's name and its two file paths.

Raises:

Type Description
ValueError

If sources is empty.

ChromosomeMismatchError

If a merged seqname is not one the chimera carries — the merge and the FASTA build disagreeing, which nothing else would catch.

ChimeraNamingError

If a component name or the separator does not obey the naming contract.

Examples:

>>> from pathlib import Path
>>> register_merged_gtf(
...     Path("/data/genome/ce11_ecHT115"),
...     "wormbase_ws298+refseq_rs_2025_06_26",
...     [MergeSource("ce11", "wormbase_ws298", Path("/data/ce11.gtf"))],
...     separator="__",
...     chrom_sizes=Path("/data/genome/ce11_ecHT115/ce11_ecHT115.chrom.sizes"),
... )
GtfAnnotation(name='wormbase_ws298+refseq_rs_2025_06_26', ...)

annotation_status

annotation_status(
    assembly: str, *, cache_dir: str | Path | None = None
) -> AnnotationStatus

Report what assembly's table offers against what is registered on this machine.

:meth:AnnotationRegistry.status for an assembly named rather than opened, which is what genome annotation list runs. Nothing is prepared, fetched, built or created to answer it — an assembly with no directory at all is the case it most needs to serve.

Parameters:

Name Type Description Default
assembly str

The assembly to report on, e.g. "hg38".

required
cache_dir str or Path

Override which assembly directory is inspected. Defaults to :func:assembly_data_dir(assembly) <genome.assembly.download.assembly_data_dir>.

None

Returns:

Type Description
AnnotationStatus

The report :meth:AnnotationRegistry.status describes.

Examples:

>>> annotation_status("sacCer3").default_annotation
'ensgene_v101'

gene_list

gene_list(
    assembly: str,
    category: str,
    *,
    annotation: str | None = None,
    cache_dir: str | Path | None = None,
) -> GeneList

Return the genes assembly's annotation puts in category.

:meth:AnnotationRegistry.gene_list for an assembly named rather than opened, which is what genome annotation gene-list runs. A registry is built for the length of the call, so there is no second code path. Nothing is prepared, fetched or built to answer it.

Parameters:

Name Type Description Default
assembly str

The assembly to ask about, e.g. "ce11".

required
category str

The Gene category, as the curated list spells it.

required
annotation str

The Registered name to ask about; the Default annotation when omitted.

None
cache_dir str or Path

Override which assembly directory is inspected, as :func:annotation_status takes it.

None

Returns:

Type Description
GeneList

The answer :meth:AnnotationRegistry.gene_list describes.

Raises:

Type Description
ValueError

If annotation is omitted and no Default annotation is decided.

AnnotationNotRegisteredError

If that annotation is not registered here.

NoGeneCategoriesError

If no curated gene list ships for it.

GeneCategoryNotDeclaredError

If it declares categories and not this one.

Examples:

>>> gene_list("ce11", "rRNA").category
'rRNA'

gene_lists

gene_lists(
    assembly: str,
    *,
    annotation: str | None = None,
    cache_dir: str | Path | None = None,
) -> tuple[GeneList, ...]

Return every Gene category assembly's annotation declares.

:meth:AnnotationRegistry.gene_lists addressed by assembly name — what genome annotation gene-categories runs, built the same way :func:gene_list is. Never an empty tuple: an annotation that declares nothing raises instead.

Parameters:

Name Type Description Default
assembly str

The assembly to ask about, e.g. "hg38".

required
annotation str

The Registered name to ask about; the Default annotation when omitted.

None
cache_dir str or Path

Override which assembly directory is inspected.

None

Returns:

Type Description
tuple of GeneList

One entry per declared category, in declaration order. Never empty.

Raises:

Type Description
ValueError

If annotation is omitted and no Default annotation is decided.

AnnotationNotRegisteredError

If that annotation is not registered here.

NoGeneCategoriesError

If no curated gene list ships for it.

Examples:

>>> [answer.category for answer in gene_lists("hg38")]
['rRNA', 'rRNA_pseudogene', 'Mt_rRNA']

genome.assembly

One Assembly on disk — which reference it is, where its files live, how a locus becomes bases.

The whole Assembly context, and its own I/O with it: nothing outside this package fetches a FASTA, derives a companion file or decides where an assembly's directory is.

  • :mod:~genome.assembly.metadata is the curated table — what the lab supports and how each reference is named across databases.
  • :mod:~genome.assembly.source resolves a name into where its bytes come from, and :mod:~genome.assembly.download gets them; :mod:~genome.assembly.registration is the half of registering that has nothing to do with where they came from — the Assembly dir layout, the staging, the Completion marker.
  • :mod:~genome.assembly.status reads that layout back: which assemblies the curated table offers, and which of them — plus which names it does not list — are prepared here.
  • :mod:~genome.assembly.fasta derives the companions with External tools and :mod:~genome.assembly.twobit reads bases back out of the .2bit.
  • :mod:~genome.assembly.chimera is the naming rules a Chimera obeys and :mod:~genome.assembly.chimera_build writes one; :mod:~genome.assembly.components is what such a build recorded, read back.
  • :mod:~genome.assembly.genome is all of it opened: :class:~genome.assembly.genome.Genome.

What this file re-exports stops short of two modules, and the omission is load-bearing. :class:~genome.assembly.genome.Genome and the chimera build both reach into :mod:genome.annotation, which reaches back here for the Assembly dir — so importing either of them from this file would make import genome.annotation run the whole open-a-genome stack through a package it is halfway through importing. Genome is exported from :mod:genome itself, which is where a caller holds it anyway, and so is :class:~genome.assembly.chimera_build.AmbiguousDefaultAnnotationError — the one error the chimera build raises. It is public at the root because of this import edge, not because it belongs to this context any less than :class:~genome.assembly.chimera.ChimeraNamingError beside it.

Examples:

>>> from genome.assembly import assembly_metadata
>>> assembly_metadata("hg38").ncbi_name
'GRCh38'

ChimeraNamingError

Bases: ValueError

A name does not obey the chimera naming contract.

One type covers every rejection this module makes — a component name that is not alphanumeric, a component set that is too small or repeats itself, an assembly name that is not spelled like a chimera's, a chromosome name carrying no component suffix, a separator that was not derived from the components it is about to be written with. The message says which, and what to do about it.

It is a :class:ValueError because every one of those is a bad value, and it is named so that a caller deciding whether a string is a chimera's name can catch exactly this and nothing else.

Examples:

>>> from genome.assembly.chimera import ChimeraNamingError, split_name
>>> try:
...     split_name("hg38")
... except ChimeraNamingError:
...     print("not spelled like a chimera")
not spelled like a chimera

ChimeraDetails dataclass

ChimeraDetails(
    separator: str,
    component_details: tuple[ComponentDetails, ...],
)

What a chimera's completion record says about the build that produced it.

The Source a finished chimera records, in the details shape :class:~genome.assembly.chimera_build.ChimeraBuilder writes — written and read here, so nothing else has to know its keys. It answers the only question that decides whether an assembly is a Chimera at runtime — the record, never the metadata row — and carries the facts a later pass needs: the separator its chromosome names were written with, and what each component and each contributed annotation was at build time.

Attributes:

Name Type Description
separator str

The run of underscores this chimera's chromosome names carry.

component_details tuple of ComponentDetails

One entry per Component, in the sorted order the chimera's name spells them.

Examples:

>>> details = ChimeraDetails(
...     "__",
...     (
...         ComponentDetails("ce11", "1a2b3c", "wormbase_ws298", "4d5e6f"),
...         ComponentDetails("ecHT115", "7a8b9c", None, None),
...     ),
... )
>>> details.components
['ce11', 'ecHT115']

components property

components: list[str]

The component assembly names, sorted — a fresh list each call.

merged_annotation property

merged_annotation: str | None

The Registered name of the Merged annotation this build wrote, or None.

Read back rather than looked up: the name is the contributing annotations' names joined by :func:merged_annotation_name, which is what spelled it when the merge was registered. None when no component contributed one, which is a build that registered no annotation at all rather than an empty one.

Examples:

>>> ChimeraDetails(
...     "__",
...     (
...         ComponentDetails("ce11", "1a2b3c", "wormbase_ws298", "4d5e6f"),
...         ComponentDetails("ecHT115", "7a8b9c", None, None),
...     ),
... ).merged_annotation
'wormbase_ws298'

as_details

as_details(*, merged: bool) -> dict[str, Any]

Return these details as a completion record writes them down.

The inverse of :meth:from_details. Kept to facts a later pass cannot re-derive from the name alone: which spelling was used, what each component was when this chimera was built, and — when there is a merged annotation — which of each component's annotations went into it.

Parameters:

Name Type Description Default
merged bool

Whether this build registered a Merged annotation.

required

Returns:

Type Description
dict

The details of a chimera's completion record, ready to serialize.

Examples:

>>> ChimeraDetails("__", (ComponentDetails("ce11", None, None, None),)).as_details(
...     merged=False
... )
{'separator': '__', 'components': [{'name': 'ce11', 'sha256': None}]}

from_record classmethod

from_record(
    record: CompletionRecord | None,
) -> ChimeraDetails | None

Read a completion record's chimera details, or None when it has none.

:meth:from_details over the record's details, and None for an absent record — the shape a caller holding a record rather than a payload wants. The record names the assembly, so a broken one is refused in its own name without the caller having to supply it.

Parameters:

Name Type Description Default
record CompletionRecord or None

The record to read, as :func:~genome.store.completion.read_record returns it.

required

Returns:

Type Description
ChimeraDetails or None

The details, or None when the record is not a chimera's.

Raises:

Type Description
RegistrationError

If the record claims to be a chimera's and cannot be read as one — see :meth:from_details.

Examples:

>>> ChimeraDetails.from_record(None) is None
True

from_details classmethod

from_details(
    details: Mapping[str, Any], *, assembly: str
) -> ChimeraDetails | None

Read a completion record's details, or None when they are not a chimera's.

Two answers, and they are not the same answer. None means this is not a chimera — the details of an ordinary downloaded or seeded assembly, which say nothing about components at all. Details that do speak of components but cannot be read as a build of this package's own writing are a broken registration, and one raises rather than reading back as an ordinary assembly: both :func:components_status and :func:~genome.assembly.download.verify_assembly decide what to check from this answer, so a chimera silently demoted to an ordinary assembly is one nothing ever compares against its components again.

A chimera's record spells the separator and the components together, and each component entry names itself. Anything short of that — one key without the other, either of the wrong type, an entry that is not an object or does not name an assembly — is the broken case. The two annotation fields are the exception and stay optional: a build that registered no merged annotation writes neither, and both then read as None.

Taking the mapping rather than the record is what lets a caller that already holds one — the CLI, whose register payload is the record — answer from what it has instead of reading the same file again. assembly is what the refusal is addressed to, since a mapping does not know whose it is.

Parameters:

Name Type Description Default
details mapping of str to object

A registration record's details. Anything else it holds is ignored, and an empty mapping reads as not a chimera.

required
assembly str

The assembly these details were recorded for, named in the refusal along with the command that repairs it.

required

Returns:

Type Description
ChimeraDetails or None

The details, or None when they are not a chimera build's.

Raises:

Type Description
RegistrationError

If details claim to be a chimera's and cannot be read as one. The message says which part did not read and quotes genome assembly register <assembly> --force, which is the repair.

Examples:

>>> ChimeraDetails.from_details({}, assembly="hg38") is None
True

ComponentDetails dataclass

ComponentDetails(
    name: str,
    sha256: str | None,
    annotation: str | None,
    annotation_sha256: str | None,
)

What a chimera's completion record says about one of its components.

One entry of the details shape :class:~genome.assembly.chimera_build.ChimeraBuilder writes, read back. Every field is a fact about the component at the time this chimera was built, taken from that component's own records rather than by rehashing anything — which is what lets a later pass notice a component re-registered underneath the chimera.

Attributes:

Name Type Description
name str

The Component assembly name.

sha256 str or None

The digest that component's completion record pinned, or None when it pinned none — unknown, rather than wrong.

annotation str or None

The Registered name of the annotation it contributed to the Merged annotation, or None when it contributed none.

annotation_sha256 str or None

That annotation's own recorded digest, or None.

Examples:

>>> ComponentDetails("ce11", "1a2b3c", "wormbase_ws298", "4d5e6f").annotation
'wormbase_ws298'

as_entry

as_entry(*, merged: bool) -> dict[str, Any]

Return this component's entry as a record writes it down.

The inverse of the reading :meth:ChimeraDetails.from_details does, so the keys are spelled once. The two annotation keys are written only when a merged annotation was registered: a build that registered none says nothing about annotations at all rather than writing null beside every component.

Parameters:

Name Type Description Default
merged bool

Whether this build registered a Merged annotation.

required

Returns:

Type Description
dict

One entry of a completion record's details, ready to serialize.

Examples:

>>> ComponentDetails("ce11", "1a2b3c", None, None).as_entry(merged=False)
{'name': 'ce11', 'sha256': '1a2b3c'}

RegisteredAssembly dataclass

RegisteredAssembly(
    assembly: str, directory: Path, record: CompletionRecord
)

What preparing an assembly on disk produced: its record, and where that landed.

:func:register_assembly's answer — what genome assembly register prints, and what its --json serializes — and :func:registered_assembly's, which reads it back without preparing anything. The Completion marker the run wrote is the answer, so it is carried whole rather than copied out field by field, and the two questions a surface then asks — which files are claimed, and is this a Chimera — are answered from that one record instead of by reading the directory again.

Attributes:

Name Type Description
assembly str

The Assembly that was registered, under the name the caller asked for.

directory Path

Its Assembly dir — where those files and that record are.

record CompletionRecord

The record the registration wrote, read back.

Examples:

>>> from pathlib import Path
>>> from genome.store.completion import CompletionRecord
>>> registered = RegisteredAssembly(
...     assembly="hg38",
...     directory=Path("/data/genome/hg38"),
...     record=CompletionRecord(
...         kind="genome",
...         name="hg38",
...         files={"hg38.fa.fai": 21, "hg38.fa": 12},
...         source_url="https://example.org/hg38.fa.gz",
...         sha256="1a2b3c",
...         tool_versions={},
...         package_version="2026.8.0",
...         completed_at="2026-08-12T09:00:00+00:00",
...         details={},
...     ),
... )
>>> registered.file_names
['hg38.fa', 'hg38.fa.fai']
>>> registered.chimera is None
True
>>> registered.genome_files.twobit
PosixPath('/data/genome/hg38/hg38.2bit')
>>> registered.as_json()["directory"]
'/data/genome/hg38'
>>> registered.as_json()["genome_files"]["fasta"]
'/data/genome/hg38/hg38.fa'

source_url property

source_url: str | None

Where the bytes were fetched from, or None when nothing was — a chimera's.

sha256 property

sha256: str | None

Digest of the unpacked FASTA, or None when none was computed.

file_names property

file_names: list[str]

Every file the record claims, sorted — a fresh list each call.

chimera property

chimera: ChimeraDetails | None

What the build recorded about its components, or None for anything else.

The record is what says an assembly is a Chimera, here as everywhere else — and the record is already in hand, so a surface reporting the registration that just happened never reads the same file a second time to find out.

genome_files property

genome_files: GenomeFiles

The four Genome files, where the Assembly dir layout puts them.

From the layout rather than :attr:record, whose files are sizes keyed by name.

as_json

as_json() -> dict[str, Any]

Return this registration as --json serializes it.

The record's own fields under the record's own names, then the assembly asked for, the directory it landed in and its genome_files — the facts a record does not hold about itself. No name is respelled here.

Returns:

Type Description
dict

The record's fields, followed by assembly, directory and genome_files (fasta, fai, twobit, chrom_sizes, as text).

UCSCGenomeDownloader

UCSCGenomeDownloader(
    assembly: str,
    cache_dir: str | Path | None = None,
    *,
    metadata: AssemblyMetadata | None = None,
)

Bases: AssemblyRegistration

Download reference-genome FASTA files from the UCSC golden path.

UCSC serves per-assembly downloads under https://hgdownload.soe.ucsc.edu/goldenPath/<assembly>/bigZips/. This fetches the soft-masked, gzipped whole-genome FASTA (<assembly>.fa.gz) and, by default, decompresses it to <assembly>.fa.

An :class:~genome.assembly.registration.AssemblyRegistration whose FASTA arrives over the network: the base owns the assembly's directory and the steps that finish a registration in it, and everything added here is about where the bytes come from — the URL, the name check, the pinned digest. The fetch itself is :func:~genome.store.fetch.fetch_url, given this assembly's working area: where a download lands is a decision the assembly's directory already made, so nothing here binds it to a cache of its own.

The assembly's metadata row is consulted first: when it pins a source URL that URL is fetched instead of the derived golden-path one, and when it pins a sha256 the unpacked FASTA is checked against it. An assembly the table does not list keeps working exactly as before — the table is a cross-reference, not an allow-list.

Parameters:

Name Type Description Default
assembly str

UCSC assembly name, e.g. "hg38", "hg19", "mm39".

required
cache_dir str or Path

Override the storage directory. Defaults to :func:assembly_data_dir(assembly) <assembly_data_dir>.

None
metadata AssemblyMetadata

A complete metadata record to use instead of the curated table's row for assembly. Omit it and the row is looked up here, so a downloader used on its own still gets the pinned source and checksum.

None

Attributes:

Name Type Description
metadata AssemblyMetadata

The record this downloader works from — the one passed in, else what :func:~genome.assembly.metadata.assembly_metadata knows about assembly. Total: an assembly the table does not list has a record whose every identifier is unknown, never no record, so nothing here asks whether there is one before reading a field off it. Whether the table lists a name is the other question and is not asked here at all.

Examples:

>>> dl = UCSCGenomeDownloader("hg38")
>>> dl.fasta_url
'https://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.fa.gz'
>>> UCSCGenomeDownloader("no_such_assembly").metadata.sha256 is None
True
>>> fasta = dl.fetch_fasta()

assembly_url property

assembly_url: str

URL of the UCSC golden-path directory for this assembly.

fasta_url property

fasta_url: str

URL of the gzipped whole-genome FASTA — the pinned source, else the golden path.

A metadata row that pins a source URL answers this outright; otherwise the URL is derived from the assembly name and UCSC's golden-path layout, as it always was.

validate_assembly

validate_assembly(*, timeout: float = 30.0) -> None

Check that assembly is a real golden-path directory at UCSC.

Sends an HTTP HEAD to :attr:assembly_url so a typo in the assembly name fails fast with a clear message, rather than surfacing later as an opaque 404 on the FASTA file itself.

Parameters:

Name Type Description Default
timeout float

Seconds to wait for the server before giving up.

30.0

Raises:

Type Description
ValueError

If no directory exists for assembly (HTTP 404) — the assembly name is almost certainly wrong.

RequestException

If the request fails for any other reason (network error, timeout, or an unexpected non-success status).

Examples:

>>> UCSCGenomeDownloader("hg38").validate_assembly()
>>> UCSCGenomeDownloader("nope99").validate_assembly()
Traceback (most recent call last):
ValueError: Unknown UCSC assembly 'nope99': no directory at ...

verify_fasta

verify_fasta(fasta: Path | None = None) -> str

Return the sha256 of the unpacked FASTA, raising when it is not the pinned one.

The digest is taken over the unpacked <assembly>.fa, never over the .fa.gz it arrived in, which is why pooch's own known_hash cannot do this job: pooch hashes what it downloaded. Gzip bytes change under recompression while the FASTA inside does not, so a content digest also matches a copy taken from a mirror or handed over by hand. The file is streamed, so a whole-genome FASTA is never held in memory.

Parameters:

Name Type Description Default
fasta Path

The file to check. Defaults to this assembly's <assembly>.fa in :attr:cache_dir.

None

Returns:

Type Description
str

The computed hex digest. An assembly whose metadata pins no sha256 — or that the table does not list — has nothing to disagree with, so the value is simply reported back for recording or for pinning later.

Raises:

Type Description
ChecksumMismatchError

If the metadata pins a sha256 and the file's digest is a different one; the message names both values.

FileNotFoundError

If the file does not exist.

Examples:

>>> UCSCGenomeDownloader("sacCer3").verify_fasta()
'6ff72f079c3268431fc514a1a88730f8290e717663d343fa8a3590af65c422c3'

fetch_fasta

fetch_fasta(
    *,
    known_hash: str | None = None,
    decompress: bool = True,
    progressbar: bool = True,
) -> Path

Download (and optionally decompress) the genome FASTA into the working area.

Both files land in the assembly's working area rather than beside its prepared files: nothing there is claimed by a completion record, and the whole area is discarded once one is written. :meth:fetch_genome is what moves the unpacked FASTA out of it.

When the URL had to be derived from the assembly name, the assembly is first confirmed to exist at UCSC via :meth:validate_assembly, so a typo fails fast with a clear message. When the metadata row pins a source URL that check is skipped: validation is a property of the source, and a pinned URL is the source, so there is nothing left to guess.

Parameters:

Name Type Description Default
known_hash str

Expected hash of the downloaded .fa.gz (checked by pooch, before decompression); see :func:~genome.store.fetch.fetch_url. Unrelated to the metadata row's sha256, which covers the unpacked FASTA — see :meth:verify_fasta.

None
decompress bool

If True, gunzip the download to <assembly>.fa and return that path. If False, keep and return the .fa.gz.

True
progressbar bool

Show a download progress bar (requires tqdm).

True

Returns:

Type Description
Path

Path inside the working area to the decompressed <assembly>.fa (or to the <assembly>.fa.gz when decompress=False). The archive is kept there for the duration of the run, so an interrupted registration repairs without downloading a whole genome again.

Raises:

Type Description
ValueError

If the URL was derived and the assembly is unknown to UCSC.

fetch_genome

fetch_genome(
    *,
    known_hash: str | None = None,
    progressbar: bool = True,
    overwrite: bool = False,
) -> GenomeFiles

Prepare the reference genome in one call — by download, or by concatenation.

The choke point every way in funnels through, and therefore where a name is resolved: :func:~genome.assembly.source.resolve_source says whether this assembly's FASTA is fetched or concatenated from components already on this disk, and only the first of those is what the rest of this method does. A :class:~genome.assembly.source.ComponentSource is handed to :func:~genome.assembly.chimera_build.build_chimera instead, which is why genome assembly register <name> --force is one command for all three kinds of Source rather than a download that fails on two of them.

A finished registration is returned from its record either way, and a chimera's components are checked against their own records as it is handed back — the one failure a digest of this assembly's own bytes cannot show, since a component registered again underneath leaves those bytes untouched and no longer a copy of anything that exists. An assembly with no components pays nothing for the question.

Chains :meth:fetch_fasta, :meth:verify_fasta and :func:genome.assembly.fasta.prepare_fasta: download <assembly>.fa.gz from the assembly's source into the working area, decompress it, check the unpacked FASTA against the sha256 its metadata pins, move it to :attr:cache_dir, then build the .fai index, .2bit encoding, and .chrom.sizes beside it (<LIULAB_DATA>/genome/<assembly>/ by default).

A completion record is written last, once all four files exist, holding the URL fetched, the digest computed, every file with its size, the tool versions, the package version and the time. That record is what makes a later call cheap: it is read, its claims are checked against disk by size alone, and nothing is fetched. The archive is deleted with the rest of the working area at that point — and only then, so an interrupted run still repairs from it.

A directory that cannot be trusted — files with no record, or a record that disagrees with what is on disk — raises rather than being rebuilt or trusted, naming genome assembly register <assembly> --force. That is what overwrite=True is: it skips the question, keeps the unpacked FASTA when its digest can be shown to be the pinned one, and fetches the source again when it cannot (see :meth:_proven_fasta). An absent or empty directory is not a broken state — it is a fresh registration and proceeds normally.

Parameters:

Name Type Description Default
known_hash str

Expected hash of the downloaded .fa.gz (before decompression); see :func:~genome.store.fetch.fetch_url. When None, pooch verifies nothing — which is independent of the metadata row's sha256 over the unpacked FASTA, always checked here.

None
progressbar bool

Show a download progress bar (requires tqdm).

True
overwrite bool

Register again from scratch: the assembly's completion record is not consulted, and the preparation steps (faidx, 2bit, chrom.sizes) rerun even when their outputs look fresh. An archive still sitting in the working area is reused rather than downloaded again.

False

Returns:

Type Description
GenomeFiles

Paths to the decompressed FASTA and its three derived files.

Raises:

Type Description
UnfinishedRegistrationError

If the assembly directory holds files but no record.

RegistrationMismatchError

If its record disagrees with what is on disk, or if a component of this chimera was registered again since it was built.

FileNotFoundError

If the name resolves to a chimera whose components are not all prepared here, or spells them in an order that is not the canonical one. Both messages name the command to run instead.

HTTPError

If the download fails (e.g. a wrong assembly name 404s).

ValueError

If the assembly is unknown to UCSC, or if known_hash is given and the download does not match.

ChecksumMismatchError

If the metadata pins a sha256 and the unpacked FASTA is not it.

ToolNotFoundError

If samtools, faToTwoBit, or twoBitInfo are not on PATH.

RuntimeError

If any native preparation tool exits non-zero.

Examples:

Skipped, since it downloads a genome: the one call decompresses and prepares it too.

>>> dl = UCSCGenomeDownloader("hg38")
>>> files = dl.fetch_genome()
>>> files.fai.name, files.twobit.name, files.chrom_sizes.name
('hg38.fa.fai', 'hg38.2bit', 'hg38.chrom.sizes')

fetch_genome_from

fetch_genome_from(
    source: str | Path,
    *,
    progressbar: bool = True,
    overwrite: bool = False,
) -> GenomeFiles

Prepare the genome from a user-provided FASTA instead of downloading from UCSC.

Use this to seed an assembly from a file you already have or a non-UCSC URL — handy when the UCSC golden path is unreachable (firewall/proxy) or for a custom reference. source is either a local filesystem path (copied into the working area) or a URL (fetched with :func:~genome.store.fetch.fetch_url, so http(s), ftp and sftp all work). Gzipped sources (.gz) are decompressed. The resulting <assembly>.fa is then indexed/2bit/chrom.sizes-prepared exactly as :meth:fetch_genome does, and a completion record is written last, recording the source it was given and the digest of what arrived. UCSC is never contacted. Neither is the metadata row's pinned source or checksum: a seeded FASTA is whatever the caller handed over — it is recorded, never compared — and the assembly name is only a label for the directory it lands in.

The :class:~genome.assembly.source.SeededSource kind, and the one of the three nothing resolves: the caller answered before anything was asked, so no name is read and no record is consulted for what this assembly is. Its record is still what says whether the work is already done — that check runs first, exactly as it does for a name that had to be resolved.

Parameters:

Name Type Description Default
source str or Path

Local FASTA path or http(s)/ftp/sftp URL. .gz is decompressed.

required
progressbar bool

Show a download progress bar while fetching a URL (ignored for a local copy).

True
overwrite bool

Re-read the source and rerun preparation even when this assembly's completion record already says it is registered.

False

Returns:

Type Description
GenomeFiles

Paths to the prepared FASTA and its three derived files.

Raises:

Type Description
RegistrationError

If the assembly directory holds files but no record, or a record that disagrees with what is on disk. The message names this same call as the repair — genome assembly register <assembly> --force --source <source> — rather than the plain one, which would fetch from somewhere this assembly never came from.

FileNotFoundError

If source is a local path that does not exist.

ValueError

If source carries a URL scheme no downloader handles.

ToolNotFoundError

If a preparation tool is not on PATH.

RuntimeError

If any native preparation tool fails.

VerifiedAssembly dataclass

VerifiedAssembly(
    assembly: str,
    fasta: Path,
    sha256: str,
    expected: str | None,
    expected_from: str | None,
    components: str | None,
)

What re-reading a FASTA proved: its digest, what that was held to, and the components.

:func:verify_assembly's answer, and three results a caller must be able to tell apart, so each is a field of its own: the digest computed, what supplied the digest it was held to — being held to the lab's pin and being held to what this machine last produced are different answers — and, for a Chimera, what comparing its components settled. A digest that disagreed raises rather than arriving here, so this is what nothing refused.

Attributes:

Name Type Description
assembly str

The Assembly whose row supplied the digest to check against.

fasta Path

The file that was read.

sha256 str

The digest computed over it.

expected str or None

The digest it was held to, or None when nothing pinned one.

expected_from str or None

What answered with expected — :data:EXPECTED_FROM_TABLE, :data:EXPECTED_FROM_RECORD, or None when nothing did.

components str or None

:data:~genome.assembly.components.COMPONENTS_UNCHANGED or :data:~genome.assembly.components.COMPONENTS_UNKNOWN for a chimera, and None for anything else — including every fasta checked on its own.

Examples:

>>> from pathlib import Path
>>> checked = VerifiedAssembly(
...     assembly="sacCer3",
...     fasta=Path("/data/genome/sacCer3/sacCer3.fa"),
...     sha256="6ff72f07",
...     expected="6ff72f07",
...     expected_from=EXPECTED_FROM_TABLE,
...     components=None,
... )
>>> checked.verified
True
>>> checked.as_json()["expected_from"]
'table'

verified property

verified: bool

Whether there was a digest to check against at all, rather than merely one computed.

as_json

as_json() -> dict[str, Any]

Return this verification as --json serializes it.

Returns:

Type Description
dict

Every attribute above, with fasta rendered as text and :attr:verified written out beside the fields it is read from.

GenomeFiles dataclass

GenomeFiles(
    fasta: Path, fai: Path, twobit: Path, chrom_sizes: Path
)

A FASTA together with its derived index and companion files.

Attributes:

Name Type Description
fasta Path

The source FASTA file.

fai Path

The samtools faidx index, <fasta>.fai.

twobit Path

The 2bit-encoded sequence.

chrom_sizes Path

Two-column <name>\t<length> chromosome sizes file.

AssemblyMetadata dataclass

AssemblyMetadata(
    assembly_name: str,
    species: str | None,
    ucsc_name: str | None,
    ncbi_name: str | None,
    ncbi_assembly_id: str | None,
    ncbi_taxid: int | None,
    source_url: str | None = None,
    sha256: str | None = None,
    intron_length_cap: int | None = None,
    intron_length_cap_rationale: str | None = None,
)

Identifiers for one reference assembly (one row of the metadata table).

The single declaration of what an assembly's metadata consists of: the table is parsed through these fields, and a complete record is what :class:~genome.assembly.genome.Genome accepts in place of the table's own row.

Only assembly_name is required. Every other column may be left blank, and a blank cell reads back as None rather than as text: the table fills in over time, and a freshly prepared assembly pins its source and digest well before anyone supplies its species, its UCSC and NCBI names or its taxonomy id.

source_url and sha256 are what makes preparing an assembly reproducible. source_url pins where its FASTA is fetched from, so nothing has to be derived or guessed; sha256 pins the digest of the unpacked FASTA that source yields — not of the compressed archive it arrives in, so a copy taken from a mirror or recompressed elsewhere still matches. A row with no digest is unverified rather than wrong.

intron_length_cap is the longest gap a spliced aligner should take for an intron on this assembly, and intron_length_cap_rationale says why that number and not another. It is a deliberately loose round number set by hand, never computed from an annotation — an annotation catalogues the transcripts someone observed, so its longest intron is a floor on what the organism does rather than a ceiling on it. Nothing in this package reads either field: they are curated here so that a consumer choosing aligner parameters reads a fact about the assembly from the same row as its identifiers. A blank cap is an assembly nobody has characterised, which is legal and says no bound has been chosen — the reading that leaves such an assembly aligning exactly as it did before.

A Chimera's row pins neither, and that is deliberate rather than pending: its bytes are not fetched from anywhere, and they are derived by a pure function from components whose own rows are pinned, so it is proven transitively. The table pins what was downloaded; what was derived is pinned by a test, where a change in this package's own concatenation code belongs. Such a row carries its name and nothing else — its identifiers are its components', reachable through :attr:~genome.assembly.genome.Genome.chrom_components — and it exists so that a machine holding none of them can still tell a chimera's name from a local key someone chose.

ucsc_name is blank permanently rather than pending in some rows, for the same reason the assembly id is a local key rather than a UCSC one: the lab supports references UCSC has never carried, and such a row simply has no name in that namespace to give.

Examples:

>>> record = AssemblyMetadata(
...     "hg38", "Homo sapiens", "hg38", "GRCh38", "GCF_000001405.40", 9606
... )
>>> record.species
'Homo sapiens'
>>> record.sha256 is None            # nothing pinned unless the row says so
True

unknown classmethod

unknown(assembly_name: str) -> AssemblyMetadata

Return the record for an assembly the table does not list: the name, and nothing else.

What unlisted looks like as a record rather than as a missing one. Every identifier is genuinely unknown, which a blank cell already means everywhere else, so a caller reads a field and gets None instead of first asking whether there is a record to read it off. It is exactly the line :func:format_table_row emits for an assembly nobody has curated yet.

The name is the local key the caller asked for, since that is the only identifier an unlisted assembly has — the assembly id is a local key and the table is a cross-reference rather than an allow-list. This answers what is known about this assembly; whether the table lists it at all is :func:lookup_assembly's question and stays a separate one.

Parameters:

Name Type Description Default
assembly_name str

The assembly the record is for.

required

Returns:

Type Description
AssemblyMetadata

A record carrying assembly_name with every other field None.

Examples:

>>> record = AssemblyMetadata.unknown("my_ref")
>>> record.assembly_name
'my_ref'
>>> record.species is None and record.sha256 is None
True

from_row classmethod

from_row(row: Mapping[str, object]) -> AssemblyMetadata

Build a record from one row of a metadata table.

The reader half of the register-then-paste flow :func:format_table_row writes: a row is a mapping of column name to cell, which is how the shipped TSV is read and how :func:dataclasses.asdict of a record spells one. Each column is parsed by its own declared type, and a blank cell — empty, absent, or the NaN pandas reads a blank as — means unknown, which only a column that has an unknown takes.

Parameters:

Name Type Description Default
row mapping of str to object

Column name to cell. A cell is the table's own text, but a value already of the column's type is taken as it stands, so a record's fields are a row. Keys outside :data:METADATA_FIELDS are ignored.

required

Returns:

Type Description
AssemblyMetadata

The record the row spells.

Raises:

Type Description
MetadataRowError

If a cell cannot be read as its column's type, or a column that has no unknown is blank. The record is built from parsed cells or not at all, so a caller is handed a whole record or an error naming the column — never a record carrying the columns that happened to come before the bad one.

Examples:

>>> row = {"assembly_name": "sacCer3", "ncbi_taxid": "559292"}
>>> AssemblyMetadata.from_row(row).ncbi_taxid
559292
>>> AssemblyMetadata.from_row(row).species is None   # blank is unknown
True

AssemblyDir dataclass

AssemblyDir(assembly: str, path: Path)

One Assembly's directory, and everything the layout puts inside it.

The Assembly dir as a value rather than a rule applied again at each call site. Where an assembly lives is decided once — by :meth:locate, which is the only implementation of an explicit directory overrides the Data dir layout — and the answer is then carried, so a caller holding one cannot resolve it differently from the caller that opened it. That is not hypothetical: an index derived from the data root rather than from the assembly it indexes lands beside a different assembly's files, and reads a completion record that is not there.

Every subtree an assembly owns is spelled here, so the layout is legible in one place: the working area, the gtf/ subtree the Annotation context files into, the index/ subtree the Index context files into, and the four Genome files.

Parameters:

Name Type Description Default
assembly str

The assembly name — the key this directory is addressed by, and the name its files carry.

required
path Path

The directory itself.

required

Examples:

>>> import os
>>> os.environ["LIULAB_DATA"] = "/scratch/liulab"
>>> here = AssemblyDir.locate("hg38")
>>> here.path
PosixPath('/scratch/liulab/genome/hg38')
>>> here.index_dir("chromap")
PosixPath('/scratch/liulab/genome/hg38/index/chromap')
>>> here.sibling("mm39").path
PosixPath('/scratch/liulab/genome/mm39')
>>> del os.environ["LIULAB_DATA"]

work_dir property

work_dir: Path

The disposable working area a build stages in — see :func:~genome.store.completion.work_dir.

record_path property

record_path: Path

Where this assembly's Completion marker is written, whether or not it exists.

is_registered property

is_registered: bool

Whether a record here vouches for this assembly — the one spelling of that rule.

By record alone: a directory holding files but no record is not registered, because nothing vouches for them, and nothing is read but the record itself. That is what :func:~genome.assembly.source.is_prepared asks of a name, and what listing the tree asks of every directory in it, so the two cannot answer differently. Whether the files a record claims are still what it claims is a separate and more expensive question — genome assembly verify owns it.

Examples:

>>> AssemblyDir.locate("hg38", "/tmp/definitely-not-a-build").is_registered
False

annotations_root property

annotations_root: Path

The gtf/ subtree, parent of every annotation directory.

indexes_root property

indexes_root: Path

The index/ subtree, parent of every Index dir.

genome_files property

genome_files: GenomeFiles

The four Genome files this assembly's preparation produces, existing or not.

Every way of producing the FASTA materializes it as <assembly>.fa and derives identically named companions, so one layout describes them all.

locate classmethod

locate(
    assembly: str, cache_dir: str | Path | None = None
) -> AssemblyDir

Return where assembly lives: cache_dir when given, else the layout's answer.

The one place the override rule is written. cache_dir is the directory itself and not a root to file under, which is what lets a caller put one assembly somewhere of its own without inventing a second Data dir.

Parameters:

Name Type Description Default
assembly str

The assembly to locate.

required
cache_dir str or Path

An explicit Assembly dir, overriding the layout.

None

Returns:

Type Description
AssemblyDir

Where that assembly's files belong. Nothing is created and nothing is read.

Examples:

>>> AssemblyDir.locate("hg38", "/tmp/elsewhere").path
PosixPath('/tmp/elsewhere')

sibling

sibling(assembly: str) -> AssemblyDir

Return another assembly's Assembly dir, found beside this one.

How an assembly named in a record is found again — a Chimera's components, say. A record carries names and never paths, which is what keeps a registered directory movable, so the name has to be resolved against something: this resolves it against where the asking assembly is, rather than against the Data dir the process happens to be pointed at. Under the ordinary layout the two agree, since every assembly is a sibling under <data dir>/genome/. They part company exactly when one assembly was placed somewhere of its own, and then beside-me is the answer that finds anything at all.

Parameters:

Name Type Description Default
assembly str

The other assembly's name.

required

Returns:

Type Description
AssemblyDir

Its directory, beside this one. Nothing is created and nothing is read.

Examples:

>>> AssemblyDir.locate("ce11_ecHT115", "/data/genome/ce11_ecHT115").sibling("ce11").path
PosixPath('/data/genome/ce11')

read_record

read_record() -> CompletionRecord | None

Return this assembly's completion record, or None when it has none.

annotation_dir

annotation_dir(name: str) -> Path

Return the directory the annotation registered as name is filed under.

index_dir

index_dir(name: str) -> Path

Return the Index dir of the index addressed as name.

completed_files

completed_files(*, repair: str) -> GenomeFiles | None

Return the prepared Genome files when the record vouches for them, else None.

Is this assembly finished here?, asked of the directory itself so that a caller holding one — a verification, say — needs no registration object to find out. The completion record is the only thing consulted: it must be there, and every file it claims must be present at the size it claims. That is one stat per file and no file contents, so reopening a prepared genome is instant.

Parameters:

Name Type Description Default
repair str

The command quoted into the refusal a directory that cannot be trusted raises.

required

Returns:

Type Description
GenomeFiles or None

The four files, or None for an absent or empty directory — a fresh registration, which proceeds normally.

Raises:

Type Description
RegistrationError

If the directory holds files with no record, or a record that disagrees with what is on disk (see :func:~genome.store.completion.check_registration).

Examples:

>>> AssemblyDir.locate("hg38", "/tmp/definitely-not-a-build").completed_files(
...     repair="genome assembly register hg38 --force"
... ) is None
True

AssemblyRegistration

AssemblyRegistration(
    assembly: str, cache_dir: str | Path | None = None
)

One assembly's directory, and the steps that finish a registration in it.

An assembly name plus the directory its files belong in, and everything a registration does against that pair whatever produced the FASTA: is it already registered, which paths does it own, where does the work happen, what repairs it, how does the FASTA get into place, and what does the record say afterwards. Subclass it and add the step that produces the FASTA.

Every step here is a private one. The class is the seam, not a public surface: what a caller registers an assembly with is :func:~genome.assembly.download.register_assembly or :class:~genome.assembly.genome.Genome.

Parameters:

Name Type Description Default
assembly str

The assembly name — the key its directory is addressed by, and the name its files and its record carry.

required
cache_dir str or Path

Override the storage directory. Defaults to :func:assembly_data_dir(assembly) <assembly_data_dir>.

None

Attributes:

Name Type Description
assembly str

The assembly name passed at construction.

cache_dir Path

The Assembly dir this registration fills.

Examples:

>>> from pathlib import Path
>>> registration = AssemblyRegistration("hg38", Path("/scratch/hg38"))
>>> registration._repair_command()
'genome assembly register hg38 --force'

cache_dir property

cache_dir: Path

The Assembly dir this registration fills — :attr:dir's path.

AssemblyStatus dataclass

AssemblyStatus(
    directory: Path,
    assemblies: tuple[AssemblyStatusRow, ...],
)

What the curated table offers, set against what the assembly tree holds here.

:func:assembly_status's answer, and what genome assembly list prints. It is the first question a new user has — genome assembly register is the first command anyone runs and nothing else in the CLI says what may follow it — and the question somebody landing on a machine they did not set up has: what is already prepared on it.

Attributes:

Name Type Description
directory Path

The assembly tree's root, whether or not anything is there.

assemblies tuple of AssemblyStatusRow

One row per name: the offered ones in table order, then whatever this tree holds that no row lists, in name order.

Examples:

>>> status = assembly_status(root="/tmp/definitely-not-a-data-root")
>>> status.registered
()
>>> status.as_json()["directory"]
'/tmp/definitely-not-a-data-root'

registered property

registered: tuple[str, ...]

The assemblies a record here vouches for, in the order they are reported.

What do I actually have on this machine — the answer as data rather than as printed lines, for a caller who imports this instead of running the command.

summary property

summary: str

The closing line: what is registered here, and the command that changes it.

Two answers, and both name what to run next: nothing is registered, which is a fresh install's ordinary state and needs the command that prepares one; or something is, and then a reader who doubts one has a command for that too — verify is where integrity is settled, and is named here because this report deliberately does not settle it.

Returns:

Type Description
str

One line, beginning registered here:.

unregistered_note property

unregistered_note: str | None

What a directory nothing vouches for means, when the tree holds one; else None.

Absent when there is nothing to explain, so the ordinary listing does not carry a sentence about a state nothing is in.

Returns:

Type Description
str or None

One line about the here, not registered rows, or None when there are none.

as_json

as_json() -> dict[str, Any]

Return this report as --json serializes it.

Returns:

Type Description
dict

The tree's directory as text, and assemblies as a list of :meth:AssemblyStatusRow.as_json rows.

AssemblyStatusRow dataclass

AssemblyStatusRow(
    assembly_name: str,
    offered: bool,
    registered: bool,
    present: bool,
    directory: str | None,
    species: str | None,
    ucsc_name: str | None,
    ncbi_name: str | None,
    source_url: str | None,
    sha256: str | None,
)

One assembly, in whichever of its states it is: offered, registered, or merely here.

One shape for all of them, so a reader never has to ask which fields a row has — a name the table does not list carries the table's columns as None, and one nothing is prepared for carries no :attr:directory.

:attr:registered and :attr:present are not independent: a record lives in the directory it vouches for, so a registered assembly is always present, and the pair that needs saying out loud is the other one — present and not registered. That is the case enumerating the tree meets and asking after a single name never did: a directory that is neither a good registration nor absent. Reporting it as absent lies to somebody looking at a full disk, and reporting it as an assembly lies about what is trustworthy, so it is reported as what it is and :attr:state says so in three words.

Attributes:

Name Type Description
assembly_name str

The assembly this row is about — the key its directory is addressed by.

offered bool

Whether the curated table lists it.

registered bool

Whether a record here vouches for it.

present bool

Whether a directory for it is in the assembly tree, registered or not.

directory str or None

Its Assembly dir, when one is there; None when nothing is prepared for it. A row that is not :attr:present carries none rather than the path one would take, which is the layout's answer and not a fact about this machine.

species str or None

The species the table names; None for an assembly no row lists.

ucsc_name str or None

Its name in UCSC's namespace, from the table; None for an unlisted one, and also for a listed reference UCSC has never carried.

ncbi_name str or None

Its name in NCBI's namespace, from the table; None for an unlisted one.

source_url str or None

Where the table says its FASTA is fetched from; None for an unlisted one, and for a Chimera, which is built rather than downloaded.

sha256 str or None

The digest the table pins for the unpacked FASTA; None when it pins none.

Examples:

>>> row = AssemblyStatusRow(
...     assembly_name="hg38",
...     offered=True,
...     registered=False,
...     present=False,
...     directory=None,
...     species="Homo sapiens",
...     ucsc_name="hg38",
...     ncbi_name="GRCh38",
...     source_url="https://example.org/hg38.fa.gz",
...     sha256=None,
... )
>>> row.state
'offered, not registered'
>>> row.as_json()["offered"]
True

state property

state: str

Which of its states this row is in, in the words a surface prints.

registered first, because it is the strongest thing that can be said and it settles the two weaker ones: a registered assembly is present, and whether the table also offers it is the only distinction left. here, not registered comes next for the same reason it exists at all — a directory is there, and answering with what the table says about the name would leave the reader to discover the directory some other way.

Returns:

Type Description
str

One of registered, registered, not offered, here, not registered, or offered, not registered.

as_json

as_json() -> dict[str, Any]

Return this row as --json serializes it: every attribute above, in order.

:attr:state is not among them: it is read from :attr:offered, :attr:registered and :attr:present, which are all here, so writing it out too would be a second spelling of the same rule for a reader to disagree with.

Returns:

Type Description
dict

The row's fields, under their own names.

TwoBit

TwoBit(path: str | Path, *, masked: bool = True)

An open UCSC 2bit file, queried in 0-based half-open coordinates.

The handle is opened at construction and held until :meth:close (or the context-manager exit). Reuse a single instance for many queries rather than re-opening per lookup.

Parameters:

Name Type Description Default
path str or Path

Path to a .2bit file.

required
masked bool

Preserve soft-masking: lower-case bases for repeat-masked regions. When False every base is upper-cased. (Soft-masking carries meaning, so it is kept by default; see :mod:genome.seq.)

True

Attributes:

Name Type Description
path Path

The 2bit file path.

Raises:

Type Description
FileNotFoundError

If path does not exist.

RuntimeError

If path exists but cannot be opened as a 2bit file.

Examples:

>>> tb = TwoBit("sacCer3.2bit")
>>> tb.sequence("chrIV", 0, 10)
'ACACCACACC'
>>> tb.close()

chroms

chroms() -> dict[str, int]

Return a {name: length} mapping of the sequences in the file.

sequence

sequence(
    chrom: str,
    start: int | None = None,
    end: int | None = None,
) -> str

Return the sequence of chrom (or a sub-range) as a plain string.

Coordinates are bounds-checked against the chromosome length: an end past the end of the sequence raises rather than being silently clamped (py2bit's default behavior).

Parameters:

Name Type Description Default
chrom str

Sequence name as stored in the file.

required
start int

0-based start, inclusive. Defaults to the start of the chromosome.

None
end int

0-based end, exclusive (half-open). Defaults to the end of the chromosome.

None

Returns:

Type Description
str

The bare sequence, case preserved when the file was opened with masked=True.

Raises:

Type Description
ValueError

If chrom is not in the file, start < 0, end exceeds the chromosome length, or start > end.

Examples:

>>> tb.sequence("chrIV", 0, 10)
'ACACCACACC'

nocheck_sequence

nocheck_sequence(
    chrom: str,
    start: int | None = None,
    end: int | None = None,
) -> str

Return the sequence of chrom (or a sub-range) without bounds-checking.

Parameters:

Name Type Description Default
chrom str

Sequence name as stored in the file.

required
start int

0-based start, inclusive. Defaults to the start of the chromosome.

None
end int

0-based end, exclusive (half-open). Defaults to the end of the chromosome.

None

Returns:

Type Description
str

The bare sequence, case preserved when the file was opened with masked=True.

Raises:

Type Description
ValueError

If chrom is not in the file.

Examples:

>>> tb.nocheck_sequence("chrIV", 0, 10)
'ACACCACACC'

close

close() -> None

Close the underlying file handle (idempotent).

__enter__

__enter__() -> Self

Return self for use as a context manager.

__exit__

__exit__(
    exc_type: type[BaseException] | None,
    exc: BaseException | None,
    tb: TracebackType | None,
) -> None

Close the handle on context-manager exit.

__del__

__del__() -> None

Best-effort close when the object is garbage-collected.

__repr__

__repr__() -> str

Return e.g. TwoBit('sacCer3.2bit', open).

derive_name

derive_name(components: Iterable[str]) -> str

Derive a chimera's assembly name from the names of its components.

The name is the component names sorted lexicographically and joined by _. It is derived rather than given and cannot be overridden: one set of components means exactly one name, and therefore one directory and one index, whatever order the caller happened to list them in.

A chimera's own name always carries a _, and a component's name never may, so this is also where a chimera is refused as a component of another chimera. That is only the spelling half of the rule: whether a prepared assembly is itself a chimera is answered by the record on its disk, which this module cannot see.

Parameters:

Name Type Description Default
components Iterable[str]

Two or more component assembly names, in any order. Each must match [A-Za-z0-9]+ and appear exactly once.

required

Returns:

Type Description
str

The derived assembly name.

Raises:

Type Description
ChimeraNamingError

If fewer than two components are given, a component repeats, or a component name is not alphanumeric.

Examples:

>>> derive_name(["ecHT115", "ce11"])
'ce11_ecHT115'
>>> derive_name(["tinySc", "tinyCe", "tinyEc"])
'tinyCe_tinyEc_tinySc'

split_name

split_name(name: str) -> tuple[str, ...]

Split a chimera's assembly name back into its candidate component names.

The inverse of :func:derive_name, and syntactic only: it says that name is spelled the way a chimera's name is spelled, never that those components exist. Deciding that belongs to the caller, which asks whether each candidate is prepared on this machine or listed in the shipped table — the step that separates ce11_ecHT115 from an ordinary assembly someone happened to call my_ref.

Candidates come back in the order the name spells them, not sorted, so that a caller can hand them straight to :func:derive_name and compare: a name whose components are real but mis-ordered is detectable, and the canonical spelling can be named back.

Parameters:

Name Type Description Default
name str

An assembly name to read as a chimera's.

required

Returns:

Type Description
tuple[str, ...]

The candidate component names, in the order name spells them.

Raises:

Type Description
ChimeraNamingError

If name does not split into two or more alphanumeric parts.

Examples:

>>> split_name("ce11_ecHT115")
('ce11', 'ecHT115')
>>> derive_name(split_name("ecHT115_ce11"))   # the canonical spelling of a mis-ordered name
'ce11_ecHT115'

split_suffixed

split_suffixed(
    name: str, separator: str = "__"
) -> tuple[str, str]

Split a suffixed chromosome name back into (chromosome, component).

The split is at the last run of the separator, which is unconditionally the right one: what follows the suffix is an alphanumeric component name, so no later run of the separator can exist to be mistaken for it. A component that already spells a chromosome bar_ce11 therefore still reads back correctly — bar_ce11__ecHT115 is ('bar_ce11', 'ecHT115').

separator defaults to '__', which is what a chimera derives whenever no component carries a doubled underscore of its own — every assembly the lab ships. When one does, the chimera's record is the authority on its separator and the caller passes it. Writing has no such default, on purpose: see :func:suffixed.

The same contract in the form to hand a tool that cannot import this module is the regex the Assembly glossary publishes, which :func:_suffix_pattern generates and a test holds against this function name by name.

Parameters:

Name Type Description Default
name str

A suffixed chromosome name.

required
separator str

The run of underscores this chimera recorded.

``"__"``

Returns:

Type Description
tuple[str, str]

(chromosome, component) — the chromosome as its component spells it, and the component assembly name.

Raises:

Type Description
ChimeraNamingError

If separator is not a run of two or more underscores, name carries no component suffix under it, or nothing precedes that suffix — the last of which is what the published pattern refuses with .+, so the two agree.

Examples:

>>> split_suffixed("I__ce11")
('I', 'ce11')
>>> split_suffixed("chr1_KI270706v1_random__tinyEc")
('chr1_KI270706v1_random', 'tinyEc')
>>> split_suffixed("NZ_TINY02__000002.1___tinyEcDub", "___")
('NZ_TINY02__000002.1', 'tinyEcDub')

suffixed

suffixed(
    chromosome: str, component: str, separator: str
) -> str

Spell one chimera chromosome name: the chromosome, the separator, the component.

Unconditional — a chromosome is suffixed whether or not another component carries the same name — so attribution is the same operation for every name in the reference and no mapping of any kind has to be stored to perform it.

separator is required and deliberately has no default, unlike :func:split_suffixed: it belongs to one chimera, comes from :func:derive_separator, and a build that wrote a constant instead would quietly lose the self-announcing property the derivation exists to preserve, in a way no round trip can detect.

An empty chromosome is not refused, and gives back the tail alone: that is how :func:~genome.annotation.registration.register_merged_gtf spells the suffix it appends to a whole file's seqnames, validating the component and the separator once rather than per line. It is the one thing this function returns that is not a chromosome name, and :func:split_suffixed refuses to read it back as one.

Parameters:

Name Type Description Default
chromosome str

The chromosome name as the component itself spells it.

required
component str

The component assembly name, alphanumeric.

required
separator str

The run of underscores this chimera derived.

required

Returns:

Type Description
str

The suffixed chromosome name.

Raises:

Type Description
ChimeraNamingError

If component is not alphanumeric, or separator is not a run of two or more underscores.

Examples:

>>> suffixed("I", "ce11", "__")
'I__ce11'
>>> suffixed("NZ_TINY02__000002.1", "tinyEcDub", "___")
'NZ_TINY02__000002.1___tinyEcDub'

components_status

components_status(assembly_dir: AssemblyDir) -> str | None

Check every component of the assembly in assembly_dir, and report the answer.

The one failure a digest of a chimera's own bytes cannot show. Those bytes are a copy of its components', so a component registered again underneath it leaves the chimera intact, agreeing with its own record, and no longer a copy of anything that exists — silently stale sequence, and stale gene models one level down. Both are caught here, and both are record against record: the digests this chimera wrote down are compared against the ones the components' own records pin now, so this reads a handful of small JSON files and not one base of sequence.

One entry point for two needs, because they are one comparison. A component proved to have changed raises, wherever the question was asked from — reopening a finished chimera, rebuilding one, verifying one. What is returned is what nothing raised over, and a caller that only wants the refusal ignores it: a comparison that could not be made is :data:COMPONENTS_UNKNOWN and is not a pass, and a surface silent about it would say exactly what it says when everything agreed.

An assembly with no components recorded has nothing to compare and returns at once, which is what makes an ordinary assembly pay nothing rather than be asked about. Likewise an absent digest on either side, which means unknown rather than wrong: a component that pinned none, or an annotation registered before its digest was recorded, leaves that component unguarded rather than refused.

Parameters:

Name Type Description Default
assembly_dir AssemblyDir

The Assembly dir the chimera was built in. It carries the assembly's name, quoted in the error along with the command that repairs it, and it is what each component is found beside.

required

Returns:

Type Description
str or None

:data:COMPONENTS_UNCHANGED when every component was compared and agreed, :data:COMPONENTS_UNKNOWN when any comparison could not be made, and None for an assembly that is not a chimera — which has no components to be asked about.

Raises:

Type Description
RegistrationMismatchError

If a component's FASTA, or the annotation it contributed to the Merged annotation, is not the one this chimera was built from. The message names both digests and genome assembly register <assembly> --force, which rebuilds both halves.

RegistrationError

If the record claims to be a chimera's and cannot be read as one — see :meth:ChimeraDetails.from_details.

Examples:

>>> from genome.assembly.registration import AssemblyDir
>>> nowhere = AssemblyDir.locate("notAChimera", "/tmp/definitely-not-a-build")
>>> components_status(nowhere) is None
True

read_chimera_details

read_chimera_details(
    directory: Path,
) -> ChimeraDetails | None

Return the chimera details recorded in directory, or None when it has none.

:meth:ChimeraDetails.from_record over :func:~genome.store.completion.read_record — the one call that answers is the assembly registered here a chimera?, and the only thing :attr:genome.assembly.genome.Genome.components consults.

Parameters:

Name Type Description Default
directory Path

An Assembly dir — the directory a registration filled.

required

Returns:

Type Description
ChimeraDetails or None

The details, or None for an assembly that is not a chimera (or is not registered at all).

Raises:

Type Description
RegistrationError

If the record there claims to be a chimera's and cannot be read as one — see :meth:ChimeraDetails.from_details.

Examples:

>>> from pathlib import Path
>>> read_chimera_details(Path("/tmp/definitely-not-a-build")) is None
True

assembly_table_row

assembly_table_row(
    assembly: str,
    *,
    cache_dir: str | Path | None = None,
    progressbar: bool = True,
) -> AssemblyMetadata

Fetch assembly's FASTA and return the metadata table row describing it.

What makes filling in the table's checksum column a copy-paste rather than a manual hashing chore: the FASTA is downloaded and unpacked, its sha256 is computed over the unpacked file, and the assembly's row comes back with source_url set to the URL that was actually fetched and sha256 to that digest. Every other field is the curated table's own — or blank for an assembly the table does not list, since those identifiers are ones only a person can supply.

Only the FASTA is fetched: no .fai, .2bit or chrom.sizes is built, so this needs no native tools. Nothing is registered either — both the download and its unpacked form stay in the assembly's working area, so hashing a genome for the table never leaves an unregistered FASTA among that assembly's own files, and re-running it reuses what is already there.

A Chimera is refused before anything is fetched, because it has no work here to do: its row pins nothing, so there is no digest to compute and no source to record, and the refusal describes that row rather than printing it.

Parameters:

Name Type Description Default
assembly str

The assembly to fetch, e.g. "sacCer3".

required
cache_dir str or Path

Override which assembly directory the download works in. Defaults to :func:assembly_data_dir(assembly) <assembly_data_dir>.

None
progressbar bool

Show a download progress bar (requires tqdm).

True

Returns:

Type Description
AssemblyMetadata

The row itself, with None for anything still unknown — the same type the curated table parses into, since that is what this computes. Hand :func:dataclasses.asdict of it to :func:~genome.assembly.metadata.format_table_row for the line to paste into data/assembly_metadata.tsv.

Raises:

Type Description
ValueError

If the URL had to be derived and the assembly is unknown to UCSC, or if the name resolves to a chimera, which has no row to compute.

FileNotFoundError

If the name spells a chimera's components in an order that is not the canonical one; the message names the spelling to use.

Notes

The digest is reported, never enforced. A row that already pins one is not consulted, because this is the command to reach for when an upstream file has legitimately changed and the pin has to be regenerated — refusing on a mismatch would refuse exactly when the command is needed. Checking a FASTA you already hold against the official row is :meth:UCSCGenomeDownloader.verify_fasta's job, not this one's.

Examples:

>>> from dataclasses import asdict
>>> from genome.assembly.metadata import format_table_row
>>> format_table_row(asdict(assembly_table_row("sacCer3")))
'sacCer3\tSaccharomyces cerevisiae\t...\t6ff72f07...'

register_assembly

register_assembly(
    assembly: str,
    *,
    source: str | Path | None = None,
    force: bool = False,
    cache_dir: str | Path | None = None,
    progressbar: bool = True,
    metadata: AssemblyMetadata | None = None,
) -> RegisteredAssembly

Prepare assembly on disk and return the record of what that did.

Naming an assembly is enough: where its FASTA comes from and which digest it must match are the metadata table's to know. The whole pipeline runs — fetch, unpack, verify, index, derive — and the completion record lands last. An assembly that is already registered is returned from its record without fetching anything.

A directory that cannot be trusted raises instead (see :meth:UCSCGenomeDownloader.fetch_genome); force=True is what repairs one, and it keeps an unpacked FASTA it can prove is the pinned one rather than downloading a whole genome again.

The name is the whole interface, chimeras included: a name whose parts are a prepared or listed assembly each is concatenated from those components instead of being fetched, so this one call prepares all three kinds of Source and force repairs all three. See :func:~genome.assembly.source.resolve_source for the order the checks run in and what each one settles.

Parameters:

Name Type Description Default
assembly str

The assembly to register, e.g. "hg38".

required
source str or Path

Seed the assembly from this FASTA — a local path or an http(s)/ftp/sftp URL — instead of fetching the source its metadata pins. See :meth:UCSCGenomeDownloader.fetch_genome_from.

None
force bool

Register again from scratch, repairing a directory that raises.

False
cache_dir str or Path

Override which directory the assembly is registered in. Defaults to :func:assembly_data_dir(assembly) <assembly_data_dir>.

None
progressbar bool

Show a download progress bar (requires tqdm).

True
metadata AssemblyMetadata

A complete metadata record to use instead of the curated table's row.

None

Returns:

Type Description
RegisteredAssembly

The completion record the run wrote — files, source_url, sha256, tool_versions, completed_at and the rest — with the assembly and the directory it lives in. :meth:RegisteredAssembly.as_json serializes it.

Raises:

Type Description
RegistrationError

If the directory holds a build that cannot be trusted as finished, or (with force) if the run somehow left no record behind.

FileNotFoundError

If the name resolves to a chimera this machine cannot build — a component that is not prepared here, or the components in an order that is not the canonical one.

ChecksumMismatchError

If the metadata pins a sha256 and the unpacked FASTA is not it.

ToolNotFoundError

If samtools, faToTwoBit or twoBitInfo are not on PATH.

Examples:

>>> register_assembly("sacCer3")
RegisteredAssembly(assembly='sacCer3', directory=PosixPath('...'), record=...)

registered_assembly

registered_assembly(
    assembly: str, *, cache_dir: str | Path | None = None
) -> RegisteredAssembly

Return assembly's registration as it stands on disk, preparing nothing.

The check reopening a registered assembly makes, and nothing registering one does: the Completion marker must be there, every file it claims present at its size, and a Chimera's components unchanged. Nothing is fetched, no External tool runs and no directory is created. Whether the bytes are intact is :func:verify_assembly's question.

Parameters:

Name Type Description Default
assembly str

The assembly to read back, e.g. "sacCer3".

required
cache_dir str or Path

Override which directory the assembly is registered in. Defaults to :func:assembly_data_dir(assembly) <assembly_data_dir>.

None

Returns:

Type Description
RegisteredAssembly

The answer :func:register_assembly gives for an assembly already registered, with its directory made absolute, so every path in :attr:~RegisteredAssembly.genome_files is absolute too.

Raises:

Type Description
FileNotFoundError

If nothing is registered there: the directory is absent, or holds nothing an assembly's record would claim. The message names genome assembly register <assembly>.

RegistrationError

If the directory holds a build that cannot be trusted as finished, or a chimera whose component was registered again since it was built. The message names genome assembly register <assembly> --force.

Examples:

>>> registered_assembly("sacCer3").genome_files.fasta
PosixPath('/data/genome/sacCer3/sacCer3.fa')
>>> registered_assembly("hg38", cache_dir="/tmp/definitely-not-a-build")
Traceback (most recent call last):
FileNotFoundError: hg38 is not registered in /tmp/definitely-not-a-build...

verify_assembly

verify_assembly(
    assembly: str,
    *,
    fasta: str | Path | None = None,
    cache_dir: str | Path | None = None,
    metadata: AssemblyMetadata | None = None,
) -> VerifiedAssembly

Re-read a FASTA and check its sha256 against the digest expected of it.

The one operation that reads bytes rather than sizes. Registering an assembly and reopening it both go by presence and size, which is what makes them instant; this is the deliberate re-verification for when integrity is actually in doubt, and it costs a full pass over the file.

What is expected of it comes from the assembly's curated row, and failing that, from the completion record its own registration wrote — a fallback rather than a question about what kind of assembly this is (see :func:_expected_digest). expected_from says which answered, because being held to a pinned digest and being held only to what this machine last produced are different results and a caller must be able to tell them apart.

With no fasta it verifies the assembly's own registered FASTA, and the registration must be intact — a directory that cannot be trusted raises here as it does anywhere else. Two things are then checked beside the digest, both by reading records rather than bytes: that each component this assembly was built from is still the one it was built from, and that each annotation merged into its own is still the one that was merged. Neither costs an assembly without components anything. Point fasta at any file to check that instead: a copy taken from a mirror or handed over by hand is checkable before anything is built on it, and nothing needs to be registered first — nothing is then asked about components, since the assembly's own registration is not what is being verified.

Parameters:

Name Type Description Default
assembly str

The assembly whose row supplies the digest to check against, e.g. "hg38".

required
fasta str or Path

A FASTA to check instead of the assembly's registered one.

None
cache_dir str or Path

Override which directory the assembly is registered in.

None
metadata AssemblyMetadata

A complete metadata record to use instead of the curated table's row.

None

Returns:

Type Description
VerifiedAssembly

The digest computed, what it was held to and what supplied that, and — for a chimera — what comparing the components settled. A digest that disagrees raises rather than reporting :attr:~VerifiedAssembly.verified False; components that disagree likewise.

Raises:

Type Description
ChecksumMismatchError

If a digest is expected and the file's is a different one.

RegistrationError

If the assembly's directory holds a build that cannot be trusted as finished — including one whose components were registered again underneath it.

FileNotFoundError

If there is no file to read — nothing registered for assembly, or no file at an explicit fasta.

Examples:

>>> verify_assembly("sacCer3").verified
True
>>> verify_assembly("sacCer3", fasta="/tmp/copied.fa")
VerifiedAssembly(assembly='sacCer3', fasta=PosixPath('/tmp/copied.fa'), ...)

prepare_fasta

prepare_fasta(
    fasta_path: str | Path,
    *,
    twobit_path: str | Path | None = None,
    sizes_path: str | Path | None = None,
    overwrite: bool = False,
) -> GenomeFiles

Index a FASTA, convert it to 2bit, and write its chrom.sizes.

Convenience wrapper that runs :func:faidx, :func:fasta_to_2bit, and :func:twobit_to_chrom_sizes in sequence and collects their outputs. Each step is cached: an output already present and newer than its input is reused rather than regenerated, so re-running is cheap (see overwrite).

Parameters:

Name Type Description Default
fasta_path str or Path

FASTA to process.

required
twobit_path str or Path

Override for the .2bit destination; see :func:fasta_to_2bit.

None
sizes_path str or Path

Override for the chrom.sizes destination; see :func:twobit_to_chrom_sizes.

None
overwrite bool

Force every step to rerun even when its output looks fresh.

False

Returns:

Type Description
GenomeFiles

Paths to the source FASTA and the three generated files.

Raises:

Type Description
FileNotFoundError

If fasta_path does not exist.

RuntimeError

If any of the underlying native tools fail.

Examples:

>>> files = prepare_fasta("genome.fa")
>>> files.fai, files.twobit, files.chrom_sizes
(PosixPath('genome.fa.fai'), PosixPath('genome.2bit'), PosixPath('genome.chrom.sizes'))

read_chrom_sizes

read_chrom_sizes(sizes_path: str | Path) -> pd.Series

Read a chrom.sizes file into a pandas Series of lengths.

Parameters:

Name Type Description Default
sizes_path str or Path

Two-column <name>\t<length> file as written by :func:twobit_to_chrom_sizes.

required

Returns:

Type Description
Series

Integer lengths indexed by chromosome name (index name "chrom", series name "length"), preserving file order.

Raises:

Type Description
FileNotFoundError

If sizes_path does not exist.

Examples:

>>> sizes = read_chrom_sizes("hg38.chrom.sizes")
>>> sizes["chr1"]
248956422

assembly_metadata

assembly_metadata(
    assembly: str,
    *,
    table: Sequence[AssemblyMetadata] | None = None,
) -> AssemblyMetadata

Return what is known about assembly — the table's row, or an unknown record.

The total accessor, and the one to reach for when the question is what are this assembly's identifiers: there is always a record, so a caller reads a field rather than a record and then a field. An assembly the table does not list has every identifier None and its own name, which is what an unlisted assembly knows about itself.

Deliberately not the same function as :func:lookup_assembly, which answers the other question — does the curated table list this name? — and keeps its None for it. That answer is what separates a chimera's derived name from a free-form local key on a machine holding neither, so it cannot be made total without reading every name as listed.

Parameters:

Name Type Description Default
assembly str

The name to look up, matched as :func:lookup_assembly matches it.

required
table sequence of AssemblyMetadata

The rows to look in, as :func:lookup_assembly takes them.

None

Returns:

Type Description
AssemblyMetadata

The table's row for assembly, else :meth:AssemblyMetadata.unknown(assembly) <AssemblyMetadata.unknown>.

Examples:

>>> assembly_metadata("hg38").ncbi_name
'GRCh38'
>>> assembly_metadata("no_such_assembly").ncbi_name is None
True
>>> assembly_metadata("no_such_assembly").assembly_name
'no_such_assembly'

assembly_table cached

assembly_table() -> tuple[AssemblyMetadata, ...]

Return every assembly the shipped table lists, in table order.

What the lab officially supports — a pinned source and a pinned checksum each — and what every lookup here reads when it is handed no table= of its own. Read once and cached; the records are frozen, so the tuple is safe to hold on to.

Returns:

Type Description
tuple of AssemblyMetadata

One record per row of data/assembly_metadata.tsv.

Raises:

Type Description
MetadataRowError

If the shipped file is empty, its header is not :data:METADATA_FIELDS, a row holds the wrong number of cells, or a cell cannot be read as its column's type.

Examples:

>>> "hg38" in {record.assembly_name for record in assembly_table()}
True

format_table_row

format_table_row(row: Mapping[str, object]) -> str

Render one metadata row as a tab-separated line, in table-column order.

Blank means unknown: a field that is None, or missing from row altogether, renders as an empty cell — exactly how the shipped table spells a value nobody has pinned yet. The result is the line to paste into data/assembly_metadata.tsv, with no trailing newline.

Parameters:

Name Type Description Default
row mapping of str to object

Field name to value, such as :func:dataclasses.asdict of an :class:AssemblyMetadata. Keys outside :data:METADATA_FIELDS are ignored.

required

Returns:

Type Description
str

The row's values joined by tabs, in :data:METADATA_FIELDS order.

Examples:

>>> format_table_row({"assembly_name": "sacCer3", "ncbi_taxid": 559292})
'sacCer3\t\t\t\t\t559292\t\t\t\t'

lookup_assembly

lookup_assembly(
    assembly: str,
    *,
    table: Sequence[AssemblyMetadata] | None = None,
) -> AssemblyMetadata | None

Return the :class:AssemblyMetadata for a UCSC (or canonical) assembly name, or None.

Parameters:

Name Type Description Default
assembly str

The name to look up, matched against each record's ucsc_name and assembly_name.

required
table sequence of AssemblyMetadata

The rows to look in; the shipped table (:func:assembly_table) when omitted. Curating rows of your own is ordinary rather than an override — the table is a cross-reference and never an allow-list — and nothing is installed by passing them: the sequence is read for this call and no other.

None

Returns:

Type Description
AssemblyMetadata or None

The row for assembly, or None when the table does not list it. An unlisted assembly is legal and its identifiers are simply unknown. A blank cell in an optional column reads back as None.

Examples:

>>> lookup_assembly("hg38").ncbi_name
'GRCh38'
>>> lookup_assembly("hg38").source_url
'https://hgdownload.soe.ucsc.edu/goldenPath/hg38/bigZips/hg38.fa.gz'
>>> lookup_assembly("no_such_assembly") is None
True
>>> mine = AssemblyMetadata.unknown("my_ref")
>>> lookup_assembly("my_ref", table=[mine]) == mine
True

assembly_data_dir

assembly_data_dir(assembly: str) -> Path

Return the directory holding all reference files for assembly.

Every file tied to a reference assembly (FASTA, indexes, annotations, …) lives under <liulab_data>/genome/<assembly>/ so they stay co-located.

Parameters:

Name Type Description Default
assembly str

Assembly name, e.g. "hg38".

required

Returns:

Type Description
Path

<liulab_data>/genome/<assembly>.

Examples:

>>> import os
>>> os.environ["LIULAB_DATA"] = "/scratch/liulab"
>>> assembly_data_dir("hg38")
PosixPath('/scratch/liulab/genome/hg38')
>>> del os.environ["LIULAB_DATA"]

assembly_repair_command

assembly_repair_command(
    assembly: str, source: str | Path | None = None
) -> str

Return the command that registers assembly again from scratch.

One spelling, wherever it is quoted: a broken Assembly dir names it, and so does a Merged annotation whose only repair is rebuilding the chimera that wrote it. A seeded assembly carries its own source into it — genome assembly register tiny --force would fetch from the golden path, which is not where such an assembly came from.

Parameters:

Name Type Description Default
assembly str

The assembly to register again.

required
source str or Path

Where its FASTA came from, for an assembly that was seeded rather than fetched.

None

Returns:

Type Description
str

A command that runs as it stands.

Examples:

>>> assembly_repair_command("hg38")
'genome assembly register hg38 --force'
>>> assembly_repair_command("tiny", "/data/my ref.fa")
"genome assembly register tiny --force --source '/data/my ref.fa'"

is_prepared

is_prepared(assembly: str) -> bool

Return whether assembly is registered under the shared data root, by its record alone.

By name and not by path, exactly as a chimera's recorded components are found again: a component is addressed by the key it was registered under. A directory holding files but no record is not prepared — nothing vouches for it.

The rule itself is :attr:~genome.assembly.registration.AssemblyDir.is_registered, asked of the directory the layout puts this name in. This is that question by name, which is what a caller holding a name rather than a directory asks, and what :func:~genome.assembly.status.assembly_status reports for every name at once.

Parameters:

Name Type Description Default
assembly str

The assembly name to look for.

required

Returns:

Type Description
bool

Whether a completion record is there.

Examples:

>>> is_prepared("definitely-not-an-assembly")
False

assembly_status

assembly_status(
    *, root: str | Path | None = None
) -> AssemblyStatus

Report what the curated table offers against what is prepared on this machine.

What genome assembly list runs, and the whole of it: the command formats this and holds no rule of its own. Unlike :func:~genome.annotation.registry.annotation_status it takes no assembly to scope it — its scope is every assembly, offered or here.

Nothing is fetched, prepared, built or created to answer. A tree that does not exist is a fresh install rather than a failure, and reports the table's rows with nothing registered.

What in the tree counts as an assembly is a rule rather than a guess, because enumerating a directory means treating whatever is in it as the answer: the layout files one directory per assembly directly under the root and names it for the assembly, so an entry counts when it is a directory whose name does not begin with a dot. A file there belongs to no assembly, and no assembly is registered under a hidden name. Whether a directory that counts is registered is then the record's business and not the name's.

Parameters:

Name Type Description Default
root str or Path

Override which directory the assembly tree is read from. Defaults to :func:~genome.assembly.registration.assembly_root_dir, the layout's answer. It is the tree itself, one level above the cache_dir that overrides a single Assembly dir — and it is not spelled cache_dir, since the thing being pointed at is the lab's reference data rather than anything an eviction policy may delete.

None

Returns:

Type Description
AssemblyStatus

The tree's root, and one :class:AssemblyStatusRow per name.

Examples:

>>> status = assembly_status(root="/tmp/definitely-not-a-data-root")
>>> next(row.state for row in status.assemblies if row.assembly_name == "hg38")
'offered, not registered'

genome.homology

Which genes in another species a gene is homologous to, on Ensembl Compara's trees.

The Orthology context, and a peer of :mod:genome.tf rather than a part of it. A Homology set is anchored to a species pair and a pinned Release — never to an Assembly or an Annotation — is downloaded once into the Data dir as a sibling of the assembly tree, and answers with no Genome open, the shape motif/ already established. It reads bulk files fetched once and never a remote API, so a pipeline built on it does not fail intermittently the way one built on BioMart does.

Orthology is served and never consumed. This answers a user's cross-species question and nothing else: no TF gene table, no Cofactor table, no list this package publishes is derived through homology, and no answer is ever silently species-mapped. A species with no census still has none — what changed is that a user can now cross the line themselves, deliberately, with the publisher's Homology type in hand.

Attribution. Ensembl Compara, Herrero J et al., Database (Oxford) 2016:bav096 (PMID 26896847), from https://ftp.ensembl.org/pub/. Every link, every Homology type and every confidence field is the publisher's; this package computes none of them and ranks nothing. :meth:~genome.homology.metadata.HomologyMetadata.attribution renders the line to print, and src/genome/data/homology/ATTRIBUTION.md carries the same facts beside the provenance table.

Examples:

>>> from genome.homology import homology_table, homology_data_dir
>>> {row.publisher for row in homology_table()}
{'Ensembl Compara'}
>>> import os
>>> os.environ["LIULAB_DATA"] = "/scratch/liulab"
>>> homology_data_dir()
PosixPath('/scratch/liulab/homology')
>>> del os.environ["LIULAB_DATA"]

ComparaFileError

Bases: ValueError

A file read as Compara's is not one, or a prepared slice is not what was recorded.

One class for three states because they mean the same thing to a caller — what is on disk is not what it should be — and the repair is the same: delete the set's directory and construct it again. The third is a cell that cannot be read as the column's own type, which is a different fact from :data:NULL_CELL and never folded into it: the publisher recorded no score and this package could not read the score it recorded would otherwise both arrive as None in a column a caller filters on. The message names the file and the repair, and for a bad cell the column and the value too.

Examples:

>>> issubclass(ComparaFileError, ValueError)
True

ComparaPartitionError

Bases: RuntimeError

The file recorded as holding a species pair holds none of its rows.

The trap this module exists for. Compara's per-species dumps are a de-duplicated partition at the pair level, so a pair lives in exactly one of its two files and the assignment is arbitrary and unstable across releases. A slice that comes back empty therefore means the partition moved, not that the two species share no homologs — and a pair is never partially present, which is what makes zero trustworthy. The message names the other file, which is where the pair now is.

A :class:RuntimeError rather than a :class:LookupError: nothing the caller asked for is missing, the shipped provenance row is out of date.

Examples:

>>> issubclass(ComparaPartitionError, RuntimeError)
True

HomologySet

HomologySet(
    species: str,
    other_species: str,
    release: str = DEFAULT_RELEASE,
    *,
    cache_dir: str | Path | None = None,
    progressbar: bool = True,
)

One Ensembl Compara Release, sliced to one species pair and read into an index.

Constructing one prepares it, as opening a :class:~genome.assembly.genome.Genome does: the per-species dump that holds the pair is fetched into :func:homology_data_dir on the first construction — verified against the publisher's own md5 as it arrives — sliced to the pair, and recorded with a Completion marker; every construction after re-reads what is there and fetches nothing. The lab's CPU cluster compute nodes have no internet, so the first construction of a set must happen on a login node, exactly as :class:~genome.tf.motif.jaspar.JasparDatabase already documents.

The pair is unordered as far as the download goes and ordered as far as the question goes: HomologySet("Homo sapiens", "Mus musculus") and its reverse read one prepared file, and each answers about the species it was named with first.

Nothing here is computed. Every field of every Homology link is a cell of the publisher's own file, the Homology type most of all: it is Compara's tree-derived label and is never recomputed, not after a filter and not after resolution into an Annotation. No quality score, ranking or "best ortholog" of this package's own exists, and no table this package publishes is derived through homology.

Parameters:

Name Type Description Default
species str

The species whose genes are asked about, in either spelling.

required
other_species str

The species whose homologous genes come back.

required
release str

The Compara Release, one of those the shipped table pins.

``"116"``
cache_dir str or Path

The directory to prepare in, overriding the one :func:set_dir lays out under :func:homology_data_dir. The directory itself, not a root to file a layout under — the same word means the same thing for a :class:~genome.xref.xref.XrefSet and a :class:~genome.tf.motif.jaspar.JasparDatabase. One set per directory either way, since each carries a Completion marker of its own.

None
progressbar bool

Show the download's progress bar. Nothing is drawn when the set is already there.

True

Attributes:

Name Type Description
species str

The species asked about, as the shipped table spells it.

other_species str

The species answered with, likewise.

release str

The Release this is.

path Path

The stored slice these links were read from — a plain gzipped TSV carrying the publisher's own header and rows.

source_url str

Where those rows came from: the per-species dump that holds this pair.

provenance HomologyMetadata

The shipped row behind all of the above. :meth:~genome.homology.metadata.HomologyMetadata.attribution renders the line to print beside anything this answered.

null_quality_scores tuple of str

Which of :data:QUALITY_SCORE_COLUMNS this set holds no value in anywhere, measured over the prepared slice. Both, for either worm pairing; empty for a pair Compara scored — so a caller filtering on one is told rather than left to discover it when the filter empties.

Raises:

Type Description
UnknownHomologySpeciesError

If either species is not one this package prepares.

NoHomologyPairError

If the pair is not pinned in that release.

ValueError

If the release is not pinned, or both species are the same one.

ComparaPartitionError

If the recorded file holds none of the pair's rows — the partition moved.

ComparaFileError

If the fetched dump is not Compara's, if a prepared slice disagrees with its record, or if one of its quality cells is neither a number nor the publisher's own null.

Examples:

>>> from genome.homology import HomologySet
>>> worms = HomologySet("Homo sapiens", "Caenorhabditis elegans")
>>> len(worms)
23982
>>> worms.null_quality_scores
('goc_score', 'wga_coverage')
>>> worms.homologs(["ENSG00000152670"]).homolog_gene_id_stems
['WBGene00001598', 'WBGene00001599', 'WBGene00001600']

__len__

__len__() -> int

Return how many Homology links this set holds, of every type.

__repr__

__repr__() -> str

Return which pair and release this is and how many links it holds.

homologs

homologs(
    stems: Iterable[str], *, paralogs: bool = False
) -> HomologyAnswer

Return the other species' genes homologous to each Gene id stem asked about.

The one question a Homology set answers. Every stem that named at least one link maps to all of them, in the order the stems were asked about, and no value is ever empty — a stem this set names no homolog for is in :attr:~HomologyAnswer.unresolved instead, so what your list holds and this release does not is visible rather than dropped.

Orthologs are the default and paralogs come back only on request, so the common question stays the easy one. A Paralogy link is kept in the set and marked by its own Homology type rather than excluded, which is what keeps not an ortholog distinguishable from absent — the same stance taken for a Cross-species link. Release 116 publishes none for these pairs; see this module's own documentation for the count.

Whatever a filter removed is counted in :attr:~HomologyAnswer.dropped_partners rather than silently gone, and the Homology type on a link that survived is untouched by it: a view can look one-to-one and still be labelled ortholog_one2many.

Parameters:

Name Type Description Default
stems iterable of str

The Gene id stems to ask about, in the order they should come back. Repeats are asked once. Compara writes its gene ids bare, so a versioned id is refused rather than answered emptily.

required
paralogs bool

Return every link the publisher wrote for these genes, rather than only the ones its label calls a speciation event.

False

Returns:

Type Description
HomologyAnswer

The stems that named homologs, mapped to every link each names, and the stems that named none.

Raises:

Type Description
VersionedGeneIdError

If a stem carries a version; the message names the stem to pass.

Examples:

>>> homologs = HomologySet("Mus musculus", "Homo sapiens")
>>> answer = homologs.homologs(["ENSMUSG00000059552"])
>>> answer.resolved["ENSMUSG00000059552"][0].homology_type
'ortholog_one2one'

HomologySetNotDownloadedError

Bases: PreparedSetNotDownloadedError

The set is not prepared here, and the publisher's dump could not be fetched.

The Orthology context's own spelling of what every Prepared set raises here, so the message names this pair and quotes :func:homology_prepare_command. :class:genome.xref.xref.XrefSetNotDownloadedError is the Xref context's; they are two classes because they are two contexts, and what they share — that fetching is the one step that needs the network, and that the lab's compute nodes have none — is said once on the class they both derive from.

Examples:

>>> issubclass(HomologySetNotDownloadedError, RuntimeError)
True

NoHomologyPairError

Bases: LookupError

No shipped row pins this species pair in this Release.

Distinct from :class:UnknownHomologySpeciesError: both species are prepared, but not together in the release asked for. The message names the pairs that are.

Examples:

>>> issubclass(NoHomologyPairError, LookupError)
True

UnknownHomologySpeciesError

Bases: LookupError

A species no Homology set is prepared for, so the question cannot be answered.

A :class:LookupError and never an empty answer: nobody pinned this species must never read as this species has no homologs. The message names the species that do.

Examples:

>>> try:
...     check_species("Danio rerio")
... except UnknownHomologySpeciesError as error:
...     print("Homo sapiens" in str(error))
True

VersionedGeneIdError

Bases: ValueError

A versioned gene id was passed where a Gene id stem is the key.

Compara keys its dumps by stem, so ENSG00000141510.18 matches nothing and would ride back in unresolved looking exactly like a gene Compara never placed in a tree. Joining a versioned id to a bare one returning zero matches in silence is the most error-prone detail in this landscape, so it is refused by name instead. The message names the stem to pass.

:class:genome.tf.link.VersionedGeneIdError refuses the same thing for the same reason in the TF context; they are two classes because they are two contexts, and nothing is expected to catch both.

Examples:

>>> try:
...     check_stem("ENSG00000141510.18")
... except VersionedGeneIdError as error:
...     print("ENSG00000141510" in str(error))
True

HomologyMetadata dataclass

HomologyMetadata(
    release: str,
    species: str,
    other_species: str,
    holding_species: str,
    publisher: str,
    pubmed_id: int,
    source_url: str,
    md5: str,
)

Where one species pair's homologies are published (one row of the table).

The single declaration of what a Homology set's provenance consists of: the table is parsed through these fields, column by column, exactly as :class:~genome.assembly.metadata.AssemblyMetadata parses its own. Every column is required — a set nobody can cite is one this package may not point anyone at, and a set with no checksum is one a truncated fetch would answer from.

The pair is unordered: a row is looked up by its two species in either order, since which of them a caller asks about is a property of the question and not of the file.

Attributes:

Name Type Description
release str

The Ensembl Compara Release, as the publisher numbers it — "116".

species str

One species of the pair, as the assembly metadata table spells it.

other_species str

The other, likewise.

holding_species str

Whose per-species file actually holds this pair's rows in this release — measured by counting, never assumed, and re-checked every time a set is prepared.

publisher str

Who published it, and who is to be cited.

pubmed_id int

PubMed id of the paper to cite.

source_url str

The published file to fetch: the holding species' own per-species dump.

md5 str

The publisher's own md5 for those bytes, read from the MD5SUM file beside them and checked against the bytes as they are fetched. That check is load-bearing and not a formality: a resumed download of one of these gzips has been seen to pass gzip -t with the wrong md5, so opening cleanly is no evidence at all. Only Compara releases 90 and 116 publish an MD5SUM, which is part of why 116 is what is pinned.

Examples:

>>> row = homology_metadata("Caenorhabditis elegans", "Homo sapiens", "116")
>>> row.holding_species, row.md5[:8]
('Homo sapiens', '59857f48')
>>> row.pair
('Caenorhabditis elegans', 'Homo sapiens')

pair property

pair: tuple[str, str]

The two species, sorted — the key a pair is looked up and filed under.

other property

other: str

The species of the pair whose file does not hold it, in this release.

What a :class:~genome.homology.compara.ComparaPartitionError names when the recorded file comes back empty: the partition moved, and this is where the pair went.

from_row classmethod

from_row(
    row: Mapping[str, str], *, origin: str
) -> HomologyMetadata

Read one row of the provenance table into a record.

Parameters:

Name Type Description Default
row mapping of str to str

One row, keyed by column name.

required
origin str

Where the row came from, named in any error.

required

Returns:

Type Description
HomologyMetadata

The record.

Raises:

Type Description
HomologyMetadataError

If a required cell is blank or a numeric one is not a number.

Examples:

>>> row = {
...     "release": "116",
...     "species": "Homo sapiens",
...     "other_species": "Mus musculus",
...     "holding_species": "Mus musculus",
...     "publisher": "Ensembl Compara",
...     "pubmed_id": "26896847",
...     "source_url": "https://example.invalid/x.tsv.gz",
...     "md5": "0" * 32,
... }
>>> HomologyMetadata.from_row(row, origin="test").pubmed_id
26896847

attribution

attribution() -> str

Return the one line to print beside anything this set answered.

What a caller owes the publisher, rendered once here so the CLI, a notebook and an error message all say it the same way.

Returns:

Type Description
str

Publisher, release, PubMed id and source URL.

Examples:

>>> print(homology_metadata("Homo sapiens", "Mus musculus", "116").attribution())
Ensembl Compara release 116 (PMID 26896847) — https://ftp.ensembl.org/pub/release-116/tsv/ensembl-compara/homologies/mus_musculus/Compara.116.protein_default.homologies.tsv.gz

HomologyMetadataError

Bases: ShippedTableError

A row of the shipped provenance table cannot be read as a record.

A defect in this package rather than anything a caller did, so the message names the file, the row and the column that refused, and the repair is to fix that cell.

Examples:

>>> try:
...     read_metadata("release\n", origin="homology_metadata.tsv")
... except HomologyMetadataError as error:
...     print("homology_metadata.tsv" in str(error))
True

resolve_homologs

resolve_homologs(
    answer: HomologyAnswer,
    registry: AnnotationRegistry,
    name: str | None = None,
) -> ResolvedHomologs

Return answer's homologous genes in one registered annotation's own gene ids.

The other species' Gene id stems are resolved in one call against registry, which is the annotation hop this package already had; every link whose partner that annotation carries a gene for is kept, with its Homology type untouched, and every partner it carries none for is reported in :attr:~ResolvedHomologs.dropped_partners rather than dropped in silence — added to whatever the answer had already dropped, since a Dropped partner is one the answer no longer names whichever step removed it. Which quality columns the set holds nothing in is a fact about the set rather than about the crossing, and rides through unchanged.

The registry must annotate the answer's other species. Nothing here checks that — an assembly's species is the assembly's own metadata and a registry does not carry one — so passing a mouse answer a worm registry is a question about the wrong genome and will simply resolve nothing. Reach the registry from the assembly whose species is :attr:~HomologyAnswer.other_species.

Parameters:

Name Type Description Default
answer HomologyAnswer

What :meth:~genome.homology.compara.HomologySet.homologs returned. Whatever it was filtered to is what is crossed: this adds nothing back and removes no more.

required
registry AnnotationRegistry

The registry of the Assembly whose annotation the ids should be in.

required
name str

The Registered name to resolve against. Omitted, that assembly's Default annotation answers.

None

Returns:

Type Description
ResolvedHomologs

The links whose partners this annotation spells, the gene ids it spells them with, the asked stems left naming nothing, every partner dropped along the way, and the quality columns the set behind it holds nothing in.

Raises:

Type Description
ValueError

If name is omitted and no Default annotation is decided.

AnnotationNotRegisteredError

If nothing of that name is registered there.

NoGeneFeaturesError

If its database holds no gene at all.

Examples:

>>> from genome.homology import HomologySet, resolve_homologs
>>> from genome.annotation import AnnotationRegistry
>>> answer = HomologySet("Homo sapiens", "Mus musculus").homologs(
...     ["ENSG00000141510"]
... )
>>> crossed = resolve_homologs(
...     answer, AnnotationRegistry.locate("mm39"), "gencode_vM39"
... )
>>> crossed.homolog_gene_ids
['ENSMUSG00000059552.13']

check_pair

check_pair(
    species: str, other_species: str, release: str
) -> HomologyMetadata

Return the shipped provenance row for one species pair and Release.

Parameters:

Name Type Description Default
species str

One species of the pair, in either spelling.

required
other_species str

The other.

required
release str

The Compara Release.

required

Returns:

Type Description
HomologyMetadata

The row, with the species as the shipped table spells them.

Raises:

Type Description
UnknownHomologySpeciesError

If either species is not one this package prepares.

ValueError

If the release is not pinned, or if the two species are the same one — a Homology set relates two species, and a gene's paralogs within one species are a different question this does not answer.

NoHomologyPairError

If both species are prepared but not together in that release.

Examples:

>>> check_pair("homo_sapiens", "Mus musculus", "116").holding_species
'Mus musculus'

check_release

check_release(release: str) -> str

Return release if the shipped table pins it, else say which it does.

Examples:

>>> check_release("116")
'116'

check_species

check_species(species: str) -> str

Return species as the shipped table spells it, else say which it prepares.

Either spelling is accepted — the assembly metadata table's "Homo sapiens" or the slug "homo_sapiens" — and the table's own spelling comes back, so an answer names a species one way however it was asked for.

Parameters:

Name Type Description Default
species str

A species name, in either spelling.

required

Returns:

Type Description
str

The species as the shipped provenance table spells it.

Raises:

Type Description
UnknownHomologySpeciesError

If no shipped row names that species.

Examples:

>>> check_species("homo_sapiens")
'Homo sapiens'

check_stem

check_stem(stem: str) -> str

Return stem if it is a Gene id stem, else refuse the versioned id it is.

Examples:

>>> check_stem("WBGene00020462")
'WBGene00020462'

compara_url

compara_url(species: str, release: str) -> str

Return the URL of one Release's homology dump for one species.

Parameters:

Name Type Description Default
species str

The species whose per-species dump is wanted, in any spelling.

required
release str

The Compara Release.

required

Returns:

Type Description
str

The published file's URL.

Notes

This builds a message, never a pin. The shipped provenance table's own source_url is the authority for a set that is actually fetched; this is what names the other file of a pair when the partition has moved, where nothing is pinned to name. It is not a template a release can be added through: release 113 ships these dumps uncompressed, so its file has no .gz, and only releases 90 and 116 publish an MD5SUM at all — 91 to 112 publish no checksum of any kind. A new release is a measured row, not a formatted string.

Ensembl's genome name is the species slug for each of the three species prepared here. It is not for every genome Ensembl carries — several are named for a subspecies or a collection — so a fourth species would bring its own name rather than this deriving one.

Examples:

>>> compara_url("Mus musculus", "116")
'https://ftp.ensembl.org/pub/release-116/tsv/ensembl-compara/homologies/mus_musculus/Compara.116.protein_default.homologies.tsv.gz'

homology_data_dir

homology_data_dir() -> Path

Return the directory holding Homology sets, which belong to no Assembly.

The Orthology context's own root under the Data dir, declared here because this is where its Prepared set is fetched into and read from.

Returns:

Type Description
Path

<liulab_data>/homology. Nothing is created by asking.

Examples:

>>> import os
>>> os.environ["LIULAB_DATA"] = "/scratch/liulab"
>>> homology_data_dir()
PosixPath('/scratch/liulab/homology')
>>> del os.environ["LIULAB_DATA"]

homology_prepare_command

homology_prepare_command(
    species: str, other_species: str, release: str
) -> str

Return the call that prepares one Homology set, for an error message to quote.

One spelling of it, so a renamed entry point is renamed once. Quoted by the error a caller repairs by fetching the set on a machine with internet.

Parameters:

Name Type Description Default
species str

One species of the pair, as the shipped table spells it.

required
other_species str

The other.

required
release str

The pinned Release.

required

Returns:

Type Description
str

A shell command, unquoted and unfenced — the caller decides how to set it.

Examples:

>>> homology_prepare_command("Homo sapiens", "Mus musculus", "116")
'python -c "from genome.homology import HomologySet; HomologySet(\'Homo sapiens\', \'Mus musculus\', \'116\')"'

pair_name

pair_name(species: str, other_species: str) -> str

Return the directory name one species pair is filed under, order-independent.

The two slugs, sorted and joined with a hyphen, so the pair asked for either way round reaches one directory and one download.

Parameters:

Name Type Description Default
species str

One species of the pair.

required
other_species str

The other.

required

Returns:

Type Description
str

The pair's directory name.

Examples:

>>> pair_name("Mus musculus", "Homo sapiens")
'homo_sapiens-mus_musculus'

set_dir

set_dir(root: Path, row: HomologyMetadata) -> Path

Return the directory one pair's set is prepared in, under root.

<root>/ensembl_compara/<release>/<pair>/: one directory per set, because each carries a Completion marker of its own, and releases sit side by side so holding two is not a re-download.

Parameters:

Name Type Description Default
root Path

The homology root, which is :func:homology_data_dir unless a caller is laying one out somewhere else. A cache_dir passed to :class:HomologySet is not this: it names the set's own directory and skips the layout entirely.

required
row HomologyMetadata

The provenance row for the pair.

required

Returns:

Type Description
Path

The set's own directory. Nothing is created by asking.

Examples:

>>> from pathlib import Path
>>> set_dir(Path("/scratch/liulab/homology"), check_pair("Homo sapiens", "Mus musculus", "116"))
PosixPath('/scratch/liulab/homology/ensembl_compara/116/homo_sapiens-mus_musculus')

slice_filename

slice_filename(row: HomologyMetadata) -> str

Return the name one pair's stored slice is cached under.

Compara's own naming with the pair in place of the member type, so a directory listing says which release and which two species a file is without opening it.

Parameters:

Name Type Description Default
row HomologyMetadata

The provenance row for the pair.

required

Returns:

Type Description
str

The local file name.

Examples:

>>> slice_filename(check_pair("Homo sapiens", "Mus musculus", "116"))
'Compara.116.homo_sapiens-mus_musculus.homologies.tsv.gz'

source_filename

source_filename(row: HomologyMetadata) -> str

Return the name the publisher's own dump is downloaded under, inside the work area.

Parameters:

Name Type Description Default
row HomologyMetadata

The provenance row for the pair.

required

Returns:

Type Description
str

The download's file name, carrying the release and the species whose dump it is.

Notes

Which species that is, is the pair's holding species and not either of the two the caller named — the partition decides it, and the shipped row records which file was counted to hold the pair.

Examples:

>>> source_filename(check_pair("Homo sapiens", "Mus musculus", "116"))
'Compara.116.mus_musculus.protein_default.homologies.tsv.gz'

homology_metadata

homology_metadata(
    species: str, other_species: str, release: str
) -> HomologyMetadata | None

Return the table's row for one species pair and Release, or None.

The pair is unordered — ("Homo sapiens", "Mus musculus") and its reverse find the same row — because which species a caller asks about is a property of the question and not of the published file.

None is the raw absence, and this is the one place it is how absence is said: everything above turns it into an error naming the pairs the table does carry, so nobody pinned this pair can never be read as these species share no homologs.

Parameters:

Name Type Description Default
species str

One species of the pair, as the assembly metadata table spells it.

required
other_species str

The other.

required
release str

The Compara Release.

required

Returns:

Type Description
HomologyMetadata or None

The row, or None when the table pins no such pair in that release.

Examples:

>>> homology_metadata("Mus musculus", "Homo sapiens", "116").holding_species
'Mus musculus'
>>> homology_metadata("Homo sapiens", "Danio rerio", "116") is None
True

homology_releases cached

homology_releases() -> tuple[str, ...]

Return every Release the shipped table pins, ascending.

What can be asked for at all, and what an error names when a release cannot be.

Returns:

Type Description
tuple of str

The release identifiers, sorted.

Examples:

>>> homology_releases()
('116',)

homology_species cached

homology_species() -> tuple[str, ...]

Return every species the shipped table names, sorted, as the metadata spells them.

The species a Homology set can be asked about, read off the table rather than kept as a second list in code — so adding a species is adding rows.

Returns:

Type Description
tuple of str

The species names, sorted.

Examples:

>>> homology_species()
('Caenorhabditis elegans', 'Homo sapiens', 'Mus musculus')

homology_table cached

homology_table() -> tuple[HomologyMetadata, ...]

Return every row of the shipped provenance table, in table order.

Which species pairs this package can prepare a Homology set for, from which release and from whose file. Read once and cached; the records are frozen, so the tuple is safe to hold on to.

Returns:

Type Description
tuple of HomologyMetadata

One record per row.

Raises:

Type Description
HomologyMetadataError

If a row cannot be read; the message names the column.

Examples:

>>> {row.publisher for row in homology_table()}
{'Ensembl Compara'}
>>> len(homology_table())
3

read_metadata

read_metadata(
    text: str, *, origin: str
) -> tuple[HomologyMetadata, ...]

Read the provenance table from text, holding it to the columns it declares.

Separate from the resource it came out of, so every way the table can be wrong is reachable without writing a broken one into the package.

Parameters:

Name Type Description Default
text str

The whole table, tab separated, header first.

required
origin str

Where the text came from, named in every message.

required

Returns:

Type Description
tuple of HomologyMetadata

One record per row, in table order.

Raises:

Type Description
HomologyMetadataError

If the file is empty, the header is not :data:METADATA_COLUMNS, a row holds the wrong number of cells, or a cell cannot be read.

Examples:

>>> header = "\t".join(METADATA_COLUMNS)
>>> read_metadata(header + "\n", origin="test")
()

genome.store

What belongs to no context and is reached by all of them.

Four modules that know about bytes, directories and hashes, and nothing about assemblies, annotations, motifs or genes. Each is here because every context needs it and none owns it:

  • :mod:~genome.store.data_dir reads $LIULAB_DATA — the Data dir the whole package files into. Which roots sit under it is each context's own to declare.
  • :mod:~genome.store.fetch is the package's one fetch step, and imports nothing else from this package at all.
  • :mod:~genome.store.completion is the Completion marker: the record a finished build writes and the working area it uses until it does.
  • :mod:~genome.store.checksum digests a file, and refuses one that disagrees with what was expected of it.
  • :mod:~genome.store.prepared is the pipeline the three Prepared sets share, built out of the four above and out of nothing else.

Nothing here imports from :mod:genome.assembly or :mod:genome.annotation, which is what makes this a place both of them can reach without reaching each other.

This file re-exports no callable on purpose, and the fetch step is why. Every caller inside and outside the package holds a modulefrom genome.store import fetch, from genome.store.completion import CompletionRecord — and the fetch step's one patch point depends on that: a callable re-exported here is a second reference that monkeypatch.setattr on the module would never reach, which is exactly the bug the suite's offline guard exists to prevent.

The exception classes below are the one exemption, because nothing patches one. They are re-exported so a caller can name in an except what this package hands them — a Assembly dir that disagrees with its record, a Prepared set nothing has prepared yet — rather than importing from a module the API reference declares free to move. The exemption is theirs alone: a function or a non-exception class added to __all__ re-opens the hole the paragraph above closes.

Examples:

>>> from genome.store import RegistrationMismatchError
>>> issubclass(RegistrationMismatchError, RuntimeError)
True

ChecksumMismatchError

ChecksumMismatchError(
    path: Path, expected: str, actual: str
)

Bases: ValueError

A file's contents disagree with the checksum that was expected of them.

Raised where a digest computed from bytes on disk is compared against a recorded one — the sha256 an assembly's metadata row pins, say. The message names all three things a caller needs to act: which file, what was expected, what was actually there.

Parameters:

Name Type Description Default
path Path

The file that was hashed.

required
expected str

The digest that was expected of it, as a hex string.

required
actual str

The digest actually computed from it.

required

Attributes:

Name Type Description
path Path

The file that was hashed.

expected str

The expected digest.

actual str

The computed digest.

Examples:

>>> from pathlib import Path
>>> raise ChecksumMismatchError(Path("hg38.fa"), "1a2b3c", "9f8e7d")
Traceback (most recent call last):
genome.store.checksum.ChecksumMismatchError: sha256 mismatch for hg38.fa: expected 1a2b3c, got 9f8e7d. ...

RegistrationError

Bases: RuntimeError

A directory holds a build that cannot be trusted as finished.

Raised on its own for a record that contradicts itself — one claiming a shape only this package writes, in a form it never would have — and subclassed for the two states a caller can tell apart by looking at the directory. Every one of them names the command that repairs it, because a caller who cannot act on the message is left guessing.

Examples:

>>> from genome.store import RegistrationError
>>> try:
...     raise RegistrationError("sacCer3: register it again with `--force`.")
... except RegistrationError as broken:
...     print(broken)
sacCer3: register it again with `--force`.

RegistrationMismatchError

Bases: RegistrationError

A record disagrees with what is on disk, so something changed behind our back.

Raised rather than rebuilt: a file that was deleted or truncated after registration is a fact worth surfacing, and guessing which of the two is right risks silently wrong results.

Examples:

>>> from genome.store import RegistrationError, RegistrationMismatchError
>>> try:
...     raise RegistrationMismatchError("sacCer3.2bit: recorded 3039745, found 0")
... except RegistrationError as broken:  # the parent catches this and the unfinished state
...     print(broken)
sacCer3.2bit: recorded 3039745, found 0

UnfinishedRegistrationError

Bases: RegistrationError

A directory holds files but no record, so a build was interrupted part-way.

Raised rather than silently resumed: the files present may be a complete build whose record was never written, or the wreckage of one killed half-way, and nothing on disk distinguishes them. An absent or empty directory is not this — that is a fresh registration and proceeds normally.

Examples:

>>> from genome.store import RegistrationError, UnfinishedRegistrationError
>>> isinstance(UnfinishedRegistrationError("files, but no record"), RegistrationError)
True

PreparedChecksumError

Bases: ValueError

What arrived is not what the set pins, so nothing is prepared from it.

A :class:ValueError: the bytes are a bad value, and a truncated download is not a smaller release. The message names the file, both digests, which bytes the pin covers and the command that prepares the set again.

Examples:

>>> issubclass(PreparedChecksumError, ValueError)
True

PreparedDecodeError

Bases: ValueError

The publisher's file is not text, and what reads it here yields text.

A :class:ValueError, for the same reason the checksum error is one: the bytes are a bad value for what is being asked of them. Every reader in this package is handed decoded lines and all three publishers ship text, so a publisher that ships bytes is a design question — whether a source should declare its form, and whether the reader protocol should carry bytes — rather than something a caller repairs on disk. The message names the file so that the wall is a wall and not a decoder's own error surfacing from mid-stream naming nothing. What a reader stores is not this: a stored form is digested as bytes and never decoded.

Examples:

>>> issubclass(PreparedDecodeError, ValueError)
True

PreparedSetNotDownloadedError

Bases: RuntimeError

A Prepared set is not on disk here and its bytes could not be fetched.

A :class:RuntimeError, because nothing about the call was wrong: the bytes are simply not here and this machine could not go and get them. Each context raises a subclass of its own naming its own set and quoting its own prepare command — three specific messages rather than one vague one — and the sentence that sends the caller to a login node is :func:login_node_help, written once.

Examples:

>>> issubclass(PreparedSetNotDownloadedError, RuntimeError)
True

genome.tf

Transcription factors and the motifs they bind.

Two halves, kept apart because they are keyed differently. :mod:genome.tf.gene is about the gene — which genes a published census judges transcription factors, and the DNA-binding-domain family it classifies each one under — and so it is keyed by gene. :mod:genome.tf.motif is about the sequence a factor recognises — the count matrix and where it occurs in an assembly — and so it is keyed by motif. The mapping between the two is many-to-many, which is why neither half owns it.

Cofactors are a carve-out and not a gap. :mod:genome.tf.cofactor is a third part and a peer of both. It is keyed by gene exactly as the census half is, and it is a peer rather than a part of it because the question differs: that half says whether a gene is a TF gene and of what DBD family, this one says whether it is a Transcription cofactor and of what class. A cofactor binds no DNA sequence-specifically and so has no motif, which is why nothing in the motif half answers for one.

The join itself is :mod:genome.tf.link, which lives here rather than in either half because it imports both and neither imports it. It is re-exported from this package for that reason: it belongs to the pair, so it is addressed at the level the pair is.

Examples:

>>> from genome.tf import motif_links
>>> motif_links("TP53", "Homo sapiens").motif_ids
('MA0106.3',)

GeneNotAssessedError

Bases: LookupError

The census for that species never assessed that gene, so nothing can answer for it.

The second absence. A gene the census assessed and turned down is not this: it has a verdict, and comes back with no links and :attr:MotifLinks.is_tf False. This is the gene the census never looked at — a symbol it does not spell, a Gene id stem of another species, or a typo — and answering it emptily would read as this gene has no motifs. A gene a publisher lists as a Transcription cofactor raises the narrower :class:TranscriptionCofactorError instead, which is this absence with a reason.

The message names the species and the census that speaks for it.

Examples:

>>> try:
...     motif_links("ENSMUSG00000005698", "Homo sapiens")
... except GeneNotAssessedError as error:
...     print("Lambert et al. 2018" in str(error))
True
MotifLink(
    release: str,
    species: str,
    gene_id_stem: str,
    symbol: str,
    motif_id: str,
    motif_name: str,
    role: str,
    partners: tuple[str, ...],
    motif_tax_ids: tuple[str, ...],
    is_cross_species: bool,
    total_information_content: float,
    rank: int,
)

One Motif link: one JASPAR profile that answers for one TF gene.

One row of a shipped table, read back and frozen. It says what the matrix is a motif of — this gene alone, or a complex and which partners — and carries everything a caller needs to re-sort the answer on: the profile's tax ids, its Cross-species link flag and its total Information content.

Attributes:

Name Type Description
release str

The JASPAR Release this link was built from. On the row rather than left to the file name, so two tables concatenate into one frame that still says which release each row came from.

species str

The gene's species, as the assembly metadata table and its census spell it.

gene_id_stem str

The Gene id stem the census is keyed by, and what this table is keyed by too.

symbol str

The census's own symbol for the gene, never JASPAR's. The two differ exactly where the shipped alias table says they do — Lambert's T is JASPAR's TBXT — and a row that mixed them would be unreadable.

motif_id str

The Motif id, versioned: MA0099.4.

motif_name str

The Motif name JASPAR publishes, in JASPAR's own spelling and case, with :: between the genes a complex names.

role str

:data:MONOMER where the profile names one gene, :data:COMPLEX otherwise.

partners tuple of str

The other genes the Motif name names, upper-cased as the name spells them. Empty for a monomer, and never empty for a complex.

motif_tax_ids tuple of str

The NCBI taxonomy ids JASPAR files this profile under, ascending. Empty for a profile the release records no species for — MA0108, TBP, is the one.

is_cross_species bool

Whether the matrix was measured on a vertebrate other than the gene's own species. A profile with no recorded species is marked True: the row cannot claim a species match it has no evidence for.

total_information_content float

The matrix's Information content summed over its columns, in bits. The third key of Attribution specificity, and not a quality score.

rank int

This link's place under Attribution specificity among all of this gene's links, dense from 1 in the shipped table. It is the row's own number and not the answer's position, so filtering out Cross-species links leaves gaps in it — deliberately, since a rank that renumbered itself would no longer say how specific the attribution was.

Examples:

>>> link = motif_links("JUN", "Homo sapiens")[6]
>>> link.motif_id, link.motif_name, link.role, link.partners
('MA0099.4', 'FOS::JUN', 'complex', ('FOS',))
>>> link.is_cross_species, link.rank
(False, 7)

is_complex property

is_complex: bool

Return whether this profile is a motif of a complex rather than of this gene alone.

Examples:

>>> [link.is_complex for link in motif_links("CTCF", "Homo sapiens")]
[False, False, False]
MotifLinks(
    species: str,
    release: str,
    tax_group: str,
    source: str,
    gene_id_stem: str,
    symbol: str,
    is_tf: bool,
    links: tuple[MotifLink, ...],
)

One gene's Motif links, in Attribution specificity order.

:func:motif_links' answer. It is a sequence of links — iterate it, index it, take its length — that also says where it was cut from: the species, the Release, the Tax group and the shipped file. That provenance is captured before any filtering, for the reason :class:~genome.tf.motif.jaspar.JasparDatabase hands back a plain :class:~genome.tf.motif.motif.MotifSet when it is filtered: what comes out of a filter is no longer the release it came from, so unless it was written down first there is nothing left to say which release it was. Drop every Cross-species link from mouse Ctcf and no link survives to carry the release on its row — and this still says which release found nothing.

An empty answer is a real answer, and it is never how absence is spelled: a species or release with no table raises :class:NoMotifLinkTableError and a gene no census assessed raises :class:GeneNotAssessedError. What is left is a gene with no links, and :attr:is_tf says which of the two kinds it is — a gene the census turned down, which receives no links by design, or one it judged a transcription factor that this Release has no profile for. 763 of human's 1,639 assessed-positive genes are the second kind on the 2026 release.

Attributes:

Name Type Description
species str

The species, as its census spells it.

release str

The JASPAR Release the table was built from.

tax_group str

The Tax group the table covers — :data:LINK_TAX_GROUP. Recorded here because it is the one key of the three that no row carries.

source str

The shipped file these links were read from.

gene_id_stem str

The Gene id stem the gene was resolved to, whichever spelling was asked for.

symbol str

The census's own symbol for that gene.

is_tf bool

The census's own verdict. True for every gene that receives links, since only assessed-positive genes do; False says an empty answer is empty by design.

links tuple of MotifLink

The links, most specifically attributable first, after any filtering asked for.

Examples:

>>> jun = motif_links("JUN", "Homo sapiens", release="2024")
>>> jun.species, jun.release, jun.gene_id_stem
('Homo sapiens', '2024', 'ENSG00000177606')
>>> jun.motif_ids[:2]
('MA0488.2', 'MA0489.3')
>>> matched = motif_links("JUN", "Homo sapiens", release="2024", cross_species=False)
>>> matched.motif_ids[:2]
('MA0488.2', 'MA1131.2')
>>> smad2 = motif_links("SMAD2", "Homo sapiens")
>>> len(smad2), smad2.is_tf
(0, False)

motif_ids property

motif_ids: tuple[str, ...]

Return every Motif id here, in Attribution specificity order.

What is handed to a scan: the matrices to pull out of a :class:~genome.tf.motif.jaspar.JasparDatabase of the same Release.

Examples:

>>> motif_links("AHR", "Homo sapiens").motif_ids
('MA0006.2',)

__len__

__len__() -> int

Return how many links this answer holds.

Examples:

>>> len(motif_links("TP53", "Homo sapiens"))
1

__iter__

__iter__() -> Iterator[MotifLink]

Iterate the links, most specifically attributable first.

Examples:

>>> [link.role for link in motif_links("AHR", "Homo sapiens")]
['complex']

__getitem__

__getitem__(index: int) -> MotifLink

Return one link by its position in this answer.

Examples:

>>> motif_links("CTCF", "Homo sapiens")[0].motif_id
'MA1930.2'

MotifLinkTable dataclass

MotifLinkTable(
    species: str,
    release: str,
    tax_group: str,
    source: str,
    links: tuple[MotifLink, ...],
)

One shipped Motif link table: one species, one Release, every gene in it.

What one file says, read back and frozen. It is the whole join for that species and release — the layer :meth:links_for cuts one gene's answer out of — and the thing that knows which release and Tax group it is, which is why a :class:MotifLinks copies that down before it filters.

Attributes:

Name Type Description
species str

The species, as every row of the file names it.

release str

The JASPAR Release, as every row of the file names it.

tax_group str

The Tax group the file covers — :data:LINK_TAX_GROUP, which no row carries because one value is all that ships.

source str

Where the bytes came from.

links tuple of MotifLink

Every link in the file, in file order: by Gene id stem, then by rank.

Examples:

>>> table = motif_link_table("Homo sapiens", "2026")
>>> table.release, table.tax_group, len(table)
('2026', 'vertebrates', 1085)
>>> len(table.gene_id_stems)
876
>>> table.links_for("TP53").motif_ids
('MA0106.3',)

gene_id_stems property

gene_id_stems: tuple[str, ...]

Return every Gene id stem the table links, once each, in file order.

The genes this release has a motif for, which is a strict subset of the genes the census judged transcription factors — the rest are assessed positive and have no JASPAR profile.

Examples:

>>> motif_link_table("Homo sapiens", "2026").gene_id_stems[0]
'ENSG00000001167'

__len__

__len__() -> int

Return how many links the table holds.

Examples:

>>> len(motif_link_table("Mus musculus", "2026"))
896

frame

frame() -> pd.DataFrame

Return the table as a fresh :class:~pandas.DataFrame, one row per link.

Built for the caller each time it is asked for, so mutating it cannot reach the cached table behind it. The columns are the file's own twelve in file order, with the multi-value cells as tuples and the flag as a boolean.

Returns:

Type Description
DataFrame

The links, indexed from zero in file order.

Examples:

>>> frame = motif_link_table("Homo sapiens", "2026").frame()
>>> list(frame.columns) == list(LINK_COLUMNS)
True
>>> int(frame["is_cross_species"].sum())
162
links_for(
    gene: str, *, cross_species: bool = True
) -> MotifLinks

Return one gene's Motif links, most specifically attributable first.

The gene is named by its Gene id stem or by the symbol its own census publishes, and a versioned gene id is refused rather than stemmed — see :func:motif_links, which is this method with the table looked up for you and is what a caller normally holds.

Parameters:

Name Type Description Default
gene str

A Gene id stem"ENSG00000177606" — or the census's own symbol for it, in any case: "JUN", "Jun".

required
cross_species bool

Whether to keep links whose profile was measured on another vertebrate. Pass False for a question that demands species-matched profiles; it can empty the answer, and for mouse it usually thins it.

True

Returns:

Type Description
MotifLinks

The links, in Attribution specificity order, carrying this table's provenance — copied down before the filter, since what a filter returns is no longer the release it came from.

Raises:

Type Description
GeneNotAssessedError

If this species' census never assessed that gene.

TranscriptionCofactorError

If it never assessed that gene and a publisher lists it as a Transcription cofactor; a narrowing of the error above.

VersionedGeneIdError

If gene is a versioned gene id whose stem the census does assess.

MotifLinkTableError

If no census ships for this table's species, which no shipped table can be in — a link table is built from a census.

Examples:

>>> table = motif_link_table("Mus musculus", "2026")
>>> table.links_for("Ctcf").motif_ids
('MA1930.2', 'MA1929.2', 'MA0139.2')
>>> table.links_for("Ctcf", cross_species=False).motif_ids
()

MotifLinkTableError

Bases: ShippedTableError

A shipped Motif link table cannot be read, so it is not allowed to answer.

A packaging defect and not a caller error: these files ship inside the package and are written by scripts/build_tf_links.py, so a header that is not the twelve columns, a row with the wrong number of cells, a Role nothing spells that way or two releases inside one file are faults in what was committed here. A :class:ValueError, because a file that says something the format does not is a bad value rather than a broken program.

The message names the file and what is wrong with it, since regenerating that file is the only thing anyone can do about it.

Examples:

>>> try:
...     parse_motif_link_table("release\tspecies\n", source="broken.tsv")
... except MotifLinkTableError as error:
...     print("broken.tsv" in str(error))
True

NoMotifLinkTableError

Bases: LookupError

No Motif link table ships for that species, Release or Tax group.

The first of the two absences, and the one a caller must never read as no motif answers for this gene: no table was ever built for what was asked, so the question was not answered rather than answered in the negative. A :class:LookupError, as the curated gene lists' own pair of absences are, so a caller may catch that and still tell this from :class:GeneNotAssessedError.

The message names the species, releases and Tax group that do ship.

Examples:

>>> try:
...     motif_links("CTCF", "Homo sapiens", tax_group="plants")
... except NoMotifLinkTableError as error:
...     print("vertebrates" in str(error))
True

TranscriptionCofactorError

Bases: GeneNotAssessedError

No census assessed that gene, and a publisher lists it as a Transcription cofactor.

The same absence with something known in its place: a cofactor acts on transcription without recognising a sequence of its own, so there is no motif to look for rather than one nobody has found yet, and the census's plain silence would read as nothing here knows this gene.

It subclasses :class:GeneNotAssessedError because that is literally true — no TF census assessed this gene — so an except clause written before this error existed keeps covering every gene it covered. The is-a is about the censuses and not about biology: a cofactor is not a kind of transcription factor, and being one never suppresses the motifs a census already reached, which is why the census is asked first.

The message names the census that did not assess the gene, the publisher that lists it as a cofactor, and that there is no motif here to look for.

Examples:

>>> try:
...     motif_links("WDR5", "Homo sapiens")
... except TranscriptionCofactorError as error:
...     print("transcription cofactor" in str(error))
True

VersionedGeneIdError

Bases: ValueError

A versioned gene id was passed where a Gene id stem is the key.

Not an absence — the gene is assessed and its links are here — so it is a :class:ValueError and a caller catching :class:LookupError for a missing gene does not swallow it. A stem may name more than one gene id in one Annotation: ENSG00000182378.14 and ENSG00000182378.14_PAR_Y are two genes of one stem in gencode_v50lift37, and the census reached one verdict for the stem. Answering a versioned id would therefore answer for the stem — which names a gene the caller did not — so it is refused, in the same spirit as :meth:~genome.annotation.registry.AnnotationRegistry.resolve_gene_ids, which answers a stem with every gene id it names and never picks one.

The message names the stem to pass instead.

Examples:

>>> try:
...     motif_links("ENSG00000177606.6", "Homo sapiens")
... except VersionedGeneIdError as error:
...     print("ENSG00000177606" in str(error))
True
motif_link_table(
    species: str,
    release: str = DEFAULT_RELEASE,
    tax_group: str = DEFAULT_TAX_GROUP,
) -> MotifLinkTable | None

Return the Motif link table shipped for one species and Release, or None.

The raw absence, and the only place None is an acceptable way to say it: this is the layer below the one a caller touches, and :func:motif_links above it turns the None into an error naming what does ship, so that no table was built for this can never be read as this gene has no motifs.

The species is slugged and then looked up among what :func:shipped_link_tables found, rather than joined onto the resource directory, so a name shaped like a path finds nothing instead of walking out of it. Read once per table and cached; everything it returns is frozen.

Parameters:

Name Type Description Default
species str

The species, either as the assembly metadata table spells it ("Homo sapiens") or as its slug ("homo_sapiens").

required
release str

The JASPAR Release to read the links of. The motif half's default, so a new analysis links against the same release a fresh scan loads.

``"2026"``
tax_group str

The Tax group. Only :data:LINK_TAX_GROUP ships; any other answers None, which is absence and not emptiness.

``"vertebrates"``

Returns:

Type Description
MotifLinkTable or None

The table, or None when none ships for that species, release and tax group.

Raises:

Type Description
MotifLinkTableError

If a table ships and cannot be read, or names a species or release its file name does not; the message names the file.

Examples:

>>> motif_link_table("Homo sapiens", "2024").release
'2024'
>>> motif_link_table("homo_sapiens") == motif_link_table("Homo sapiens")
True
>>> motif_link_table("Danio rerio") is None
True
>>> motif_link_table("Homo sapiens", tax_group="plants") is None
True
motif_links(
    gene: str,
    species: str,
    *,
    release: str = DEFAULT_RELEASE,
    tax_group: str = DEFAULT_TAX_GROUP,
    cross_species: bool = True,
) -> MotifLinks

Return the JASPAR motifs that answer for one TF gene, most specific first.

The entry point. It reads the shipped table for that species and Release and cuts one gene's links out of it — nothing is downloaded, nothing on disk is touched, and no Assembly is involved.

What names a gene here. A Gene id stem, which is what the tables and the censuses are keyed by, or the symbol the gene's own census publishes, in any case — the two censuses spell one factor JUN and Jun, and each spelling is unique within its own census. A versioned gene id is refused rather than stemmed: a stem may name more than one gene id in one Annotation, so answering ENSG00000182378.14 would answer for a stem that also names ENSG00000182378.14_PAR_Y, and this package never picks a gene the caller did not name (see :meth:~genome.annotation.registry.AnnotationRegistry.resolve_gene_ids, which crosses that gap in the other direction). Pass the stem, and the error says which one.

The species is passed and never inferred. A table is named by a species and a release, and a Gene id stem's prefix is not a claim about which species it belongs to — deriving one from the other is the guess a local assembly key exists to forbid. A caller holding an assembly has its species already, in the assembly's own metadata row.

The census is asked first, and the order is what keeps this correct. A gene the census assessed is answered whatever else is known about it — the 151 human genes that are both a TF gene and a Transcription cofactor, TBP and KMT2A and DNMT1 among them, come back exactly as they always did, because a second table must never suppress an answer the census already reached. Only then is that species' Cofactor table asked, and a gene it lists raises :class:TranscriptionCofactorError: a cofactor recognises no sequence of its own, so no motif to look for is a truer answer than the census's silence. A gene neither knows raises :class:GeneNotAssessedError, as it always has, and so does every gene of a species that ships no cofactor table.

Parameters:

Name Type Description Default
gene str

A Gene id stem or the census's own symbol for the gene.

required
species str

The species, as the assembly metadata table spells it or as its slug.

required
release str

The JASPAR Release to link against. Both releases the package prepares have tables; asking for another raises and names the ones that ship.

``"2026"``
tax_group str

The Tax group. Only :data:LINK_TAX_GROUP ships, and asking for another raises rather than answering emptily.

``"vertebrates"``
cross_species bool

Whether to keep links whose profile was measured on another vertebrate. False is how a question that demands species-matched profiles asks for them.

True

Returns:

Type Description
MotifLinks

The gene's links in Attribution specificity order, carrying the release, tax group and file they were cut from.

Raises:

Type Description
NoMotifLinkTableError

If no table ships for that species, release or tax group; the message names what does.

GeneNotAssessedError

If that species' census never assessed that gene.

TranscriptionCofactorError

If it never assessed that gene and a publisher lists it as a Transcription cofactor; a narrowing of the error above, so an except clause written for that one catches this too.

VersionedGeneIdError

If gene is a versioned gene id whose stem the census does assess.

Examples:

>>> ctcf = motif_links("CTCF", "Homo sapiens")
>>> ctcf.motif_ids
('MA1930.2', 'MA1929.2', 'MA0139.2')
>>> jun = motif_links("Jun", "Mus musculus")
>>> [(link.motif_id, link.is_cross_species) for link in jun][:2]
[('MA0489.3', False), ('MA0488.2', True)]
>>> motif_links("Jun", "Mus musculus", cross_species=False).motif_ids
('MA0489.3',)
>>> motif_links("T", "Homo sapiens").links[0].motif_name
'TBXT'
>>> len(motif_links("FOXM1", "Homo sapiens", release="2024"))
0
parse_motif_link_table(
    text: str, *, source: str
) -> MotifLinkTable

Read one Motif link table's text into links, holding it to what a table promises.

A pure function from text to links: it opens nothing, downloads nothing and decompresses nothing. Separate from the resource it came out of, as :func:~genome.tf.motif.jaspar.parse_transfac is, so every way a file can be wrong is reachable without writing a broken one into the package. The shipped tables are gzipped and :func:motif_link_table unpacks them at the resource boundary, which is where :func:~genome.tf.gene.tf_gene_table unpacks a census too — the seam is between the bytes and the format, and it does not move because the bytes are compressed.

The Release and the species are read off the rows rather than passed in, and every row must agree about both — they are on each row so that two tables concatenate into one frame that still says where each row came from, which is a promise only if they are uniform within a file. The Tax group is the one key no row carries, since :data:LINK_TAX_GROUP is all that ships.

Parameters:

Name Type Description Default
text str

The whole table, unpacked. These are a few hundred kilobytes, so they are read whole.

required
source str

Where the text came from; named in every message, since regenerating that file is the only repair.

required

Returns:

Type Description
MotifLinkTable

The table the text spells, in file order.

Raises:

Type Description
MotifLinkTableError

If the header is not :data:LINK_COLUMNS, a row holds the wrong number of cells, a Role or a flag is spelled a way no table spells one, a number is not one, a key cell is blank, the file declares no links, or two rows name different releases or species.

Examples:

>>> header = "\t".join(LINK_COLUMNS)
>>> row = "2026\tHomo sapiens\tENSG00000141510\tTP53\tMA0106.3\tTP53\tmonomer\t\t9606\tno\t20.6607\t1"
>>> table = parse_motif_link_table(f"{header}\n{row}\n", source="one.tsv")
>>> table.species, table.release, table.links[0].motif_name
('Homo sapiens', '2026', 'TP53')
shipped_link_tables() -> tuple[tuple[str, str], ...]

Return every Motif link table that ships, as (species slug, release).

What can be asked about at all, and what an error names when something cannot be. The directory is enumerated and the two keys read out of each file name, so neither the species nor the releases are listed in code and adding either is dropping a file in.

Returns:

Type Description
tuple of (str, str)

One pair per shipped table, sorted. Empty only if the package ships no table.

Examples:

>>> ("homo_sapiens", "2026") in shipped_link_tables()
True

genome.tf.motif

TF binding motifs — the matrices themselves, and finding where they occur.

MotifComparison

MotifComparison(data: Dataset)

How a set of query Motifs compares against a set of target motifs.

A thin wrapper over an :class:xarray.Dataset — the array is the answer, and this class exists to say which of its two shapes it is in, to keep the ranking rule in one place, and to refuse the one question a limited comparison cannot answer. Reach for :attr:data whenever you want the array itself.

Two shapes, and every method here handles both.

  • Complete, from compare(queries): dimensions (query, target), both labelled with Motif ids, every pair scored. :attr:target_ids answers, and data.sel(query=..., target=...) reaches any cell.
  • Limited, from compare(queries, top=n): dimensions (query, rank), where rank 0 is each query's best target. The target axis is per query — rank 0 names a different motif for each row — so the target ids ride along as a target data variable rather than as a coordinate, and there is no shared axis to index or to widen.

Parameters:

Name Type Description Default
data Dataset

The labelled array, in either shape. Must carry a query dimension labelled with Motif ids, every variable in :data:COMPARISON_VARIABLES, and either a target coordinate (complete) or a rank dimension with a target variable over (query, rank) (limited). attrs["targets_compared"] records how many targets the comparison ran against, which is the only thing a limited array cannot say for itself.

required

Raises:

Type Description
ValueError

If data is in neither shape. The message names what is missing.

Examples:

>>> import numpy as np
>>> from genome.tf.motif import Motif, MotifSet
>>> counts = np.array([[19.0, 1.0, 1.0], [1.0, 19.0, 1.0],
...                    [1.0, 1.0, 19.0], [1.0, 1.0, 1.0]])
>>> published = MotifSet([Motif("MA0001.1", "", np.tile(counts, 3))])
>>> comparison = published.compare(Motif("pattern_0", "", np.tile(counts, 3)))
>>> comparison
MotifComparison(queries=1, targets=1, top=None)
>>> comparison.is_ragged
False
>>> float(comparison.data["neg_log10_p"].sel(query="pattern_0", target="MA0001.1")) > 0
True

data property

data: Dataset

The labelled array itself, indexable by Motif id.

Examples:

>>> import numpy as np
>>> from genome.tf.motif import Motif, MotifSet
>>> published = MotifSet([Motif("MA0001.1", "", np.eye(4)[:, [0, 1, 2, 3, 0, 1, 2]] + 1)])
>>> sorted(published.compare(published).data.data_vars)
['neg_log10_p', 'offset', 'overlap', 'score', 'strand']

is_ragged property

is_ragged: bool

Whether the target axis is per query, which a top limit makes it.

Examples:

>>> import numpy as np
>>> from genome.tf.motif import Motif, MotifSet
>>> published = MotifSet([Motif("MA0001.1", "", np.ones((4, 8)) + np.eye(4, 8))])
>>> published.compare(published).is_ragged
False
>>> published.compare(published, top=1).is_ragged
True

top property

top: int | None

The limit this comparison was run with — None when every pair was scored.

query_ids property

query_ids: tuple[str, ...]

Every query Motif id, in the order the queries were given.

target_ids property

target_ids: tuple[str, ...]

Every target Motif id, in the order the target set held them.

Returns:

Type Description
tuple of str

The shared target axis.

Raises:

Type Description
RaggedComparisonError

If this comparison was limited. A limited comparison has no shared target axis: rank 0 names a different motif for each query.

targets_compared property

targets_compared: int

How many target motifs the comparison ran against, whichever shape it is in.

__repr__

__repr__() -> str

Return the two sizes and the limit, never the array itself.

to_frame

to_frame(top: int | None = 1) -> pd.DataFrame

Flatten to one row per query-target pair, best first within each query.

The default is the one question most callers have — what does each of my motifs look like — so it hands back one row per query. Ranking is by neg_log10_p descending, ties broken by score descending and then by the target set's own order, which is also the order tomtom's nearest-neighbour path returns, so the two shapes agree on which target is best.

Parameters:

Name Type Description Default
top int or None

How many targets to keep per query, best first. None keeps every pair the comparison holds. A limit above the number of targets is not an error for a complete comparison — nothing is missing from one, so it simply returns every pair.

1

Returns:

Type Description
DataFrame

Columns :data:FRAME_COLUMNS, with a fresh range index. Numeric dtypes are the array's own, unchanged: the frame is the array flattened, not a copy of it at a different precision.

Raises:

Type Description
ValueError

If top is below 1.

RaggedComparisonError

If this comparison was limited and top asks for more targets per query than it kept. Those pairs were never scored, so widening means recomputing.

Examples:

>>> import numpy as np
>>> from genome.tf.motif import Motif, MotifSet
>>> def spelled(motif_id, bases):
...     columns = [[19.0 if b == l else 1.0 for l in bases] for b in "ACGT"]
...     return Motif(motif_id, "", np.array(columns))
>>> published = MotifSet([spelled("MA0001.1", "ACGTACGTA"),
...                       spelled("MA0002.1", "TTTTTTTTT")])
>>> published.compare(spelled("pattern_0", "ACGTACGTA")).to_frame()["target"]
0    MA0001.1
Name: target, dtype: object

RaggedComparisonError

Bases: ValueError

A limited comparison was asked about targets it never scored.

Raised by :attr:MotifComparison.target_ids and by :meth:MotifComparison.to_frame when a comparison run with a top limit is asked for a shared target axis or for more targets per query than it kept.

A limited comparison cannot be widened without recomputing, and that is accepted rather than a defect. Passing top sends the work down tomtom's nearest-neighbour path, which never scores the targets that lose — so the missing cells were not discarded, they were never computed, and no amount of rearranging the array will produce them. Recompute with a larger top, or with none at all for the complete query x target array.

Examples:

>>> raise RaggedComparisonError("kept 3 targets per query")
Traceback (most recent call last):
    ...
genome.tf.motif.compare.RaggedComparisonError: kept 3 targets per query

JasparDatabase

JasparDatabase(
    release: str = DEFAULT_RELEASE,
    tax_group: str = DEFAULT_TAX_GROUP,
    *,
    cache_dir: str | Path | None = None,
    progressbar: bool = True,
)

Bases: MotifSet

One JASPAR Release and Tax group, prepared on disk and read into a set.

A :class:~genome.tf.motif.motif.MotifSet that also knows which release it is — :attr:release, :attr:tax_group, :attr:source_url and the :attr:path its bytes are cached at — so a Hit table produced from it can say what it was scanned with months later. Everything a motif set does, it does; and :meth:~MotifSet.filter hands back a plain motif set rather than another database, because a filtered release is no longer that release.

Constructing one prepares it, as opening a :class:~genome.assembly.genome.Genome does: the file is fetched into :func:jaspar_set_dir on the first construction and recorded with a Completion marker, and every construction after re-reads what is there and fetches nothing. The lab's CPU cluster compute nodes have no internet, so the first construction of a release must happen on a login node.

Parameters:

Name Type Description Default
release str

One of :data:JASPAR_RELEASES.

``"2026"``
tax_group str

One of :data:JASPAR_TAX_GROUPS. It chooses which file is downloaded rather than filtering one afterwards, so a worm scan never pays for a thousand plant matrices.

``"vertebrates"``
cache_dir str or Path

The directory to prepare in, overriding the one :func:jaspar_set_dir lays out. The directory itself, not a root to file under — the same word means the same thing for an :class:~genome.xref.xref.XrefSet and a :class:~genome.homology.compara.HomologySet. One set per directory either way, since each carries a Completion marker of its own.

None
progressbar bool

Show the download's progress bar. Nothing is drawn when the file is already there.

True

Attributes:

Name Type Description
release str

The Release this is.

tax_group str

The Tax group this is.

path Path

The cached file these motifs were read from.

source_url str

Where those bytes came from.

Raises:

Type Description
ValueError

If the release or tax group is not one this package prepares.

MotifSetNotDownloadedError

If the release is not prepared here and could not be fetched. The message names the call to make on a login node.

TransfacError

If a record in the file cannot be read.

JasparReleaseError

If the file holds the wrong number of motifs, two versions of one matrix, or bytes that are not the ones its Completion marker recorded.

RegistrationError

If the directory holds a file with no marker, or a marker that disagrees with what is on disk — an interrupted run, which reads as unfinished rather than as present.

Examples:

>>> from genome.tf.motif import JasparDatabase
>>> worms = JasparDatabase("2024", "nematodes")
>>> len(worms)
103
>>> worms["MA0260"].motif_name
'che-1'
>>> worms.filter(tf_class="zinc finger")
MotifSet(motifs=27)

__repr__

__repr__() -> str

Return which release this is and how many motifs it holds.

JasparReleaseError

Bases: ValueError

A file read as a Release is not the release it should be.

Not a parse failure — every record read cleanly — but the file as a whole is wrong: the wrong number of motifs, two versions of one matrix where a non-redundant release ships exactly one, or bytes that are not the ones its Completion marker recorded. All three mean the same thing to a caller, which is why they are one class: what is on disk is not what was asked for, and the repair is to prepare the release again. The message names the file and the command.

Examples:

>>> from pathlib import Path
>>> try:
...     _check_count((), release="2024", tax_group="diatoms", path=Path("/tmp/x.txt"))
... except JasparReleaseError as error:
...     print("holds 0 motifs" in str(error))
True

MotifSetNotDownloadedError

Bases: PreparedSetNotDownloadedError

The release is not prepared here, and JASPAR's file could not be fetched.

The Motif context's own spelling of what every Prepared set raises here, so the message names this release and tax group and quotes :func:jaspar_prepare_command. Before there was one, a compute node with no internet met pooch's own transport error and was left to work out that the repair is to run this somewhere else first.

Examples:

>>> issubclass(MotifSetNotDownloadedError, RuntimeError)
True

TransfacError

Bases: ValueError

A transfac record cannot be read, so no motif is made from it.

A bad file, not a bad call: the message names the record it stopped on — by its accession, or by its position when it has none — and what was wrong with it.

Examples:

>>> try:
...     parse_transfac("AC MA0001.1\nXX\nID x\nXX\n//\n")
... except TransfacError as error:
...     print("MA0001.1" in str(error))
True

MotifScanMixin

Scan Regions of this Genome and get hits in the assembly's own frame.

One method, because there is one thing a genome adds to a scan: the coordinates. The raw forms stay exactly as they were — a :class:~genome.tf.motif.motif.MotifSet handed a string, a mapping of sequences or a FASTA still answers in the frame of what it was given and names no assembly.

scan_regions

scan_regions(
    motifs: MotifSet,
    regions: Region | Iterable[Region],
    *,
    threshold: float = DEFAULT_THRESHOLD,
    background: BackgroundArg = None,
    workers: int | None = DEFAULT_WORKERS,
    output: str | Path | None = None,
) -> pd.DataFrame

Scan Regions of this genome and return the Hit table, in its coordinates.

Each region's bases are fetched exactly as :meth:~genome.assembly.genome.Genome.fetch_sequence returns them — reverse-complemented for a - region — scanned in one call, and the hits lifted into the assembly's frame. sequence_name carries the Chromosome name as this assembly spells it, since a region whose chromosome the assembly does not carry raises rather than being reconciled.

The arithmetic, so it can be checked without being run. Write the region as [S, E) on a chromosome, L = E - S, and a hit as the 0-based half-open [s, e) the scan found in the fetched bases:

  • +, and . with it: the bases run along the chromosome, so local i is chromosome S + i. The hit is [S + s, S + e) and its Strand is unchanged. An unknown strand is not promoted to + — it is that the fetch returns forward bases for it, so there is nothing to flip.
  • -: the bases are the reverse complement of [S, E), so local i is chromosome E - 1 - i. A hit covering local s .. e - 1 therefore covers chromosome E - e .. E - s - 1, which as a half-open interval is [E - e, E - s): the two ends swap, and the - 1s cancel exactly because the interval is half-open — in 1-based-inclusive coordinates they would not. Its strand flips too: what matched the forward strand of the fetched bases matched the reverse strand of the chromosome.

Two checks on the - case: a hit spanning the whole region, s = 0 and e = L, comes back as [E - L, E) = [S, E); and (E - s) - (E - e) = e - s, so the length is preserved.

Parameters:

Name Type Description Default
motifs MotifSet

The motifs to scan with — a Release, a filtered set, or de novo matrices. Those shorter than :data:~genome.tf.motif.motif.MIN_MOTIF_LENGTH are not scanned and are named in the result's motifs_skipped.

required
regions Region or iterable of Region

One region or many, on any chromosomes, in any order and overlapping freely: two regions on one chromosome are ordinary here, and each is scanned in its own right. A locus string is not accepted, because it carries no Strand and the strand is the whole question — write Region.from_string("chrI:0-600", strand="-").

required
threshold float

The Threshold: one per-position p-value, converted per motif against background into the score that motif must clear. In (0, 1).

1e-4
background sequence of float or {"auto", "uniform", "derive"}

The Background: four frequencies over :data:~genome.tf.motif.motif.BASES, above zero and summing to 1, or one of the three modes. Automatic when omitted — derived from the regions' own bases when they hold at least :data:~genome.tf.motif.background.BACKGROUND_FLOOR unambiguous ones, uniform below that. A GC-rich peak set is therefore scored against its own composition rather than against a null it does not follow, and whichever background it was is recorded on the result.

None
workers int

How many processes to shard the scan across — see :meth:~genome.tf.motif.motif.MotifSet.scan_sequences. One by default, as everywhere in the library; None resolves the count with :func:~genome.workers.resolve_workers, which is what an allocation on a cluster is read by. Regions are distributed whole, so more than one produces the identical table, lifted coordinates included.

1
output None

Refused, and the one scan argument that is: a scan handed an output path streams to Parquet and answers with the path, and a path holds no coordinates to lift into this assembly's frame. The refusal names what to do instead.

None

Returns:

Type Description
DataFrame

The Hit table — :data:~genome.tf.motif.scan.HIT_COLUMNS with :data:~genome.tf.motif.scan.HIT_DTYPES, one row per Motif hit — with sequence_name, start, end and strand in this assembly's frame. :data:REGION_HIT_PROVENANCE is on frame.attrs: the scan's own provenance and the Assembly these coordinates belong to. Empty when nothing cleared its cutoff, or when no region was given, and carrying its provenance either way.

Raises:

Type Description
TypeError

If regions holds anything that is not a :class:~genome.region.Region, or if output is given.

ValueError

If a region names a chromosome this assembly does not carry or falls outside it; or if threshold is not in (0, 1), background is neither a mode nor four positive frequencies summing to 1, or workers is below 1.

See Also

genome.tf.motif.motif.MotifSet.scan_sequences : the same scan, region-local and naming no assembly.

Examples:

>>> from genome import Genome, Region
>>> from genome.tf.motif import JasparDatabase
>>> sacCer3 = Genome("sacCer3")
>>> peaks = [Region("chrI", 0, 5000, "+"), Region("chrII", 100, 900, "-")]
>>> hits = sacCer3.scan_regions(JasparDatabase(), peaks)
>>> sorted(set(hits["sequence_name"]))
['chrI', 'chrII']

AmbiguousBaseIdError

AmbiguousBaseIdError(
    base_id: str, motif_ids: Iterable[str]
)

Bases: LookupError

A bare base id matches several Motif ids here, so it addresses none of them.

Ordinarily impossible and checked anyway: a non-redundant Release ships exactly one version of each matrix, which is what makes MA0139 mean MA0139.2 without a caller having to remember the version. A set holding two versions of one matrix — a hand-built comparison of MA0139.1 against MA0139.2, or a release that is not the non-redundant one — breaks that, and it is said rather than resolved by whichever came first.

Parameters:

Name Type Description Default
base_id str

The bare base id, versionless.

required
motif_ids iterable of str

Every Motif id sharing it, in the set's own order.

required

Attributes:

Name Type Description
base_id str

The base id that was asked for.

motif_ids tuple of str

Every Motif id sharing it.

Examples:

>>> try:
...     raise AmbiguousBaseIdError("MA0139", ["MA0139.1", "MA0139.2"])
... except LookupError as error:
...     print("MA0139.1" in str(error))
True

AmbiguousMotifNameError

AmbiguousMotifNameError(
    name: str, motif_ids: Iterable[str]
)

Bases: LookupError

A Motif name labels several motifs here, so it addresses none of them.

A name is a label and not a key: 66 names collide in the 2024 Release and 71 in 2026, so indexing a Motif set by one of them would have to pick one of four CTCFs and say nothing about the other three. It raises instead, naming every matching Motif id — which is what a caller addresses one by — and the call that hands back all of them.

Parameters:

Name Type Description Default
name str

The Motif name that labels several motifs.

required
motif_ids iterable of str

Every matching Motif id, in the set's own order.

required

Attributes:

Name Type Description
name str

The name that was asked for.

motif_ids tuple of str

Every matching Motif id.

Examples:

>>> try:
...     raise AmbiguousMotifNameError("CTCF", ["MA0139.2", "MA1929.2"])
... except LookupError as error:
...     print("MA1929.2" in str(error))
True

Motif dataclass

Motif(
    motif_id: str,
    motif_name: str,
    counts: NDArray[float64],
    offset: int = 0,
    tax_group: str = "",
    tf_class: tuple[str, ...] = (),
    tf_family: tuple[str, ...] = (),
    uniprot_ids: tuple[str, ...] = (),
    pubmed_ids: tuple[str, ...] = (),
    data_type: str = "",
)

One motif: a Count matrix, the identity it is addressed by, and its annotation.

Frozen, and frozen all the way down — the count matrix is copied at construction and marked read-only, so motif.counts[0, 0] = 1 raises rather than quietly turning a motif into a different one behind its own hash.

Equality and hashing are split on purpose. == compares every field, the matrix element by element (a numpy array cannot be compared with the tuple equality a dataclass generates, which is why eq=False). The hash covers the identity only — id, name, offset and shape — never the several thousand floats behind them. Equal motifs always agree on those four, so the contract holds; and two motifs sharing a Motif id are meant to be the same motif, so collisions are theoretical.

Parameters:

Name Type Description Default
motif_id str

The Motif id — JASPAR's versioned matrix accession, "MA0139.2". Never empty: it is what a motif is addressed by.

required
motif_name str

The Motif name — the factor name, "CTCF", or "Ptf1a::Rbpj" for a dimer. A label and not a key; names collide.

required
counts array_like

The Count matrix, 4 x L: one row per base in :data:BASES order, one column per position. Copied to float64. Values must be finite and non-negative and every column must sum above zero.

required
offset int

Where this motif's column zero sits in the frame of the motif with this id as the source published it, so position_in_this_frame + offset is the position in the full frame. Non-zero only on the result of :meth:trim.

0
tax_group str

The Tax group the source filed this motif under — "vertebrates". One value: a motif is filed under exactly one group.

``""``
tf_class iterable of str

The structural classes of the factor, e.g. ("C2H2 zinc finger factors",). Plural because a dimer has one per half — MA0119.1's NFIC::TLX1 is a SMAD/NF-1 factor joined to a homeo domain factor — and the source separates them with a semicolon. Stored as a tuple.

``()``
tf_family iterable of str

The families within those classes, e.g. ("Nuclear factor 1", "NK"). Plural for the same reason. Stored as a tuple.

``()``
uniprot_ids iterable of str

UniProt accessions for the factor — several for a dimer. Stored as a tuple.

``()``
pubmed_ids iterable of str

PubMed ids for the experiment behind the matrix. Stored as a tuple.

``()``
data_type str

How the matrix was measured, e.g. "ChIP-seq". One value, and commas inside it are part of it: "PBM, CSA and/or DIP-chip" names one method.

``""``

Raises:

Type Description
ValueError

If motif_id is empty, offset is negative, the count matrix is not a finite, non-negative 4 x L table with every column summing above zero, or one of the four plural annotations was given a bare string.

Notes

An empty string or an empty tuple in one of the six annotations means the source stated nothing there — a de novo matrix out of a model carries none of them, and everything on this class still works.

Four of the six are plural and two are not. tf_class, tf_family, uniprot_ids and pubmed_ids hold a tuple, because the source publishes several of each for a dimer and separates them with a semicolon; tax_group and data_type hold one string each. A bare string handed to one of the plural four is refused rather than stored letter by letter.

Examples:

>>> import numpy as np
>>> motif = Motif(
...     "MA0139.2",
...     "CTCF",
...     np.array([[9.0, 1.0], [1.0, 1.0], [0.0, 7.0], [0.0, 1.0]]),
...     tax_group="vertebrates",
...     uniprot_ids=("P49711",),
... )
>>> len(motif)
2
>>> motif.consensus
DNA('AG')
>>> motif.counts.flags.writeable
False

length property

length: int

Number of positions, L (alias of len(self)).

Examples:

>>> import numpy as np
>>> Motif("MA9999.1", "x", np.ones((4, 11))).length
11

probabilities property

probabilities: NDArray[float64]

The counts with each column normalised to sum to 1, as a fresh 4 x L array.

Derived on every call and never stored — the counts are the single source of truth, and a stored copy is a second one waiting to disagree.

Returns:

Type Description
ndarray

Shape (4, L), float64, each column summing to 1.

Examples:

>>> import numpy as np
>>> motif = Motif("MA9999.1", "x", np.array([[3.0], [1.0], [0.0], [0.0]]))
>>> motif.probabilities.ravel()
array([0.75, 0.25, 0.  , 0.  ])

information_content property

information_content: NDArray[float64]

How much each position says, in bits, in [0, 2].

Measured against a uniform reference, which is what puts the ceiling at 2: a position fixed on one base says 2 bits, one that says nothing says 0. This is the y-axis :meth:plot draws and the quantity :meth:trim thresholds on, so the height you see is the number you set. Clipped to [0, 2], which only ever moves floating-point noise.

Returns:

Type Description
ndarray

Shape (L,), float64, one value per position.

Examples:

>>> import numpy as np
>>> fixed_then_flat = np.array([[8.0, 2.0], [0.0, 2.0], [0.0, 2.0], [0.0, 2.0]])
>>> Motif("MA9999.1", "x", fixed_then_flat).information_content
array([2., 0.])

consensus property

consensus: DNA

The most common base at each position, as a typed :class:~genome.seq.DNA.

A one-letter-per-position rendering of the counts, and nothing more: it is not the motif, and a Motif hit matched the matrix rather than this string. Positions saying nothing still get a letter, so read it beside :attr:information_content. Ties go to the first base in :data:BASES order, and no IUPAC ambiguity code is ever produced — this package's alphabet is ACGT.

Returns:

Type Description
DNA

One upper-case base per position, L long.

Examples:

>>> import numpy as np
>>> counts = np.array([[9.0, 1.0], [1.0, 1.0], [0.0, 7.0], [0.0, 1.0]])
>>> Motif("MA9999.1", "x", counts).consensus
DNA('AG')
>>> Motif("MA9999.1", "x", np.ones((4, 3))).consensus     # every column tied
DNA('AAA')

__post_init__

__post_init__() -> None

Validate the identity and the matrix, then freeze an owned copy of the counts.

__len__

__len__() -> int

Return L, the number of positions.

__eq__

__eq__(other: object) -> bool

Compare every field, the count matrix element by element.

__hash__

__hash__() -> int

Hash the identity — id, name, offset and shape — never the counts.

__repr__

__repr__() -> str

Return the identity and the shape, never the matrix itself.

log_odds

log_odds(
    background: Sequence[float]
    | NDArray[float64]
    | None = None,
    pseudocount: float = 0.01,
) -> npt.NDArray[np.float64]

Score each base at each position against a background, in bits.

The background and the pseudocount are arguments and never fields, so the same motif can be scored against two backgrounds without becoming two motifs. Each entry is log2(p / q), where p is the pseudocounted column probability (count + pseudocount * q) / (column_sum + pseudocount) and q is the background frequency for that base — the same arithmetic the scan engine does, converted from natural log to bits.

Parameters:

Name Type Description Default
background sequence of float

Four frequencies over :data:BASES, each above zero and summing to 1. Uniform when omitted — a motif holds no input to derive one from, which is what a scan does instead.

None
pseudocount float

Added to each column, split by the background, so an unobserved base scores very low rather than -inf. Must be above zero.

0.01

Returns:

Type Description
ndarray

Shape (4, L), float64, in bits.

Raises:

Type Description
ValueError

If the background is not four positive frequencies summing to 1, or the pseudocount is not above zero.

Examples:

>>> import numpy as np
>>> motif = Motif("MA9999.1", "x", np.array([[100.0], [0.0], [0.0], [0.0]]))
>>> motif.log_odds().round(2)[:, 0]
array([  2.  , -13.29, -13.29, -13.29])
>>> motif.log_odds([0.4, 0.1, 0.1, 0.4]).round(2)[0, 0]
np.float64(1.32)

trim

trim(
    threshold: float = _DEFAULT_TRIM_THRESHOLD,
    *,
    max_length: int | None = None,
    min_length: int = MIN_MOTIF_LENGTH,
) -> Motif

Drop uninformative flanks, keeping the id, the name and a mapping back.

Only the ends move. A position under threshold bits is dropped from the left while every position so far has been under it, and likewise from the right — so an uninformative spacer in the middle of a dimeric motif is kept and the motif can never be split in two.

The result carries the same Motif id and Motif name and an offset such that position_in_trimmed_frame + offset is the position in the full motif's frame. Trimming a trimmed motif composes: the offsets add up, so a Motif hit found with either is readable in the full frame.

Two bounds hold whatever the threshold says. It never returns a motif shorter than min_length — if the flank walk went too far, the window grows back one position at a time, always taking the more informative of the two neighbours, and the front one when they say the same, so the offset stays as small as it can. And a motif already shorter than min_length is returned untouched, since trimming cannot repair it and making it shorter would only make it worse.

Parameters:

Name Type Description Default
threshold float

Bits below which a flanking position is uninformative.

0.25
max_length int

The most positions to keep. When the surviving window is longer, the less informative end is dropped one position at a time until it fits — the back one when the two ends say the same. Must be at least min_length.

None
min_length int

The fewest positions to keep, defaulting to :data:MIN_MOTIF_LENGTH — the shortest motif this package can scan with.

7

Returns:

Type Description
Motif

The trimmed motif, or self when nothing was dropped.

Raises:

Type Description
ValueError

If min_length is below 1, or max_length is below min_length.

Examples:

>>> import numpy as np
>>> flat = np.full((4, 3), 5.0)                       # 0 bits per position
>>> fixed = np.repeat(np.array([[9.0], [1.0], [0.0], [0.0]]), 8, axis=1)
>>> motif = Motif("MA9999.1", "x", np.hstack([flat, fixed, flat]))
>>> len(motif)
14
>>> trimmed = motif.trim()
>>> trimmed
Motif(motif_id='MA9999.1', motif_name='x', length=8, offset=3)
>>> trimmed.trim().offset                             # trimming composes
3

plot

plot(ax: Axes | None = None, **kwargs: Any) -> Axes

Draw the sequence logo, in bits, and return the axes it was drawn on.

A new figure when ax is None, the caller's axes when one is given — which is how a grid of motifs is built in one figure. The y-axis is :attr:information_content, so the height drawn is the same quantity :meth:trim thresholds on; the transform from counts to those heights is logomaker's own, rather than a second derivation of it here. The x-axis is this motif's own frame, 0 .. L - 1; add :attr:offset to read a trimmed motif's positions in the full frame.

Parameters:

Name Type Description Default
ax Axes

Axes to draw into. A new figure and axes are made when omitted.

None
**kwargs Any

Passed through to logomaker.Logocolor_scheme, font_name, shade_below and the rest of what it takes.

{}

Returns:

Type Description
Axes

The axes drawn on, whether it was made here or handed in.

Examples:

>>> import matplotlib
>>> matplotlib.use("Agg")                              # no window, no display
>>> import numpy as np
>>> motif = Motif("MA9999.1", "x", np.array([[9.0, 1.0], [1.0, 1.0],
...                                          [0.0, 7.0], [0.0, 1.0]]))
>>> axes = motif.plot()
>>> axes.get_ylabel()
'Information content (bits)'
>>> low, high = axes.get_xlim()
>>> float(high - low)                                  # one unit per position
2.0

MotifNotFoundError

MotifNotFoundError(key: str, size: int)

Bases: LookupError

Nothing in this Motif set is addressed by that key.

The plain absence: the key is not a Motif id, not a bare base id, and not a Motif name any motif in the set carries. Not an empty answer and never an empty collection — a set that does not hold CTCF says so rather than handing back nothing.

A :class:LookupError, so it can be caught together with :class:AmbiguousMotifNameError and :class:AmbiguousBaseIdError and still be told apart from them: those two mean too many, and this one means none.

Parameters:

Name Type Description Default
key str

The key that was asked for.

required
size int

How many motifs the set holds, so an empty set explains itself.

required

Attributes:

Name Type Description
key str

The key that was asked for.

Examples:

>>> import numpy as np
>>> empty = MotifSet([])
>>> try:
...     empty["CTCF"]
... except LookupError as error:
...     print("holds no motifs" in str(error))
True

MotifSet

MotifSet(motifs: Iterable[Motif])

Motifs held as one addressable group: indexed, filtered, and nothing else read.

The container the whole motif API hangs off, and it is built from any motifs — a Release parsed off disk, a filtered part of one, or the de novo matrices a model found, which is what gives those everything a release can do rather than a smaller API. It reads no file and reaches no network; preparing a release does, and that is :class:~genome.tf.motif.jaspar.JasparDatabase.

Indexing always returns exactly one motif, never a union type. set[key] resolves a Motif id, then a bare base id, then a Motif name — in that order, and the first that matches wins. A name labelling several motifs raises rather than picking one of them; :meth:by_name is how all of them are had, and it hands back a tuple whether the name labels four motifs or one.

Iterating yields :class:Motif objects rather than keys — the one place this reads unlike a dict, and it reads like the collection of motifs it is. Order is the order the motifs were given, everywhere: the tuple from :meth:by_name, the ids in an ambiguity error, and what a filtered set holds.

Parameters:

Name Type Description Default
motifs iterable of Motif

The motifs to hold, in the order they should be held. May be empty — a filter that matched nothing is a real answer.

required

Raises:

Type Description
ValueError

If two motifs share a Motif id. An id is what a motif is addressed by, so a set holding two of one id could not answer for either.

Examples:

>>> import numpy as np
>>> counts = np.array([[9.0, 1.0], [1.0, 1.0], [0.0, 7.0], [0.0, 1.0]])
>>> ctcf = Motif("MA0139.2", "CTCF", counts, tf_class=("C2H2 zinc finger factors",))
>>> prrx2 = Motif("MA0075.3", "PRRX2", counts, tf_class=("Homeo domain factors",))
>>> motifs = MotifSet([ctcf, prrx2])
>>> len(motifs)
2
>>> motifs["MA0139.2"] is motifs["MA0139"] is motifs["CTCF"]
True
>>> motifs.by_name("PRRX2")
(Motif(motif_id='MA0075.3', motif_name='PRRX2', length=2, offset=0),)
>>> motifs.filter(tf_class="zinc finger").motif_ids
('MA0139.2',)

motifs property

motifs: tuple[Motif, ...]

Every Motif held, in the order it was given.

Examples:

>>> import numpy as np
>>> MotifSet([Motif("MA9999.1", "x", np.ones((4, 7)))]).motifs
(Motif(motif_id='MA9999.1', motif_name='x', length=7, offset=0),)

motif_ids property

motif_ids: tuple[str, ...]

Every Motif id, in the order the motifs were given. Unique, always.

Examples:

>>> import numpy as np
>>> MotifSet([Motif("MA9999.1", "x", np.ones((4, 7)))]).motif_ids
('MA9999.1',)

motif_names property

motif_names: tuple[str, ...]

Every Motif name, one per motif and parallel to :attr:motif_ids.

Duplicates are kept and never collapsed: a name labelling two motifs appears twice, because these are labels and there are as many of them as motifs.

Examples:

>>> import numpy as np
>>> counts = np.ones((4, 7))
>>> pair = [Motif("MA0139.2", "CTCF", counts), Motif("MA1929.2", "CTCF", counts)]
>>> MotifSet(pair).motif_names
('CTCF', 'CTCF')

__len__

__len__() -> int

Return how many motifs are held.

__iter__

__iter__() -> Iterator[Motif]

Iterate the Motifs themselves, in order — not their keys.

__contains__

__contains__(key: object) -> bool

Answer whether a key matches anything here, or whether a Motif is one of ours.

A key that matches several motifs is still in the set, though indexing by it raises: membership asks whether the set knows the key, and indexing asks it to name one motif.

Examples:

>>> import numpy as np
>>> motifs = MotifSet([Motif("MA0139.2", "CTCF", np.ones((4, 7)))])
>>> "MA0139" in motifs, "CTCF" in motifs, "SOX2" in motifs
(True, True, False)

__repr__

__repr__() -> str

Return how many motifs are held, never the motifs themselves.

__getitem__

__getitem__(key: str) -> Motif

Return the one Motif key addresses — id, base id, or unique name.

Parameters:

Name Type Description Default
key str

A Motif id ("MA0139.2"), a bare base id ("MA0139"), or a Motif name labelling exactly one motif here ("CTCF"). Tried in that order.

required

Returns:

Type Description
Motif

Exactly one motif, always — never a tuple and never a union type.

Raises:

Type Description
MotifNotFoundError

If nothing here is addressed by key.

AmbiguousMotifNameError

If key is a name labelling several motifs. It names every matching id.

AmbiguousBaseIdError

If key is a base id shared by several motifs, which a non-redundant Release cannot produce.

Examples:

>>> import numpy as np
>>> motifs = MotifSet([Motif("MA0139.2", "CTCF", np.ones((4, 7)))])
>>> motifs["MA0139"].motif_name
'CTCF'

by_name

by_name(name: str) -> tuple[Motif, ...]

Return every Motif labelled name, always as a tuple.

A tuple of one where the name is unique, so a caller writes one code path for the common case and the four-CTCFs case alike. Absence is not emptiness: a name nothing here carries raises rather than handing back ().

Parameters:

Name Type Description Default
name str

The Motif name to look up, matched exactly.

required

Returns:

Type Description
tuple of Motif

Every motif with that name, in the set's own order. Never empty.

Raises:

Type Description
MotifNotFoundError

If no motif here carries that name.

Examples:

>>> import numpy as np
>>> counts = np.ones((4, 7))
>>> pair = [Motif("MA0139.2", "CTCF", counts), Motif("MA1929.2", "CTCF", counts)]
>>> [motif.motif_id for motif in MotifSet(pair).by_name("CTCF")]
['MA0139.2', 'MA1929.2']

filter

filter(
    predicate: Callable[[Motif], bool] | None = None,
    **annotations: str | Iterable[str],
) -> MotifSet

Return a plain :class:MotifSet of the motifs that match.

A plain motif set, never a database, even when called on one: a filtered Release is no longer that release, and calling it one would let a Hit table claim provenance it does not have. Everything a set does, the result still does.

Every condition given must hold — the predicate and each annotation keyword. With nothing given, every motif matches.

Parameters:

Name Type Description Default
predicate callable

Called with each :class:Motif; kept where it returns true. For anything the keywords do not express — a length, a consensus, an information content.

None
**annotations str | Iterable[str]

One or more of tax_group, tf_class, tf_family, data_type, uniprot_ids and pubmed_ids. Each value is one string or an iterable of them, and a motif matches when any of them matches any value the motif holds for that annotation. The four prose annotations match on a case-insensitive substring, so tf_class="zinc finger" finds every spelling of one; the two id annotations match exactly, since a substring of an accession is a different accession rather than a looser one.

{}

Returns:

Type Description
MotifSet

The matching motifs, in this set's own order. Empty when nothing matched, which is a real answer and not an absence.

Raises:

Type Description
TypeError

If a keyword is not one of the six annotations. The message lists them.

Examples:

>>> import numpy as np
>>> counts = np.ones((4, 9))
>>> zinc = Motif("MA0139.2", "CTCF", counts, tf_class=("C2H2 zinc finger factors",))
>>> homeo = Motif("MA0075.3", "PRRX2", counts, tf_class=("Homeo domain factors",))
>>> motifs = MotifSet([zinc, homeo])
>>> motifs.filter(tf_class="zinc finger").motif_ids
('MA0139.2',)
>>> motifs.filter(lambda motif: len(motif) == 9).motif_ids
('MA0139.2', 'MA0075.3')
>>> motifs.filter(tf_class=("zinc finger", "homeo")).motif_ids
('MA0139.2', 'MA0075.3')

scan

scan(
    sequence: str,
    name: str = ...,
    *,
    threshold: float = ...,
    background: BackgroundArg = ...,
    output: None = ...,
    workers: int | None = ...,
) -> pd.DataFrame
scan(
    sequence: str,
    name: str = ...,
    *,
    threshold: float = ...,
    background: BackgroundArg = ...,
    output: str | Path,
    workers: int | None = ...,
) -> Path
scan(
    sequence: str,
    name: str = DEFAULT_SEQUENCE_NAME,
    *,
    threshold: float = DEFAULT_THRESHOLD,
    background: BackgroundArg = None,
    output: str | Path | None = None,
    workers: int | None = DEFAULT_WORKERS,
) -> pd.DataFrame | Path

Scan one sequence with every motif here and return the Hit table.

The quick case — one locus, checked once. It answers with exactly the table :meth:scan_sequences and :meth:scan_fasta answer with, down to the column order and the dtypes, so nothing downstream branches on how the scan was called.

Both strands are scanned. Coordinates are 0-based half-open and always in the forward frame, whichever strand matched, and Strand is + or - — never ., because a scan knows which of the two it scored. The interval covers the bases the matrix scored, so a trimmed motif's hit is as long as the trimmed motif; :attr:Motif.offset maps a position within the motif and not the interval.

The sequence is upper-cased, so a soft-masked one yields exactly the hits its upper-case equivalent does and there is no argument that would change that.

Parameters:

Name Type Description Default
sequence str

The bases to scan. A :class:~genome.seq.DNA or a plain string.

required
name str

What the sequence_name column carries. A fixed literal by default, so every row names its sequence even when the caller did not.

``"sequence"``
threshold float

The Threshold: one per-position p-value, converted per motif against background into the score that motif must clear. In (0, 1).

1e-4
background sequence of float or {"auto", "uniform", "derive"}

The Background: four frequencies over :data:BASES, above zero and summing to 1, or one of the three modes. Automatic when omitted — derived from sequence when it holds at least :data:~genome.tf.motif.background.BACKGROUND_FLOOR unambiguous bases, uniform below that. "uniform" pins it; "derive" derives whatever the input holds, floor or no floor. Whichever it is, it is recorded on the result.

None
output str or Path

Where to stream the hits as Parquet instead of building a table. A scan too large to hold goes to disk and hands back the path; there is no row-count guard, because a genome-scale scan is the caller's decision. Read it back with :func:~genome.tf.motif.parquet.read_hits, which restores the dtypes and the provenance both — :func:pandas.read_parquet alone drops the provenance.

None
workers int

How many processes to shard the scan across. One by default, so importing this package and calling a scan never starts a process unasked: under the spawn start method a pool re-imports the caller's script, and an unguarded one would re-execute itself. None resolves the count with :func:~genome.workers.resolve_workers — the Slurm allocation first, then process affinity, then the machine — which is what the command line passes. More than one produces the identical table, row for row; the choice is about wall time and nothing else.

1

Returns:

Type Description
DataFrame or Path

One row per Motif hit: motif_id, motif_name, sequence_name, start, end, strand, score — the score in bits, not a p-value. The scan's provenance is on frame.attrs: the background, the threshold, the Release and Tax group where the set knows them, and which motifs were scanned and which were skipped. With output, the path written.

Raises:

Type Description
ValueError

If threshold is not in (0, 1), or background is neither one of the three modes nor four positive frequencies summing to 1.

Notes

Motifs shorter than :data:MIN_MOTIF_LENGTH are not scanned and are named in frame.attrs["motifs_skipped"]. A 6-mer cannot reach the default threshold at all, and an engine asked for it anyway would fall back to that matrix's best attainable cutoff and over-call in silence.

A Release answers with its release and tax group on the table; a set that :meth:filter returned answers with None for both, since a filtered release is no longer that release and must not claim to be.

Examples:

>>> import numpy as np
>>> counts = np.zeros((4, 8))
>>> for column, base in enumerate("GATTACAG"):
...     counts["ACGT".index(base), column] = 100.0
>>> motifs = MotifSet([Motif("MA9999.1", "Gattacag", counts)])
>>> hits = motifs.scan("TTTTTGATTACAGTTTTT")
>>> hits[["motif_id", "sequence_name", "start", "end", "strand"]]
   motif_id sequence_name  start  end strand
0  MA9999.1      sequence      5   13      +
>>> hits.attrs["motifs_scanned"], hits.attrs["threshold"]
(('MA9999.1',), 0.0001)

scan_sequences

scan_sequences(
    sequences: Mapping[str, str],
    *,
    threshold: float = ...,
    background: BackgroundArg = ...,
    output: None = ...,
    workers: int | None = ...,
) -> pd.DataFrame
scan_sequences(
    sequences: Mapping[str, str],
    *,
    threshold: float = ...,
    background: BackgroundArg = ...,
    output: str | Path,
    workers: int | None = ...,
) -> Path
scan_sequences(
    sequences: Mapping[str, str],
    *,
    threshold: float = DEFAULT_THRESHOLD,
    background: BackgroundArg = None,
    output: str | Path | None = None,
    workers: int | None = DEFAULT_WORKERS,
) -> pd.DataFrame | Path

Scan named sequences — a peak set in one call — and return the Hit table.

The same table :meth:scan returns, with sequence_name carrying the mapping's own keys and the rows in the mapping's own order. See :meth:scan for the coordinate convention, the strand rule, the upper-casing and the provenance.

Parameters:

Name Type Description Default
sequences mapping of str to str

Name to bases. Scanned one at a time, so the peak cost is the longest sequence rather than all of them.

required
threshold float

The Threshold, as a per-position p-value.

1e-4
background sequence of float or {"auto", "uniform", "derive"}

The Background. Automatic when omitted — see :meth:scan. Deciding reads a bounded prefix of sequences, which is then scanned like the rest.

None
output str or Path

Where to stream the hits as Parquet instead of building a table — see :meth:scan.

None
workers int

How many processes to shard the scan across — see :meth:scan. Sequences are distributed whole, so a peak set parallelises without any of them being cut.

1

Returns:

Type Description
DataFrame or Path

The Hit table, empty of rows but not of schema when nothing matched. With output, the path written.

Raises:

Type Description
ValueError

If threshold is not in (0, 1), or the background is neither a mode nor four positive frequencies summing to 1.

Examples:

>>> import numpy as np
>>> counts = np.zeros((4, 8))
>>> for column, base in enumerate("GATTACAG"):
...     counts["ACGT".index(base), column] = 100.0
>>> motifs = MotifSet([Motif("MA9999.1", "Gattacag", counts)])
>>> peaks = {"peak1": "TTTTT" + "GATTACAG" + "TTTTT",
...          "peak2": "CCCCC" + "CTGTAATC" + "CCCCC"}   # the same site, flipped
>>> hits = motifs.scan_sequences(peaks)
>>> list(zip(hits["sequence_name"], hits["strand"], hits["start"], hits["end"]))
[('peak1', '+', 5, 13), ('peak2', '-', 5, 13)]

scan_fasta

scan_fasta(
    path: str | Path,
    *,
    threshold: float = ...,
    background: BackgroundArg = ...,
    output: None = ...,
    workers: int | None = ...,
) -> pd.DataFrame
scan_fasta(
    path: str | Path,
    *,
    threshold: float = ...,
    background: BackgroundArg = ...,
    output: str | Path,
    workers: int | None = ...,
) -> Path
scan_fasta(
    path: str | Path,
    *,
    threshold: float = DEFAULT_THRESHOLD,
    background: BackgroundArg = None,
    output: str | Path | None = None,
    workers: int | None = DEFAULT_WORKERS,
) -> pd.DataFrame | Path

Scan every record of a FASTA and return the Hit table.

The same table :meth:scan returns. Records are read and scanned one at a time, so the file is never held whole; plain or gzipped.

A record's name is its header up to the first whitespace, which is what STAR and chromap write into an alignment produced from the same file — so this table joins against that alignment with nobody renaming anything.

Parameters:

Name Type Description Default
path str or Path

The FASTA to scan, .fa or .fa.gz.

required
threshold float

The Threshold, as a per-position p-value.

1e-4
background sequence of float or {"auto", "uniform", "derive"}

The Background. Automatic when omitted — see :meth:scan. Deciding reads records off the front of the file, which are then scanned like the rest, so the file is still read exactly once.

None
output str or Path

Where to stream the hits as Parquet instead of building a table — see :meth:scan. The whole-genome case this exists for: a FASTA in, a Parquet out, and nothing the size of either in memory.

None
workers int

How many processes to shard the scan across — see :meth:scan. A record long enough is cut into pieces with an overlap, so one chromosome still uses the whole allocation.

1

Returns:

Type Description
DataFrame or Path

The Hit table, with sequence_name carrying the truncated record names. With output, the path written.

Raises:

Type Description
FileNotFoundError

If path does not exist.

ValueError

If the file is not FASTA, a record carries no name, threshold is not in (0, 1), or the background is neither a mode nor four positive frequencies summing to 1.

Examples:

>>> import tempfile
>>> from pathlib import Path
>>> import numpy as np
>>> counts = np.zeros((4, 8))
>>> for column, base in enumerate("GATTACAG"):
...     counts["ACGT".index(base), column] = 100.0
>>> motifs = MotifSet([Motif("MA9999.1", "Gattacag", counts)])
>>> with tempfile.TemporaryDirectory() as directory:
...     fasta = Path(directory) / "peaks.fa"
...     _ = fasta.write_text(">peak1 chrI:100-118 of nowhere\nTTTTTGATTACAGTTTTT\n")
...     hits = motifs.scan_fasta(fasta)
>>> list(zip(hits["sequence_name"], hits["start"], hits["strand"]))
[('peak1', 5, '+')]

compare

compare(
    queries: Motif | Iterable[Motif],
    *,
    top: int | None = None,
) -> MotifComparison

Ask what one or more motifs look like, against the motifs held here.

The motifs held here are the targets, and the argument is the queries — read it as compare these against this release. The use case is naming: a chromBPNet or TF-MoDISco run hands back matrices with no names on them, and release.compare(de_novo) says which published motif each one most resembles.

The comparison is tomtom's, from memelite, handed the same 4 x L probability matrices :attr:Motif.probabilities produces. What comes back is a labelled array indexed by Motif id on both axes; see :class:MotifComparison for its two shapes and :meth:MotifComparison.to_frame for the flat table.

Parameters:

Name Type Description Default
queries Motif or iterable of Motif

One motif, several, or a whole :class:MotifSet. Two queries sharing a Motif id are refused: the array's query axis is labelled with them, so a repeated label could answer for neither.

required
top int

Keep only this many targets per query, best first. It is not a convenience over the complete answer — it sends the work down tomtom's faster nearest-neighbour path, which never scores the targets that lose. The result is then ragged: its target axis is per query, and it cannot be widened without recomputing, which is accepted rather than a defect. Omit it for the complete query x target array.

None

Returns:

Type Description
MotifComparison

The labelled array and the methods that read it.

Raises:

Type Description
ValueError

If queries is empty, two queries share a Motif id, this set holds no motifs, or top is below 1 or above the number of motifs held here.

Notes

A motif compared against itself aligns to itself perfectly — offset 0, the whole length overlapping, on the + strand, and no target scores higher. It is usually ranked first too, and the exception is worth knowing: TOMTOM's p-value rewards a short dense alignment, so a long motif that embeds a shorter one can rank the shorter one above itself. Both of the fixture's 31- and 33-column CTCF matrices do exactly that with the 15-column MA0139.2 they contain. That is a property of the statistic, not of this wrapper, and it is what a caller naming a de novo pattern should expect to see from a family of nested matrices.

Examples:

>>> import numpy as np
>>> def spelled(motif_id, bases):
...     columns = [[19.0 if b == l else 1.0 for l in bases] for b in BASES]
...     return Motif(motif_id, "", np.array(columns))
>>> published = MotifSet([spelled("MA0001.1", "ACGTACGTA"),
...                       spelled("MA0002.1", "TTTTTTTTT")])
>>> published.compare(spelled("pattern_0", "ACGTACGTA")).to_frame()["target"]
0    MA0001.1
Name: target, dtype: object
>>> published.compare(published, top=1).is_ragged
True

FastaFormatError

Bases: ValueError

A file handed to a scan is not FASTA, or holds a record with no name.

A bad file, not a bad call. A Hit table is keyed by sequence name, so a record that has none could not be joined to anything and is refused rather than given one.

Examples:

>>> from pathlib import Path
>>> import tempfile
>>> with tempfile.TemporaryDirectory() as directory:
...     path = Path(directory) / "bad.fa"
...     _ = path.write_text("ACGTACGT\n")
...     try:
...         list(read_fasta(path))
...     except FastaFormatError as error:
...         print("'>'" in str(error))
True

parse_transfac

parse_transfac(text: str) -> tuple[Motif, ...]

Read transfac text into Motifs, in the order the file spells them.

A pure function from text to motifs: it opens nothing, downloads nothing, and holds no opinion about which Release the text came from. Records are separated by //, the Count matrix is the tab-separated block under the PO header, and the six annotations are the CC key:value lines.

Annotation values are separated by a semicolon and never by a comma. Commas occur inside single values — "C3H(C),C2HC zinc-fingers like factors" is one class and "PBM, CSA and/or DIP-chip" is one data type — so splitting on one would corrupt roughly fifty records per release silently. An empty value means the source stated nothing and becomes an empty tuple or an empty string, which is common and not an error.

Parameters:

Name Type Description Default
text str

The whole transfac file. These are under 1 MB, so they are read whole.

required

Returns:

Type Description
tuple of Motif

One motif per record, in file order. Empty for empty text.

Raises:

Type Description
TransfacError

If a record has no accession, no count matrix, a PO header that is not A C G T, or a count row that is not four numbers.

Examples:

>>> record = '''AC MA0260.1
... XX
... ID che-1
... XX
... PO\tA\tC\tG\tT
... 01\t0.0\t0.0\t37.0\t0.0
... 02\t37.0\t0.0\t0.0\t0.0
... XX
... CC tax_group:nematodes
... CC tf_class:C2H2 zinc finger factors; Homeo domain factors
... XX
... //
... '''
>>> (motif,) = parse_transfac(record)
>>> motif
Motif(motif_id='MA0260.1', motif_name='che-1', length=2, offset=0)
>>> motif.tf_class
('C2H2 zinc finger factors', 'Homeo domain factors')
>>> motif.consensus
DNA('GA')

hit_count

hit_count(path: str | Path) -> int

Return how many Motif hits a written Hit table holds, reading none of them.

Off the file's footer, where Parquet records the row count of every row group, so this costs the same on a genome-scale scan as on an empty one. It is what a summary of a finished scan reports: reading 550 million rows back to count them is the one thing a scan that streamed to disk exists to avoid.

Parameters:

Name Type Description Default
path str or Path

A Parquet file written by :func:write_hits.

required

Returns:

Type Description
int

How many rows the file holds. Zero for a scan that found nothing, which still wrote a file.

Raises:

Type Description
FileNotFoundError

If path does not exist.

Examples:

>>> import tempfile
>>> from pathlib import Path
>>> import numpy as np
>>> from genome.tf.motif import Motif, MotifSet
>>> counts = np.zeros((4, 8))
>>> for column, base in enumerate("GATTACAG"):
...     counts["ACGT".index(base), column] = 100.0
>>> motifs = MotifSet([Motif("MA9999.1", "Gattacag", counts)])
>>> with tempfile.TemporaryDirectory() as directory:
...     written = motifs.scan("TTTTTGATTACAGTTTTT", output=Path(directory) / "h.parquet")
...     hit_count(written)
1

provenance_of

provenance_of(path: str | Path) -> dict[Hashable, Any]

Read what a written Hit table was scanned with, without reading a row of it.

The provenance alone — the Background, the Threshold, the Release, the Tax group and the two motif lists, exactly what :func:read_hits puts on frame.attrs. It comes out of the file's own key-value metadata, so a 550-million-row scan answers as fast as an empty one; :func:read_hits is for when the rows are wanted too, and a genome-scale scan is precisely when reading them back is fatal.

Parameters:

Name Type Description Default
path str or Path

A Parquet file written by :func:write_hits.

required

Returns:

Type Description
dict

What :data:~genome.tf.motif.scan.HIT_PROVENANCE names, with every JSON array read back as a tuple — which is what a background and the two motif lists are in memory. Empty for a Parquet file written by something else, which simply carries none.

Raises:

Type Description
FileNotFoundError

If path does not exist.

Examples:

>>> import tempfile
>>> from pathlib import Path
>>> from genome.tf.motif.scan import empty_hits
>>> with tempfile.TemporaryDirectory() as directory:
...     written = write_hits([empty_hits()], Path(directory) / "none.parquet",
...                          {"threshold": 0.0001, "motifs_skipped": ("MA0261.1",)})
...     provenance_of(written)
{'threshold': 0.0001, 'motifs_skipped': ('MA0261.1',)}

read_hits

read_hits(path: str | Path) -> pd.DataFrame

Read a Hit table back from Parquet, provenance and dtypes included.

The counterpart of :func:write_hits, and the only reader that restores frame.attrs: :func:pandas.read_parquet alone gives the rows and drops what the scan was, which is the same table with its meaning removed.

Parameters:

Name Type Description Default
path str or Path

A Parquet file written by :func:write_hits.

required

Returns:

Type Description
DataFrame

The Hit table, equal to the in-memory form of the same scan — column order, dtypes and category order included — with :data:~genome.tf.motif.scan.HIT_PROVENANCE on frame.attrs. A file written by something other than :func:write_hits simply carries no provenance, and attrs is then empty.

Raises:

Type Description
FileNotFoundError

If path does not exist.

Examples:

>>> import tempfile
>>> from pathlib import Path
>>> import numpy as np
>>> from genome.tf.motif import Motif, MotifSet
>>> counts = np.zeros((4, 8))
>>> for column, base in enumerate("GATTACAG"):
...     counts["ACGT".index(base), column] = 100.0
>>> motifs = MotifSet([Motif("MA9999.1", "Gattacag", counts)])
>>> with tempfile.TemporaryDirectory() as directory:
...     written = motifs.scan("TTTTTGATTACAGTTTTT", output=Path(directory) / "h.parquet")
...     hits = read_hits(written)
>>> {name: str(dtype) for name, dtype in hits.dtypes.items()}["score"]
'float16'
>>> hits.attrs["threshold"]
0.0001

threshold_cache_dir

threshold_cache_dir() -> Path

Return the directory the per-motif cutoffs are cached in.

<liulab_data>/motif/thresholds/. Nothing is created by asking — the write creates what it needs. Delete the directory to force every scan on this machine to recompute.

Returns:

Type Description
Path

<liulab_data>/motif/thresholds.

Examples:

>>> import os
>>> os.environ["LIULAB_DATA"] = "/scratch/liulab"
>>> threshold_cache_dir()
PosixPath('/scratch/liulab/motif/thresholds')
>>> del os.environ["LIULAB_DATA"]

resolve_workers

resolve_workers(workers: int | None = None) -> int

Return how many processes to run with.

A number is taken as given — a caller who says 2 gets 2, on a laptop or on a login node. None means work it out, and working it out is :data:SLURM_CPU_VARS, then process affinity, then the machine's cores. Never the machine's cores alone: that is the answer that ignores a cluster allocation.

Nothing here starts a process, and nothing here is cached — an allocation is a property of the process, and reading it at the call site is what keeps it current.

Parameters:

Name Type Description Default
workers int

The count to use, or None to resolve it from the environment.

None

Returns:

Type Description
int

At least 1.

Raises:

Type Description
ValueError

If workers is below 1. Zero workers is not a serial scan, it is no scan.

Examples:

>>> resolve_workers(2)
2
>>> resolve_workers(None) >= 1
True

genome.xref

Which foreign identifiers name a gene, and which genes a foreign identifier names.

The Xref context, and a peer of :mod:genome.tf rather than a part of it. An Xref set is one species, one Xref source and one pinned Release: constructing it fetches the publisher's file once into the Data dir, slices it to that species as a plain gzipped TSV, and re-reads it on every construction after. It holds no coordinates, opens no Genome and belongs to no Assembly — an identifier is a name and not a place.

Two directions and only two: :meth:~genome.xref.xref.XrefSet.to_stems toward the hub and :meth:~genome.xref.xref.XrefSet.from_stems away from it — with :meth:~genome.xref.xref.XrefSet.match_symbols the first of them asked about a gene symbol, which is answered unlike an id and so has a verb of its own. Nothing here converts one foreign Namespace directly into another, and nothing merges two publishers — a query reads exactly one set, so two sources that disagree are two answers rather than a contradiction to resolve.

Four sources ship, and they are not equals. :data:~genome.xref.alliance.ALLIANCE is the Default xref source for all three species when the question is identifiers; :data:~genome.xref.ensembl.ENSEMBL_TSV is selectable and pins its own numbered Release, and its mapping fans out to 72 stems for one GeneID where the other's is near-one-to-one. Which one answers is a scientific choice — read :class:~genome.xref.xref.XrefSet's source parameter before making it.

The other two carry symbols, which the first two do not. :data:~genome.xref.hgnc.HGNC_ARCHIVE is human's, from a pinned quarterly archive file, and is the only source that publishes previous and alias spellings typed; :data:~genome.xref.bgi.ALLIANCE_BGI is mouse's and worm's, and carries the current approved symbol alone. So a symbol is matched with :meth:~genome.xref.xref.XrefSet.match_symbols, whose answer says on every hit which kind of spelling matched and, on the answer as a whole, which kinds that source could not match and why.

A default is therefore per species and per question, and the question is named where the source is filled in and nowhere else: :meth:~genome.xref.xref.XrefSet.for_symbols is the constructor that reaches one of those two, :meth:~genome.xref.xref.XrefSet.for_namespace the same fill-in for a caller holding a Namespace rather than a verb, and the plain constructor reaches the identifier default. They sit side by side on purpose — a set built for one publisher is never answered out of another's bytes, so XrefSet("Homo sapiens") matches no symbol at all and raises naming the source that does.

Putting an answer into your own annotation's gene ids is the call that already existed, :meth:~genome.annotation.registry.AnnotationRegistry.resolve_gene_ids, which is why the hub is the Gene id stem: what comes out of here goes straight in there.

Examples:

>>> from genome.xref import XrefSet, xref_table
>>> sorted({record.species for record in xref_table()})
['Caenorhabditis elegans', 'Homo sapiens', 'Mus musculus']
>>> human = XrefSet("Homo sapiens")
>>> human.to_stems(["HGNC:11998"], "hgnc").gene_id_stems
['ENSG00000141510']

AllianceFileError

Bases: ValueError

The Alliance file is not the file this reader reads.

A bad file, not a bad call: a missing or re-spelled header, a row with the wrong number of fields, or a GeneID under a prefix no species authority claims. Each means the publisher changed the file's shape, and the message names the file and what was wrong with it so the fix is a reader change rather than a guess.

Examples:

>>> try:
...     read_alliance(["nothing like a header"], ncbi_taxid=9606, origin="x")
... except AllianceFileError as error:
...     print("GeneID" in str(error))
True

BgiFileError

Bases: ValueError

The Alliance gene submission is not the file this reader reads.

A bad file, not a bad call: no data array, JSON that does not parse, or a gene keyed by a prefix no species authority claims. The message names the file and what was wrong with it, so the fix is a reader change rather than a guess.

Examples:

>>> try:
...     read_bgi(["{}"], ncbi_taxid=10090, origin="x")
... except BgiFileError as error:
...     print("data" in str(error))
True

EnsemblTsvFileError

Bases: ValueError

The Ensembl TSV is not the file this reader reads.

A bad file, not a bad call: a missing or re-spelled header, or a row with the wrong number of fields. Each means the publisher changed the file's shape, so the message names the file and what was wrong with it and the fix is a reader change rather than a guess.

Examples:

>>> try:
...     read_ensembl(["nothing like a header"], ncbi_taxid=9606, origin="x")
... except EnsemblTsvFileError as error:
...     print("gene_stable_id" in str(error))
True

EmptyEvidenceFilterError

Bases: LookupError

The filter kept no rows, so the set would answer every query with nothing.

A :class:LookupError and not a :class:ValueError: the evidence types named are ones this release resolves nothing under, which is the same kind of miss as an unknown Namespace, and the message answers it the same way — by naming what is there.

Examples:

>>> from genome.xref import ENSEMBL_TSV, XrefSet
>>> XrefSet("Homo sapiens", ENSEMBL_TSV, "116", evidence="DIRECT")
Traceback (most recent call last):
EmptyEvidenceFilterError: ...

EvidenceNotRecordedError

Bases: LookupError

The Xref source's file grades nothing, so there is no evidence to filter on.

A :class:LookupError for the same reason :class:~genome.xref.xref.NamespaceNotCarriedError is one: the caller named something this set does not carry, and the message names a source that does. Raised rather than ignored, because a filter that quietly does nothing leaves the caller believing their answer was graded when it was not.

Examples:

>>> from genome.xref import ALLIANCE, XrefSet
>>> XrefSet("Homo sapiens", ALLIANCE, evidence="DIRECT")
Traceback (most recent call last):
EvidenceNotRecordedError: ...

HgncFileError

Bases: ValueError

The HGNC archive file is not the file this reader reads.

A bad file, not a bad call: no header line, or a header that names none of the columns this reader needs. Not raised for a column this reader ignores, and not raised for a re-ordered one — reading by name is what makes both harmless, and a schema that has already gone from 52 columns to 54 will move again.

Examples:

>>> try:
...     read_hgnc(["nothing\tlike\ta\theader"], ncbi_taxid=9606, origin="x")
... except HgncFileError as error:
...     print("prev_symbol" in str(error))
True

NoXrefSetError

Bases: LookupError

Nothing this package prepares answers for that species, source or release.

Raised by :func:lookup_xref and so by constructing an :class:~genome.xref.xref.XrefSet. A :class:LookupError, because it is a name that resolves to nothing rather than a malformed one, and it is the same shape as the census and cofactor misses: the message names what is available, so a caller who guessed a species reads the three that have a set instead of guessing again.

A species with no Ensembl presence is answered by this permanently rather than pending — the registered E. coli HT115 assembly has no hub to hang a Namespace off, so it has no Xref set and says so instead of being served a fudged one.

Examples:

>>> try:
...     lookup_xref("Danio rerio")
... except NoXrefSetError as error:
...     print("Homo sapiens" in str(error))
True

XrefMetadata dataclass

XrefMetadata(
    species: str,
    ncbi_taxid: int,
    source: str,
    release: str,
    publisher: str,
    version: str,
    pubmed_id: int | None,
    url: str,
    source_checksum: str,
    default: bool = False,
    symbol_default: bool = False,
)

One Xref set this package prepares (one row of the curated table).

The single declaration of what such a row consists of: the table is parsed through these fields, in this order, and every one is required except pubmed_id — a publisher with no paper is cited by name and URL, and a set nobody can cite is one this package will not fetch.

Attributes:

Name Type Description
species str

The species, as the assembly metadata table spells it — "Homo sapiens". Its slug names the set's directory, and either spelling is accepted on the way in.

ncbi_taxid int

NCBI taxonomy id, which is how the species' rows are picked out of a publisher's multi-species file. Read from the row and never inferred from the species name.

source str

The Xref source, lower-cased and stable — "alliance". It names both the reader and the directory the set is filed under.

release str

The pinned Release, and the string a caller names this set by.

publisher str

Who published it, and who is to be cited for it.

version str

The publisher's own release identifier, which is often the same string as release and is not required to be: Alliance's file states 9.0.0 inside its own header, where a quarterly archive states a date.

pubmed_id int or None

PubMed id of the paper to cite, or None for a publisher with none.

url str

Where the publisher's file is fetched from.

source_checksum str

The publisher's own checksum of that file, as "<algorithm>:<hexdigest>", taken over the unpacked bytes — which is what Alliance publishes, and what this package would have computed anyway. It is provenance rather than the integrity check: what is stored on disk is a per-species slice and not the publisher's bytes, so the slice carries a digest of its own.

default bool

Whether this is the species' Default xref source when the question is identifiers.

symbol_default bool

Whether this is it when the question is symbols. A separate flag because the two answers are usually different rows: the source carrying a species' ids often publishes none of its symbols.

Examples:

>>> record = lookup_xref("Caenorhabditis elegans")
>>> record.ncbi_taxid, record.default, record.symbol_default
(6239, True, False)
>>> record.url.endswith(".tsv.gz")
True
>>> lookup_xref("Caenorhabditis elegans", for_symbols=True).symbol_default
True

from_row classmethod

from_row(row: Mapping[str, object]) -> XrefMetadata

Build a record from one row of the curated table.

Parameters:

Name Type Description Default
row mapping of str to object

Column name to cell, as the shipped TSV spells one. Keys outside :data:XREF_FIELDS are ignored.

required

Returns:

Type Description
XrefMetadata

The record the row spells.

Raises:

Type Description
MetadataRowError

If a cell cannot be read as its column's type, or a column that has no unknown is blank. The message names the column.

Examples:

>>> XrefMetadata.from_row(
...     {
...         "species": "Tiny beast",
...         "ncbi_taxid": "1",
...         "source": "somewhere",
...         "release": "1.0",
...         "publisher": "Someone et al. 1999",
...         "version": "1.0",
...         "pubmed_id": "",
...         "url": "https://example.org/beast.tsv.gz",
...         "source_checksum": "md5:" + "0" * 32,
...         "default": "yes",
...         "symbol_default": "no",
...     }
... ).pubmed_id is None
True

attribution

attribution() -> str

Return the one line to print beside anything this set answered.

What a caller owes the publisher, rendered once here so a notebook, the CLI and an error message all say it the same way.

Returns:

Type Description
str

Publisher, release, PubMed id where there is one, and the source URL.

Examples:

>>> print(lookup_xref("Mus musculus").attribution())
Alliance of Genome Resources 9.0.0 (PMID 38552170) — https://download.alliancegenome.org/9.0.0/GENECROSSREFERENCE/COMBINED/GENECROSSREFERENCE_COMBINED_11.tsv.gz

SymbolDirectionError

Bases: ValueError

A symbol was asked for the way an id is, and the two directions are not the same.

Raised by :meth:~genome.xref.xref.XrefSet.to_stems when the Namespace named is the symbol one. A :class:ValueError rather than a :class:~genome.xref.xref.NamespaceNotCarriedError: the set does carry symbols, and what is wrong is the verb. Answering it anyway would match approved spellings only and silently drop every row that spells its gene the way the authority used to, which is the measured failure — so the message names :meth:~genome.xref.xref.XrefSet.match_symbols instead.

Examples:

>>> human = XrefSet("Homo sapiens", "hgnc")
>>> human.to_stems(["ARNTL"], "symbol")
Traceback (most recent call last):
SymbolDirectionError: ...

NamespaceNotCarriedError

Bases: LookupError

The set carries no such Namespace, and the message names the ones it does.

A :class:LookupError and not a :class:ValueError: the namespace is a name that this release resolves nothing under, which is the same kind of miss as an unknown species. The three species carry three different authorities, so a mouse set asked for hgnc lands here rather than answering nothing — the failure that would otherwise look like a gene list with no matches.

Examples:

>>> mouse = XrefSet("Mus musculus")
>>> mouse.to_stems(["HGNC:11998"], "hgnc")
Traceback (most recent call last):
NamespaceNotCarriedError: ...

ResolvedStems dataclass

ResolvedStems(
    species: str,
    source: str,
    release: str,
    namespace: str,
    resolved: Mapping[str, tuple[str, ...]],
    unresolved: tuple[str, ...],
)

The Gene id stems one Xref set says a foreign id names.

:meth:XrefSet.to_stems's answer — the hop toward the hub, defined beside the set that builds it. Every field before :attr:resolved says what produced it, because one publisher's assertions are not another's: NCBI and Ensembl agree on 57.6% of human gene-level (GeneID, ENSG) pairs, so an answer that did not name its Xref source and Release would be unreproducible a year later. A query reads exactly one set, which is why the source is one field here rather than a column on every row.

A foreign id naming two stems answers with both, and nothing picks one — the same guarantee :class:~genome.annotation.stems.ResolvedGeneIds gives for a stem naming two gene ids. What named nothing rides back in :attr:unresolved rather than shortening the answer.

The keys are the caller's own spelling of the ids it asked about, so a versioned and an unversioned spelling of one id are two keys with identical values and the answer still zips against the caller's table row for row.

Attributes:

Name Type Description
species str

The species this set is for, as the curated metadata table spells it.

source str

The Xref source whose assertions these are.

release str

The pinned Release of that source.

namespace str

The Namespace the ids asked about belong to.

resolved mapping of str to tuple of str

Every id that named at least one stem, in the order they were asked about, to the stems it names, in ascending order. No value is ever an empty tuple — an id that named nothing is in :attr:unresolved instead.

unresolved tuple of str

The ids this release names no stem for, in the order they were asked about.

Examples:

>>> answer = ResolvedStems(
...     species="Homo sapiens",
...     source="alliance",
...     release="8.4.0",
...     namespace="entrez",
...     resolved={"7157": ("ENSG00000141510",)},
...     unresolved=("999999999",),
... )
>>> answer.gene_id_stems
['ENSG00000141510']
>>> answer.as_json()["source"]
'alliance'

gene_id_stems property

gene_id_stems: list[str]

Every stem resolved, ask order and then stem order — a fresh list each call.

Every stem, not one per id. Flattening loses which id named which stem, and with it the fact that an id named more than one: a reader taking the first stem of each id would silently pick one of two genes a Namespace is ambiguous between. :attr:resolved is what says which id a stem came from. It also loses the ask order of the ids, since one id contributing two stems contributes two entries here.

as_json

as_json() -> dict[str, Any]

Return this answer as --json serializes it.

Returns:

Type Description
dict

species, source, release and namespace, resolved as a mapping of id to a list of stems, unresolved as a list, and the flattened gene_id_stems. The last is written out beside the mapping it is read from for the reason :attr:gene_id_stems gives.

ResolvedSymbols dataclass

ResolvedSymbols(
    species: str,
    source: str,
    release: str,
    case_insensitive: bool,
    kinds: tuple[str, ...],
    limits: str | None,
    resolved: Mapping[str, tuple[SymbolMatch, ...]],
    unresolved: tuple[str, ...],
)

The genes one Xref set says each gene symbol names, and how each one matched.

:meth:XrefSet.match_symbols's answer — the hop toward the hub from the one Namespace that is not answered like an identifier. :class:ResolvedStems's shape in every respect a caller relies on — ask order, no empty resolved value, what named nothing riding back — with one difference: a value is a tuple of :class:SymbolMatch rather than of stems, because ambiguity is the return type here and not an edge case. A symbol naming several genes answers with all of them and nothing picks one, and each says whether it matched an approved, a previous or an alias spelling so the caller can judge the ambiguity themselves.

What the set could not have matched is on the answer too. :attr:kinds says which kinds of spelling this Xref source publishes and :attr:limits says why the others are missing, so this gene is not in the release and this source cannot match the way you spelled it are distinguishable rather than both being silence.

Attributes:

Name Type Description
species str

The species this set is for, as the curated metadata table spells it.

source str

The Xref source whose assertions these are.

release str

The pinned Release of that source.

case_insensitive bool

Whether case was ignored. False is the default: the species is fixed by the set, so a mouse-cased spelling asked of a human set is the wrong authority's and matches nothing rather than half-working.

kinds tuple of str

The kinds of Symbol match this set could make, in :data:~genome.xref.symbols.SYMBOL_KINDS order.

limits str or None

Why the kinds not in :attr:kinds are missing, or None when all three are there.

resolved mapping of str to tuple of SymbolMatch

Every symbol that matched at least one gene, in the order they were asked about, to every match it made — approved first, then previous, then alias, and by stem within a kind. No value is ever an empty tuple.

unresolved tuple of str

The symbols this release matched nothing for, in the order they were asked about.

Examples:

>>> answer = ResolvedSymbols(
...     species="Homo sapiens",
...     source="hgnc",
...     release="2026-07-07",
...     case_insensitive=False,
...     kinds=("approved", "previous", "alias"),
...     limits=None,
...     resolved={
...         "ADCY3": (
...             SymbolMatch("ADCY3", "ENSG00000138031", "approved"),
...             SymbolMatch("ADCY3", "ENSG00000155897", "previous"),
...         )
...     },
...     unresolved=("Brca1",),
... )
>>> answer.gene_id_stems
['ENSG00000138031', 'ENSG00000155897']
>>> answer.as_json()["resolved"]["ADCY3"][1]["kind"]
'previous'

gene_id_stems property

gene_id_stems: list[str]

Every stem matched, ask order and then match order — a fresh list each call.

Every stem, not one per symbol, and it may repeat: one gene answering a symbol on both an approved and an alias spelling contributes two matches and so two entries. Flattening loses the two things this answer exists to carry — which symbol named which gene, and which kind of spelling each match was on — so a reader who takes this list has thrown away the means of judging the ambiguity. :attr:resolved is what keeps both.

as_json

as_json() -> dict[str, Any]

Return this answer as --json serializes it.

Returns:

Type Description
dict

species, source, release, case_insensitive, kinds and limits, resolved as a mapping of symbol to a list of match objects, unresolved as a list, and the flattened gene_id_stems — written out beside the mapping it is read from for the reason :attr:gene_id_stems gives.

ResolvedXrefIds dataclass

ResolvedXrefIds(
    species: str,
    source: str,
    release: str,
    namespace: str,
    resolved: Mapping[str, tuple[str, ...]],
    unresolved: tuple[str, ...],
)

The foreign ids one Xref set says a Gene id stem names.

:meth:XrefSet.from_stems's answer — the hop away from the hub, and :class:ResolvedStems's mirror in every respect: the same four provenance fields, the same ask order, the same never-empty resolved value, and the same tuple of what named nothing. Two verbs and only two, so a caller wanting one Namespace from another makes both calls and owns the join.

Attributes:

Name Type Description
species str

The species this set is for, as the curated metadata table spells it.

source str

The Xref source whose assertions these are.

release str

The pinned Release of that source.

namespace str

The Namespace the answering ids belong to.

resolved mapping of str to tuple of str

Every stem that named at least one id in that namespace, in the order the stems were asked about, to the ids it names, in ascending order. No value is ever an empty tuple.

unresolved tuple of str

The stems this release gives no id in that namespace, in the order they were asked about. One bucket and not two: a stem this release never carried and a stem it carries with no id in this namespace are both this set answers nothing, and no id history is held that could tell a retired stem from an unknown one.

Examples:

>>> answer = ResolvedXrefIds(
...     species="Homo sapiens",
...     source="alliance",
...     release="8.4.0",
...     namespace="hgnc",
...     resolved={"ENSG00000141510": ("HGNC:11998",)},
...     unresolved=("ENSG00000288541",),
... )
>>> answer.xref_ids
['HGNC:11998']
>>> answer.as_json()["namespace"]
'hgnc'

xref_ids property

xref_ids: list[str]

Every foreign id resolved, ask order and then id order — a fresh list each call.

Every id, not one per stem. Flattening loses which stem named which id, so a reader taking the first id of each stem would hand a collaborator one of two accessions a gene genuinely has without saying it had chosen; and a stem naming two ids contributes two entries, so the flattened list no longer runs parallel to the stems asked about. :attr:resolved is what says which stem an id came from.

as_json

as_json() -> dict[str, Any]

Return this answer as --json serializes it.

Returns:

Type Description
dict

species, source, release and namespace, resolved as a mapping of stem to a list of ids, unresolved as a list, and the flattened xref_ids, written out for the reason :attr:xref_ids gives.

SymbolMatch dataclass

SymbolMatch(symbol: str, gene_id_stem: str, kind: str)

One hit of a gene symbol against an Xref set, and which spelling matched it.

The kind rides on the match rather than being filtered away on the way out, because a table that spells a gene the way it was spelled five years ago is otherwise dropped without a word — the failure that would have hit 31 of EpiFactors' 801 rows.

Attributes:

Name Type Description
symbol str

The authority's own spelling that matched, which is not always the one asked about: on the case-insensitive path brca1 matches and this says BRCA1.

gene_id_stem str

The Gene id stem that spelling names.

kind str

approved, previous or alias — see :mod:genome.xref.symbols.

Examples:

>>> match = SymbolMatch(symbol="ARNTL", gene_id_stem="ENSG00000133794", kind="previous")
>>> match.as_json()
{'symbol': 'ARNTL', 'gene_id_stem': 'ENSG00000133794', 'kind': 'previous'}

as_json

as_json() -> dict[str, Any]

Return this match as --json serializes it, in field order.

Returns:

Type Description
dict

symbol, gene_id_stem and kind.

XrefSet

XrefSet(
    species: str,
    source: str | None = None,
    release: str | None = None,
    *,
    evidence: str | Iterable[str] | None = None,
    cache_dir: str | Path | None = None,
    progressbar: bool = True,
)

One species, one Xref source, one pinned Release, prepared on disk.

Constructing one prepares it, as opening a :class:~genome.assembly.genome.Genome does: the publisher's file is fetched on the first construction, sliced to this species and written under :func:xref_set_dir, and every construction after re-reads what is there and fetches nothing. It answers with no genome open and belongs to no assembly.

It answers two questions and only two — :meth:to_stems and :meth:from_stems, with :meth:match_symbols the first of them asked about a symbol, where the answer carries the kind of spelling that matched — and never converts one foreign Namespace directly into another. Gene level only: a gene, a transcript and a protein have different keys and different sources and are three objects rather than one table with a level column.

Parameters:

Name Type Description Default
species str

The species, in either the curated table's spelling ("Homo sapiens") or its slug ("homo_sapiens").

required
source str

The Xref source. Omitted, the species' Default xref source answers — which is a default and not a recommendation: naming one is how the scientific choice gets made deliberately, and two publishers disagreeing are two answers rather than one merged one.

This constructor fills in the identifier default, and a set that carries no symbol keeps refusing to match one — it holds one publisher's bytes and answering from another's is exactly what one query reading one set forbids. :meth:for_symbols is the constructor with the symbol question named, and is what fills in the source that carries them; :meth:for_namespace is the same fill-in for a caller holding a Namespace rather than a verb.

The sources are not equals, and the choice is nearly half the answer. Measured on human release 116 against NCBI's own file, Ensembl and NCBI agree on only 57.6% of the gene-level (GeneID, ENSG) pairs they assert between them. The cause is method rather than release skew: NCBI's mapping is a sequence match at a published overlap threshold and is all but one-to-one, while Ensembl's fans out to 72 stems for one GeneID (79166) and 208 GeneIDs for one stem (ENSG00000278233). Ask ensembl for an id and expect a wider answer than alliance gives for the same one; the width is the publisher's assertion, and nothing here narrows it or reconciles the two.

None
release str

The pinned Release. Omitted, the newest the curated table lists. Each source pins its own numbering and they do not correspond — 9.0.0 is Alliance's and 116 is Ensembl's.

None
evidence str or iterable of str

Keep only the rows the publisher graded with one of these info_types, or None for every row. A capability of the source rather than of every set: a publisher whose file grades nothing raises :class:~genome.xref.evidence.EvidenceNotRecordedError rather than ignoring the filter. A filter that keeps nothing raises too, because every human EntrezGene row Ensembl release 116 publishes is DEPENDENT and not one is DIRECT, so the intuitive quality filter empties the set rather than narrowing it. A filtered set is prepared beside the unfiltered one and never over it.

None
cache_dir str or Path

The directory to prepare in, overriding :func:xref_set_dir. The directory itself, not a root to file under.

None
progressbar bool

Show the download's progress bar. Nothing is drawn when the set is already there.

True

Attributes:

Name Type Description
species str

The species, as the curated table spells it.

source str

The Xref source whose assertions this carries.

release str

The pinned Release.

evidence tuple of str

The evidence filter this set was built under, empty for none.

path Path

The stored slice these mappings were read from — a plain gzipped TSV.

source_url str

Where the publisher's own file was fetched from.

provenance XrefMetadata

The curated row this set actually resolved to, defaults filled in — who published it, which release, and the paper to cite. Read off the set rather than looked up again, so what is cited is what answered.

namespaces tuple of str

The Namespaces this set actually carries, read off the slice rather than declared, in :data:~genome.xref.ids.NAMESPACES order. One that is not here raises rather than answering nothing.

symbol_kinds tuple of str

Which kinds of Symbol match this set can make — approved, previous, alias — read off the slice the same way, and empty for a source that carries no symbols at all.

symbol_limits str or None

Why the kinds not in :attr:symbol_kinds are missing, or None when all three are there or none is. It rides back on every :meth:match_symbols answer, because this gene is not in the release and this source does not publish the spelling you used are different answers and must not both be silence.

Raises:

Type Description
NoXrefSetError

If no set exists for that species, source or release. The message names what does.

XrefSetNotDownloadedError

If the set is not on disk and could not be fetched.

XrefTableError

If the publisher's file or the stored slice is not the shape it must be.

EvidenceNotRecordedError

If an evidence filter is named and this source's file grades nothing.

EmptyEvidenceFilterError

If an evidence filter is named and it keeps none of the release's rows.

RegistrationMismatchError

If the Completion marker disagrees with what is on disk, either about the slice's size or about its checksum — both mean unfinished rather than present.

Examples:

>>> from genome.xref import XrefSet
>>> human = XrefSet("Homo sapiens")
>>> human.namespaces
('ensembl', 'entrez', 'uniprot', 'hgnc')
>>> human.to_stems(["7157"], "entrez").resolved
{'7157': ('ENSG00000141510',)}
>>> human.from_stems(["ENSG00000141510.18"], "hgnc").resolved
{'ENSG00000141510.18': ('HGNC:11998',)}
>>> print(human.provenance.attribution())
Alliance of Genome Resources 9.0.0 (PMID 38552170) — https://download.alliancegenome.org/...

for_namespace classmethod

for_namespace(
    species: str,
    namespace: str,
    source: str | None = None,
    release: str | None = None,
    *,
    cache_dir: str | Path | None = None,
    progressbar: bool = True,
) -> XrefSet

Return the set that answers a question about namespace when none is named.

A Default xref source is per species and per question, and this is where a caller holding a Namespace rather than a verb names the question: the symbol one fills in the species' symbol-carrying default, every other one fills in its identifier default. It is :meth:for_symbols generalised to the shape a caller who read a namespace off a flag already has, so that surface has nothing left to decide — the choosing happens here and in no second place.

Which namespace a set actually carries is still the set's to say: this fills a source in and does not check, so a namespace the resolved set does not carry raises on the verb, naming the ones it does.

Parameters:

Name Type Description Default
species str

The species, in either the curated table's spelling or its slug.

required
namespace str

The Namespace the question is about, which is what picks the default. Case is not significant, as it is not on the verbs.

required
source str

The Xref source. Omitted, the default this namespace's question implies answers. Named, it is honoured whatever the namespace is — naming one is the deliberate scientific choice and is never swapped for a flagged row.

None
release str

The pinned Release. Omitted, the newest that source has.

None
cache_dir str or Path

The directory to prepare in, as on the ordinary constructor.

None
progressbar bool

Show the download's progress bar.

True

Returns:

Type Description
XrefSet

The prepared set, which is an ordinary one in every respect — every verb answers on it and nothing about it remembers which question filled its source in.

Raises:

Type Description
NoXrefSetError

If no set exists for that species or release, or if the namespace is the symbol one and no row for the species is flagged to answer symbols.

Examples:

>>> from genome.xref import XrefSet
>>> XrefSet.for_namespace("Homo sapiens", "symbol").source
'hgnc'
>>> XrefSet.for_namespace("Homo sapiens", "entrez").source
'alliance'

for_symbols classmethod

for_symbols(
    species: str,
    source: str | None = None,
    release: str | None = None,
    *,
    cache_dir: str | Path | None = None,
    progressbar: bool = True,
) -> XrefSet

Return the set that answers symbols for a species when none is named.

The ordinary constructor with the question named, and nothing else: a Default xref source is per species and per question, because the publisher carrying a species' identifiers is usually not the one carrying its symbols — human ids default to the Alliance, whose cross-reference file publishes no human symbol at all, and HGNC's quarterly archive is what does. Mouse and worm reach a third source again, alliance_bgi.

A source named here is honoured exactly as it is anywhere else. Naming one is how the scientific choice gets made deliberately, so this fills in a default and never overrides a choice: XrefSet.for_symbols(species, "alliance") is XrefSet(species, "alliance"), symbols and all — which for human means a set that matches none and says so.

This is :meth:for_namespace with the namespace fixed, so the two cannot answer differently: one fill-in, named by a verb here and by a namespace there.

Parameters:

Name Type Description Default
species str

The species, in either the curated table's spelling or its slug.

required
source str

The Xref source. Omitted, the species' symbol-carrying default answers.

None
release str

The pinned Release. Omitted, the newest that source has. Honoured against whichever source was filled in, exactly as on the ordinary constructor.

None
cache_dir str or Path

The directory to prepare in, as on the ordinary constructor.

None
progressbar bool

Show the download's progress bar.

True

Returns:

Type Description
XrefSet

The prepared set, which is an ordinary one in every respect — every verb answers on it and nothing about it remembers which question filled its source in.

Raises:

Type Description
NoXrefSetError

If no set exists for that species or release, or if no row for the species is flagged to answer symbols. The message names what does exist.

Examples:

>>> from genome.xref import XrefSet
>>> human = XrefSet.for_symbols("Homo sapiens")
>>> human.source
'hgnc'
>>> human.match_symbols(["ARNTL"]).gene_id_stems
['ENSG00000133794']

__len__

__len__() -> int

Return how many Gene id stems this set carries.

Examples:

>>> len(XrefSet("Caenorhabditis elegans"))
46926

__repr__

__repr__() -> str

Return which set this is and how many stems it holds.

The evidence filter appears only when there is one, so a filtered set never reads as the unfiltered set it is not.

to_stems

to_stems(
    ids: Iterable[str], namespace: str
) -> ResolvedStems

Return the Gene id stems this release says each foreign id names.

The hop toward the hub. Every id is reduced to one spelling on the way in — version dropped, the namespace's own CURIE prefix accepted whether or not it is written — so ENSG00000141510.18 and ENSG00000141510, or HGNC:11998 and 11998, are one identifier and resolve identically. Joining a versioned id to a bare one otherwise returns zero matches and says nothing, which is the most error-prone detail in this landscape.

Every stem, and never a chosen one. A foreign id naming two stems answers with both: 2,535 of 40,665 human genes carry more than one Ensembl cross-reference in Alliance 9.0.0, so 6.2% of HGNC ids are ambiguous and nothing here picks a side.

Nothing is dropped. Ids this release names no stem for come back in :attr:~ResolvedStems.unresolved, in ask order, so what a list holds and this release does not is visible rather than silently shorter.

Parameters:

Name Type Description Default
ids iterable of str

The foreign ids, in the order they should come back. Repeats are asked once, on the caller's own spelling, so a versioned and an unversioned spelling of one id are two entries with identical values and the answer still zips against the caller's table row for row.

required
namespace str

The Namespace those ids belong to, one of :attr:namespaces. Named rather than sniffed, because the string does not say. Case is not significant.

required

Returns:

Type Description
ResolvedStems

The ids that named stems, mapped to every stem each names, and the ids that named none — with the species, source, release and namespace that answered.

Raises:

Type Description
NamespaceNotCarriedError

If this set carries no such namespace. The message names the ones it does.

SymbolDirectionError

If the namespace is the symbol one. A symbol is not answered like an id — it matches previous and alias spellings too, and each match carries which kind it was — so the message names :meth:match_symbols rather than answering here on approved spellings alone.

Examples:

>>> human = XrefSet("Homo sapiens")
>>> human.to_stems(["7157", "999999999"], "entrez")
ResolvedStems(species='Homo sapiens', ...)

from_stems

from_stems(
    stems: Iterable[str], namespace: str
) -> ResolvedXrefIds

Return the foreign ids this release says each Gene id stem names.

The hop away from the hub, and :meth:to_stems's mirror in every respect: the same normalisation on the way in, every id and never a chosen one, and the stems that named nothing riding back in ask order. A stem this release never carried and a stem it carries with no id in this namespace are one bucket, since no id history is held that could tell a retirement from an absence.

Parameters:

Name Type Description Default
stems iterable of str

The stems, in the order they should come back. A versioned gene id is accepted and reduced to its stem, so an annotation's own ids may be passed straight in.

required
namespace str

The Namespace to answer in, one of :attr:namespaces. Case is not significant.

required

Returns:

Type Description
ResolvedXrefIds

The stems that named ids, mapped to every id each names, and the stems that named none — with the species, source, release and namespace that answered.

Raises:

Type Description
NamespaceNotCarriedError

If this set carries no such namespace. The message names the ones it does, and for the symbol namespace it names this species' symbol source and :meth:for_symbols too — labelling is a symbol question like matching one, and misses on a set carrying none for the same reason.

Examples:

>>> worms = XrefSet("Caenorhabditis elegans")
>>> worms.from_stems(["WBGene00000001"], "uniprot").xref_ids
['G5EDP9']

match_symbols

match_symbols(
    symbols: Iterable[str],
    *,
    case_insensitive: bool = False,
) -> ResolvedSymbols

Return every gene this release says each symbol names, and how each matched.

The hop toward the hub from the one Namespace answered unlike the rest, and deliberately not :meth:from_stems's mirror. A symbol is matched against approved, previous and alias spellings, answers with every Gene id stem any of them names, and each hit says which kind of spelling it was — so ambiguity is the return type and not an edge case. ADCY3 is HGNC's approved symbol for one gene and a symbol it retired from another, and both come back.

Exact by default. The species is fixed by the set, so Brca1 asked of a human set is a mouse spelling asked of the wrong authority and matches nothing rather than half-working. case_insensitive=True folds both sides and still answers with every gene matched rather than picking one.

What this source could not have matched rides back on the answer, in :attr:~ResolvedSymbols.kinds and :attr:~ResolvedSymbols.limits: mouse and worm match approved spellings only, their authorities' typed previous and alias spellings belonging to publishers that cannot be pinned or cannot be fetched, and an answer that did not say so would look exactly like a gene that is not in the release.

Parameters:

Name Type Description Default
symbols iterable of str

The symbols, in the order they should come back. Surrounding whitespace goes; case does not, unless case_insensitive is set. Repeats are asked once, on the caller's own spelling, so the answer still zips against their table.

required
case_insensitive bool

Fold case on both sides — the caller's spelling and the authority's.

False

Returns:

Type Description
ResolvedSymbols

The symbols that matched, mapped to every match each made, and the symbols that matched nothing — with the species, source and release that answered, which kinds it could match and why the others are missing.

Raises:

Type Description
NamespaceNotCarriedError

If this set carries no symbols at all — which the species' identifier default does not, for human. It raises rather than reaching for another publisher's bytes, and the message names the one source that answers this species' symbols and the constructor that fills it in, :meth:for_symbols.

Examples:

>>> human = XrefSet("Homo sapiens", "hgnc")
>>> human.match_symbols(["ARNTL"]).resolved["ARNTL"]
(SymbolMatch(symbol='ARNTL', gene_id_stem='ENSG00000133794', kind='previous'),)
>>> XrefSet.for_symbols("Homo sapiens").source
'hgnc'

XrefSetNotDownloadedError

Bases: PreparedSetNotDownloadedError

The set is not on disk and could not be fetched, so nothing can answer.

The Xref context's own spelling of what every Prepared set raises here, so the message names this set and quotes :func:xref_prepare_command; what the base class says about a compute node with no internet is said once, there.

Examples:

>>> XrefSet("Homo sapiens")
Traceback (most recent call last):
XrefSetNotDownloadedError: ...

XrefTableError

Bases: ValueError

A table read here is not the shape it must be, so it is not allowed to answer.

Covers both files this module reads: the publisher's, when what arrived does not match the checksum the curated row pins or carries no row for the species asked for; and the stored slice, when its header, its columns or a Namespace in it is not what this package writes. A :class:ValueError, because a file that says something the format does not is a bad value rather than a broken program, and the message names the file and the repair.

Examples:

>>> try:
...     parse_slice("wrong\theader\n", origin="example.tsv")
... except XrefTableError as error:
...     print("namespace" in str(error))
True

normalise_evidence

normalise_evidence(
    evidence: str | Iterable[str] | None,
) -> tuple[str, ...]

Return the evidence types asked for, in one canonical spelling.

Upper-cased because that is how every publisher surveyed writes them, stripped, emptied of blanks, deduplicated and sorted — the types are a set and their order carries no meaning, so sorting them is what lets the filter name a directory that two callers who asked in different orders both land on.

Idempotent, and total: anything that is not asked for comes back as the empty tuple, which is what no filter at all is spelled as everywhere below.

Parameters:

Name Type Description Default
evidence str or iterable of str or None

One evidence type, several, or None for no filter. A bare string is one type and never a sequence of one-character types.

required

Returns:

Type Description
tuple of str

The types, upper-cased, unique and ascending. Empty when nothing was asked for.

Examples:

>>> normalise_evidence("DIRECT")
('DIRECT',)
>>> normalise_evidence(" dependent ")
('DEPENDENT',)
>>> normalise_evidence(("DIRECT", "DEPENDENT"))
('DEPENDENT', 'DIRECT')
>>> normalise_evidence(normalise_evidence(["direct", ""]))
('DIRECT',)

gene_id_stem

gene_id_stem(gene_id: str) -> str

Return gene_id with its version dropped — its Gene id stem.

Everything before the first ., and the whole id when it carries none, which is what makes an unversioned id its own stem. The Annotation half of this package reduces a GTF's gene ids by the same rule, which is what lets a stem answered here be handed straight to :meth:~genome.annotation.registry.AnnotationRegistry.resolve_gene_ids.

Idempotent: the stem of a stem is that stem.

Parameters:

Name Type Description Default
gene_id str

A gene id, versioned or not.

required

Returns:

Type Description
str

Its stem.

Examples:

>>> gene_id_stem("ENSG00000141510.18")
'ENSG00000141510'
>>> gene_id_stem("WBGene00000001")
'WBGene00000001'
>>> gene_id_stem(gene_id_stem("ENSMUSG00000059552.16"))
'ENSMUSG00000059552'

normalise_id

normalise_id(xref_id: str, namespace: str) -> str

Return xref_id in namespace's canonical spelling, version dropped.

The one door every identifier comes through, a publisher's and a caller's alike. Four things happen, in this order: surrounding whitespace goes; every CURIE prefix the namespace is published under is stripped, repeatedly, case-insensitively; the version suffix goes (:func:gene_id_stem) and whitespace goes again; and the namespace's own canonical prefix goes back on. So HGNC:1100, hgnc:1100 and 1100 are one identifier, and ENSEMBL:ENSG00000141510.18 and ENSG00000141510 are another.

Idempotent, which is the property that matters: the same id read from a file and typed by a caller must land on the same string, or the join returns nothing and says nothing. The second strip is what makes that true rather than nearly true — a version separator hides trailing whitespace behind it, so "7157\r." stems to "7157\r" on the first pass and only reaches "7157" on the second, and two spellings of one id that settle on different strings after a different number of passes join to nothing.

Parameters:

Name Type Description Default
xref_id str

The identifier, in whatever spelling it arrived in.

required
namespace str

The Namespace it belongs to, one of :data:NAMESPACES. An identifier does not say which it is, so it is named rather than sniffed. A namespace this module does not know is not an error here: the id is stripped of nothing and its version is still dropped, because the caller of this function has already checked which namespaces its set carries and this is not the place to check it twice.

required

Returns:

Type Description
str

The canonical spelling. Empty for an id that is empty or whitespace.

Examples:

>>> normalise_id("HGNC:1100", "hgnc")
'HGNC:1100'
>>> normalise_id("1100", "hgnc")
'HGNC:1100'
>>> normalise_id("MGI:MGI:88276", "mgi")
'MGI:88276'
>>> normalise_id("UniProtKB:P38398", "uniprot")
'P38398'
>>> normalise_id("  ENSEMBL:ENSG00000141510.18  ", "ensembl")
'ENSG00000141510'
>>> normalise_id("7157\r.", "entrez")
'7157'

lookup_xref

lookup_xref(
    species: str,
    source: str | None = None,
    release: str | None = None,
    *,
    for_symbols: bool = False,
    table: Sequence[XrefMetadata] | None = None,
) -> XrefMetadata

Return the curated row for one Xref set, filling in the defaults.

The one lookup, and the one place a miss is turned into an error that says what is there. It is total in the other direction: it always answers with a row or raises, so nothing downstream guards a None before reading a field.

A default is per species and per question, which is what for_symbols selects. It changes what an unnamed source resolves to and nothing else: a named source is the caller's deliberate scientific choice and is never swapped for another, and a named release is still honoured against whichever source was filled in.

Parameters:

Name Type Description Default
species str

The species, in either the table's own spelling ("Homo sapiens") or its slug ("homo_sapiens").

required
source str

The Xref source. Omitted, the species' Default xref source answers — the one flagged for identifiers, or the one flagged for symbols when for_symbols.

None
release str

The Release. Omitted, the newest the table lists for that source answers — which is the last row, the table being in release order. Named, it is honoured whether or not a source was named too: a release asked for against the default source either answers with that release or raises naming the ones that source has, and is never quietly swapped for another. Each source numbers its own releases and they do not correspond, so "116" is not a release the default source has.

None
for_symbols bool

Fill an unnamed source in with the species' symbol-carrying default rather than its identifier one. The question a caller is about to ask, named here because a row is resolved before anybody knows it — an Xref set is built for a species, a source and a release, and which of the two verbs it will be asked is not part of that.

False
table sequence of XrefMetadata

The rows to read; the shipped table when omitted. A caller curating rows of their own hands them over here, and nothing is installed by passing them.

None

Returns:

Type Description
XrefMetadata

The row, with source and release resolved to what actually answered.

Raises:

Type Description
NoXrefSetError

If no set exists for that species, that source, or that release of it — or, under for_symbols, if no row for the species is flagged to answer symbols. The message names the species, sources or releases that do exist, whichever missed.

Examples:

>>> lookup_xref("homo_sapiens").release
'9.0.0'
>>> lookup_xref("Homo sapiens", "alliance", "9.0.0").ncbi_taxid
9606
>>> lookup_xref("Homo sapiens", release="9.0.0").source
'alliance'
>>> lookup_xref("Homo sapiens", for_symbols=True).source
'hgnc'
>>> lookup_xref("Homo sapiens", "alliance", for_symbols=True).source
'alliance'
>>> try:
...     lookup_xref("Homo sapiens", "alliance", "1.0")
... except NoXrefSetError as error:
...     print("9.0.0" in str(error))
True

xref_releases

xref_releases(
    species: str,
    source: str,
    *,
    table: Sequence[XrefMetadata] | None = None,
) -> tuple[str, ...]

Return every Release of source that answers for species, oldest first.

Parameters:

Name Type Description Default
species str

The species, in either the table's spelling or its slug.

required
source str

The Xref source.

required
table sequence of XrefMetadata

The rows to read; the shipped table when omitted.

None

Returns:

Type Description
tuple of str

The release strings in table order, which is oldest first — so the last is what a caller who names no release gets.

Examples:

>>> xref_releases("Homo sapiens", "alliance")
('9.0.0',)

xref_sources

xref_sources(
    species: str,
    *,
    table: Sequence[XrefMetadata] | None = None,
) -> tuple[str, ...]

Return every Xref source that answers for species, in first-listed order.

Parameters:

Name Type Description Default
species str

The species, in either the table's spelling or its slug.

required
table sequence of XrefMetadata

The rows to read; the shipped table when omitted.

None

Returns:

Type Description
tuple of str

The source names. Empty for a species no set exists for.

Examples:

>>> xref_sources("Mus musculus")
('alliance', 'ensembl', 'alliance_bgi')
>>> xref_sources("Caenorhabditis elegans")
('alliance', 'alliance_bgi')
>>> xref_sources("Danio rerio")
()

xref_species

xref_species(
    *, table: Sequence[XrefMetadata] | None = None
) -> tuple[str, ...]

Return every species an Xref set exists for, as the table spells them.

What can be asked about at all, and what :class:NoXrefSetError names when a species cannot be.

Parameters:

Name Type Description Default
table sequence of XrefMetadata

The rows to read; the shipped table when omitted.

None

Returns:

Type Description
tuple of str

The species names, in first-listed order.

Examples:

>>> xref_species()
('Homo sapiens', 'Mus musculus', 'Caenorhabditis elegans')

xref_table cached

xref_table() -> tuple[XrefMetadata, ...]

Return every Xref set the shipped table lists, in table order.

Read once and cached; the records are frozen, so the tuple is safe to hold on to. Rows for one (species, source) are in release order, oldest first.

Returns:

Type Description
tuple of XrefMetadata

One record per row of data/xref/xref_metadata.tsv.

Raises:

Type Description
MetadataRowError

If the shipped file is empty, its header is not :data:XREF_FIELDS, a row holds the wrong number of cells, or a cell cannot be read as its column's type.

Examples:

>>> sorted({record.release for record in xref_table()})
['116', '2026-07-07', '9.0.0']

fold_symbol

fold_symbol(symbol: str) -> str

Return the key symbol is matched under when case is not to be significant.

:func:normalise_symbol and then :meth:str.casefold, which is the full-Unicode fold rather than :meth:str.lower. Used only on the opt-in path, and used on both sides of it — the caller's spelling and the authority's — so that folding is a property of the lookup rather than of what was stored.

Parameters:

Name Type Description Default
symbol str

The symbol, in any spelling.

required

Returns:

Type Description
str

Its case-folded key. Empty for an empty or all-whitespace symbol.

Examples:

>>> fold_symbol("Brca1")
'brca1'
>>> fold_symbol(" p53 ") == fold_symbol("P53")
True

normalise_symbol

normalise_symbol(symbol: str) -> str

Return symbol in the one spelling a match is looked up under.

Surrounding whitespace and nothing else. A symbol is not put through :func:~genome.xref.ids.normalise_id: that drops everything after the first ., which is right for a versioned gene id and wrong for a symbol — WormBase names thousands of genes by their sequence name, and Y110A7A.10 stemmed to Y110A7A is a different gene's spelling or none at all.

Idempotent, and total: an empty or all-whitespace symbol comes back empty and matches nothing.

Parameters:

Name Type Description Default
symbol str

The symbol, as a caller typed it or a publisher wrote it.

required

Returns:

Type Description
str

The symbol with surrounding whitespace removed. Case is untouched, because case is significant: exact matching is the default and a mouse-cased spelling asked of a human set is the wrong authority's, not a typo to absorb.

Examples:

>>> normalise_symbol("  TP53\n")
'TP53'
>>> normalise_symbol("Y110A7A.10")
'Y110A7A.10'
>>> normalise_symbol(normalise_symbol(" daf-16 "))
'daf-16'

xref_data_dir

xref_data_dir() -> Path

Return the directory holding Xref sets, which belong to no Assembly.

The Xref context's own root under the Data dir, declared here because this is where its Prepared set is fetched into and read from.

Returns:

Type Description
Path

<liulab_data>/xref. Nothing is created by asking.

Examples:

>>> import os
>>> os.environ["LIULAB_DATA"] = "/scratch/liulab"
>>> xref_data_dir()
PosixPath('/scratch/liulab/xref')
>>> del os.environ["LIULAB_DATA"]

xref_prepare_command

xref_prepare_command(
    species: str,
    source: str,
    release: str,
    *,
    evidence: Sequence[str] = (),
) -> str

Return the call that prepares one Xref set, for an error message to quote.

One spelling of it, so a renamed entry point is renamed once. Quoted by every error here that a caller repairs by fetching the set on a machine with internet — which is why an evidence filter travels in it: repairing a filtered set by preparing the unfiltered one would leave the caller exactly where they started.

Parameters:

Name Type Description Default
species str

The species, as the curated table spells it.

required
source str

The Xref source.

required
release str

The pinned Release.

required
evidence sequence of str

The evidence filter the set was built under. Omitted from the command when empty.

()

Returns:

Type Description
str

A shell command, unquoted and unfenced — the caller decides how to set it.

Examples:

>>> xref_prepare_command("Homo sapiens", "alliance", "9.0.0")
'python -c "from genome.xref import XrefSet; XrefSet(\'Homo sapiens\', \'alliance\', \'9.0.0\')"'
>>> "evidence=('DEPENDENT',)" in xref_prepare_command(
...     "Homo sapiens", "ensembl", "116", evidence=("DEPENDENT",)
... )
True

xref_set_dir

xref_set_dir(
    species: str,
    source: str,
    release: str,
    *,
    evidence: Sequence[str] = (),
) -> Path

Return the directory one Xref set is prepared in, whether or not it exists.

<liulab_data>/xref/<source>/<release>/<species slug>/. Source and release above species, so two releases of one publisher sit side by side and neither is the xref directory — holding two releases at once is the whole point of pinning one.

A set built under an evidence filter is a different set and gets a directory of its own beside the unfiltered one, since a filter that changes which rows are read changes what the stored slice holds. Two callers who named the same types in different orders land on one directory, the filter being sorted on the way in.

Parameters:

Name Type Description Default
species str

The species, in either the curated table's spelling or its slug.

required
source str

The Xref source.

required
release str

The pinned Release.

required
evidence sequence of str

The evidence filter the set was built under, as :func:~genome.xref.evidence.normalise_evidence spells it. Empty for none.

()

Returns:

Type Description
Path

The set's own directory. Nothing is created by asking.

Examples:

>>> import os
>>> os.environ["LIULAB_DATA"] = "/scratch/liulab"
>>> xref_set_dir("Homo sapiens", "alliance", "9.0.0")
PosixPath('/scratch/liulab/xref/alliance/9.0.0/homo_sapiens')
>>> xref_set_dir("Homo sapiens", "ensembl", "116", evidence=("DEPENDENT",))
PosixPath('/scratch/liulab/xref/ensembl/116/homo_sapiens.evidence-dependent')
>>> del os.environ["LIULAB_DATA"]

xref_slice_name

xref_slice_name(species: str) -> str

Return the file name one species' stored slice is written under.

Parameters:

Name Type Description Default
species str

The species, in either the curated table's spelling or its slug.

required

Returns:

Type Description
str

<species slug>.xref_table.tsv.gz.

Examples:

>>> xref_slice_name("Caenorhabditis elegans")
'caenorhabditis_elegans.xref_table.tsv.gz'