From f854237e8328f5a4ac94eab3a3acd1f49870ca03 Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Fri, 13 Feb 2026 07:28:34 -0500 Subject: [PATCH 1/2] update cat-vrs submodules --- .gitmodules | 2 +- submodules/cat_vrs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 59b7193..f51b824 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "submodules/cat_vrs"] path = submodules/cat_vrs url = https://github.com/ga4gh/cat-vrs - branch = 1.0 + branch = 1.1.0-snapshot.2026-02 diff --git a/submodules/cat_vrs b/submodules/cat_vrs index 9c66151..8fe3864 160000 --- a/submodules/cat_vrs +++ b/submodules/cat_vrs @@ -1 +1 @@ -Subproject commit 9c6615159eb6360f3931c63cb88ad9b60cd658b0 +Subproject commit 8fe3864ec6db18ccf2085297c75403299c591580 From dc7ec05513ea62e690dc24c2321adf698d36c952 Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Fri, 13 Feb 2026 08:18:08 -0500 Subject: [PATCH 2/2] add models + tests --- src/ga4gh/cat_vrs/models.py | 21 +++++- src/ga4gh/cat_vrs/recipes.py | 61 ++++++++++++++++- tests/validation/test_cat_vrs_models.py | 91 +++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 2 deletions(-) diff --git a/src/ga4gh/cat_vrs/models.py b/src/ga4gh/cat_vrs/models.py index de42ff7..c905b6e 100644 --- a/src/ga4gh/cat_vrs/models.py +++ b/src/ga4gh/cat_vrs/models.py @@ -80,7 +80,9 @@ class CopyCountConstraint(BaseModelForbidExtra): class CopyChangeConstraint(BaseModelForbidExtra): - """A representation of copy number change""" + """The relative assessment of the change in copies that members of this categorical + variant satisfy. + """ type: Literal["CopyChangeConstraint"] = Field( default="CopyChangeConstraint", description="MUST be 'CopyChangeConstraint'" @@ -101,6 +103,22 @@ class FeatureContextConstraint(BaseModelForbidExtra): featureContext: MappableConcept = Field(..., description="A feature identifier.") +class FunctionConstraint(BaseModelForbidExtra): + """A classification of the protein functional consequence that characterizes members of this categorical variant.""" + + type: Literal["FunctionConstraint"] = Field( + default="FunctionConstraint", + description='MUST be "FunctionConstraint"', + ) + functionConsequence: MappableConcept = Field( + ..., + description="The functional consequence of members of this categorical variant, as defined by an external ontology. We recommend using one of the defined terms from [The Sequence Ontology](http://www.sequenceontology.org). See Implementation Guidance for more details. ", + ) + description: str | None = Field( + default=None, description="A free-text description of the function change." + ) + + class Constraint(RootModel): """Constraints are used to construct an intensional semantics of categorical variant types.""" @@ -110,6 +128,7 @@ class Constraint(RootModel): | CopyCountConstraint | CopyChangeConstraint | FeatureContextConstraint + | FunctionConstraint ) = Field(..., discriminator="type") diff --git a/src/ga4gh/cat_vrs/recipes.py b/src/ga4gh/cat_vrs/recipes.py index bc3e62a..6d0da88 100644 --- a/src/ga4gh/cat_vrs/recipes.py +++ b/src/ga4gh/cat_vrs/recipes.py @@ -15,6 +15,8 @@ CopyCountConstraint, DefiningAlleleConstraint, DefiningLocationConstraint, + FeatureContextConstraint, + FunctionConstraint, Relation, ) @@ -78,7 +80,7 @@ class CanonicalAllele(CategoricalVariant): """A canonical allele is defined by an `Allele `_ that is representative of a collection of congruent Alleles, each of which depict - the same nucleic acid change on different underlying reference sequences. Congruent + the same nucleic acid on different underlying reference sequences. Congruent representations of an Allele often exist across different genome assemblies and associated cDNA transcript representations. """ @@ -210,3 +212,60 @@ def validate_constraints(cls, v: list[Constraint]) -> list[Constraint]: raise ValueError(err_msg) return v + + +class FunctionVariant(CategoricalVariant): + """A representation of the constraints for matching knowledge about function + variants; e.g., gain-of-function or loss-of-function. + """ + + constraints: list[Constraint] = Field( + ..., + min_length=2, + description=( + "The constraints must contain at least two items: a FunctionConstraint and either a DefiningAlleleConstraint, DefiningLocationConstraint, or FeatureContextConstraint." + ), + ) + + @field_validator("constraints") + @classmethod + def validate_constraints(cls, v: list[Constraint]) -> list[Constraint]: + """Validate constraints property + + ``constraints`` must: + 1. Contain at least one ``FunctionConstraint`` + 2. Contain at least one of: + - ``DefiningAlleleConstraint`` + - ``DefiningLocationConstraint`` + - ``FeatureContextConstraint`` + + :param v: Constraints property to validate + :raises ValueError: If constraints property does not satisfy requirements + :return: Constraints property + """ + has_function_constraint = False + has_context_constraint = False + + for constraint in v: + root = constraint.root + + if isinstance(root, FunctionConstraint): + has_function_constraint = True + + if isinstance( + root, + DefiningAlleleConstraint + | DefiningLocationConstraint + | FeatureContextConstraint, + ): + has_context_constraint = True + + if not has_function_constraint: + msg = "Must contain at least one `FunctionConstraint`." + raise ValueError(msg) + + if not has_context_constraint: + msg = "Must contain at least one of: `DefiningAlleleConstraint`, `DefiningLocationConstraint`, or `FeatureContextConstraint`." + raise ValueError(msg) + + return v diff --git a/tests/validation/test_cat_vrs_models.py b/tests/validation/test_cat_vrs_models.py index 7b7f3e7..8e26b0d 100644 --- a/tests/validation/test_cat_vrs_models.py +++ b/tests/validation/test_cat_vrs_models.py @@ -56,6 +56,33 @@ def defining_loc_constr(): ) +@pytest.fixture(scope="module") +def feature_context_constr(): + """Create test fixture for feature context constraint""" + return models.FeatureContextConstraint( + featureContext=MappableConcept( + primaryCoding=Coding( + code=code("HGNC:1097"), + system="https://www.genenames.org/data/gene-symbol-report/#!/hgnc_id/", + ) + ) + ) + + +@pytest.fixture(scope="module") +def function_constr(): + """Create test fixture for function constraint""" + return models.FunctionConstraint( + functionConsequence=MappableConcept( + primaryCoding=Coding( + code=code("SO:0002219"), + system="http://www.sequenceontology.org/browser/current_release/term/", + ) + ), + description="Function consequence described as functionally normal using Sequence Ontology.", + ) + + def test_copy_count_constraint(): """Test the CopyCountConstraint validator""" # Valid Copy Count Constraint @@ -445,3 +472,67 @@ def test_categorical_cnv( match="Must contain either a `CopyCountConstraint` or `CopyChangeConstraint`.", ): recipes.CategoricalCnv(**invalid_params) + + +def test_function_variant( + members_and_name: dict, + function_constr: models.FunctionConstraint, + defining_loc_constr: models.DefiningLocationConstraint, + feature_context_constr: models.FeatureContextConstraint, +): + """Test the FunctionVariant validator""" + # Valid FunctionVariant with DefiningAlleleConstraint + valid_params = deepcopy(members_and_name) + valid_params["constraints"] = [ + models.Constraint(root=function_constr), + models.Constraint(root=def_allele_constr_empty_relations(is_empty_list=True)), + ] + assert recipes.FunctionVariant(**valid_params) + + # Valid FunctionVariant with DefiningLocationConstraint + valid_params = deepcopy(members_and_name) + valid_params["constraints"] = [ + models.Constraint(root=function_constr), + models.Constraint(root=defining_loc_constr), + ] + assert recipes.FunctionVariant(**valid_params) + + # Valid FunctionVariant with FeatureContextConstraint + valid_params = deepcopy(members_and_name) + valid_params["constraints"] = [ + models.Constraint(root=function_constr), + models.Constraint(root=feature_context_constr), + ] + assert recipes.FunctionVariant(**valid_params) + + # Invalid FunctionVariant: No FunctionConstraint + invalid_params = deepcopy(members_and_name) + invalid_params["constraints"] = [ + models.Constraint(root=def_allele_constr_empty_relations(is_empty_list=True)), + models.Constraint(root=defining_loc_constr), + ] + with pytest.raises( + ValueError, + match="Must contain at least one `FunctionConstraint`.", + ): + recipes.FunctionVariant(**invalid_params) + + # Invalid FunctionVariant: No contextual constraint + invalid_params = deepcopy(members_and_name) + invalid_params["constraints"] = [ + models.Constraint(root=function_constr), + models.Constraint(root=function_constr.model_copy(deep=True)), + ] + with pytest.raises( + ValueError, + match="Must contain at least one of:", + ): + recipes.FunctionVariant(**invalid_params) + + # Invalid FunctionVariant: Only one constraint provided + invalid_params = deepcopy(members_and_name) + invalid_params["constraints"] = [ + models.Constraint(root=function_constr), + ] + with pytest.raises(ValueError, match="List should have at least 2 items"): + recipes.FunctionVariant(**invalid_params)