From c81f92530439a19d5dc678bbd06b12a930d84edf Mon Sep 17 00:00:00 2001 From: Peter-J-Freeman Date: Fri, 15 May 2026 22:17:12 +0100 Subject: [PATCH 1/6] Fixes for Selenon code handling in issue #815 variant NM_020451.3:c_447dup Also fix a few tests --- VariantValidator/modules/vvMixinInit.py | 98 ++++++++++++++++++++++--- tests/test_inputs.py | 40 +++++----- 2 files changed, 112 insertions(+), 26 deletions(-) diff --git a/VariantValidator/modules/vvMixinInit.py b/VariantValidator/modules/vvMixinInit.py index 47a2f838..d3e5ed59 100644 --- a/VariantValidator/modules/vvMixinInit.py +++ b/VariantValidator/modules/vvMixinInit.py @@ -14,10 +14,12 @@ import vvhgvs.edit import vvhgvs.normalizer from vvhgvs.location import AAPosition, Interval -from vvhgvs.edit import AARefAlt, AAExt, Dup +from vvhgvs.edit import AARefAlt, AAExt, Dup, AAFs +from vvhgvs.posedit import PosEdit from Bio.Seq import Seq import re +import logging from .vvDatabase import Database from . import utils from VariantValidator.settings import CONFIG_DIR @@ -25,6 +27,8 @@ from VariantValidator.modules.hgvs_utils import hgvs_delins_parts_to_hgvs_obj,\ VVPosEdit +logger = logging.getLogger(__name__) + class InitialisationError(Exception): pass @@ -463,8 +467,8 @@ def _remake_unc(prot,nucleotide_not_equal=False): try: prot_ref_seq = utils.translate(ref_seq, cds_start, modified_aa) except IndexError: - import traceback - traceback.print_exc() + # import traceback + # traceback.print_exc() hgvs_transcript_to_hgvs_protein['error'] = \ 'ProteinTranslationError: Cannot generate a protein without an identifiable in-' +\ 'frame Termination codon in the reference mRNA sequence, this transcript may be ' +\ @@ -472,8 +476,8 @@ def _remake_unc(prot,nucleotide_not_equal=False): hgvs_transcript_to_hgvs_protein['hgvs_protein'] = _tot_unc(associated_protein_accession) return hgvs_transcript_to_hgvs_protein except KeyError: - import traceback - traceback.print_exc() + # import traceback + # traceback.print_exc() hgvs_transcript_to_hgvs_protein['error'] = \ 'ProteinTranslationError: Unable to build protein sequence due to a non-CATG ' +\ 'base included in the reference mRNA sequence, only standard unambiguous bases '+\ @@ -482,6 +486,7 @@ def _remake_unc(prot,nucleotide_not_equal=False): return hgvs_transcript_to_hgvs_protein + logger.info("Translating reference and variant CDS outcomes") try: prot_var_seq = utils.translate(var_seq, cds_start, modified_aa) except IndexError: @@ -580,6 +585,8 @@ def _remake_unc(prot,nucleotide_not_equal=False): prot_var_seq, in_frame) + logger.info(f"pro_inv_info: {pro_inv_info}") + # Error has occurred if pro_inv_info['error'] == 'true': error = 'Translation error occurred, please contact admin' @@ -644,9 +651,21 @@ def _remake_unc(prot,nucleotide_not_equal=False): # Adjust extended aas if necessary if modified_aa == "Sec": if "U" in pro_inv_info['prot_ins_seq'] and "U" not in pro_inv_info['prot_del_seq']: - pro_inv_info['prot_ins_seq'] = pro_inv_info['prot_ins_seq'].replace("U", "*") + logger.info("Sec identified in pro_inv_info['prot_ins_seq']") + logger.info(f"RefSeq\n{prot_ref_seq}") + logger.info(f"VarSeq\n{prot_var_seq}") + + # legacy code ################################################################# + # pro_inv_info['prot_ins_seq'] = pro_inv_info['prot_ins_seq'].replace("U", "*") + # pro_inv_info['ter_pos'] = pro_inv_info['edit_start'] + len( + # pro_inv_info['prot_ins_seq'].split("*")[0]) + ################################################################################ + pro_inv_info['ter_pos'] = pro_inv_info['edit_start'] + len( - pro_inv_info['prot_ins_seq'].split("*")[0]) + pro_inv_info['prot_ins_seq']) + + # Set posedit + posedit = False # Early termination i.e. stop gained if pro_inv_info['terminate'] == 'true' and \ @@ -654,9 +673,69 @@ def _remake_unc(prot,nucleotide_not_equal=False): hgvs_transcript.posedit.edit.type == 'dup' or hgvs_transcript.posedit.edit.type == 'inv'): + # Identify missed frameshifts + hgvs_n_transcript = self.vm.c_to_n(hgvs_transcript) + + edit = hgvs_n_transcript.posedit.edit + pos = hgvs_n_transcript.posedit.pos + frameshift = False + + if edit.type == "dup": + length = pos.end.base - pos.start.base + 1 + frameshift = length % 3 != 0 + + elif edit.type == "del": + ref = edit.ref or "" + frameshift = len(ref) % 3 != 0 + + elif edit.type == "ins": + alt = edit.alt or "" + frameshift = len(alt) % 3 != 0 + + elif edit.type == "delins": + ref = edit.ref or "" + alt = edit.alt or "" + frameshift = (len(alt) - len(ref)) % 3 != 0 + + elif edit.type == "inv": + frameshift = False + + if frameshift: + logger.info("Identified unhandled frameshift in pro_inv_info['pro_ins_seq']") + + ref = pro_inv_info['prot_del_seq'][0] # "D" + alt = pro_inv_info['prot_ins_seq'][0] # "U" + + length = pro_inv_info['prot_ins_seq'].find("*") + length = length if length >= 0 else None + + posedit = PosEdit( + pos=Interval( + start=AAPosition( + base=pro_inv_info['edit_start'], + aa=ref # THIS is the fix + ), + end=AAPosition( + base=pro_inv_info['edit_start'], + aa=ref + ) + ), + edit=AAFs( + ref=ref, + alt=alt, + length=length + ) + ) + logger.info(f"Posedit updated to {posedit}") + hgvs_protein = vvhgvs.sequencevariant.SequenceVariant( + ac=associated_protein_accession, type='p', posedit=posedit) + + hgvs_transcript_to_hgvs_protein['hgvs_protein'] = hgvs_protein + return hgvs_transcript_to_hgvs_protein + # This deals with early terminating delins in-frame prventing the format # NP_733765.1:p.(Gln259_Ser1042delinsProAla*) in issue #214 also #282 - if len(pro_inv_info['prot_del_seq']) + \ + elif len(pro_inv_info['prot_del_seq']) + \ int(pro_inv_info['edit_start'] - 1) == int(pro_inv_info['ter_pos']): end = 'Ter' + str(pro_inv_info['ter_pos']) pro_inv_info['prot_ins_seq'].replace('*', end) @@ -680,7 +759,6 @@ def _remake_unc(prot,nucleotide_not_equal=False): pro_inv_info['prot_ins_seq'] # Complete variant description - # Write the HGVS position and edit # start by handling delins->ins transitions from the cds to prot mapping if not pro_inv_info['prot_del_seq']: @@ -800,6 +878,7 @@ def _remake_unc(prot,nucleotide_not_equal=False): alt = pro_inv_info["prot_ins_seq"][1:]),#[3:]), uncertain = True, nucleotide_not_equal=nucleotide_not_equal) + # Handle extended proteins i.e. stop_lost elif pro_inv_info["prot_del_seq"] == '*' and ( len(pro_inv_info["prot_ins_seq"]) > len(pro_inv_info["prot_del_seq"])): @@ -858,6 +937,7 @@ def _remake_unc(prot,nucleotide_not_equal=False): posedit = posedit ) hgvs_transcript_to_hgvs_protein['hgvs_protein'] = hgvs_protein + # Return return hgvs_transcript_to_hgvs_protein diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 95679fc5..6e4403bc 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -30900,9 +30900,9 @@ def test_issue_503b(self): variant = 'NM_020451.2:c.481C>T' results = self.vv.validate(variant, 'GRCh37', 'all', liftover_level='primary').format_as_dict(test=True) print(results) - assert 'NP_065184.2:p.(Arg161Ter)' in \ + assert 'NP_065184.2:p.(Arg161Sec)' in \ results['NM_020451.2:c.481C>T']['hgvs_predicted_protein_consequence']['tlr'] - assert 'NP_065184.2:p.(R161*)' in \ + assert 'NP_065184.2:p.(R161U)' in \ results['NM_020451.2:c.481C>T']['hgvs_predicted_protein_consequence']['slr'] def test_issue_503c(self): @@ -31261,46 +31261,52 @@ def polyadenylation_b(self): results = self.vv.validate('2:g.46707878_46707879insGCAGCGGGAGCG', 'GRCh37', 'mane_select').format_as_dict(test=True) assert 'NM_001145051.2:c.457_458insGGGAGCGGCAGC' in results - def cnv_del_vcf(self): - # Test that it fails for genome mismatch + def test_cnv_del_vcf(self): + # Test that copy number variant VCF input works results = self.vv.validate('chr1\t1000000\t.\tN\t\t.\tPASS\tSVTYPE=DEL;END=1005000', 'GRCh38', 'all').format_as_dict(test=True) assert 'intergenic_variant_1' in results assert results["intergenic_variant_1"]["primary_assembly_loci"]["grch38"][ "hgvs_genomic_description"] == "NC_000001.11:g.1000000_1005000del" - def issue_786_part_B(self): - # Test that it fails for genome mismatch + def test_issue_786_part_B(self): + # Test that we successfully round trip results = self.vv.validate('NC_000023.10:g.18908328_18911824delinsGCCTGCAGAG', 'GRCh37', 'all', liftover_level=True).format_as_dict(test=True) - assert 'intergenic_variant_1' in results + print(results) assert results["intergenic_variant_1"]["primary_assembly_loci"]["grch37"][ "hgvs_genomic_description"] == "NC_000023.10:g.18908328_18911824delinsGCCTGCAGAG" assert results["intergenic_variant_1"]["primary_assembly_loci"]["grch38"][ "hgvs_genomic_description"] == "NC_000023.11:g.18890210_18893706delinsGCCTGCAGAG" - def issue_786_part_A1(self): - # Test that it fails for genome mismatch + def test_issue_786_part_A1(self): + # Test that we successfully round trip results = self.vv.validate('NC_000015.9:g.72105928del', 'GRCh37', 'mane_select', liftover_level=True).format_as_dict(test=True) - assert 'intergenic_variant_1' in results + print(results) assert results["NM_014249.4:c.947A>C"]["primary_assembly_loci"]["grch37"][ "hgvs_genomic_description"] == "NC_000015.9:g.72105928del" assert results["NM_014249.4:c.947A>C"]["primary_assembly_loci"]["grch38"][ "hgvs_genomic_description"] == "NC_000015.10:g.71813588A>C" - def issue_786_part_A2(self): - # Test that it fails for genome mismatch + def test_issue_786_part_A2(self): + # Test that we successfully round trip results = self.vv.validate('NC_000015.9:g.72105933dup', 'GRCh37', 'mane_select', liftover_level=True).format_as_dict(test=True) - assert 'intergenic_variant_1' in results - assert results["NM_014249.4:c.947A>C"]["primary_assembly_loci"]["grch37"][ + print(results) + assert results["NM_014249.4:c.950_951dup"]["primary_assembly_loci"]["grch37"][ "hgvs_genomic_description"] == "NC_000015.9:g.72105933dup" - assert results["NM_014249.4:c.947A>C"]["primary_assembly_loci"]["grch38"][ + assert results["NM_014249.4:c.950_951dup"]["primary_assembly_loci"]["grch38"][ "hgvs_genomic_description"] == "NC_000015.10:g.71813591_71813592dup" - def issue_801(self): + def test_issue_801(self): results = self.vv.validate('15-71818229-TA-T', 'GRCh38', 'all', liftover_level=True).format_as_dict(test=True) assert "NM_014249.4:c.*557del" in results.keys() assert "NM_001281446.1:c.*557del" in results.keys() - + def test_issue_815a(self): + results = self.vv.validate('NM_020451.3:c.447dup', 'GRCh38', 'all', liftover_level=True).format_as_dict(test=True) + assert "NM_020451.3:c.447dup" in results.keys() + assert results["NM_020451.3:c.447dup"]["hgvs_predicted_protein_consequence"]['slr'] == "NP_065184.2:p.D150Ufs*2" + assert results["NM_020451.3:c.447dup"]["hgvs_predicted_protein_consequence"]['tlr'] == "NP_065184.2:p.Asp150SecfsTer2" + #assert results["NM_020451.3:c.447dup"]["hgvs_predicted_protein_consequence"]['lrg_slr'] == "LRG_857p1:p.D150Ufs*2" + #assert results["NM_020451.3:c.447dup"]["hgvs_predicted_protein_consequence"]['lrg_tlr'] == "LRG_857p1:p.Asp150SecfsTer2" # # Copyright (C) 2016-2026 VariantValidator Contributors From 31737d439a7563edb604f0569fe4d103d7657372 Mon Sep 17 00:00:00 2001 From: Peter-J-Freeman Date: Sat, 16 May 2026 00:11:33 +0100 Subject: [PATCH 2/6] Updates to issue #815 to fix dels. Note, dels translations are now fully handled by VV ocde and not vv_hgvs. --- VariantValidator/modules/mappers.py | 8 ++++++ VariantValidator/modules/utils.py | 2 ++ VariantValidator/modules/vvMixinInit.py | 33 ++++++++++++++++++------- tests/test_inputs.py | 16 ++++++++++++ 4 files changed, 50 insertions(+), 9 deletions(-) diff --git a/VariantValidator/modules/mappers.py b/VariantValidator/modules/mappers.py index da933e29..4202ae35 100644 --- a/VariantValidator/modules/mappers.py +++ b/VariantValidator/modules/mappers.py @@ -759,15 +759,23 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version hgvs_refseq = 'RefSeqGene record not available' # Predicted effect on protein + logger.info(f"Translating {hgvs_coding} in transcripts_to_gene") try: protein_dict = validator.myc_to_p(hgvs_coding, variant.evm, re_to_p=False, hn=variant.hn) except NotImplementedError as e: + # import traceback + # traceback.print_exc() logger.info(f"Protein dict creation failed with exception: {str(e)}") protein_dict = {'hgvs_protein': None, 'error': str(e)} variant.warnings.append(str(e)) except vvhgvs.exceptions.HGVSDataNotAvailableError as e: + # import traceback + # traceback.print_exc() + logger.info(f"Protein dict creation failed with exception: {str(e)}") protein_dict = {'hgvs_protein': None, 'error': str(e)} variant.warnings.append(str(e)) + else: + logger.info(f"Protein dict creation successful: {protein_dict}") if protein_dict['error'] == '': hgvs_protein = protein_dict['hgvs_protein'] diff --git a/VariantValidator/modules/utils.py b/VariantValidator/modules/utils.py index 928c60d5..2d873b3e 100644 --- a/VariantValidator/modules/utils.py +++ b/VariantValidator/modules/utils.py @@ -294,6 +294,7 @@ def pro_inv_info(prot_ref_seq, prot_var_seq): """ Function which predicts the protein effect of c. inversions """ + logger.info("pro_inv_info function called") info = { 'variant': 'true', 'prot_del_seq': '', @@ -392,6 +393,7 @@ def pro_inv_info(prot_ref_seq, prot_var_seq): def pro_delins_info(prot_ref_seq, prot_var_seq, in_frame=False): + logger.info(f"pro_delins_info function called") info = { 'variant': 'true', 'prot_del_seq': '', diff --git a/VariantValidator/modules/vvMixinInit.py b/VariantValidator/modules/vvMixinInit.py index d3e5ed59..72ccb4d3 100644 --- a/VariantValidator/modules/vvMixinInit.py +++ b/VariantValidator/modules/vvMixinInit.py @@ -250,6 +250,7 @@ def my_config(self): } def myc_to_p(self, hgvs_transcript, evm, re_to_p, hn): + logger.info(f"Translating {hgvs_transcript} to with myc_to_p") # Create dictionary to store the information hgvs_transcript_to_hgvs_protein = {'error': '', 'hgvs_protein': '', 'ref_residues': ''} @@ -291,6 +292,7 @@ def _fb_unc(prot,base): aa=base)), edit = "", # this sets the response to ? uncertain = True)) + # same for unknown without set pos def _tot_unc(prot): return vvhgvs.sequencevariant.SequenceVariant( @@ -300,6 +302,7 @@ def _tot_unc(prot): pos=Interval(),# empty interval start means '' edit = "", # this sets the response to ? uncertain=True)) + # recreate obj to set PosEdit to a VVPosEdit, to handle formatting def _remake_unc(prot,nucleotide_not_equal=False): if prot.posedit is None: @@ -314,8 +317,10 @@ def _remake_unc(prot,nucleotide_not_equal=False): nucleotide_not_equal=nucleotide_not_equal )) - # Handle non inversions with simple c_to_p mapping - if hgvs_transcript.posedit.edit.type not in ['inv', 'dup', 'delins', 'sub', 'identity'] and (re_to_p is False): + # Handle unlisted variant types with simple c_to_p mapping + if (hgvs_transcript.posedit.edit.type not in ['inv', 'dup', 'delins', 'sub', 'identity', 'del'] + and (re_to_p is False)): + logger.info(f"Passing {hgvs_transcript} into simple c_to_p mapping") hgvs_protein = None # Does the edit affect the start codon? if ((1 <= hgvs_transcript.posedit.pos.start.base <= 3 and hgvs_transcript.posedit.pos.start.offset == 0) @@ -365,6 +370,7 @@ def _remake_unc(prot,nucleotide_not_equal=False): # Note, this code was developed for VariantValidator and is not native to the biocommons hgvs # Python package # Convert positions to n. position + logger.info(f"Passing {hgvs_transcript} into VV handled c_to_p mapping") hgvs_naughty = self.vm.c_to_n(hgvs_transcript) # Collect the deleted sequence using fetch_seq @@ -537,9 +543,12 @@ def _remake_unc(prot,nucleotide_not_equal=False): # Gather the required information regarding variant interval and sequences if hgvs_transcript.posedit.edit.type != 'delins' and \ - hgvs_transcript.posedit.edit.type != 'dup': + hgvs_transcript.posedit.edit.type != 'dup' and \ + hgvs_transcript.posedit.edit.type != 'del': + logger.info(f"passing {hgvs_transcript} translations to pro_inv_info function") pro_inv_info = utils.pro_inv_info(prot_ref_seq, prot_var_seq) else: + logger.info(f"passing {hgvs_transcript} translations to pro_delins_info function") # Test whether the length of the deletion, plus the insertion can be divided by 3 # This is trying to spot the difference between amino acid deletions # and early terminations @@ -551,6 +560,10 @@ def _remake_unc(prot,nucleotide_not_equal=False): minus = False plus = False + # Handle deletions + if hgvs_naughty.posedit.edit.type == 'del': + hgvs_naughty.posedit.edit.alt = "" + try: if len(hgvs_naughty.posedit.edit.ref) > len(hgvs_naughty.posedit.edit.alt): var_cds_len = cds_len - (len(hgvs_naughty.posedit.edit.ref) @@ -569,7 +582,7 @@ def _remake_unc(prot,nucleotide_not_equal=False): # Do we have an in-frame variant i.e. divisible by 3? in_frame = False if minus is True: - loss_gain = (cds_len) - (var_cds_len) + loss_gain = cds_len - var_cds_len if loss_gain % 3 == 0: loss_gain = loss_gain / 3 loss_gain = 0 - loss_gain @@ -585,6 +598,8 @@ def _remake_unc(prot,nucleotide_not_equal=False): prot_var_seq, in_frame) + logger.info(f"RefSeq: {prot_ref_seq}") + logger.info(f"VarSeq: {prot_var_seq}") logger.info(f"pro_inv_info: {pro_inv_info}") # Error has occurred @@ -652,8 +667,6 @@ def _remake_unc(prot,nucleotide_not_equal=False): if modified_aa == "Sec": if "U" in pro_inv_info['prot_ins_seq'] and "U" not in pro_inv_info['prot_del_seq']: logger.info("Sec identified in pro_inv_info['prot_ins_seq']") - logger.info(f"RefSeq\n{prot_ref_seq}") - logger.info(f"VarSeq\n{prot_var_seq}") # legacy code ################################################################# # pro_inv_info['prot_ins_seq'] = pro_inv_info['prot_ins_seq'].replace("U", "*") @@ -674,10 +687,12 @@ def _remake_unc(prot,nucleotide_not_equal=False): hgvs_transcript.posedit.edit.type == 'inv'): # Identify missed frameshifts - hgvs_n_transcript = self.vm.c_to_n(hgvs_transcript) - edit = hgvs_n_transcript.posedit.edit - pos = hgvs_n_transcript.posedit.pos + # hgvs_n_transcript = self.vm.c_to_n(hgvs_transcript) + # edit = hgvs_n_transcript.posedit.edit + # pos = hgvs_n_transcript.posedit.pos + edit = hgvs_naughty.posedit.edit + pos = hgvs_naughty.posedit.pos frameshift = False if edit.type == "dup": diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 6e4403bc..a4335165 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -31308,6 +31308,22 @@ def test_issue_815a(self): #assert results["NM_020451.3:c.447dup"]["hgvs_predicted_protein_consequence"]['lrg_slr'] == "LRG_857p1:p.D150Ufs*2" #assert results["NM_020451.3:c.447dup"]["hgvs_predicted_protein_consequence"]['lrg_tlr'] == "LRG_857p1:p.Asp150SecfsTer2" + def test_issue_815b(self): + results = self.vv.validate('NM_020451.3:c.407del', 'GRCh38', 'all', liftover_level=True).format_as_dict(test=True) + assert "NM_020451.3:c.407del" in results.keys() + assert results["NM_020451.3:c.407del"]["hgvs_predicted_protein_consequence"]['slr'] == "NP_065184.2:p.(S136*)" + assert results["NM_020451.3:c.407del"]["hgvs_predicted_protein_consequence"]['tlr'] == "NP_065184.2:p.(Ser136Ter)" + #assert results["NM_020451.3:c.407del"]["hgvs_predicted_protein_consequence"]['lrg_slr'] == "LRG_857p1:p.(S136*)" + #assert results["NM_020451.3:c.407del"]["hgvs_predicted_protein_consequence"]['lrg_tlr'] == "LRG_857p1:p.(Ser136Ter)" + + def test_issue_815c(self): + results = self.vv.validate('NM_020451.3:c.532del', 'GRCh38', 'all', liftover_level=True).format_as_dict(test=True) + assert "NM_020451.3:c.532del" in results.keys() + assert results["NM_020451.3:c.532del"]["hgvs_predicted_protein_consequence"]['slr'] == "NP_065184.2:p.(L178*)" + assert results["NM_020451.3:c.532del"]["hgvs_predicted_protein_consequence"]['tlr'] == "NP_065184.2:p.(Leu178Ter)" + #assert results["NM_020451.3:c.532del"]["hgvs_predicted_protein_consequence"]['lrg_slr'] == "LRG_857p1:p.(L178*)" + #assert results["NM_020451.3:c.532del"]["hgvs_predicted_protein_consequence"]['lrg_tlr'] == "LRG_857p1:p.(Leu178Ter)" + # # Copyright (C) 2016-2026 VariantValidator Contributors # From d7530a00b05d029aa15a8b66ad5679b44d09bb35 Mon Sep 17 00:00:00 2001 From: Peter-J-Freeman Date: Mon, 18 May 2026 17:57:29 +0100 Subject: [PATCH 3/6] Final fixes for #815, inc. switch to only VV c > p --- VariantValidator/modules/vvMixinInit.py | 28 ++++++++++++++++++------- tests/test_inputs.py | 8 +++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/VariantValidator/modules/vvMixinInit.py b/VariantValidator/modules/vvMixinInit.py index 72ccb4d3..f72e48b9 100644 --- a/VariantValidator/modules/vvMixinInit.py +++ b/VariantValidator/modules/vvMixinInit.py @@ -318,10 +318,11 @@ def _remake_unc(prot,nucleotide_not_equal=False): )) # Handle unlisted variant types with simple c_to_p mapping - if (hgvs_transcript.posedit.edit.type not in ['inv', 'dup', 'delins', 'sub', 'identity', 'del'] + if (hgvs_transcript.posedit.edit.type not in ['inv', 'dup', 'delins', 'sub', 'identity', 'del', 'ins'] and (re_to_p is False)): logger.info(f"Passing {hgvs_transcript} into simple c_to_p mapping") hgvs_protein = None + # Does the edit affect the start codon? if ((1 <= hgvs_transcript.posedit.pos.start.base <= 3 and hgvs_transcript.posedit.pos.start.offset == 0) or (1 <= hgvs_transcript.posedit.pos.end.base <= 3 and hgvs_transcript.posedit.pos.end.offset @@ -392,6 +393,8 @@ def _remake_unc(prot,nucleotide_not_equal=False): inv_seq = hgvs_transcript.posedit.edit.alt elif 'identity' in hgvs_transcript.posedit.edit.type: inv_seq = hgvs_transcript.posedit.edit.ref + elif 'ins' in hgvs_transcript.posedit.edit.type: + inv_seq = f"{del_seq[0]}{hgvs_transcript.posedit.edit.alt}{del_seq[-1]}" shifts = '' # Look for p. delins or del @@ -510,6 +513,8 @@ def _remake_unc(prot,nucleotide_not_equal=False): ' accepted input for protein generation.' hgvs_transcript_to_hgvs_protein['hgvs_protein'] = _tot_unc(associated_protein_accession) return hgvs_transcript_to_hgvs_protein + + # Continue processing no_start_err = 'ProteinTranslationError: Unable to generate protein variant description '+\ 'due to the sequence missing an accepted start codon.' if prot_ref_seq == 'error': @@ -544,7 +549,8 @@ def _remake_unc(prot,nucleotide_not_equal=False): # Gather the required information regarding variant interval and sequences if hgvs_transcript.posedit.edit.type != 'delins' and \ hgvs_transcript.posedit.edit.type != 'dup' and \ - hgvs_transcript.posedit.edit.type != 'del': + hgvs_transcript.posedit.edit.type != 'del' and \ + hgvs_transcript.posedit.edit.type != 'ins': logger.info(f"passing {hgvs_transcript} translations to pro_inv_info function") pro_inv_info = utils.pro_inv_info(prot_ref_seq, prot_var_seq) else: @@ -563,6 +569,10 @@ def _remake_unc(prot,nucleotide_not_equal=False): # Handle deletions if hgvs_naughty.posedit.edit.type == 'del': hgvs_naughty.posedit.edit.alt = "" + if hgvs_naughty.posedit.edit.type == 'ins': + hgvs_naughty.posedit.edit.ref = del_seq + hgvs_naughty.posedit.edit.alt = f"{del_seq[0]}{hgvs_naughty.posedit.edit.alt}{del_seq[-1]}" + try: if len(hgvs_naughty.posedit.edit.ref) > len(hgvs_naughty.posedit.edit.alt): @@ -681,19 +691,18 @@ def _remake_unc(prot,nucleotide_not_equal=False): posedit = False # Early termination i.e. stop gained + logger.info(f"Identify early termination") if pro_inv_info['terminate'] == 'true' and \ (hgvs_transcript.posedit.edit.type == 'delins' or hgvs_transcript.posedit.edit.type == 'dup' or - hgvs_transcript.posedit.edit.type == 'inv'): + hgvs_transcript.posedit.edit.type == 'inv' or + hgvs_transcript.posedit.edit.type == 'ins'): # Identify missed frameshifts - - # hgvs_n_transcript = self.vm.c_to_n(hgvs_transcript) - # edit = hgvs_n_transcript.posedit.edit - # pos = hgvs_n_transcript.posedit.pos edit = hgvs_naughty.posedit.edit pos = hgvs_naughty.posedit.pos frameshift = False + logger.info(f"Early termination identified from edit type {edit.type}") if edit.type == "dup": length = pos.end.base - pos.start.base + 1 @@ -752,15 +761,18 @@ def _remake_unc(prot,nucleotide_not_equal=False): # NP_733765.1:p.(Gln259_Ser1042delinsProAla*) in issue #214 also #282 elif len(pro_inv_info['prot_del_seq']) + \ int(pro_inv_info['edit_start'] - 1) == int(pro_inv_info['ter_pos']): + logger.info(f"Identified unhandled frameshift") end = 'Ter' + str(pro_inv_info['ter_pos']) pro_inv_info['prot_ins_seq'].replace('*', end) pro_inv_info['prot_ins_seq'] = pro_inv_info['prot_ins_seq'] pro_inv_info['prot_del_seq'] = pro_inv_info['prot_del_seq'][0] pro_inv_info['edit_end'] = pro_inv_info['edit_start'] + elif hgvs_transcript.posedit.edit.type == 'dup' and pro_inv_info["prot_del_seq"] \ == "" and (int(pro_inv_info["edit_end"]) < int(pro_inv_info["edit_start"])): # Handles in-frame dups only + logger.info(f"Identified unhandled frameshift from dup") dup_len = (int(hgvs_transcript.posedit.pos.end.base) - int( hgvs_transcript.posedit.pos.start.base) + 1) / 3 pro_inv_info['prot_del_seq'] = pro_inv_info['prot_ins_seq'] @@ -772,6 +784,8 @@ def _remake_unc(prot,nucleotide_not_equal=False): pro_inv_info['prot_del_seq'] = start_aa pro_inv_info['prot_ins_seq'] = start_aa + \ pro_inv_info['prot_ins_seq'] + else: + logger.info("No unhandled frameshift in pro_inv_info['pro_ins_seq']") # Complete variant description # Write the HGVS position and edit diff --git a/tests/test_inputs.py b/tests/test_inputs.py index a4335165..0f51932f 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -31324,6 +31324,14 @@ def test_issue_815c(self): #assert results["NM_020451.3:c.532del"]["hgvs_predicted_protein_consequence"]['lrg_slr'] == "LRG_857p1:p.(L178*)" #assert results["NM_020451.3:c.532del"]["hgvs_predicted_protein_consequence"]['lrg_tlr'] == "LRG_857p1:p.(Leu178Ter)" + def test_issue_815d(self): + results = self.vv.validate('NM_020451.3:c.406_407insG', 'GRCh38', 'all', liftover_level=True).format_as_dict(test=True) + assert "NM_020451.3:c.406_407insG" in results.keys() + assert results["NM_020451.3:c.406_407insG"]["hgvs_predicted_protein_consequence"]['slr'] == "NP_065184.2:p.S136Cfs*16" + assert results["NM_020451.3:c.406_407insG"]["hgvs_predicted_protein_consequence"]['tlr'] == "NP_065184.2:p.Ser136CysfsTer16" + #assert results["NM_020451.3:c.406_407insG"]["hgvs_predicted_protein_consequence"]['lrg_slr'] == "LRG_857p1:p.S136Cfs*16" + #assert results["NM_020451.3:c.406_407insG"]["hgvs_predicted_protein_consequence"]['lrg_tlr'] == "LRG_857p1:p.Ser136CysfsTer16" + # # Copyright (C) 2016-2026 VariantValidator Contributors # From d0c94c05e660b5b00b7f8c79b3e07850396d6a84 Mon Sep 17 00:00:00 2001 From: John-F-Wagstaff Date: Wed, 27 May 2026 23:11:10 +0100 Subject: [PATCH 4/6] Add user info when Selenocysteine might affect Ter Test included. --- VariantValidator/modules/mappers.py | 12 +++++++++--- VariantValidator/modules/vvMixinInit.py | 3 +++ tests/test_inputs.py | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/VariantValidator/modules/mappers.py b/VariantValidator/modules/mappers.py index 4202ae35..6416816c 100644 --- a/VariantValidator/modules/mappers.py +++ b/VariantValidator/modules/mappers.py @@ -777,8 +777,10 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version else: logger.info(f"Protein dict creation successful: {protein_dict}") - if protein_dict['error'] == '': + if protein_dict['error'] == '' or protein_dict['error'].startswith('ProteinTranslationInfo:'): hgvs_protein = protein_dict['hgvs_protein'] + if protein_dict['error']: + variant.warnings.append(protein_dict['error']) else: error = protein_dict['error'] if not error.startswith('ProteinTranslationError:' ): @@ -837,8 +839,10 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version try: # Predicted effect on protein protein_dict = validator.myc_to_p(c_for_p, variant.evm, re_to_p=False, hn=variant.hn) - if protein_dict['error'] == '': + if protein_dict['error'] == '' or protein_dict['error'].startswith('ProteinTranslationInfo:'): hgvs_protein = protein_dict['hgvs_protein'] + if protein_dict['error']: + variant.warnings.append(protein_dict['error']) else: error = protein_dict['error'] if error == 'Cannot identify an in-frame Termination codon in the variant mRNA sequence': @@ -854,8 +858,10 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version if hgvs_coding.posedit.pos.start.offset == 0 and hgvs_coding.posedit.pos.start.offset == 0 and \ '?' in str(hgvs_protein): protein_dict = validator.myc_to_p(hgvs_coding, variant.evm, re_to_p=False, hn=variant.hn) - if protein_dict['error'] == '': + if protein_dict['error'] == '' or protein_dict['error'].startswith('ProteinTranslationInfo:'): hgvs_protein = protein_dict['hgvs_protein'] + if protein_dict['error']: + variant.warnings.append(protein_dict['error']) else: error = protein_dict['error'] if error == 'Cannot identify an in-frame Termination codon in the variant mRNA sequence': diff --git a/VariantValidator/modules/vvMixinInit.py b/VariantValidator/modules/vvMixinInit.py index f72e48b9..983db35c 100644 --- a/VariantValidator/modules/vvMixinInit.py +++ b/VariantValidator/modules/vvMixinInit.py @@ -469,6 +469,9 @@ def _remake_unc(prot,nucleotide_not_equal=False): prot_seq = self.sf.fetch_seq(associated_protein_accession) if "U" in prot_seq: modified_aa = "Sec" + hgvs_transcript_to_hgvs_protein['error'] = \ + 'ProteinTranslationInfo: Selenocysteine detected in the original protein sequnce'+\ + ' it may be incorporated instead of terminating at TGA/UGA termination codons' else: modified_aa = None diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 0f51932f..2e071f72 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -31332,6 +31332,22 @@ def test_issue_815d(self): #assert results["NM_020451.3:c.406_407insG"]["hgvs_predicted_protein_consequence"]['lrg_slr'] == "LRG_857p1:p.S136Cfs*16" #assert results["NM_020451.3:c.406_407insG"]["hgvs_predicted_protein_consequence"]['lrg_tlr'] == "LRG_857p1:p.Ser136CysfsTer16" + def test_issue_815e(self): + # test that variants affected by selenocysteine edits inform users + results = self.vv.validate('NM_020451.3:c.406_407insG', 'GRCh38', 'all', liftover_level=True).format_as_dict(test=True) + print(results) + assert "NM_020451.3:c.406_407insG" in results.keys() + assert results["NM_020451.3:c.406_407insG"]["hgvs_predicted_protein_consequence"]['slr'] == "NP_065184.2:p.S136Cfs*16" + assert results["NM_020451.3:c.406_407insG"]["hgvs_predicted_protein_consequence"]['tlr'] == "NP_065184.2:p.Ser136CysfsTer16" + #assert results["NM_020451.3:c.406_407insG"]["hgvs_predicted_protein_consequence"]['lrg_slr'] == "LRG_857p1:p.S136Cfs*16" + #assert results["NM_020451.3:c.406_407insG"]["hgvs_predicted_protein_consequence"]['lrg_tlr'] == "LRG_857p1:p.Ser136CysfsTer16" + + sec_warn_found = False + for warn in results["NM_020451.3:c.406_407insG"]["validation_warnings"]: + if warn.startswith('ProteinTranslationInfo: Sel'): + sec_warn_found = True + assert sec_warn_found + # # Copyright (C) 2016-2026 VariantValidator Contributors # From b386fa053a7185e9d9b7b417def9de85aea915bd Mon Sep 17 00:00:00 2001 From: Peter-J-Freeman Date: Tue, 26 May 2026 15:09:30 +0100 Subject: [PATCH 5/6] slight tweaks to the chrName to ID dicts --- VariantValidator/modules/seq_data.py | 2 -- VariantValidator/modules/vvMixinInit.py | 7 +++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/VariantValidator/modules/seq_data.py b/VariantValidator/modules/seq_data.py index 6d267aab..f483cd28 100644 --- a/VariantValidator/modules/seq_data.py +++ b/VariantValidator/modules/seq_data.py @@ -1742,7 +1742,6 @@ def to_accession(chr_num, primary_assembly): 'HSCHR1_6_CTG31': 'NW_025791755.1', 'HG1384_PATCH': 'NW_021159988.1', 'HG2231_HG2496_PATCH': 'NW_025791767.1', - 'NW_025791768.1': 'NW_025791768.1', 'HG2052_PATCH' : 'NW_025791766.1', 'HSCHR2_6_CTG1': 'NW_025791763.1', 'HSCHR2_10_CTG7_2': 'NW_025791760.1', @@ -3305,7 +3304,6 @@ def to_chr_num_refseq(accession, primary_assembly): 'NW_025791755.1': 'HSCHR1_6_CTG31', 'NW_021159988.1': 'HG1384_PATCH', 'NW_025791767.1': 'HG2231_HG2496_PATCH', - 'NW_025791768.1': 'NW_025791768.1', 'NW_025791766.1': 'HG2052_PATCH', 'NW_025791763.1': 'HSCHR2_6_CTG1', 'NW_025791760.1': 'HSCHR2_10_CTG7_2', diff --git a/VariantValidator/modules/vvMixinInit.py b/VariantValidator/modules/vvMixinInit.py index 983db35c..b9b24e5b 100644 --- a/VariantValidator/modules/vvMixinInit.py +++ b/VariantValidator/modules/vvMixinInit.py @@ -465,6 +465,8 @@ def _remake_unc(prot,nucleotide_not_equal=False): hgvs_naughty.posedit.pos.start.base, hgvs_naughty.posedit.pos.end.base) + logger.info(f"\nReference sequence:\n{ref_seq}\nDeletion sequence:\n{del_seq}\nInserted sequence:\n{inv_seq}\nVar sequence:\n{var_seq}") + # Check for modified amino acids prot_seq = self.sf.fetch_seq(associated_protein_accession) if "U" in prot_seq: @@ -472,10 +474,13 @@ def _remake_unc(prot,nucleotide_not_equal=False): hgvs_transcript_to_hgvs_protein['error'] = \ 'ProteinTranslationInfo: Selenocysteine detected in the original protein sequnce'+\ ' it may be incorporated instead of terminating at TGA/UGA termination codons' + logger.info(f"Modified amino acid {modified_aa} identified, update translation dict") else: modified_aa = None + logger.info("No modified amino acid identified, use standard translation dict") # Translate the reference and variant proteins + logger.info("Translating reference and variant CDS outcomes") try: prot_ref_seq = utils.translate(ref_seq, cds_start, modified_aa) except IndexError: @@ -497,8 +502,6 @@ def _remake_unc(prot,nucleotide_not_equal=False): hgvs_transcript_to_hgvs_protein['hgvs_protein'] = _tot_unc(associated_protein_accession) return hgvs_transcript_to_hgvs_protein - - logger.info("Translating reference and variant CDS outcomes") try: prot_var_seq = utils.translate(var_seq, cds_start, modified_aa) except IndexError: From ed2032285ee39fdaf693656b4448bea08e1cab8c Mon Sep 17 00:00:00 2001 From: Peter-J-Freeman Date: Tue, 26 May 2026 15:48:33 +0100 Subject: [PATCH 6/6] Add additional Sec inclusion test related to issue #818 --- tests/test_inputs.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_inputs.py b/tests/test_inputs.py index 2e071f72..5fc3bc61 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -31348,6 +31348,17 @@ def test_issue_815e(self): sec_warn_found = True assert sec_warn_found + def issue_818(self): + results = self.vv.validate('NC_000022.10:g.19929250_19929251insCCCCGCC', 'GRCh38', 'mane_select', liftover_level=True).format_as_dict(test=True) + assert "NM_006440.5:c.70_76dup" in results.keys() + assert results["NM_006440.5:c.70_76dup"][ + "hgvs_predicted_protein_consequence"] == { + "lrg_slr": "LRG_417p1:p.V26Gfs*132", + "lrg_tlr": "LRG_417p1:p.Val26GlyfsTer132", + "slr": "NP_006431.2:p.V26Gfs*132", + "tlr": "NP_006431.2:p.Val26GlyfsTer132" + } + # # Copyright (C) 2016-2026 VariantValidator Contributors #