Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "submodules/cat_vrs"]
path = submodules/cat_vrs
url = https://github.com/ga4gh/cat-vrs
branch = 1.1.0-snapshot.2026-02
branch = 1.1
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ keywords = ["bioinformatics", "ga4gh", "genomics", "variation"]
requires-python = ">=3.10"
dynamic = ["version"]
dependencies = [
"ga4gh.vrs~=2.4.0-a3",
"ga4gh.vrs~=2.4.0-a4",
"pydantic>=2.0,<3.0",
]

Expand Down
77 changes: 74 additions & 3 deletions src/ga4gh/cat_vrs/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
the GA4GH website for more information.
"""

from enum import Enum
from typing import ClassVar, Literal

from pydantic import Field, RootModel
from pydantic import ConfigDict, Field, RootModel

from ga4gh.cat_vrs.metadata import CatVRSMetadataMixin
from ga4gh.core.metadata import Maturity
Expand All @@ -17,7 +18,16 @@
MappableConcept,
iriReference,
)
from ga4gh.vrs.models import Allele, CopyChange, Range, SequenceLocation, Variation
from ga4gh.vrs.models import (
Allele,
CopyChange,
Location,
Range,
SequenceExpression,
SequenceLocation,
Terminus,
Variation,
)


class DefiningAlleleConstraint(CatVRSMetadataMixin, BaseModelForbidExtra):
Expand Down Expand Up @@ -60,6 +70,66 @@ class DefiningLocationConstraint(CatVRSMetadataMixin, BaseModelForbidExtra):
)


class FunctionalDomainStatus(str, Enum):
"""Define whether a functional domain is preserved or lost."""

LOST = "lost"
PRESERVED = "preserved"


class FunctionalDomain(CatVRSMetadataMixin, Entity, BaseModelForbidExtra):
"""A sequence location whose presence or absence is used to define an adjacency."""

model_config = ConfigDict(use_enum_values=True)

_maturity: ClassVar[Maturity] = Maturity.DRAFT

type: Literal["FunctionalDomain"] = Field(
default="FunctionalDomain", description='Must be "FunctionalDomain"'
)
location: Location = Field(..., description="A Sequence Location for the domain.")
status: FunctionalDomainStatus = Field(
..., description='MUST be either "preserved" or "lost"'
)


class UnspecifiedElement(CatVRSMetadataMixin, Entity, BaseModelForbidExtra):
"""Represents an unspecified element that satisfies the described adjacency."""

_maturity: ClassVar[Maturity] = Maturity.DRAFT

type: Literal["UnspecifiedElement"] = Field(
default="UnspecifiedElement", description='Must be "UnspecifiedElement"'
)


class AdjacencyConstraint(CatVRSMetadataMixin, BaseModelForbidExtra):
"""Components that define a molecular adjacency of congruent elements."""

_maturity: ClassVar[Maturity] = Maturity.DRAFT

type: Literal["AdjacencyConstraint"] = Field(
default="AdjacencyConstraint",
description="MUST be 'AdjacencyConstraint'",
)
adjoinedElements: list[
UnspecifiedElement | MappableConcept | iriReference | Location | Terminus
] = Field(
..., min_length=2, max_length=2, description="The elements of the adjacency."
)
functionalDomains: list[FunctionalDomain] | None = Field(
default=None,
description="Functional domains whose presence or absence is required to satisfy the adjacency.",
)
linker: SequenceExpression | None = Field(
default=None, description="The sequence found between the adjoined elements."
)
orderKnown: bool = Field(
...,
description="When orderKnown is true, the order of adjoinedElements is assumed to denote the 5' partner first and the 3' partner second. If orderKnown is false, then the order of adjoinedElements assumed not in fact to be known, as in the case of a fusion where only one or both partners are known, but not their relative order. This field is redundant and may be set to true when using Sequence Locations and following the VRS 2 Adjacency model, as the order is implied by the usage of start and end on respective adjoinedElements.",
)


class CopyCountConstraint(CatVRSMetadataMixin, BaseModelForbidExtra):
"""The exact or range of copies that members of this categorical variant must
satisfy.
Expand Down Expand Up @@ -130,9 +200,10 @@ class Constraint(CatVRSMetadataMixin, RootModel):
root: (
DefiningAlleleConstraint
| DefiningLocationConstraint
| AdjacencyConstraint
| FeatureContextConstraint
| CopyCountConstraint
| CopyChangeConstraint
| FeatureContextConstraint
| FunctionConstraint
) = Field(..., discriminator="type")

Expand Down
43 changes: 42 additions & 1 deletion src/ga4gh/cat_vrs/recipes.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pydantic import Field, field_validator

from ga4gh.cat_vrs.models import (
AdjacencyConstraint,
CategoricalVariant,
Constraint,
CopyChangeConstraint,
Expand All @@ -17,14 +18,16 @@
DefiningLocationConstraint,
FeatureContextConstraint,
FunctionConstraint,
UnspecifiedElement,
)
from ga4gh.cat_vrs.relations import (
LIFTOVER_TO_RELATION,
TRANSCRIBED_TO_RELATION,
TRANSLATION_OF_RELATION,
)
from ga4gh.core.metadata import Maturity
from ga4gh.core.models import MappableConcept
from ga4gh.core.models import MappableConcept, iriReference
from ga4gh.vrs.models import Location


class ProteinSequenceConsequence(CategoricalVariant):
Expand Down Expand Up @@ -246,3 +249,41 @@ def validate_constraints(cls, v: list[Constraint]) -> list[Constraint]:
raise ValueError(msg)

return v


class GeneFusion(CategoricalVariant):
"""A representation of the joining of two genes resulting in a chimeric transcript
and/or novel interaction between a rearranged regulatory elements with the
expression of a partner gene product (a regulatory fusion).
"""

_maturity: ClassVar[Maturity] = Maturity.DRAFT

constraints: list[Constraint] = Field(
...,
min_length=1,
description="The constraints must contain at least one item of an Adjacency Constraint.",
)

@field_validator("constraints")
@classmethod
def validate_constraints(cls, v: list[Constraint]) -> list[Constraint]:
"""Require an adjacency with at least one gene fusion element."""
if not any(
isinstance(constraint.root, AdjacencyConstraint)
and any(
isinstance(
element,
iriReference | MappableConcept | Location | UnspecifiedElement,
)
for element in constraint.root.adjoinedElements
)
for constraint in v
):
msg = (
"Must contain at least one `AdjacencyConstraint` whose "
"`adjoinedElements` contains an `iriReference`, `MappableConcept`, "
"`Location`, or `UnspecifiedElement`."
)
raise ValueError(msg)
return v
2 changes: 1 addition & 1 deletion src/ga4gh/cat_vrs/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Define Cat-VRS version"""

CATVRS_VERSION = "1.1.0-snapshot.2026-02.3"
CATVRS_VERSION = "1.1.0"
2 changes: 1 addition & 1 deletion submodules/cat_vrs
Submodule cat_vrs updated 197 files
45 changes: 45 additions & 0 deletions tests/validation/test_cat_vrs_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
MappableConcept,
code,
)
from ga4gh.vrs.models import SequenceLocation, Terminus

DUMMY_ALLELE_IRI = "allele.json#/1" # Valid IRI but does not reference anything

Expand Down Expand Up @@ -441,3 +442,47 @@ def test_function_variant(
]
with pytest.raises(ValueError, match="List should have at least 2 items"):
recipes.FunctionVariant(**invalid_params)


def test_gene_fusion(members_and_name: dict):
"""Test the GeneFusion adjacency requirements."""
location = SequenceLocation(sequenceReference="ga4gh:SQ.test", start=1, end=2)
terminus = Terminus(location=location)
valid_elements = [
"gene:partner",
MappableConcept(name="partner gene"),
location,
models.UnspecifiedElement(),
]
for element in valid_elements:
valid_adjacency = models.AdjacencyConstraint(
adjoinedElements=[element, terminus], orderKnown=False
)
valid_params = deepcopy(members_and_name)
valid_params["constraints"] = [models.Constraint(root=valid_adjacency)]
assert recipes.GeneFusion(**valid_params)

invalid_adjacency = models.AdjacencyConstraint(
adjoinedElements=[terminus, terminus], orderKnown=True
)
invalid_params = deepcopy(members_and_name)
invalid_params["constraints"] = [models.Constraint(root=invalid_adjacency)]
with pytest.raises(
ValueError,
match=(
"Must contain at least one `AdjacencyConstraint` whose "
"`adjoinedElements` contains an `iriReference`, `MappableConcept`, "
"`Location`, or `UnspecifiedElement`."
),
):
recipes.GeneFusion(**invalid_params)


def test_functional_domain_status():
"""Test functional domain status string-enum coercion."""
functional_domain = models.FunctionalDomain(
location=SequenceLocation(sequenceReference="ga4gh:SQ.test", start=1, end=2),
status="preserved",
)
assert functional_domain.status == "preserved"
assert functional_domain.model_dump(mode="json")["status"] == "preserved"
Loading