From 8e1fcb76e4d13da0539a439b65ca528b42ca5b20 Mon Sep 17 00:00:00 2001 From: John-F-Wagstaff Date: Tue, 9 Jun 2026 17:17:41 +0100 Subject: [PATCH 1/3] Reduce unneeded globals in variant The variant object set up has accumulated a number of now unneeded global variables for hgvs data, several of which where only needed prior to converting the quibble to a hgvs object. These make the code more complex to understand and manage and interfered with finding the correct data for VRS input, so purge them while the understanding is still fresh from untangling the VRS inputs. --- .../modules/complex_descriptions.py | 84 ++-- VariantValidator/modules/exon_numbering.py | 12 +- VariantValidator/modules/format_converters.py | 59 ++- VariantValidator/modules/gapped_mapping.py | 4 +- VariantValidator/modules/gene2transcripts.py | 6 +- VariantValidator/modules/hgvs_utils.py | 16 +- VariantValidator/modules/mappers.py | 167 ++++---- VariantValidator/modules/use_checking.py | 371 +++++++++--------- VariantValidator/modules/variant.py | 22 +- VariantValidator/modules/vvMixinCore.py | 140 ++++--- tests/test_variant.py | 9 - 11 files changed, 460 insertions(+), 430 deletions(-) diff --git a/VariantValidator/modules/complex_descriptions.py b/VariantValidator/modules/complex_descriptions.py index 991618f7..89351f0e 100644 --- a/VariantValidator/modules/complex_descriptions.py +++ b/VariantValidator/modules/complex_descriptions.py @@ -2,8 +2,10 @@ import copy from vvhgvs.assemblymapper import AssemblyMapper from VariantValidator.modules import utils as vv_utils, format_converters -from VariantValidator.modules.hgvs_utils import hgvs_obj_from_existing_edit +from VariantValidator.modules.hgvs_utils import hgvs_obj_from_existing_edit,\ + _hgvs_offset_pos_from_str_in import vvhgvs.exceptions +from vvhgvs.enums import Datum, ValidationLevel from vvhgvs.location import Interval class UncertainConversionError(Exception): @@ -12,8 +14,10 @@ class UncertainConversionError(Exception): # fuzzy but specified ended intervals, take a normal interval for both ends # can take a pair of Intervals or BaseOffsetIntervals class FEInterval(Interval): - uncertain = True - pass + def __init__(self,start = None,end = None): + self.uncertain = True + self.start = start + self.end = end def validate(self): if self.start: (res, msg) = self.start.validate() @@ -258,12 +262,12 @@ def fuzzy_ends(my_variant, validator): raise FuzzyRangeError("Fuzzy/unknown variant end position in submitted variant description") else: - if "?" in str(my_variant.hgvs_formatted.posedit.pos): - if "?" in str(my_variant.hgvs_formatted.posedit.pos.end) and "?" not in str( - my_variant.hgvs_formatted.posedit.pos.start): + if "?" in str(my_variant.quibble.posedit.pos): + if "?" in str(my_variant.quibble.posedit.pos.end) and "?" not in str( + my_variant.quibble.posedit.pos.start): raise FuzzyPositionError("Fuzzy/unknown variant end position in submitted variant description") - elif "?" in str(my_variant.hgvs_formatted.posedit.pos.start) and "?" not in str( - my_variant.hgvs_formatted.posedit.pos.end): + elif "?" in str(my_variant.quibble.posedit.pos.start) and "?" not in str( + my_variant.quibble.posedit.pos.end): raise FuzzyPositionError("Fuzzy/unknown variant start position in submitted variant description") else: raise FuzzyPositionError("Fuzzy/unknown variant start and end positions " @@ -369,9 +373,28 @@ def uncertain_positions(my_variant, validator): # Genomic Variants if "NC_" in my_variant.quibble: - my_variant.hgvs_genomic = my_variant.quibble - start_pos = position_1.split("_")[0] - end_pos = position_2.split("_")[1] + start_pos, _sep, start_end = position_1.partition("_") + o_start_pos, start_end = _hgvs_offset_pos_from_str_in( + start_pos, + None, + ref_type=var_type, + end=start_end) + start = Interval(start=o_start_pos,end=start_end) + end_start, _sep, end_pos = position_2.partition("_") + end_start, o_end_pos = _hgvs_offset_pos_from_str_in( + end_start, + None, + ref_type=var_type, + end=end_pos) + end = Interval(start=end_start,end=o_end_pos) + full_obj = hgvs_obj_from_existing_edit( + accession, + var_type, + FEInterval( + start=start, + end=end), + edit) + my_variant.hgvs_genomic = full_obj parsed_v3 = hgvs_obj_from_existing_edit( accession, var_type, @@ -438,7 +461,6 @@ def uncertain_positions(my_variant, validator): end=t_position_2), edit) - my_variant.hgvs_coding = tx_variant my_variant.quibble = copy.copy(tx_variant) my_variant.hgvs_transcript_variant = tx_variant else: @@ -472,15 +494,15 @@ def uncertain_positions(my_variant, validator): end=g_position_2), edit) my_variant.hgvs_genomic = gen_variant - my_variant.hgvs_coding = hgvs_obj_from_existing_edit( + hgvs_coding = hgvs_obj_from_existing_edit( parsed_v1.ac, parsed_v2.type, FEInterval(start=parsed_v1.posedit.pos, end=parsed_v2.posedit.pos), edit ) - my_variant.hgvs_transcript_variant = copy.copy(my_variant.hgvs_coding) - my_variant.quibble = copy.copy(my_variant.hgvs_coding) + my_variant.hgvs_transcript_variant = copy.copy(hgvs_coding) + my_variant.quibble = copy.copy(hgvs_coding) my_variant.output_type_flag = "gene" elif ")_(" not in my_variant.quibble and not "?" in my_variant.quibble: @@ -489,7 +511,6 @@ def uncertain_positions(my_variant, validator): accession, _sep, var_type_and_posedit = my_variant.quibble.partition(':') var_type, _sep ,position_and_edit = var_type_and_posedit.partition(".(") position_1, variation = position_and_edit.split(")") - v1 = f"{accession}:{var_type}.{position_1}=" my_variant.reftype = f":{var_type}." try: if var_type == 'p': @@ -517,7 +538,7 @@ def uncertain_positions(my_variant, validator): raise IncompatibleTypeError(str(e)) elif "base start position must be <= end position" in str(e): raise InvalidRangeError(f"{str(e)} in position {str(parsed_v1.posedit.pos)}") - elif re.search("[+-]", str(parsed_v1.posedit.pos)) or re.search("[+-]", str(parsed_v1.posedit.pos)): + elif "+" in str(parsed_v1.posedit.pos) or "-" in str(parsed_v1.posedit.pos): pass else: raise InvalidRangeError(f"{position_1} is an invalid range for " @@ -529,8 +550,23 @@ def uncertain_positions(my_variant, validator): # Genomic Variants if "NC_" in my_variant.quibble: - my_variant.hgvs_genomic = my_variant.quibble - + start_pos, _sep, start_end = position_1.partition("_") + start_pos, end_pos = _hgvs_offset_pos_from_str_in( + start_pos, + None, + end=start_end, + ref_type=var_type) + pos = Interval(start=start_pos,end=end_pos) + try: + full_obj = hgvs_obj_from_existing_edit( + accession, + var_type, + pos, + edit) + except Exception as e: + raise e + full_obj.posedit.pos.uncertain = True + my_variant.hgvs_genomic = full_obj # Make select_transcriopts "select" unless specified if (validator.select_transcripts != "select" and ("NM_" in validator.select_transcripts or "ENST" in validator.select_transcripts) and @@ -567,8 +603,6 @@ def uncertain_positions(my_variant, validator): ptv1[0].posedit.pos, edit) tx_variant.posedit.pos.uncertain = True - - my_variant.hgvs_coding = tx_variant my_variant.quibble = copy.copy(tx_variant) my_variant.hgvs_transcript_variant = tx_variant else: @@ -587,14 +621,14 @@ def uncertain_positions(my_variant, validator): gen_variant.posedit.pos.uncertain = True my_variant.hgvs_genomic = gen_variant - my_variant.hgvs_coding = hgvs_obj_from_existing_edit( + hgvs_coding = hgvs_obj_from_existing_edit( parsed_v1.ac, parsed_v1.type, parsed_v1.posedit.pos, edit) - my_variant.hgvs_coding.posedit.pos.uncertain = True - my_variant.hgvs_transcript_variant = my_variant.hgvs_coding - my_variant.quibble = copy.copy(my_variant.hgvs_coding) + hgvs_coding.posedit.pos.uncertain = True + my_variant.hgvs_transcript_variant = hgvs_coding + my_variant.quibble = copy.copy(hgvs_coding) my_variant.output_type_flag = "gene" else: diff --git a/VariantValidator/modules/exon_numbering.py b/VariantValidator/modules/exon_numbering.py index ac455c76..15ce9205 100644 --- a/VariantValidator/modules/exon_numbering.py +++ b/VariantValidator/modules/exon_numbering.py @@ -37,7 +37,7 @@ def finds_exon_number(variant, validator): info_dict = {} for i in range(len(response_dictionary["transcripts"])): - if response_dictionary["transcripts"][i]["reference"] == variant.hgvs_coding.ac: + if response_dictionary["transcripts"][i]["reference"] == variant.quibble.ac: # Create record info_dict[(response_dictionary["transcripts"][i]["reference"])] = {} @@ -53,7 +53,7 @@ def finds_exon_number(variant, validator): # Step 2 - Get the necessary variant information # Find the variant position from the variant nomenclature - coordinates = str(variant.hgvs_coding.posedit.pos) + coordinates = str(variant.quibble.posedit.pos) # Identify start and end of variant from input coordinates if '_' in coordinates: @@ -72,9 +72,9 @@ def finds_exon_number(variant, validator): # Create c_to_n varint try: - to_n = validator.vm.c_to_n(variant.hgvs_coding) + to_n = validator.vm.c_to_n(variant.quibble) except vvhgvs.exceptions.HGVSInvalidVariantError: - to_n = variant.hgvs_coding + to_n = variant.quibble """ This for loop identifies the exon/intron number for the transcript @@ -85,8 +85,8 @@ def finds_exon_number(variant, validator): keys: start_exon and end_exon values: start and position of variant in the reference sequence """ - exon_structure_dict = info_dict[variant.hgvs_coding.ac]["exon_structure_dict"] - coding_start = info_dict[variant.hgvs_coding.ac]['coding_start'] + exon_structure_dict = info_dict[variant.quibble.ac]["exon_structure_dict"] + coding_start = info_dict[variant.quibble.ac]['coding_start'] if coding_start is None: coding_start = 1 diff --git a/VariantValidator/modules/format_converters.py b/VariantValidator/modules/format_converters.py index 42d352c7..3d3a4ba5 100644 --- a/VariantValidator/modules/format_converters.py +++ b/VariantValidator/modules/format_converters.py @@ -106,7 +106,9 @@ def initial_format_conversions(variant, validator, select_transcripts_dict_plus_ # fail if un-corrected errors persist (warning should already have been generated) if toskip: return True - + # make sure ref type and source are set if we intend to continue + variant.set_reftype() + variant.set_refsource() # Tackle compound variant descriptions NG or NC (NM_) i.e. correctly input NG/NC_(NM_):c. intronic_converter(variant, validator) return False @@ -1077,21 +1079,19 @@ def lrg_to_refseq(variant, validator): """ caution = '' if variant.refsource == 'LRG': - if variant.hgvs_formatted.ac.startswith('LRG') and variant.hgvs_formatted.ac[3:4].isdigit(): - reference = variant.hgvs_formatted.ac.replace('LRG', 'LRG_') - caution = variant.hgvs_formatted.ac + ' updated to ' + reference + ': ' - variant.hgvs_formatted.ac = reference - variant.set_quibble(variant.hgvs_formatted) + if variant.quibble.ac.startswith('LRG') and variant.quibble.ac[3:4].isdigit(): + reference = variant.quibble.ac.replace('LRG', 'LRG_') + caution = variant.quibble.ac + ' updated to ' + reference + ': ' + variant.quibble.ac = reference if re.match(r'^LRG_\d+t\d+$', variant.quibble.ac): lrg_reference = variant.quibble.ac refseqtrans_reference = validator.db.get_refseq_transcript_id_from_lrg_transcript_id(lrg_reference) if refseqtrans_reference != 'none': - old_var_str = str(variant.hgvs_formatted) - variant.hgvs_formatted.ac = refseqtrans_reference - variant.set_quibble(variant.hgvs_formatted) + old_var_str = str(variant.quibble) + variant.quibble.ac = refseqtrans_reference caution += old_var_str + ' automapped to equivalent RefSeq record ' \ - '' + str(variant.hgvs_formatted) + '' + str(variant.quibble) variant.warnings.append(caution) logger.info(caution) @@ -1099,11 +1099,10 @@ def lrg_to_refseq(variant, validator): lrg_reference = variant.quibble.ac refseqprot_reference = validator.db.get_refseq_protein_id_from_lrg_protein_id(lrg_reference) if refseqprot_reference != 'none': - old_var_str = str(variant.hgvs_formatted) - variant.hgvs_formatted.ac = refseqprot_reference - variant.set_quibble(variant.hgvs_formatted) + old_var_str = str(variant.quibble) + variant.quibble.ac = refseqprot_reference caution += old_var_str + ' automapped to equivalent RefSeq record ' \ - '' + str(variant.hgvs_formatted) + '' + str(variant.quibble) variant.warnings.append(caution) logger.info(caution) @@ -1111,11 +1110,10 @@ def lrg_to_refseq(variant, validator): lrg_reference = variant.quibble.ac refseqgene_reference = validator.db.get_refseq_id_from_lrg_id(lrg_reference) if refseqgene_reference != 'none': - old_var_str = str(variant.hgvs_formatted) - variant.hgvs_formatted.ac = refseqgene_reference - variant.set_quibble(variant.hgvs_formatted) + old_var_str = str(variant.quibble) + variant.quibble.ac = refseqgene_reference caution += old_var_str + ' automapped to equivalent RefSeq record ' \ - '' + str(variant.hgvs_formatted) + '' + str(variant.quibble) variant.warnings.append(caution) logger.info(caution) @@ -1124,14 +1122,14 @@ def mitochondrial(variant, validator): """Will check if variant is mitochondrial and if so it will reformat the type to 'm' and save a value to the variant hgvs_genomic attribute""" - if variant.reftype == ':m.' or variant.hgvs_formatted.ac == 'NC_012920.1' or \ - variant.hgvs_formatted.ac == 'NC_001807.4': + if variant.reftype == ':m.' or variant.quibble.ac == 'NC_012920.1' or \ + variant.quibble.ac == 'NC_001807.4': # set flag variant.output_type_flag = 'mitochondrial' # Ensure the correct reference sequence type is used, if not, warn the user - hgvs_mito = copy.deepcopy(variant.hgvs_formatted) + hgvs_mito = copy.deepcopy(variant.quibble) if hgvs_mito.type == 'g' and (hgvs_mito.ac == 'NC_012920.1' or hgvs_mito.ac == 'NC_001807.4'): hgvs_mito.type = 'm' if "NC_012920.1" in hgvs_mito.ac and "hg19" in variant.selected_assembly: @@ -1164,7 +1162,7 @@ def mitochondrial(variant, validator): # Check for movement during normalization try: - norm_check = variant.hn.normalize(variant.hgvs_formatted) + norm_check = variant.hn.normalize(variant.quibble) if hgvs_mito.posedit.pos != norm_check.posedit.pos: norm_check.type = "m" error = "%s updated to %s" % (fn.valstr(hgvs_mito), fn.valstr(norm_check)) @@ -1184,7 +1182,6 @@ def mitochondrial(variant, validator): # Add a description of the reference sequence type and continue variant.hgvs_genomic = hgvs_mito if len(rel_var) == 0: - variant.genomic_g = unset_hgvs_obj_ref(hgvs_mito) variant.description = 'Homo sapiens mitochondrion, complete genome' logger.info('Homo sapiens mitochondrion, complete genome') return True @@ -1198,7 +1195,7 @@ def proteins(variant, validator): error = None hgvs_object = None # Try to validate the variant - hgvs_object = variant.hgvs_formatted + hgvs_object = variant.quibble try: validator.vr.validate(hgvs_object) @@ -1375,17 +1372,17 @@ def rna(variant, validator): convert r, into c. """ if variant.reftype == ':r.' or ":r." in variant.original: - if ":r.(" in str(variant.hgvs_formatted): - if type(variant.hgvs_formatted) is str: - strip_prediction = str(variant.hgvs_formatted).replace("(", "") + if ":r.(" in str(variant.quibble): + if type(variant.quibble) is str: + strip_prediction = str(variant.quibble).replace("(", "") strip_prediction = strip_prediction[:-1] hgvs_input = validator.hp.parse_hgvs_variant(strip_prediction) else: - hgvs_input = variant.hgvs_formatted + hgvs_input = variant.quibble hgvs_input.posedit.pos.uncertain = False #hgvs_input.posedit.uncertain = False else: - hgvs_input = variant.hgvs_formatted + hgvs_input = variant.quibble tx_info = validator.hdp.get_tx_identity_info(hgvs_input.ac) if tx_info[3] is None: @@ -1403,7 +1400,7 @@ def rna(variant, validator): variant.warnings.append(error) logger.info(str(error)) return True - variant.hgvs_formatted = hgvs_c + variant.quibble = hgvs_c # Create variant.rna_data dictionary rnd = VariantValidator.modules.rna_formatter.RnaDescriptions(validator.alt_aln_method, @@ -1492,4 +1489,4 @@ def uncertain_pos(variant, validator): # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -# \ No newline at end of file +# diff --git a/VariantValidator/modules/gapped_mapping.py b/VariantValidator/modules/gapped_mapping.py index 9a9b0572..bdd76690 100644 --- a/VariantValidator/modules/gapped_mapping.py +++ b/VariantValidator/modules/gapped_mapping.py @@ -409,11 +409,11 @@ def gapped_g_to_c(self, rel_var, select_transcripts_dict): # take a look at the input genomic variant for potential base salvage stash_ac = vcf_dict['chr'] - stash_input = self.variant.post_format_conversion + stash_input = self.variant.quibble if type(stash_input) is str: stash_input = self.validator.hp.parse_hgvs_variant(stash_input) # Re-Analyse genomic positions - if 'NG_' in str(self.variant.hgvs_formatted): + if 'NG_' in str(self.variant.quibble): c = rel_var[0] if hasattr(c.posedit.edit, 'ref') and c.posedit.edit.ref is not None: c.posedit.edit.ref = c.posedit.edit.ref.upper() diff --git a/VariantValidator/modules/gene2transcripts.py b/VariantValidator/modules/gene2transcripts.py index 62f93bb7..ff5257c4 100644 --- a/VariantValidator/modules/gene2transcripts.py +++ b/VariantValidator/modules/gene2transcripts.py @@ -124,7 +124,7 @@ def gene2transcripts(g2t, # Gather transcript information lists if bypass_web_searches is True: tx_for_gene = [] - tx_info = g2t.hdp.get_tx_identity_info(query.hgvs_coding.ac) + tx_info = g2t.hdp.get_tx_identity_info(query.quibble.ac) # Add primary assembly queries for builds in query.primary_assembly_loci.keys(): @@ -132,7 +132,7 @@ def gene2transcripts(g2t, tx_for_gene.append([query.gene_symbol, tx_info[3], 0, - query.hgvs_coding.ac, + query.quibble.ac, query.primary_assembly_loci[builds]['hgvs_genomic_description'].ac, validator.alt_aln_method]) @@ -141,7 +141,7 @@ def gene2transcripts(g2t, tx_for_gene.append([query.gene_symbol, tx_info[3], 0, - query.hgvs_coding.ac, + query.quibble.ac, query.hgvs_refseqgene_variant.ac, validator.alt_aln_method]) diff --git a/VariantValidator/modules/hgvs_utils.py b/VariantValidator/modules/hgvs_utils.py index 2f2a8801..4ea6ffe2 100644 --- a/VariantValidator/modules/hgvs_utils.py +++ b/VariantValidator/modules/hgvs_utils.py @@ -164,10 +164,16 @@ def unset_hgvs_obj_ref(hgvs): """ Remove/unset ref bases from hgvs object, in the manner needed for output, but without re-parsing from text. + + Skips known cases where ref should not be unset, including any unc + positions, or other N ref. """ + if hgvs.posedit.pos.uncertain: + return hgvs edit = hgvs.posedit.edit if edit.type in ['inv', 'dup']: - edit.ref = '' + if edit.ref and not 'N' in edit.ref: + edit.ref = '' elif edit.ref is not None and edit.alt is not None: #if #edit.alt != edit.ref and \ if len(edit.alt) == 1 and len(edit.ref) == 1: @@ -2695,16 +2701,16 @@ def hgvs_ref_alt(hgvs_variant, sf): return ref_alt_dict -def incomplete_alignment_mapping_t_to_g(validator, variant): +def incomplete_alignment_mapping_t_to_g(validator, variant,t_hgvs_var): output = None - mapping_options = variant.map_dat.mapping_options(variant.input_parses.ac,hdp=validator.hdp) + mapping_options = validator.hdp.get_tx_mapping_options(t_hgvs_var.ac,hdp=validator.hdp) for option in mapping_options: if option[2] == validator.alt_aln_method and "NC_" not in option[1]: in_assembly = seq_data.to_chr_num_refseq(option[1], variant.primary_assembly) if in_assembly is not None: try: - output = validator.vm.t_to_g(variant.input_parses, option[1]) - if variant.input_parses.posedit.edit.type == "identity": + output = validator.vm.t_to_g(t_hgvs_var, option[1]) + if t_hgvs_var.posedit.edit.type == "identity": output.posedit.edit.alt = output.posedit.edit.ref except vvhgvs.exceptions.HGVSError: pass diff --git a/VariantValidator/modules/mappers.py b/VariantValidator/modules/mappers.py index 44278613..6837c045 100644 --- a/VariantValidator/modules/mappers.py +++ b/VariantValidator/modules/mappers.py @@ -22,8 +22,8 @@ class TranscriptMappingError(Exception): pass def gene_to_transcripts(variant, validator, select_transcripts_dict, batch_list): - logger.info(f"Mapping {variant.hgvs_formatted} to transcripts") - g_query = variant.hgvs_formatted + logger.info(f"Mapping {variant.quibble} to transcripts") + g_query = variant.quibble # set hdp for exon mapping fetch before first use if not variant.map_dat.hdp: variant.map_dat.hdp = validator.hdp @@ -58,7 +58,7 @@ def gene_to_transcripts(variant, validator, select_transcripts_dict, batch_list) # use updated position if normalized to a different position if g_query.posedit.pos != g_test.posedit.pos: variant.hgvs_genomic = g_test - variant.hgvs_formatted = g_test + variant.quibble = g_test else: variant.hgvs_genomic = g_query @@ -135,14 +135,14 @@ def gene_to_transcripts(variant, validator, select_transcripts_dict, batch_list) except Exception as e: logger.info(f"nw_rel_var creation failed with exception {str(e)}") raise TranscriptMappingError(f"Encountered an issue when mapping genomic description " - f"{variant.hgvs_formatted} to available transcripts") + f"{variant.quibble} to available transcripts") rel_var = nw_rel_var if len(rel_var) == 0: # Check for NG_ - if variant.hgvs_formatted.ac.startswith('NG_'): - hgvs_refseqgene =variant.hgvs_formatted + if variant.quibble.ac.startswith('NG_'): + hgvs_refseqgene =variant.quibble # Convert to chromosomal position refseqgene_data = validator.rsg_to_chr(hgvs_refseqgene, variant.primary_assembly, variant.hn) # There should only ever be one description returned @@ -153,11 +153,11 @@ def gene_to_transcripts(variant, validator, select_transcripts_dict, batch_list) variant.output_type_flag = 'intergenic' # set genomic and where available RefSeqGene outputs variant.warnings.append(no_tx_found_error) - error = 'TranscriptIdentificationWarning: Mapping unavailable for RefSeqGene ' + str(variant.hgvs_formatted) + \ + error = 'TranscriptIdentificationWarning: Mapping unavailable for RefSeqGene ' + str(variant.quibble) + \ ' using alignment method = ' + validator.alt_aln_method variant.warnings.append(error) - variant.genomic_r = variant.hgvs_formatted - variant.refseqgene_variant = variant.hgvs_formatted + variant.hgvs_refseqgene_variant = variant.quibble + variant.refseqgene_variant = variant.quibble return True # Extract data @@ -165,15 +165,16 @@ def gene_to_transcripts(variant, validator, select_transcripts_dict, batch_list) genomic_input = refseqgene_data['hgvs_genomic'] # re_submit # Tag the line so that it is not written out - variant.warnings.append(str(variant.hgvs_formatted) + ' automapped to genome position ' + + variant.warnings.append(str(variant.quibble) + ' automapped to genome position ' + str(genomic_input)) + variant.write = False query = Variant(variant.original, quibble=genomic_input, warnings=variant.warnings, primary_assembly=variant.primary_assembly, order=variant.order, selected_assembly=variant.selected_assembly) batch_list.append(query) logger.info('Submitting new variant with format %s', genomic_input) else: - error = 'TranscriptIdentificationWarning: Mapping unavailable for RefSeqGene ' + str(variant.hgvs_formatted) + \ + error = 'TranscriptIdentificationWarning: Mapping unavailable for RefSeqGene ' + str(variant.quibble) + \ ' using alignment method = ' + validator.alt_aln_method variant.warnings.append(error) logger.info(str(error)) @@ -217,9 +218,8 @@ def gene_to_transcripts(variant, validator, select_transcripts_dict, batch_list) variant.output_type_flag = 'intergenic' # set genomic and where available RefSeqGene outputs variant.warnings.append(error) - variant.genomic_g = unset_hgvs_obj_ref(variant.hgvs_genomic) - variant.genomic_r = rsg_data[0] - logger.info(str(error)) + variant.hgvs_refseqgene_variant = rsg_data[0] + logger.warning(str(error)) return True else: error = 'Validation will fail if the selected chromosome reference sequence does not corresponds to ' \ @@ -267,20 +267,15 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version valid = False caution = '' error = '' - # Collect information for genomic level validation - obj = variant.hgvs_formatted - if type(obj) is str: #still happens for some error cases - variant.hgvs_formatted = validator.hp.parse_hgvs_variant( - variant.hgvs_formatted) - obj = variant.hgvs_formatted - tx_ac = obj.ac - - quibble_input = str(variant.quibble) - quibble_input_hgvs_obj = variant.quibble - if isinstance(quibble_input_hgvs_obj, str): - quibble_input_hgvs_obj = validator.hp.parse_hgvs_variant(variant.quibble) - formatted_variant = str(variant.hgvs_formatted) - out_hgvs_obj = variant.hgvs_formatted + if isinstance(variant.quibble, str): + variant.quibble = validator.hp.parse_hgvs_variant(variant.quibble) + # preserved input variables + input_formatted_variant= str(variant.quibble) + input_hgvs_obj = copy.copy(variant.quibble) + tx_ac = input_hgvs_obj.ac + # to be changed versions for output + formatted_variant = str(variant.quibble) + out_hgvs_obj = variant.quibble # Do we keep it? if (validator.select_transcripts != 'all' and validator.select_transcripts != 'raw') \ and "select" not in validator.select_transcripts and \ @@ -288,7 +283,7 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version if tx_ac not in list(select_transcripts_dict_plus_version.keys()): # By marking it as Do Not Write and continuing through the validation loop if ":g." not in variant.original: - error = (f"TranscriptSelectionError: Variant {variant.hgvs_formatted} is not in the list of " + error = (f"TranscriptSelectionError: Variant {variant.quibble} is not in the list of " f"transcripts selected for validation {validator.select_transcripts}") logger.info(error) variant.warnings.append(error) @@ -303,10 +298,10 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version # First task is to get the genomic equivalent, and output a useful error messages if it can't be found. try: reset_g_origin = False - if obj.rel_ac.startswith('NG_'): + if out_hgvs_obj.rel_ac.startswith('NG_'): reset_g_origin=True to_g = validator.myevm_t_to_g( - obj, + out_hgvs_obj, variant.no_norm_evm, variant.primary_assembly, variant.hn, @@ -352,7 +347,7 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version plus = re.compile(r"\d\+\d") # finds digit + digit minus = re.compile(r"\d-\d") # finds digit - digit - if plus.search(quibble_input) or minus.search(quibble_input): + if plus.search(input_formatted_variant) or minus.search(input_formatted_variant): if 'error' in str(to_g): if validator.alt_aln_method != 'genebuild': error = "If the following error message does not address the issue and the problem persists please " \ @@ -371,7 +366,7 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version else: # Incorrect genomic reference base try: - check_base = validator.nr_vm.t_to_g(obj, to_g.ac) + check_base = validator.nr_vm.t_to_g(out_hgvs_obj, to_g.ac) except vvhgvs.exceptions.HGVSError: pass else: @@ -381,26 +376,28 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version if "does not agree with reference sequence" in str(e): a, b = re.findall(r'\(([GATC]+)\)', str(e))[:2] if (len(a) == len(b) and - obj.posedit.edit.type == check_base.posedit.edit.type): - error = f"{str(obj)} mapped to {str(e)}" + out_hgvs_obj.posedit.edit.type == check_base.posedit.edit.type): + error = f"{str(out_hgvs_obj)} mapped to {str(e)}" variant.warnings.append(error) logger.info(error) # Insertions at exon boundaries are miss-handled by vm.g_to_t - if (obj.posedit.edit.type == 'ins' and - obj.posedit.pos.start.offset == 0 and - obj.posedit.pos.end.offset != 0) or (obj.posedit.edit.type == 'ins' and - obj.posedit.pos.start.offset != 0 and - obj.posedit.pos.end.offset == 0): - formatted_variant = str(obj) - out_hgvs_obj = obj + if ( + out_hgvs_obj.posedit.edit.type == 'ins' and + out_hgvs_obj.posedit.pos.start.offset == 0 and + out_hgvs_obj.posedit.pos.end.offset != 0 + ) or ( + out_hgvs_obj.posedit.edit.type == 'ins' and + out_hgvs_obj.posedit.pos.start.offset != 0 and + out_hgvs_obj.posedit.pos.end.offset == 0): + formatted_variant = str(out_hgvs_obj) else: try: out_hgvs_obj = validator.myevm_g_to_t(variant.evm, to_g, tx_ac) formatted_variant = str(out_hgvs_obj) except vvhgvs.exceptions.HGVSError as e: if "Alignment is incomplete" in str(e): - to_g = hgvs_utils.incomplete_alignment_mapping_t_to_g(validator, variant) + to_g = hgvs_utils.incomplete_alignment_mapping_t_to_g(validator, variant,variant.quibble) if to_g is None: error = str(e) variant.warnings.append(error) @@ -411,7 +408,7 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version out_hgvs_obj = validator.vm.g_to_t(to_g, tx_ac) formatted_variant = str(out_hgvs_obj) - elif ':g.' in quibble_input: + elif ':g.' in formatted_variant: if plus.search(formatted_variant) or minus.search(formatted_variant): to_g = validator.genomic(out_hgvs_obj, variant.no_norm_evm, variant.primary_assembly, variant) if 'error' in str(to_g): @@ -430,13 +427,14 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version return True else: # Insertions at exon boundaries are miss-handled by vm.g_to_t - if (obj.posedit.edit.type == 'ins' and - obj.posedit.pos.start.offset == 0 and - obj.posedit.pos.end.offset != 0) or (obj.posedit.edit.type == 'ins' and - obj.posedit.pos.start.offset != 0 and - obj.posedit.pos.end.offset == 0): - out_hgvs_obj = obj - formatted_variant = str(obj) + if ( + out_hgvs_obj.posedit.edit.type == 'ins' and + out_hgvs_obj.posedit.pos.start.offset == 0 and + out_hgvs_obj.posedit.pos.end.offset != 0) or ( + out_hgvs_obj.posedit.edit.type == 'ins' and + out_hgvs_obj.posedit.pos.start.offset != 0 and + out_hgvs_obj.posedit.pos.end.offset == 0): + formatted_variant = str(out_hgvs_obj) else: # Normalize was I believe to replace ref. Mapping does this anyway # to_g = hn.normalize(to_g) @@ -446,7 +444,7 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version else: # Normalize the variant try: - h_variant = variant.hn.normalize(obj) + h_variant = variant.hn.normalize(out_hgvs_obj) except vvhgvs.exceptions.HGVSUnsupportedOperationError as error: if 'Unsupported normalization of variants spanning the exon-intron boundary' in str(error): formatted_variant = formatted_variant @@ -471,17 +469,17 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version # Tackle the plus intronic offset cck = False - if plus.search(quibble_input): + if plus.search(input_formatted_variant): # Regular expression catches the start of the interval only based on .00+00 pattern inv_start = re.compile(r"\.\d+\+\d") inv_end = re.compile(r"_\d+\+\d") - if inv_start.search(quibble_input) or inv_end.search(quibble_input): + if inv_start.search(input_formatted_variant) or inv_end.search(input_formatted_variant): cck = True - if minus.search(quibble_input): + if minus.search(input_formatted_variant): # Regular expression catches the start of the interval only based on .00-00 pattern inv_start = re.compile(r"\.\d+-\d") inv_end = re.compile(r"_\d+-\d") - if inv_start.search(quibble_input) or inv_end.search(quibble_input): + if inv_start.search(input_formatted_variant) or inv_end.search(input_formatted_variant): cck = True # COORDINATE CHECKER @@ -493,7 +491,7 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version if 'del' in formatted_variant: if out_hgvs_obj.type == 'c': coding = out_hgvs_obj - elif quibble_input_hgvs_obj.type == 'c': + elif input_hgvs_obj.type == 'c': coding = validator.coding(out_hgvs_obj) else:# not actually coding coding = out_hgvs_obj @@ -520,7 +518,7 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version post_var = validator.myevm_g_to_t(variant.evm, pre_var, trans_acc) except vvhgvs.exceptions.HGVSError as error: if "Alignment is incomplete" in str(error): - output = hgvs_utils.incomplete_alignment_mapping_t_to_g(validator, variant) + output = hgvs_utils.incomplete_alignment_mapping_t_to_g(validator, variant,out_hgvs_obj) if output is None: error = str(error) variant.warnings.append(error) @@ -534,7 +532,7 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version logger.info(str(error)) return True - test = quibble_input_hgvs_obj + test = input_hgvs_obj if post_var.posedit.pos.start.base != test.posedit.pos.start.base or \ post_var.posedit.pos.end.base != test.posedit.pos.end.base: @@ -578,7 +576,7 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version if out_hgvs_obj.type == 'c': coding = out_hgvs_obj - elif quibble_input_hgvs_obj.type == 'c': + elif input_hgvs_obj.type == 'c': coding = validator.coding(out_hgvs_obj) else:# not actually coding coding = out_hgvs_obj @@ -594,7 +592,7 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version except vvhgvs.exceptions.HGVSError as e: logger.info(f"Error: {str(e)}. Variant: {pre_var}") if "Alignment is incomplete" in str(e): - pre_var = hgvs_utils.incomplete_alignment_mapping_t_to_g(validator, variant) + pre_var = hgvs_utils.incomplete_alignment_mapping_t_to_g(validator, variant,out_hgvs_obj) if to_g is None: error = str(e) variant.warnings.append(error) @@ -606,7 +604,7 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version else: logger.info(f"{pre_var} mapped to {post_var}") - test = quibble_input_hgvs_obj + test = input_hgvs_obj if post_var.posedit.pos.start.base != test.posedit.pos.start.base or \ post_var.posedit.pos.end.base != test.posedit.pos.end.base: @@ -646,9 +644,9 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version variant.warnings.extend([caution]) raise MappersError(caution) - elif ':g.' not in quibble_input: + elif ':g.' not in input_formatted_variant: query = out_hgvs_obj - test = quibble_input_hgvs_obj + test = input_hgvs_obj if str(query.posedit.pos) != str(test.posedit.pos): automap = str(test) + ' automapped to ' + str(query) @@ -656,12 +654,12 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version variant.quibble=out_hgvs_obj # VALIDATION of intronic variants - pre_valid = quibble_input_hgvs_obj + pre_valid = input_hgvs_obj post_valid = out_hgvs_obj # valid is false if the input contains a \d+\d, \d-\d or :g. if not valid: - genomic_validation = validator.genomic(quibble_input_hgvs_obj, variant.no_norm_evm, variant.primary_assembly, + genomic_validation = validator.genomic(input_hgvs_obj, variant.no_norm_evm, variant.primary_assembly, variant) if fn.valstr(pre_valid) != fn.valstr(post_valid): if variant.reftype != ':g.': @@ -980,9 +978,12 @@ def transcripts_to_gene(variant, validator, select_transcripts_dict_plus_version str(updated_transcript_variant) + ' MUST be fully validated prior to ' 'use in reports: ' 'select_variants=' + fn.valstr(updated_transcript_variant)) - variant.coding = hgvs_coding - variant.genomic_r = hgvs_refseq - variant.genomic_g = unset_hgvs_obj_ref(hgvs_genomic) + + if hgvs_coding: + print("coding:" + str(hgvs_coding)) + variant.hgvs_transcript_variant = hgvs_coding + variant.hgvs_genomic = hgvs_genomic + variant.hgvs_refseqgene_variant = hgvs_refseq variant.protein = hgvs_protein return False @@ -993,14 +994,14 @@ def final_tx_to_multiple_genomic(variant, validator, tx_variant, liftover_level= try: tx_variant.ac - variant.hgvs_coding = tx_variant + hgvs_coding = tx_variant except AttributeError: - variant.hgvs_coding = validator.hp.parse_hgvs_variant(str(tx_variant)) + hgvs_coding = validator.hp.parse_hgvs_variant(str(tx_variant)) # Look for variants spanning introns disable_gap_compensation = False try: - variant.hn.normalize(variant.hgvs_coding) + variant.hn.normalize(hgvs_coding) except vvhgvs.exceptions.HGVSUnsupportedOperationError as e: error = str(e) if 'boundary' in error or 'spanning' in error: @@ -1012,12 +1013,11 @@ def final_tx_to_multiple_genomic(variant, validator, tx_variant, liftover_level= multi_g = [] multi_list = [] - mapping_options = variant.map_dat.mapping_options(variant.hgvs_coding.ac,hdp=validator.hdp) + mapping_options = variant.map_dat.mapping_options(hgvs_coding.ac,hdp=validator.hdp) mapping_options = sorted(mapping_options, key=itemgetter(1)) - for alt_chr in mapping_options: if liftover_level is None: - multi_list.append(variant.genomic_g.ac) + multi_list.append(variant.hgvs_genomic.ac) elif liftover_level == 'primary': if ('NC_' in alt_chr[1]) and alt_chr[2] == validator.alt_aln_method: multi_list.append(alt_chr[1]) @@ -1036,22 +1036,21 @@ def final_tx_to_multiple_genomic(variant, validator, tx_variant, liftover_level= try: # Re set ori ori = variant.map_dat.tx_exons( - variant.hgvs_coding.ac, alt_chr, + hgvs_coding.ac, alt_chr, validator.alt_aln_method, hdp=validator.hdp) # Map the variant to the genome - hgvs_alt_genomic = validator.myvm_t_to_g(variant.hgvs_coding, alt_chr, variant.no_norm_evm, + hgvs_alt_genomic = validator.myvm_t_to_g(hgvs_coding, alt_chr, variant.no_norm_evm, variant.hn, variant.map_dat) # Loop out gap code under these circumstances! - if variant.map_dat.is_gapped_map(variant.hgvs_coding.ac,hgvs_alt_genomic.ac,validator): + if variant.map_dat.is_gapped_map(hgvs_coding.ac,hgvs_alt_genomic.ac,validator): # warn on gap_compensation for gap_mapper = gapped_mapping.GapMapper(variant, validator) hgvs_alt_genomic, hgvs_coding = gap_mapper.g_to_t_gap_compensation_version3( - hgvs_alt_genomic, variant.hgvs_coding, ori, alt_chr, rec_var) - logger.info(f"gap_compensation_3 done for {variant.hgvs_coding} mapped to {hgvs_alt_genomic}") - variant.hgvs_coding = hgvs_coding + hgvs_alt_genomic, hgvs_coding, ori, alt_chr, rec_var) + logger.info(f"gap_compensation_3 done for {hgvs_coding} mapped to {hgvs_alt_genomic}") logger.info(f"hgvs_coding updated to {hgvs_coding}") # Check for mismatched sequence in dup variants @@ -1071,10 +1070,10 @@ def final_tx_to_multiple_genomic(variant, validator, tx_variant, liftover_level= multi_g.append(hgvs_alt_genomic) else: - if hgvs_alt_genomic.posedit.edit.type == variant.hgvs_coding.posedit.edit.type and \ + if hgvs_alt_genomic.posedit.edit.type == hgvs_coding.posedit.edit.type and \ "ins" not in hgvs_alt_genomic.posedit.edit.type: try: - rev_tx = variant.reverse_normalizer.normalize(variant.hgvs_coding) + rev_tx = variant.reverse_normalizer.normalize(hgvs_coding) rev_g = validator.myvm_t_to_g(rev_tx, alt_chr, variant.no_norm_evm, variant.hn, variant.map_dat) rev_g = variant.hn.normalize(rev_g) @@ -1088,7 +1087,7 @@ def final_tx_to_multiple_genomic(variant, validator, tx_variant, liftover_level= # truly gapped alignment. except KeyError: warnings = warnings + ': Suspected incomplete alignment between transcript %s and ' \ - 'genomic reference sequence %s' % (variant.hgvs_coding.ac, alt_chr) + 'genomic reference sequence %s' % (hgvs_coding.ac, alt_chr) except vvhgvs.exceptions.HGVSError as e: logger.info(str(e)) diff --git a/VariantValidator/modules/use_checking.py b/VariantValidator/modules/use_checking.py index 868f953c..894c6f49 100644 --- a/VariantValidator/modules/use_checking.py +++ b/VariantValidator/modules/use_checking.py @@ -342,80 +342,77 @@ def structure_checks(variant, validator): Primarily, this code filters out variants that cannot realistically be auto corrected and will cause the downstream functions to return errors """ - if type(variant.quibble) is not str: - input_parses = copy.deepcopy(variant.quibble) - else: - input_parses = validator.hp.parse_hgvs_variant(variant.quibble) - variant.input_parses = input_parses - variant.gene_symbol = validator.db.get_gene_symbol_from_transcript_id(variant.input_parses.ac) + + input_parses = copy.deepcopy(variant.quibble) + variant.gene_symbol = validator.db.get_gene_symbol_from_transcript_id(input_parses.ac) if variant.gene_symbol == 'none': variant.gene_symbol = '' if input_parses.type == 'g' or input_parses.type == 'm': - check = structure_checks_g(variant, validator) + check = structure_checks_g(variant, validator, input_parses) if check: return True elif input_parses.type == 'c': - check = structure_checks_c(variant, validator) + check = structure_checks_c(variant, validator, input_parses) if check: # Also check intron boundaries to provide additional warnings - if "beyond the bounds" in str(variant.warnings) and (variant.input_parses.posedit.pos.start.offset != 0 or - variant.input_parses.posedit.pos.end.offset != 0): - if variant.input_parses.posedit.pos.start.offset != 0: - variant.input_parses.posedit.pos.start.offset = 1 - if variant.input_parses.posedit.pos.end.offset != 0: - variant.input_parses.posedit.pos.end.offset =1 - hgvs_genomic_vt = validator.myevm_t_to_g(variant.input_parses, variant.no_norm_evm, + if "beyond the bounds" in str(variant.warnings) and (input_parses.posedit.pos.start.offset != 0 or + input_parses.posedit.pos.end.offset != 0): + if input_parses.posedit.pos.start.offset != 0: + input_parses.posedit.pos.start.offset = 1 + if input_parses.posedit.pos.end.offset != 0: + input_parses.posedit.pos.end.offset =1 + hgvs_genomic_vt = validator.myevm_t_to_g(input_parses, variant.no_norm_evm, variant.primary_assembly, variant.hn, variant) - format_converters.remap_intronic(variant.input_parses, hgvs_genomic_vt, variant, validator) + format_converters.remap_intronic(input_parses, hgvs_genomic_vt, variant, validator) return True elif input_parses.type == 'n': - check = structure_checks_n(variant, validator) + check = structure_checks_n(variant, validator, input_parses) if check: return True else: pass -def structure_checks_g(variant, validator): +def structure_checks_g(variant, validator, input_parses): """ Structure checks for when reftype is genomic """ - if variant.input_parses.ac[:3] not in ['NC_', 'NG_', 'NT_', 'NW_']: - error = 'Invalid reference sequence identifier (' + variant.input_parses.ac + ')' + if input_parses.ac[:3] not in ['NC_', 'NG_', 'NT_', 'NW_']: + error = 'Invalid reference sequence identifier (' + input_parses.ac + ')' variant.warnings.append(error) logger.info(error) return True try: - validator.vr.validate(variant.input_parses) + validator.vr.validate(input_parses) except Exception as e: if "does not agree with reference sequence ()" in str(e): - e = "The specified coordinate is outside the boundaries of reference sequence %s " % variant.input_parses.ac + e = "The specified coordinate is outside the boundaries of reference sequence %s " % input_parses.ac if "base start position must be <= end position" in str(e) and ( - "NC_012920.1" in variant.hgvs_formatted.ac or - "NC_001807.4" in variant.hgvs_formatted.ac): + "NC_012920.1" in variant.quibble.ac or + "NC_001807.4" in variant.quibble.ac): - if variant.hgvs_formatted.ac not in variant.original: + if variant.quibble.ac not in variant.original: err = "This is not a valid HGVS variant description, because no reference sequence ID has " \ - "been provided, instead use %s" % str(variant.hgvs_formatted) + "been provided, instead use %s" % str(variant.quibble) variant.warnings.append(err) variant.warnings.append("The variant positions are valid but we cannot normalize variants spanning " "the origin of circular reference sequences") return True - elif "insertion length must be 1" in str(e) and "(" in str(variant.input_parses.posedit.pos) and ")" in \ - str(variant.input_parses.posedit.pos): + elif "insertion length must be 1" in str(e) and "(" in str(input_parses.posedit.pos) and ")" in \ + str(input_parses.posedit.pos): return True - elif "insertion length must be 1" in str(e) and "(" not in str(variant.input_parses.posedit.pos) and ")" not in\ - str(variant.input_parses.posedit.pos): + elif "insertion length must be 1" in str(e) and "(" not in str(input_parses.posedit.pos) and ")" not in\ + str(input_parses.posedit.pos): ins_warning = (f'Insertion length must be 1 e.g. ' - f'{str(int(variant.input_parses.posedit.pos.start.base))}' - f'_{str(int(variant.input_parses.posedit.pos.start.base)+1)}' - f'ins{variant.input_parses.posedit.edit.alt}') + f'{str(int(input_parses.posedit.pos.start.base))}' + f'_{str(int(input_parses.posedit.pos.start.base)+1)}' + f'ins{input_parses.posedit.edit.alt}') variant.warnings.append(ins_warning) for warning in variant.warnings: if warning == "insertion length must be 1": @@ -424,7 +421,7 @@ def structure_checks_g(variant, validator): return True elif "Length implied by coordinates must equal sequence deletion length" in str(e) and \ - "(" in str(variant.input_parses.posedit.pos) and ")" in str(variant.input_parses.posedit.pos): + "(" in str(input_parses.posedit.pos) and ")" in str(input_parses.posedit.pos): return True else: error = str(e) @@ -434,7 +431,7 @@ def structure_checks_g(variant, validator): # Additional test try: - np = variant.hn.normalize(variant.input_parses) + np = variant.hn.normalize(input_parses) except vvhgvs.exceptions.HGVSError as e: error = str(e) variant.warnings.append(error) @@ -443,8 +440,8 @@ def structure_checks_g(variant, validator): # Look for variants in runs of N bases try: - if "N" in variant.input_parses.posedit.edit.ref: - error = (f"UncertainSequenceError: The submitted variant description {variant.input_parses} refers to a " + if "N" in input_parses.posedit.edit.ref: + error = (f"UncertainSequenceError: The submitted variant description {input_parses} refers to a " f"genomic reference region with " f"an uncertain base composition (N)") variant.warnings.append(error) @@ -456,7 +453,7 @@ def structure_checks_g(variant, validator): return False -def structure_checks_c(variant, validator): +def structure_checks_c(variant, validator, input_parses): """ structure checks for when reftype is coding :param variant: @@ -464,18 +461,18 @@ def structure_checks_c(variant, validator): :return: """ - if '*' in str(variant.input_parses) or 'c.-' in str(variant.input_parses): + if '*' in str(input_parses) or 'c.-' in str(input_parses): # Catch variation in UTRs # These should be in the sequence so can be directly validated. Need to pass to n. try: - validator.vr.validate(variant.input_parses) + validator.vr.validate(input_parses) except vvhgvs.exceptions.HGVSError as e: error = str(e) if 'datums is ill-defined' in error: - called_ref = variant.input_parses.posedit.edit.ref + called_ref = input_parses.posedit.edit.ref try: - to_n = variant.evm.c_to_n(variant.input_parses) + to_n = variant.evm.c_to_n(input_parses) except vvhgvs.exceptions.HGVSInvalidVariantError as e: error = str(e) variant.warnings.append(error) @@ -490,61 +487,61 @@ def structure_checks_c(variant, validator): logger.info(error) return True else: - if variant.input_parses.posedit.edit.type == "ins": - variant.input_parses.posedit.edit.ref = None + if input_parses.posedit.edit.type == "ins": + input_parses.posedit.edit.ref = None else: - variant.input_parses.posedit.edit.ref = '' - variant.hgvs_formatted = variant.input_parses + input_parses.posedit.edit.ref = '' + variant.quibble = input_parses else: if 'bounds' in error or 'intronic variant' in error: try: - variant.hn.normalize(variant.input_parses) + variant.hn.normalize(input_parses) except vvhgvs.exceptions.HGVSError as e: logger.debug("Except passed, %s", e) if 'bounds' in error: try: - identity_info = validator.hdp.get_tx_identity_info(variant.input_parses.ac) + identity_info = validator.hdp.get_tx_identity_info(input_parses.ac) ref_start = identity_info[3] ref_end = identity_info[4] - if '-' in str(variant.input_parses.posedit.pos.start) and \ - variant.input_parses.posedit.pos.start.offset == 0: + if '-' in str(input_parses.posedit.pos.start) and \ + input_parses.posedit.pos.start.offset == 0: # upstream positions boundary = -ref_start - remainder = variant.input_parses.posedit.pos.start.base - boundary - variant.input_parses.posedit.pos.start.base = boundary - variant.input_parses.posedit.pos.start.offset = remainder - if '-' in str(variant.input_parses.posedit.pos.end) and \ - variant.input_parses.posedit.pos.end.offset == 0: + remainder = input_parses.posedit.pos.start.base - boundary + input_parses.posedit.pos.start.base = boundary + input_parses.posedit.pos.start.offset = remainder + if '-' in str(input_parses.posedit.pos.end) and \ + input_parses.posedit.pos.end.offset == 0: boundary = -ref_start - remainder = variant.input_parses.posedit.pos.end.base - boundary - variant.input_parses.posedit.pos.end.base = boundary - variant.input_parses.posedit.pos.end.offset = remainder - if '*' in str(variant.input_parses.posedit.pos.start) and \ - variant.input_parses.posedit.pos.start.offset == 0: + remainder = input_parses.posedit.pos.end.base - boundary + input_parses.posedit.pos.end.base = boundary + input_parses.posedit.pos.end.offset = remainder + if '*' in str(input_parses.posedit.pos.start) and \ + input_parses.posedit.pos.start.offset == 0: # downstream positions - tot_end_pos = str(variant.input_parses.posedit.pos.start).replace('*', '') - ts_seq = validator.sf.fetch_seq(variant.input_parses.ac) + tot_end_pos = str(input_parses.posedit.pos.start).replace('*', '') + ts_seq = validator.sf.fetch_seq(input_parses.ac) boundary = len(ts_seq) - ref_end - variant.input_parses.posedit.pos.start.base = boundary + input_parses.posedit.pos.start.base = boundary offset = int(tot_end_pos) - boundary - variant.input_parses.posedit.pos.start.offset = offset - if '*' in str(variant.input_parses.posedit.pos.end) and \ - variant.input_parses.posedit.pos.end.offset == 0: - tot_end_pos = str(variant.input_parses.posedit.pos.end).replace('*', '') - ts_seq = validator.sf.fetch_seq(variant.input_parses.ac) + input_parses.posedit.pos.start.offset = offset + if '*' in str(input_parses.posedit.pos.end) and \ + input_parses.posedit.pos.end.offset == 0: + tot_end_pos = str(input_parses.posedit.pos.end).replace('*', '') + ts_seq = validator.sf.fetch_seq(input_parses.ac) boundary = len(ts_seq) - ref_end - variant.input_parses.posedit.pos.end.base = boundary + input_parses.posedit.pos.end.base = boundary offset = int(tot_end_pos) - boundary - variant.input_parses.posedit.pos.end.offset = offset + input_parses.posedit.pos.end.offset = offset # Create a lose vm instance variant.lose_vm = vvhgvs.variantmapper.VariantMapper(validator.hdp, replace_reference=True, prevalidation_level=None) - report_gen = validator.myevm_t_to_g(variant.input_parses, variant.no_norm_evm, + report_gen = validator.myevm_t_to_g(input_parses, variant.no_norm_evm, variant.primary_assembly, variant.hn, variant) report_gen = variant.hn.normalize(report_gen) error = 'Using a transcript reference sequence to specify a variant position that lies ' \ @@ -557,15 +554,15 @@ def structure_checks_c(variant, validator): return True try: - variant.input_parses = variant.evm.c_to_n(variant.input_parses) + input_parses = variant.evm.c_to_n(input_parses) except vvhgvs.exceptions.HGVSError as e: error = str(e) variant.warnings.append(error) logger.info(e) return True - if 'n.1-' in str(variant.input_parses): - input_parses = variant.evm.n_to_c(variant.input_parses) + if 'n.1-' in str(input_parses): + input_parses = variant.evm.n_to_c(input_parses) error = 'Using a transcript reference sequence to specify a variant position that lies outside of the ' \ 'reference sequence is not HGVS-compliant. Instead re-submit ' genomic_position = validator.myevm_t_to_g(input_parses, variant.no_norm_evm, variant.primary_assembly, @@ -577,54 +574,54 @@ def structure_checks_c(variant, validator): return True # Re-map input_parses back to c. variant - variant.input_parses = variant.evm.n_to_c(variant.input_parses) + input_parses = variant.evm.n_to_c(input_parses) # Intronic positions in UTRs - if re.search(r'\d-\d', str(variant.input_parses)) or re.search(r'\d\+\d', str(variant.input_parses)): + if re.search(r'\d-\d', str(input_parses)) or re.search(r'\d\+\d', str(input_parses)): # Can we go c-g-c try: - to_genome = validator.myevm_t_to_g(variant.input_parses, variant.no_norm_evm, + to_genome = validator.myevm_t_to_g(input_parses, variant.no_norm_evm, variant.primary_assembly, variant.hn, variant) - to_tx = variant.evm.g_to_t(to_genome, variant.input_parses.ac) + to_tx = variant.evm.g_to_t(to_genome, input_parses.ac) except vvhgvs.exceptions.HGVSInvalidIntervalError as e: error = str(e) if 'bounds' in error: try: - identity_info = validator.hdp.get_tx_identity_info(variant.input_parses.ac) + identity_info = validator.hdp.get_tx_identity_info(input_parses.ac) ref_start = identity_info[3] ref_end = identity_info[4] - if '-' in str(variant.input_parses.posedit.pos.start): + if '-' in str(input_parses.posedit.pos.start): # upstream positions boundary = -ref_start - remainder = variant.input_parses.posedit.pos.start.base - boundary - variant.input_parses.posedit.pos.start.base = boundary - variant.input_parses.posedit.pos.start.offset = remainder - if '-' in str(variant.input_parses.posedit.pos.end): + remainder = input_parses.posedit.pos.start.base - boundary + input_parses.posedit.pos.start.base = boundary + input_parses.posedit.pos.start.offset = remainder + if '-' in str(input_parses.posedit.pos.end): boundary = -ref_start - remainder = variant.input_parses.posedit.pos.end.base - boundary - variant.input_parses.posedit.pos.end.base = boundary - variant.input_parses.posedit.pos.end.offset = remainder - if '*' in str(variant.input_parses.posedit.pos.start): + remainder = input_parses.posedit.pos.end.base - boundary + input_parses.posedit.pos.end.base = boundary + input_parses.posedit.pos.end.offset = remainder + if '*' in str(input_parses.posedit.pos.start): # downstream positions - tot_end_pos = str(variant.input_parses.posedit.pos.start).replace('*', '') - ts_seq = validator.sf.fetch_seq(variant.input_parses.ac) + tot_end_pos = str(input_parses.posedit.pos.start).replace('*', '') + ts_seq = validator.sf.fetch_seq(input_parses.ac) boundary = len(ts_seq) - ref_end - variant.input_parses.posedit.pos.start.base = boundary + input_parses.posedit.pos.start.base = boundary te1, te2 = tot_end_pos.split('+') tot_end_pos = int(te1) + int(te2) offset = tot_end_pos - boundary - variant.input_parses.posedit.pos.start.offset = offset - if '*' in str(variant.input_parses.posedit.pos.end): - tot_end_pos = str(variant.input_parses.posedit.pos.end).replace('*', '') - ts_seq = validator.sf.fetch_seq(variant.input_parses.ac) + input_parses.posedit.pos.start.offset = offset + if '*' in str(input_parses.posedit.pos.end): + tot_end_pos = str(input_parses.posedit.pos.end).replace('*', '') + ts_seq = validator.sf.fetch_seq(input_parses.ac) boundary = len(ts_seq) - ref_end - variant.input_parses.posedit.pos.end.base = boundary + input_parses.posedit.pos.end.base = boundary te1, te2 = tot_end_pos.split('+') tot_end_pos = int(te1) + int(te2) offset = tot_end_pos - boundary - variant.input_parses.posedit.pos.end.offset = offset + input_parses.posedit.pos.end.offset = offset - report_gen = validator.myevm_t_to_g(variant.input_parses, variant.no_norm_evm, + report_gen = validator.myevm_t_to_g(input_parses, variant.no_norm_evm, variant.primary_assembly, variant.hn, variant) report_gen = variant.hn.normalize(report_gen) error = 'Using a transcript reference sequence to specify a variant position that lies ' \ @@ -647,14 +644,14 @@ def structure_checks_c(variant, validator): continue gens.append(el_l[-1]) acs = '; '.join(gens) - error = 'Cannot map ' + fn.valstr(variant.input_parses) + ' to a genomic position. '\ - + variant.input_parses.ac + ' can only be partially aligned to genomic reference ' \ + error = 'Cannot map ' + fn.valstr(input_parses) + ' to a genomic position. '\ + + input_parses.ac + ' can only be partially aligned to genomic reference ' \ 'sequences ' + acs variant.warnings.append(error) logger.info(error) return True - elif re.search(r'\d-', str(variant.input_parses)) or re.search(r'\d\+', str(variant.input_parses)): + elif re.search(r'\d-', str(input_parses)) or re.search(r'\d\+', str(input_parses)): # Create a no_replace vm instance variant.no_replace_vm = vvhgvs.variantmapper.VariantMapper(validator.hdp, @@ -663,12 +660,12 @@ def structure_checks_c(variant, validator): # Quick look at syntax validation try: - validator.vr.validate(variant.input_parses) + validator.vr.validate(input_parses) except vvhgvs.exceptions.HGVSInvalidVariantError as e: error = str(e) if 'bounds' in error: try: - report_gen = validator.myevm_t_to_g(variant.input_parses, variant.no_norm_evm, + report_gen = validator.myevm_t_to_g(input_parses, variant.no_norm_evm, variant.primary_assembly, variant.hn, variant) report_gen = variant.hn.normalize(report_gen) except vvhgvs.exceptions.HGVSError as e: @@ -680,18 +677,18 @@ def structure_checks_c(variant, validator): logger.info(error) return True elif 'insertion length must be 1' in error: - if "-" in str(variant.input_parses.posedit.pos.start.offset): - start_offset = str(variant.input_parses.posedit.pos.start.offset - 1) - end_offset = str(variant.input_parses.posedit.pos.start.offset) - elif ("-" not in str(variant.input_parses.posedit.pos.start.offset) and - variant.input_parses.posedit.pos.start.offset != 0): - start_offset = f"+{str(variant.input_parses.posedit.pos.start.offset)}" - end_offset = f"+{str(variant.input_parses.posedit.pos.start.offset + 1)}" - if "(" not in str(variant.input_parses.posedit.pos): + if "-" in str(input_parses.posedit.pos.start.offset): + start_offset = str(input_parses.posedit.pos.start.offset - 1) + end_offset = str(input_parses.posedit.pos.start.offset) + elif ("-" not in str(input_parses.posedit.pos.start.offset) and + input_parses.posedit.pos.start.offset != 0): + start_offset = f"+{str(input_parses.posedit.pos.start.offset)}" + end_offset = f"+{str(input_parses.posedit.pos.start.offset + 1)}" + if "(" not in str(input_parses.posedit.pos): ins_warning = (f'Insertion length must be 1 e.g. ' - f'{variant.input_parses.posedit.pos.start.base}{start_offset}' - f'_{str(int(variant.input_parses.posedit.pos.start.base))}{end_offset}' - f'ins{variant.input_parses.posedit.edit.alt}') + f'{input_parses.posedit.pos.start.base}{start_offset}' + f'_{str(int(input_parses.posedit.pos.start.base))}{end_offset}' + f'ins{input_parses.posedit.edit.alt}') variant.warnings.append(ins_warning) for warning in variant.warnings: if warning == "insertion length must be 1": @@ -699,9 +696,9 @@ def structure_checks_c(variant, validator): return True elif 'base start position must be <= end position' in error: - correction = copy.deepcopy(variant.input_parses) - st = variant.input_parses.posedit.pos.start - ed = variant.input_parses.posedit.pos.end + correction = copy.deepcopy(input_parses) + st = input_parses.posedit.pos.start + ed = input_parses.posedit.pos.end correction.posedit.pos.start = ed correction.posedit.pos.end = st error = error + ': Did you mean ' + str(correction) + '?' @@ -713,34 +710,34 @@ def structure_checks_c(variant, validator): # Have to use this method due to potential multi chromosome error, note normalizes but does not replace sequence output = None try: - output = validator.noreplace_myevm_t_to_g(variant.input_parses, variant) + output = validator.noreplace_myevm_t_to_g(input_parses, variant) except vvhgvs.exceptions.HGVSDataNotAvailableError: - tx_info = validator.hdp.get_tx_identity_info(variant.input_parses.ac) - if (variant.input_parses.posedit.pos.end.base > int(tx_info[4]) or variant.input_parses.posedit.pos.end.base - > int(tx_info[4])) and ("*" not in str(variant.input_parses.posedit.pos.end) or "*" not in - str(variant.input_parses.posedit.pos.start)): + tx_info = validator.hdp.get_tx_identity_info(input_parses.ac) + if (input_parses.posedit.pos.end.base > int(tx_info[4]) or input_parses.posedit.pos.end.base + > int(tx_info[4])) and ("*" not in str(input_parses.posedit.pos.end) or "*" not in + str(input_parses.posedit.pos.start)): errors = ["CDSError: Variant start position and/or end position are beyond the CDS end position " "and likely also beyond the end of the selected reference sequence"] else: - errors = ['Required information for ' + variant.input_parses.ac + ' is missing from the Universal ' + errors = ['Required information for ' + input_parses.ac + ' is missing from the Universal ' 'Transcript Archive', 'Query gene2transcripts with search term %s for ' - 'available transcripts' % variant.input_parses.ac.split('.')[0]] + 'available transcripts' % input_parses.ac.split('.')[0]] variant.warnings.extend(errors) logger.info(str(errors)) return True except ValueError as e: error = str(e) if '> end' in error: - error = 'Interval start position ' + str(variant.input_parses.posedit.pos.start) + ' > interval end '\ - 'position ' + str(variant.input_parses.posedit.pos.end) + error = 'Interval start position ' + str(input_parses.posedit.pos.start) + ' > interval end '\ + 'position ' + str(input_parses.posedit.pos.end) variant.warnings.append(error) logger.info(error) return True except vvhgvs.exceptions.HGVSInvalidVariantError as e: error = str(e) if 'base start position must be <= end position' in error: - error = 'Interval start position ' + str(variant.input_parses.posedit.pos.start) + ' > interval end' \ - ' position ' + str(variant.input_parses.posedit.pos.end) + error = 'Interval start position ' + str(input_parses.posedit.pos.start) + ' > interval end' \ + ' position ' + str(input_parses.posedit.pos.end) variant.warnings.append(error) logger.info(error) return True @@ -750,10 +747,10 @@ def structure_checks_c(variant, validator): return True try: - variant.evm.g_to_t(output, variant.input_parses.ac) + variant.evm.g_to_t(output, input_parses.ac) except vvhgvs.exceptions.HGVSError as e: if "Alignment is incomplete" in str(e): - output = hgvs_utils.incomplete_alignment_mapping_t_to_g(validator, variant) + output = hgvs_utils.incomplete_alignment_mapping_t_to_g(validator, variant,input_parses) if output is None: error = str(e) variant.warnings.append(error) @@ -766,14 +763,14 @@ def structure_checks_c(variant, validator): return True # Check that the reference is correct by direct mapping without replacing reference - check_ref_g = variant.no_replace_vm.t_to_g(variant.input_parses, output.ac, + check_ref_g = variant.no_replace_vm.t_to_g(input_parses, output.ac, alt_aln_method=validator.alt_aln_method) - check_ref_t = variant.no_replace_vm.g_to_t(check_ref_g, variant.input_parses.ac, + check_ref_t = variant.no_replace_vm.g_to_t(check_ref_g, input_parses.ac, alt_aln_method=validator.alt_aln_method) # Snapshot current variant error log - if "*" in str(check_ref_t) and "*" not in str(variant.input_parses): - convert = "%s auto-mapped to %s" % (variant.input_parses, check_ref_t) + if "*" in str(check_ref_t) and "*" not in str(input_parses): + convert = "%s auto-mapped to %s" % (input_parses, check_ref_t) variant.warnings.append(convert) snap = copy.copy(variant.warnings) @@ -800,24 +797,24 @@ def structure_checks_c(variant, validator): else: # All other variation try: - validator.vr.validate(variant.input_parses) + validator.vr.validate(input_parses) except vvhgvs.exceptions.HGVSUnsupportedOperationError as e: logger.debug("Except passed, %s", e) except vvhgvs.exceptions.HGVSInvalidVariantError as e: error = str(e) # This catches errors in introns if 'base start position must be <= end position' in error: - error = 'Interval start position ' + str(variant.input_parses.posedit.pos.start) + ' > interval end '\ - 'position ' + str(variant.input_parses.posedit.pos.end) - if "(" not in str(variant.input_parses.posedit.pos): + error = 'Interval start position ' + str(input_parses.posedit.pos.start) + ' > interval end '\ + 'position ' + str(input_parses.posedit.pos.end) + if "(" not in str(input_parses.posedit.pos): variant.warnings.append(error) logger.info(error) if 'insertion length must be 1' in error: - if "(" not in str(variant.input_parses.posedit.pos): + if "(" not in str(input_parses.posedit.pos): ins_warning = (f'Insertion length must be 1 e.g. ' - f'{str(int(variant.input_parses.posedit.pos.start.base))}' - f'_{str(int(variant.input_parses.posedit.pos.start.base)+1)}' - f'ins{variant.input_parses.posedit.edit.alt}') + f'{str(int(input_parses.posedit.pos.start.base))}' + f'_{str(int(input_parses.posedit.pos.start.base)+1)}' + f'ins{input_parses.posedit.edit.alt}') variant.warnings.append(ins_warning) for warning in variant.warnings: if warning == "insertion length must be 1": @@ -832,7 +829,7 @@ def structure_checks_c(variant, validator): except vvhgvs.exceptions.HGVSError as e: error = str(e) if 'bounds' in error: - error += ' (' + variant.input_parses.ac + ')' + error += ' (' + input_parses.ac + ')' variant.warnings.append(error) logger.info(error) return True @@ -840,25 +837,25 @@ def structure_checks_c(variant, validator): return False -def structure_checks_n(variant, validator): +def structure_checks_n(variant, validator, input_parses): """ structure checks for reftype nucleotide :param variant: :param validator: :return: """ - if '+' in str(variant.input_parses) or '-' in str(variant.input_parses): + if '+' in str(input_parses) or '-' in str(input_parses): # Catch variation in UTRs # These should be in the sequence so can be directly validated. Need to pass to n. try: - validator.vr.validate(variant.input_parses) + validator.vr.validate(input_parses) except vvhgvs.exceptions.HGVSError as e: error = str(e) if 'intronic variant' in error: pass elif 'datums is ill-defined' in error: - called_ref = variant.input_parses.posedit.edit.ref - to_n = variant.evm.c_to_n(variant.input_parses) + called_ref = input_parses.posedit.edit.ref + to_n = variant.evm.c_to_n(input_parses) actual_ref = to_n.posedit.edit.ref if called_ref != actual_ref: error = 'Variant reference (' + called_ref + ') does not agree with reference sequence (' + \ @@ -867,27 +864,27 @@ def structure_checks_n(variant, validator): logger.info(str(error)) return True else: - variant.input_parses.posedit.edit.ref = '' - variant.hgvs_formatted = variant.input_parses + input_parses.posedit.edit.ref = '' + variant.quibble = input_parses elif 'base must be >=1 for datum = SEQ_START or CDS_END' in error: error = 'The given coordinate is outside the bounds of the reference sequence.' try: - if '-' in str(variant.input_parses.posedit.pos.start): + if '-' in str(input_parses.posedit.pos.start): # upstream positions boundary = 1 - remainder = variant.input_parses.posedit.pos.start.base - boundary + remainder = input_parses.posedit.pos.start.base - boundary remainder = remainder + 1 - variant.input_parses.posedit.pos.start.base = boundary - variant.input_parses.posedit.pos.start.offset = remainder - if '-' in str(variant.input_parses.posedit.pos.end): + input_parses.posedit.pos.start.base = boundary + input_parses.posedit.pos.start.offset = remainder + if '-' in str(input_parses.posedit.pos.end): boundary = 1 - remainder = variant.input_parses.posedit.pos.end.base - boundary + remainder = input_parses.posedit.pos.end.base - boundary remainder = remainder + 1 - variant.input_parses.posedit.pos.end.base = boundary - variant.input_parses.posedit.pos.end.offset = remainder - report_gen = validator.myevm_t_to_g(variant.input_parses, variant.no_norm_evm, + input_parses.posedit.pos.end.base = boundary + input_parses.posedit.pos.end.offset = remainder + report_gen = validator.myevm_t_to_g(input_parses, variant.no_norm_evm, variant.primary_assembly, variant.hn, variant) report_gen = variant.hn.normalize(report_gen) error = 'Using a transcript reference sequence to specify a variant position that lies outside of' \ @@ -902,10 +899,10 @@ def structure_checks_n(variant, validator): logger.info(error) return True - if 'n.1-' in str(variant.input_parses): + if 'n.1-' in str(input_parses): error = 'Using a transcript reference sequence to specify a variant position that lies outside of the ' \ 'reference sequence is not HGVS-compliant. Instead re-submit ' - genomic_position = validator.myevm_t_to_g(variant.input_parses, variant.no_norm_evm, variant.primary_assembly, + genomic_position = validator.myevm_t_to_g(input_parses, variant.no_norm_evm, variant.primary_assembly, variant.hn,variant) genomic_position = variant.hn.normalize(genomic_position) error = error + fn.valstr(genomic_position) @@ -913,15 +910,15 @@ def structure_checks_n(variant, validator): logger.info(error) return True - if re.search(r'\d-', str(variant.input_parses)) or re.search(r'\d\+', str(variant.input_parses)): + if re.search(r'\d-', str(input_parses)) or re.search(r'\d\+', str(input_parses)): # Quick look at syntax validation try: - validator.vr.validate(variant.input_parses) + validator.vr.validate(input_parses) except vvhgvs.exceptions.HGVSInvalidVariantError as e: error = str(e) if 'bounds' in error: try: - report_gen = validator.myevm_t_to_g(variant.input_parses, variant.no_norm_evm, + report_gen = validator.myevm_t_to_g(input_parses, variant.no_norm_evm, variant.primary_assembly, variant.hn,variant) report_gen = variant.hn.normalize(report_gen) except vvhgvs.exceptions.HGVSError as e: @@ -937,9 +934,9 @@ def structure_checks_n(variant, validator): logger.info(error) return True elif 'base start position must be <= end position' in error: - correction = copy.deepcopy(variant.input_parses) - st = variant.input_parses.posedit.pos.start - ed = variant.input_parses.posedit.pos.end + correction = copy.deepcopy(input_parses) + st = input_parses.posedit.pos.start + ed = input_parses.posedit.pos.end correction.posedit.pos.start = ed correction.posedit.pos.end = st error = error + ': Did you mean ' + str(correction) + '?' @@ -950,13 +947,13 @@ def structure_checks_n(variant, validator): return True elif 'Cannot validate sequence of an intronic variant' in error: try: - test_g = validator.myevm_t_to_g(variant.input_parses, variant.no_norm_evm, variant.primary_assembly, + test_g = validator.myevm_t_to_g(input_parses, variant.no_norm_evm, variant.primary_assembly, variant.hn,variant) - variant.evm.g_to_t(test_g, variant.input_parses.ac) + variant.evm.g_to_t(test_g, input_parses.ac) except vvhgvs.exceptions.HGVSError as e: error = str(e) if 'bounds' in error: - report_gen = validator.myevm_t_to_g(variant.input_parses, variant.no_norm_evm, + report_gen = validator.myevm_t_to_g(input_parses, variant.no_norm_evm, variant.primary_assembly, variant.hn,variant) report_gen = variant.hn.normalize(report_gen) error = 'Using a transcript reference sequence to specify a variant position that lies ' \ @@ -971,12 +968,12 @@ def structure_checks_n(variant, validator): # sequence output = None try: - output = validator.noreplace_myevm_t_to_g(variant.input_parses, variant) + output = validator.noreplace_myevm_t_to_g(input_parses, variant) except vvhgvs.exceptions.HGVSDataNotAvailableError: - errors = ['Required information for ' + variant.input_parses.ac + ' is missing from the Universal ' + errors = ['Required information for ' + input_parses.ac + ' is missing from the Universal ' 'Transcript Archive', 'Query gene2transcripts with search term %s for ' - 'available transcripts' % variant.input_parses.ac.split('.')[0]] + 'available transcripts' % input_parses.ac.split('.')[0]] variant.warnings.extend(errors) logger.info(str(errors)) return True @@ -984,23 +981,23 @@ def structure_checks_n(variant, validator): error = str(e) if '> end' in error: error = 'Interval start position ' + str( - variant.input_parses.posedit.pos.start) + ' > interval end position ' + str( - variant.input_parses.posedit.pos.end) + input_parses.posedit.pos.start) + ' > interval end position ' + str( + input_parses.posedit.pos.end) variant.warnings.append(error) logger.info(error) return True except vvhgvs.exceptions.HGVSInvalidVariantError as e: error = str(e) if 'base start position must be <= end position' in error: - correction = copy.deepcopy(variant.input_parses) - st = variant.input_parses.posedit.pos.start - ed = variant.input_parses.posedit.pos.end + correction = copy.deepcopy(input_parses) + st = input_parses.posedit.pos.start + ed = input_parses.posedit.pos.end correction.posedit.pos.start = ed correction.posedit.pos.end = st # error = error + ': Did you mean ' + str(correction) + '?' error = 'Interval start position ' + str( - variant.input_parses.posedit.pos.start) + ' > interval end position ' + str( - variant.input_parses.posedit.pos.end) + input_parses.posedit.pos.start) + ' > interval end position ' + str( + input_parses.posedit.pos.end) variant.warnings.append(error) logger.info(error) return True @@ -1015,7 +1012,7 @@ def structure_checks_n(variant, validator): else: # All other variation try: - validator.vr.validate(variant.input_parses) + validator.vr.validate(input_parses) except vvhgvs.exceptions.HGVSUnsupportedOperationError as e: logger.debug("Except passed, %s", e) except vvhgvs.exceptions.HGVSInvalidVariantError as e: @@ -1039,15 +1036,15 @@ def structure_checks_n(variant, validator): # continue # This catches errors in introns if 'base start position must be <= end position' in error: - # correction = copy.deepcopy(variant.input_parses) - # st = variant.input_parses.posedit.pos.start - # ed = variant.input_parses.posedit.pos.end + # correction = copy.deepcopy(input_parses) + # st = input_parses.posedit.pos.start + # ed = input_parses.posedit.pos.end # correction.posedit.pos.start = ed # correction.posedit.pos.end = st # error = error + ': Did you mean ' + str(correction) + '?' error = 'Interval start position ' + str( - variant.input_parses.posedit.pos.start) + ' > interval end position ' + str( - variant.input_parses.posedit.pos.end) + input_parses.posedit.pos.start) + ' > interval end position ' + str( + input_parses.posedit.pos.end) logger.info(error) variant.warnings.append(error) return True @@ -1062,7 +1059,7 @@ def structure_checks_n(variant, validator): except vvhgvs.exceptions.HGVSError as e: error = str(e) if 'bounds' in error: - error = error + ' (' + variant.input_parses.ac + ')' + error = error + ' (' + input_parses.ac + ')' variant.warnings.append(error) logger.info(error) return True diff --git a/VariantValidator/modules/variant.py b/VariantValidator/modules/variant.py index 5b882f25..03ace01d 100644 --- a/VariantValidator/modules/variant.py +++ b/VariantValidator/modules/variant.py @@ -11,22 +11,22 @@ class Variant(object): def __init__(self, original, quibble=None, warnings=None, write=True, primary_assembly=False, order=False, selected_assembly=False, reformat_output=False, expanded_repeat=None): self.original = original - if quibble is None: + + # Managed variant mappings (hgvs)objects that are updated over the course of input processing + if quibble is None:# working version of the input variant, hgvs object after initial input processing self.quibble = original else: self.quibble = quibble - self.hgvs_formatted = None - self.hgvs_genomic = None - self.hgvs_coding = None - self.post_format_conversion = None # Used for first gapped_mapping function - self.pre_RNA_conversion = None - self.input_parses = None # quibble as hgvs variant object + self.hgvs_transcript_variant = None # specifically main hgvs transcript mapping if available + self.hgvs_genomic = None # main hgvs genomic mapping if available + self.hgvs_refseqgene_variant = None # main hgvs RefSeqGene genomic mapping, used to set LRG output data + + # Variant annotations self.transcript_type = None self.lovd_syntax_check = None self.shorthand_vcf = None self.lovd_messages = None self.lovd_corrections = None - if warnings is None: self.warnings = [] else: @@ -36,10 +36,6 @@ def __init__(self, original, quibble=None, warnings=None, write=True, primary_as self.warnings = [warnings] self.description = '' # hgnc_gene_info variable self.annotations = '' - self.coding = '' - self.coding_g = '' - self.genomic_r = '' - self.genomic_g = '' # should be a hgvs obj or nothing self.protein = '' self.write = write self.primary_assembly = primary_assembly @@ -68,10 +64,8 @@ def __init__(self, original, quibble=None, warnings=None, write=True, primary_as # Required for output self.stable_gene_ids = None - self.hgvs_transcript_variant = None # variant.coding but edited self.genome_context_intronic_sequence = None self.refseqgene_context_intronic_sequence = None - self.hgvs_refseqgene_variant = None # genomic_r but edited self.hgvs_predicted_protein_consequence = None self.hgvs_lrg_transcript_variant = None self.hgvs_lrg_variant = None # Same as hgvs_refseqgene_variant but with LRG accession diff --git a/VariantValidator/modules/vvMixinCore.py b/VariantValidator/modules/vvMixinCore.py index 746f09be..4ef327d9 100644 --- a/VariantValidator/modules/vvMixinCore.py +++ b/VariantValidator/modules/vvMixinCore.py @@ -30,6 +30,7 @@ convert_seq_state_to_expanded_repeat from VariantValidator.modules.hgvs_utils import hgvs_delins_parts_to_hgvs_obj,\ unset_hgvs_obj_ref, to_vv_hgvs +from VariantValidator.modules.complex_descriptions import FEInterval logger = logging.getLogger(__name__) @@ -180,10 +181,10 @@ def validate(self, if type(my_variant.quibble) is not str: # already tidied input mapped to transcripts so no need to re-validate for user input type issues - if not my_variant.hgvs_formatted: - my_variant.hgvs_formatted = my_variant.quibble if not my_variant.reftype: my_variant.reftype = f':{my_variant.quibble.type}.' + if not my_variant.refsource: + my_variant.set_refsource() if my_variant.reftype != ':g.': toskip = self._get_transcript_info(my_variant) if toskip: @@ -216,6 +217,8 @@ def validate(self, my_variant.gene_symbol = self.db.get_gene_symbol_from_transcript_id( my_variant.quibble.ac) try: + # creates mapping from quibble, also sets + # hgvs_transcript_variant with info from genome mapping toskip = mappers.transcripts_to_gene( my_variant, self, @@ -235,7 +238,6 @@ def validate(self, my_variant.gene_symbol = '' if toskip: continue - my_variant.hgvs_transcript_variant = my_variant.quibble # set output to variant type specific if my_variant.reftype in [':n.',':t.',':c.'] and my_variant.hgvs_transcript_variant != '': @@ -403,10 +405,9 @@ def validate(self, try: # Test intronic variants for incorrect boundaries (see issue #169) test_variant = copy.copy(my_variant) - test_variant.hgvs_formatted = my_variant.quibble - if type(test_variant.hgvs_formatted) is str: - test_variant.hgvs_formatted = self.hp.parse_hgvs_variant( - test_variant.hgvs_formatted) + if type(test_variant.quibble) is str: + test_variant.quibble = self.hp.parse_hgvs_variant( + test_variant.quibble) # Create easy variant mapper (over variant mapper) and splign locked evm test_variant.evm = AssemblyMapper(self.hdp, @@ -491,8 +492,6 @@ def validate(self, # Set some configurations formatted_variant = my_variant.quibble - stash_input = my_variant.quibble - my_variant.post_format_conversion = stash_input logger.debug("Variant input formatted, proceeding to validate.") @@ -515,41 +514,39 @@ def validate(self, logger.info(str(e)) - my_variant.hgvs_formatted = formatted_variant + my_variant.quibble = formatted_variant - if 'LRG' in my_variant.hgvs_formatted.ac: - my_variant.hgvs_formatted.ac.replace('T', 't') + if 'LRG' in my_variant.quibble.ac: + my_variant.quibble.ac.replace('T', 't') else: - my_variant.hgvs_formatted.ac = my_variant.hgvs_formatted.ac.upper() + my_variant.quibble.ac = my_variant.quibble.ac.upper() - if my_variant.hgvs_formatted.type == "p" and my_variant.hgvs_formatted.posedit is None \ - and ":p.?" in str(my_variant.hgvs_formatted): + if my_variant.quibble.type == "p" and my_variant.quibble.posedit is None \ + and ":p.?" in str(my_variant.quibble): # Protein variants needed early! toskip = format_converters.proteins(my_variant, self) if toskip: continue - if hasattr(my_variant.hgvs_formatted.posedit.edit, 'alt'): - if my_variant.hgvs_formatted.posedit.edit.alt is not None: - my_variant.hgvs_formatted.posedit.edit.alt = \ - my_variant.hgvs_formatted.posedit.edit.alt.upper() - if hasattr(my_variant.hgvs_formatted.posedit.edit, 'ref'): - if my_variant.hgvs_formatted.posedit.edit.ref is not None: - my_variant.hgvs_formatted.posedit.edit.ref = \ - my_variant.hgvs_formatted.posedit.edit.ref.upper() - + if hasattr(my_variant.quibble.posedit.edit, 'alt'): + if my_variant.quibble.posedit.edit.alt is not None: + my_variant.quibble.posedit.edit.alt = \ + my_variant.quibble.posedit.edit.alt.upper() + if hasattr(my_variant.quibble.posedit.edit, 'ref'): + if my_variant.quibble.posedit.edit.ref is not None: + my_variant.quibble.posedit.edit.ref = \ + my_variant.quibble.posedit.edit.ref.upper() try: - formatted_variant = str(my_variant.hgvs_formatted) + formatted_variant = str(my_variant.quibble) except KeyError as e: - if "p" in my_variant.hgvs_formatted.type: + if "p" in my_variant.quibble.type: error = "Invalid amino acid %s stated in description %s" % ( str(e), str(my_variant.quibble.format({'p_3_letter':False}))) my_variant.warnings.append(error) continue - my_variant.set_quibble(my_variant.hgvs_formatted) logger.debug("HVGS acceptance test passed") # Check whether supported genome build is requested for non g. descriptions @@ -603,12 +600,12 @@ def validate(self, # i.e. out of bounds # skip if we have fuzzy ends - if isinstance(my_variant.quibble.posedit.pos.start, Interval) or \ - isinstance(my_variant.quibble.posedit.pos.end, Interval): + if isinstance(my_variant.quibble.posedit.pos, FEInterval) or \ + my_variant.quibble.posedit.pos.uncertain: continue - if '*' in str(my_variant.hgvs_formatted.posedit): - input_parses_copy = copy.deepcopy(my_variant.hgvs_formatted) + if '*' in str(my_variant.quibble.posedit): + input_parses_copy = copy.deepcopy(my_variant.quibble) input_parses_copy.type = "c" # Map to n. position # Create easy variant mapper (over variant mapper) and splign locked evm @@ -626,13 +623,13 @@ def validate(self, logger.info(error) continue - elif my_variant.hgvs_formatted.posedit.pos.end.base < \ - my_variant.hgvs_formatted.posedit.pos.start.base: - if my_variant.hgvs_formatted.ac not in ["NC_012920.1", "NC_001807.4"]: + elif my_variant.quibble.posedit.pos.end.base < \ + my_variant.quibble.posedit.pos.start.base: + if my_variant.quibble.ac not in ["NC_012920.1", "NC_001807.4"]: error = 'Interval end position ' +\ - str(my_variant.hgvs_formatted.posedit.pos.end.base) + \ + str(my_variant.quibble.posedit.pos.end.base) + \ ' < interval start position ' + \ - str(my_variant.hgvs_formatted.posedit.pos.start.base) + str(my_variant.quibble.posedit.pos.start.base) my_variant.warnings.append(error) logger.info(error) continue @@ -640,7 +637,7 @@ def validate(self, # Catch missing version number in refseq/ens is_version = re.compile(r"\d\.\d") if ((my_variant.refsource == 'RefSeq' or my_variant.refsource == 'ENS') and - not is_version.search(my_variant.hgvs_formatted.ac)): + not is_version.search(my_variant.quibble.ac)): error = 'RefSeq variant accession numbers MUST include a version number' my_variant.warnings.append(error) continue @@ -676,8 +673,7 @@ def validate(self, continue # RNA variants - trapped_input = str(my_variant.hgvs_formatted) - my_variant.pre_RNA_conversion = trapped_input + trapped_input = str(my_variant.quibble) toskip = format_converters.rna(my_variant, self) if toskip: continue @@ -772,16 +768,16 @@ def validate(self, # Genomic sequence variation # Check for gapped delins - if (variant.genomic_g and variant.genomic_g.posedit.edit.type == 'delins' and - variant.genomic_g.posedit.edit.alt == ""): - logger.info(f"Delins minus an ALT sequence identified {variant.genomic_g}") - variant.genomic_g = hgvs_delins_parts_to_hgvs_obj( - variant.genomic_g.ac, - variant.genomic_g.type, - variant.genomic_g.posedit.pos, + if (variant.hgvs_genomic and variant.hgvs_genomic.posedit.edit.type == 'delins' and + variant.hgvs_genomic.posedit.edit.alt == ""): + logger.info(f"Delins minus an ALT sequence identified {variant.hgvs_genomic}") + variant.hgvs_genomic = hgvs_delins_parts_to_hgvs_obj( + variant.hgvs_genomic.ac, + variant.hgvs_genomic.type, + variant.hgvs_genomic.posedit.pos, '', '') - hgvs_genomic_variant = variant.genomic_g + hgvs_genomic_variant = variant.hgvs_genomic # genomic accession logger.debug("genomic accession") @@ -793,10 +789,11 @@ def validate(self, # RefSeqGene variation logger.debug("RefSeqGene variation") - refseqgene_variant = variant.genomic_r + refseqgene_variant = variant.hgvs_refseqgene_variant if not refseqgene_variant or type(refseqgene_variant) is str and 'RefSeqGene' in refseqgene_variant: - variant.warnings.append(refseqgene_variant) + if type(refseqgene_variant) is str and 'RefSeqGene' in refseqgene_variant: + variant.warnings.append(refseqgene_variant) lrg_variant = '' refseqgene_variant = '' else: @@ -814,13 +811,14 @@ def validate(self, # Transcript sequence variation logger.debug("Transcript sequence variation") hgvs_tx_variant = None - if variant.coding: - if '(' in str(hgvs_tx_variant) and ')' in str(hgvs_tx_variant): - assert False + # ambiguous pos variants with spans instead of start and/or stop locations can't be mapped, + # yet (unlike simpler ones), so we null the mapping variables for them + if variant.hgvs_transcript_variant and not isinstance( + variant.hgvs_transcript_variant.posedit.pos,FEInterval): # transcript accession logger.debug("transcript accession") - hgvs_tx_variant = unset_hgvs_obj_ref(variant.coding) + hgvs_tx_variant = unset_hgvs_obj_ref(variant.hgvs_transcript_variant) hgvs_transcript_variant = copy.deepcopy(hgvs_tx_variant) transcript_accession = hgvs_transcript_variant.ac @@ -937,6 +935,10 @@ def validate(self, if 'NC_000' not in alt_gen_var.ac and 'NC_012920.1' not in alt_gen_var.ac and \ 'NC_001807.4' not in alt_gen_var.ac: continue + # skip if we have fuzzy ends in a currently non mappable form + if isinstance(alt_gen_var.posedit.pos.start, Interval) or \ + isinstance(alt_gen_var.posedit.pos.end, Interval): + continue try: alt_gen_var = variant.hn.normalize(alt_gen_var) except vvhgvs.exceptions.HGVSInvalidVariantError: @@ -991,11 +993,14 @@ def validate(self, # Warn not directly mapped to specified genome build if genomic_accession: - if primary_assembly.lower() not in list(primary_genomic_dicts.keys()): - errors = [str(variant.hgvs_coding) + ' is not part of genome build ' + primary_assembly] + if genomic_accession.startswith("NG_"): + # Skip for NG_ which fail to find genomic mapping (done via transcript) without extra errors + pass + elif primary_assembly.lower() not in list(primary_genomic_dicts.keys()): + errors = [str(variant.quibble) + ' is not part of genome build ' + primary_assembly] if self.alt_aln_method == "splign": - errors.append(str(variant.hgvs_coding) + ' cannot be mapped directly to genome build ' + primary_assembly) + errors.append(str(variant.quibble) + ' cannot be mapped directly to genome build ' + primary_assembly) errors.append('See alternative genomic loci or alternative genome builds for aligned genomic positions') elif self.alt_aln_method == "genebuild": @@ -1005,7 +1010,7 @@ def validate(self, elif primary_assembly == "GRCh37" or primary_assembly == "hg19": alt_build = "GRCh38" # Shows the alternative genome build too - errors.append(str(variant.hgvs_coding) + ' cannot be mapped directly to genome build ' + primary_assembly + errors.append(str(variant.quibble) + ' cannot be mapped directly to genome build ' + primary_assembly + ', did you mean ' + alt_build + '?') variant.warnings.extend(errors) @@ -1053,12 +1058,12 @@ def validate(self, # convert UTR variants from p.? to p.(?) try: if ( - variant.hgvs_coding.posedit.pos.end.base < 0 or - variant.hgvs_coding.posedit.pos.start.datum == + variant.hgvs_transcript_variant.posedit.pos.end.base < 0 or + variant.hgvs_transcript_variant.posedit.pos.start.datum == Datum.CDS_END ): logger.info( - f"UTR variant {variant.hgvs_coding} identified. " + f"UTR variant {variant.hgvs_transcript_variant} identified. " f"Updating from p.? to p.(=)" ) @@ -1375,7 +1380,8 @@ def validate(self, variant.alt_genomic_loci = alt_genomic_loci # Add exon numbering information see issue # - if variant.coding != "": + if variant.hgvs_transcript_variant and not isinstance( + variant.hgvs_transcript_variant.posedit.pos,FEInterval): try: exs = exon_numbering.finds_exon_number(variant, self) variant.exonic_positions = exs @@ -1643,10 +1649,16 @@ def _vcf_abrv(hgvs,vcf,max_non_abrv_len=500): vcf["alt"] = hgvs.posedit.edit.type.upper() return vcf + if not variant.primary_assembly_loci: + variant.primary_assembly_loci = {} for gen in variant.primary_assembly_loci.keys(): variant.primary_assembly_loci[gen]['vcf'] = _vcf_abrv( variant.primary_assembly_loci[gen][hgd], variant.primary_assembly_loci[gen]['vcf']) + + if not variant.primary_assembly_loci: + variant.primary_assembly_loci = {} + for gen in variant.primary_assembly_loci.keys(): variant.primary_assembly_loci[gen][hgd] = \ variant.primary_assembly_loci[gen][hgd].format({'max_ref_length': 0}) @@ -1777,7 +1789,7 @@ def _get_transcript_info(self, variant): Should only be called during the validator process """ logger.debug("Looking for transcript info") - hgvs_vt = variant.hgvs_formatted + hgvs_vt = variant.quibble try: self.hdp.get_tx_identity_info(str(hgvs_vt.ac)) except vvhgvs.exceptions.HGVSError as e: @@ -1793,7 +1805,7 @@ def _get_transcript_info(self, variant): if self.alt_aln_method != 'genebuild': # Gene description - requires GenBank search to get all the required info, i.e. transcript variant ID # accession number - hgvs_object = variant.hgvs_formatted + hgvs_object = variant.quibble accession = hgvs_object.ac # Look for the accession in our database # Connect to database and send request @@ -1874,7 +1886,7 @@ def _get_transcript_info(self, variant): # Ensembl databases else: # accession number - hgvs_object = variant.hgvs_formatted + hgvs_object = variant.quibble accession = hgvs_object.ac # Look for the accession in our database # Connect to database and send request diff --git a/tests/test_variant.py b/tests/test_variant.py index 09be1379..673f7cc7 100644 --- a/tests/test_variant.py +++ b/tests/test_variant.py @@ -64,18 +64,9 @@ def test_order_not_set(self): def test_all_defaults(self): var = Variant('NM_015120.4:c.34=') - self.assertEqual(var.hgvs_formatted, None) self.assertEqual(var.hgvs_genomic, None) - self.assertEqual(var.hgvs_coding, None) - self.assertEqual(var.post_format_conversion, None) - self.assertEqual(var.pre_RNA_conversion, None) - self.assertEqual(var.input_parses, None) self.assertEqual(var.description, '') - self.assertEqual(var.coding, '') - self.assertEqual(var.coding_g, '') - self.assertEqual(var.genomic_r, '') - self.assertEqual(var.genomic_g, '') self.assertEqual(var.protein, '') self.assertEqual(var.output_type_flag, 'warning') self.assertEqual(var.gene_symbol, '') From 4fb1a24459c3210d704c5f476ad124a0340b2f07 Mon Sep 17 00:00:00 2001 From: John-F-Wagstaff Date: Tue, 30 Jun 2026 15:44:42 +0100 Subject: [PATCH 2/3] Use hgvs objects in VV until output processing We used to store a number of hgvs descriptions for variation as mapped to specific targets e.g. genome, RSG, protein. Now that we need these as objects, to make VRS output, switch this to happen only at the last output processing step. --- VariantValidator/modules/valoutput.py | 44 +++++++++++--- VariantValidator/modules/variant.py | 64 +++++++++++++++++--- VariantValidator/modules/vvDBGet.py | 12 ++-- VariantValidator/modules/vvMixinCore.py | 77 +++++++++---------------- 4 files changed, 123 insertions(+), 74 deletions(-) diff --git a/VariantValidator/modules/valoutput.py b/VariantValidator/modules/valoutput.py index 3eb95063..e3e0d5a6 100644 --- a/VariantValidator/modules/valoutput.py +++ b/VariantValidator/modules/valoutput.py @@ -35,8 +35,11 @@ def format_as_dict(self, with_meta=True, test=False): if variant.is_obsolete() and variant.hgvs_transcript_variant == '': validation_obsolete_counter += 1 identification_key = 'obsolete_record_%s' % validation_obsolete_counter + elif variant.hgvs_transcript_variant: + identification_key = variant.hgvs_transcript_variant.format({ + 'max_ref_length': 0}) else: - identification_key = str(variant.hgvs_transcript_variant) + identification_key = 'None' validation_output[identification_key] = variant.output_dict(test=test) @@ -121,19 +124,22 @@ def format_as_table(self, with_meta=True): prot = '' if variant.hgvs_predicted_protein_consequence is not None: - prot = variant.hgvs_predicted_protein_consequence['tlr'] + prot = variant.hgvs_predicted_protein_consequence['prot'].format({ + 'max_ref_length': 0}) if variant.rna_data is not None: prot = variant.rna_data["translation"] grch37 = '' grch37_vcf = {'chr': '', 'pos': '', 'ref': '', 'alt': '', 'id': ''} if variant.primary_assembly_loci and 'grch37' in variant.primary_assembly_loci: - grch37 = variant.primary_assembly_loci['grch37']['hgvs_genomic_description'] + grch37 = variant.primary_assembly_loci['grch37']['hgvs_genomic_description'].format({ + 'max_ref_length': 0}) grch37_vcf = variant.primary_assembly_loci['grch37']['vcf'] grch37_vcf['id'] = '.' grch38 = '' grch38_vcf = {'chr': '', 'pos': '', 'ref': '', 'alt': '', 'id': ''} if variant.primary_assembly_loci and 'grch38' in variant.primary_assembly_loci: - grch38 = variant.primary_assembly_loci['grch38']['hgvs_genomic_description'] + grch38 = variant.primary_assembly_loci['grch38']['hgvs_genomic_description'].format({ + 'max_ref_length': 0}) grch38_vcf = variant.primary_assembly_loci['grch38']['vcf'] grch38_vcf['id'] = '.' alt_genomic = [] @@ -141,7 +147,7 @@ def format_as_table(self, with_meta=True): for alt in variant.alt_genomic_loci: for k, v in alt.items(): if k == 'grch37' or k == 'grch38': - alt_genomic.append(v['hgvs_genomic_description']) + alt_genomic.append(v['hgvs_genomic_description'].format({'max_ref_length': 0})) gene_id = '' if variant.stable_gene_ids: if 'hgnc_id' in variant.stable_gene_ids: @@ -156,16 +162,36 @@ def format_as_table(self, with_meta=True): pass if variant.rna_data is None: + if variant.hgvs_transcript_variant is not None: + hgvs_transcript = variant.hgvs_transcript_variant.format({'max_ref_length': 0}) + else: + hgvs_transcript = None + + if variant.hgvs_refseqgene_variant: + hgvs_refseqgene = variant.hgvs_refseqgene_variant.format({'max_ref_length': 0}) + else: + hgvs_refseqgene = None + + if variant.hgvs_lrg_variant: + hgvs_lrg = variant.hgvs_lrg_variant.format({'max_ref_length': 0}) + else: + hgvs_lrg = None + + if variant.hgvs_lrg_transcript_variant: + hgvs_lrg_transcript = variant.hgvs_lrg_transcript_variant.format({'max_ref_length': 0}) + else: + hgvs_lrg_transcript = None + outputstrings.append([ variant.original, '|'.join(variant.process_warnings(string_all=True)), select_tx, - variant.hgvs_transcript_variant, + hgvs_transcript, variant.genome_context_intronic_sequence, variant.refseqgene_context_intronic_sequence, - variant.hgvs_refseqgene_variant, - variant.hgvs_lrg_variant, - variant.hgvs_lrg_transcript_variant, + hgvs_refseqgene, + hgvs_lrg, + hgvs_lrg_transcript, prot, grch37, grch37_vcf['chr'], diff --git a/VariantValidator/modules/variant.py b/VariantValidator/modules/variant.py index 03ace01d..ab44d772 100644 --- a/VariantValidator/modules/variant.py +++ b/VariantValidator/modules/variant.py @@ -236,8 +236,7 @@ def output_dict(self, test=False): except KeyError: pass try: - del self.hgvs_predicted_protein_consequence['lrg_tlr'] - del self.hgvs_predicted_protein_consequence['lrg_slr'] + del self.hgvs_predicted_protein_consequence['lrg_prot'] except KeyError: pass @@ -248,22 +247,69 @@ def output_dict(self, test=False): 'gene_ids': self.stable_gene_ids, 'annotations': self.annotations, 'transcript_description': self.description, - 'hgvs_transcript_variant': self.hgvs_transcript_variant, + 'hgvs_transcript_variant': None if self.hgvs_transcript_variant is None else \ + self.hgvs_transcript_variant.format({'max_ref_length': 0}), 'rna_variant_descriptions': self.rna_data, 'genome_context_intronic_sequence': self.genome_context_intronic_sequence, 'refseqgene_context_intronic_sequence': self.refseqgene_context_intronic_sequence, - 'hgvs_refseqgene_variant': self.hgvs_refseqgene_variant, - 'hgvs_predicted_protein_consequence': self.hgvs_predicted_protein_consequence, + 'hgvs_refseqgene_variant': None if self.hgvs_refseqgene_variant is None else \ + self.hgvs_refseqgene_variant.format({'max_ref_length': 0}), 'validation_warnings': self.process_warnings(), 'lovd_messages': self.lovd_messages, 'lovd_corrections': self.lovd_corrections, - 'hgvs_lrg_transcript_variant': self.hgvs_lrg_transcript_variant, - 'hgvs_lrg_variant': self.hgvs_lrg_variant, - 'alt_genomic_loci': self.alt_genomic_loci, - 'primary_assembly_loci': self.primary_assembly_loci, + 'hgvs_lrg_transcript_variant': None if self.hgvs_lrg_transcript_variant is None else \ + self.hgvs_lrg_transcript_variant.format({'max_ref_length': 0}), + 'hgvs_lrg_variant': None if self.hgvs_lrg_variant is None else \ + self.hgvs_lrg_variant.format({'max_ref_length': 0}), 'variant_exonic_positions': self.exonic_positions, 'reference_sequence_records': self.reference_sequence_records } + + primary_loci = None + if self.primary_assembly_loci is not None: + primary_loci = {} + for gen, dat in self.primary_assembly_loci.items(): + primary_loci[gen] = {} + primary_loci[gen]['vcf'] = dat['vcf'] + primary_loci[gen]['hgvs_genomic_description'] = \ + dat['hgvs_genomic_description'].format({ + 'max_ref_length': 0}) + dict_out['primary_assembly_loci'] = primary_loci + + alt_loci = None + if self.alt_genomic_loci is not None: + alt_loci = [] + for loc in self.alt_genomic_loci: + rep_loc = {} + for gen, dat in loc.items(): + rep_loc[gen] = {} + rep_loc[gen]['vcf'] = dat['vcf'] + rep_loc[gen]['hgvs_genomic_description'] = \ + dat['hgvs_genomic_description'].format({ + 'max_ref_length': 0}) + alt_loci.append(rep_loc) + dict_out['alt_genomic_loci'] = alt_loci + + protein_dict = None + if self.hgvs_predicted_protein_consequence is not None: + if not test: + protein_dict = {'tlr':'','slr':'','lrg_tlr':'','lrg_slr':''} + else: + protein_dict = {'tlr':'','slr':''} + if 'prot' in self.hgvs_predicted_protein_consequence: + protein_dict['tlr'] = self.hgvs_predicted_protein_consequence['prot'].format({ + 'max_ref_length': 0}) + protein_dict['slr'] = self.hgvs_predicted_protein_consequence['prot'].format({ + 'max_ref_length': 0, + 'p_3_letter':False}) + if 'lrg_prot' in self.hgvs_predicted_protein_consequence: + protein_dict['lrg_tlr'] = self.hgvs_predicted_protein_consequence['lrg_prot'].format({ + 'max_ref_length': 0}) + protein_dict['lrg_slr'] =self.hgvs_predicted_protein_consequence['lrg_prot'].format({ + 'max_ref_length': 0, + 'p_3_letter':False}) + dict_out['hgvs_predicted_protein_consequence'] = protein_dict + return dict_out def is_obsolete(self): diff --git a/VariantValidator/modules/vvDBGet.py b/VariantValidator/modules/vvDBGet.py index 4c974dd6..88773881 100644 --- a/VariantValidator/modules/vvDBGet.py +++ b/VariantValidator/modules/vvDBGet.py @@ -223,10 +223,10 @@ def get_urls(self, dict_out): if 'NM_' in dict_out['hgvs_transcript_variant'] or 'NR_' in dict_out['hgvs_transcript_variant']: report_urls['transcript'] = 'https://www.ncbi.nlm.nih.gov' \ '/nuccore/%s' % dict_out['hgvs_transcript_variant'].split(':')[0] - if 'NP_' in str(dict_out['hgvs_predicted_protein_consequence']['slr']): + if 'NP_' in str(dict_out['hgvs_predicted_protein_consequence']['prot']): report_urls['protein'] = 'https://www.ncbi.nlm.nih.gov' \ '/nuccore/%s' % str( - dict_out['hgvs_predicted_protein_consequence']['slr']).split(':')[0] + dict_out['hgvs_predicted_protein_consequence']['prot']).split(':')[0] if 'NG_' in dict_out['hgvs_refseqgene_variant']: report_urls['refseqgene'] = 'https://www.ncbi.nlm.nih.gov' \ '/nuccore/%s' % dict_out['hgvs_refseqgene_variant'].split(':')[0] @@ -247,19 +247,19 @@ def get_urls(self, dict_out): if 'ENST' in dict_out['hgvs_transcript_variant'] and str(dict_out['selected_assembly']).lower() == 'grch37': report_urls['transcript'] = 'https://grch37.ensembl.org/Homo_sapiens/Transcript/Summary?' \ 'db=core;t=%s' % dict_out['hgvs_transcript_variant'].split(':')[0] - if 'ENSP' in str(dict_out['hgvs_predicted_protein_consequence']['slr']) and str(dict_out['selected_assembly']).lower() == 'grch37': + if 'ENSP' in str(dict_out['hgvs_predicted_protein_consequence']['prot']) and str(dict_out['selected_assembly']).lower() == 'grch37': report_urls['protein'] = 'https://grch37.ensembl.org/Homo_sapiens/Transcript/ProteinSummary?' \ 'db=core;p=%s' % str( - dict_out['hgvs_predicted_protein_consequence']['slr']).split(':')[0] + dict_out['hgvs_predicted_protein_consequence']['prot']).split(':')[0] # When selected_assembly is GRCh38 if 'ENST' in dict_out['hgvs_transcript_variant'] and str(dict_out['selected_assembly']).lower() == 'grch38': report_urls['transcript'] = 'https://www.ensembl.org/Homo_sapiens/Transcript/Summary?' \ 'db=core;t=%s' % dict_out['hgvs_transcript_variant'].split(':')[0] - if 'ENSP' in str(dict_out['hgvs_predicted_protein_consequence']['slr']) and str(dict_out['selected_assembly']).lower() == 'grch38': + if 'ENSP' in str(dict_out['hgvs_predicted_protein_consequence']['prot']) and str(dict_out['selected_assembly']).lower() == 'grch38': report_urls['protein'] = 'https://www.ensembl.org/Homo_sapiens/Transcript/ProteinSummary?' \ 'db=core;p=%s' % str( - dict_out['hgvs_predicted_protein_consequence']['slr']).split(':')[0] + dict_out['hgvs_predicted_protein_consequence']['prot']).split(':')[0] # "http://www.ensembl.org/id/" ? What about historic versions????? return report_urls diff --git a/VariantValidator/modules/vvMixinCore.py b/VariantValidator/modules/vvMixinCore.py index 4ef327d9..b212fe5f 100644 --- a/VariantValidator/modules/vvMixinCore.py +++ b/VariantValidator/modules/vvMixinCore.py @@ -1027,13 +1027,11 @@ def validate(self, gene_symbol = self.db.get_gene_symbol_from_refseq_id(refseqgene_variant.ac) variant.gene_symbol = gene_symbol - # Add predicted protein variant dictionary this is the output form so str for final is OK + # Add predicted protein variant dictionary if predicted_protein_variant != '': predicted_protein_variant_dict = {} - predicted_protein_variant_dict["slr"] = '' - predicted_protein_variant_dict["tlr"] = '' - predicted_protein_variant_dict["lrg_tlr"] = '' - predicted_protein_variant_dict["lrg_slr"] = '' + predicted_protein_variant_dict["prot"] = '' + predicted_protein_variant_dict["lrg_prot"] = '' if type(predicted_protein_variant) is not str: # add protein descriptions if not N type edit add_p_descps = True @@ -1078,9 +1076,8 @@ def validate(self, # Add single letter AA code to protein descriptions - predicted_protein_variant_dict = {"tlr": str( - predicted_protein_variant.format({'max_ref_length': 0}) - ), "slr": ''} + predicted_protein_variant_dict = { + "prot":predicted_protein_variant} if re.search("[A-Z][a-z][a-z]1[A-Z][a-z][a-z]", str( predicted_protein_variant.posedit)): @@ -1105,34 +1102,22 @@ def validate(self, posedit="({aa_1}1?)") variant.warnings = cp_warnings - # Set formatted tlr - predicted_protein_variant_dict['tlr'] = \ - predicted_protein_variant.format({ - 'max_ref_length': 0}) - predicted_protein_variant_dict['slr']= \ - predicted_protein_variant.format({ - 'max_ref_length': 0, - 'p_3_letter':False}) - # set LRG outputs + # Set prot for output + predicted_protein_variant_dict['prot'] = predicted_protein_variant + # set LRG output if format_lrg is not None: - predicted_protein_variant_dict["lrg_tlr"] = \ - format_lrg + ':' + \ - predicted_protein_variant_dict["tlr"].split(':')[1] - predicted_protein_variant_dict["lrg_slr"] = \ - format_lrg + ':' + \ - predicted_protein_variant_dict["slr"].split(':')[1] + predicted_protein_variant_dict["lrg_prot"] = copy.copy( + predicted_protein_variant) + predicted_protein_variant_dict["lrg_prot"].ac = format_lrg else: - predicted_protein_variant_dict["lrg_tlr"] = '' - predicted_protein_variant_dict["lrg_slr"] = '' + predicted_protein_variant_dict["lrg_prot"] = '' except vvhgvs.exceptions.HGVSParseError as e: logger.debug("Except passed, %s", e) else: predicted_protein_variant_dict = {} - predicted_protein_variant_dict["slr"] = '' - predicted_protein_variant_dict["tlr"] = '' - predicted_protein_variant_dict["lrg_tlr"] = '' - predicted_protein_variant_dict["lrg_slr"] = '' + predicted_protein_variant_dict["prot"] = '' + predicted_protein_variant_dict["lrg_prot"] = '' # Add missing gene info which should be there (May have come from uncertain positions for example) if variant.hgvs_transcript_variant and variant.gene_symbol == '': @@ -1227,8 +1212,11 @@ def validate(self, variant.stable_gene_ids = stable_gene_ids variant.annotations = reference_annotations + + # for now string versions of equivalent variants + rel_acc variant.genome_context_intronic_sequence = genome_context_transcript_variant variant.refseqgene_context_intronic_sequence = refseqgene_context_transcript_variant + variant.hgvs_refseqgene_variant = refseqgene_variant variant.hgvs_predicted_protein_consequence = predicted_protein_variant_dict variant.hgvs_lrg_transcript_variant = lrg_transcript_variant @@ -1254,7 +1242,7 @@ def validate(self, # Add links to reference_sequence_records pre_out = { 'hgvs_transcript_variant':'', - 'hgvs_predicted_protein_consequence':{'slr':''}, + 'hgvs_predicted_protein_consequence':{'prot':''}, 'hgvs_refseqgene_variant':'', 'hgvs_lrg_variant':'', 'selected_assembly':self.selected_assembly} @@ -1264,9 +1252,10 @@ def validate(self, pre_out['hgvs_refseqgene_variant'] = variant.hgvs_refseqgene_variant.ac if variant.hgvs_lrg_variant:# is str pre_out['hgvs_lrg_variant'] = variant.hgvs_lrg_variant - if variant.hgvs_predicted_protein_consequence: - pre_out['hgvs_predicted_protein_consequence']['slr'] = \ - variant.hgvs_predicted_protein_consequence['slr'] + if variant.hgvs_predicted_protein_consequence and \ + variant.hgvs_predicted_protein_consequence['prot']: + pre_out['hgvs_predicted_protein_consequence']['prot'] = \ + variant.hgvs_predicted_protein_consequence['prot'].ac ref_records = self.db.get_urls(pre_out) if ref_records != {}: variant.reference_sequence_records = ref_records @@ -1613,17 +1602,12 @@ def _apply_met_variation(data): f"{variant.hgvs_lrg_transcript_variant.split(':c.')[0]}:c."\ + variant.hgvs_transcript_variant.posedit.format( {'max_ref_length': 0}) - - # Some variant objects must be strings for back-compatibility with old output - if variant.hgvs_transcript_variant: - variant.hgvs_transcript_variant = \ - variant.hgvs_transcript_variant.format({'max_ref_length': 0}) - else: + # Some variant objects must be set on successful completion for back-compatibility + # with old output (we currently assert that the object starts as None in tests but + # also assert that it has ''/{} as output, VF needs this for primary_assembly_loci) + if not variant.hgvs_transcript_variant: variant.hgvs_transcript_variant = '' - if variant.hgvs_refseqgene_variant: - variant.hgvs_refseqgene_variant = \ - variant.hgvs_refseqgene_variant.format({'max_ref_length': 0}) - else: + if not variant.hgvs_refseqgene_variant: variant.hgvs_refseqgene_variant = '' hgd = "hgvs_genomic_description" def _vcf_abrv(hgvs,vcf,max_non_abrv_len=500): @@ -1656,18 +1640,11 @@ def _vcf_abrv(hgvs,vcf,max_non_abrv_len=500): variant.primary_assembly_loci[gen][hgd], variant.primary_assembly_loci[gen]['vcf']) - if not variant.primary_assembly_loci: - variant.primary_assembly_loci = {} - for gen in variant.primary_assembly_loci.keys(): - variant.primary_assembly_loci[gen][hgd] = \ - variant.primary_assembly_loci[gen][hgd].format({'max_ref_length': 0}) - for loc in variant.alt_genomic_loci: for gen in loc.keys(): loc[gen]['vcf'] = _vcf_abrv( loc[gen][hgd], loc[gen]['vcf']) - loc[gen][hgd] = loc[gen][hgd].format({'max_ref_length': 0}) # Append to a list for return batch_out.append(variant) From 52f211bf6a1bcea28744aa9004f759c1786a56f8 Mon Sep 17 00:00:00 2001 From: John-F-Wagstaff Date: Thu, 2 Jul 2026 20:11:39 +0100 Subject: [PATCH 3/3] Add VRS output capacity to VV via format_as_vrs --- VariantValidator/modules/valoutput.py | 28 +- VariantValidator/modules/vrs_utils.py | 872 +++++++ tests/test_vrs.py | 3393 +++++++++++++++++++++++++ 3 files changed, 4291 insertions(+), 2 deletions(-) create mode 100644 VariantValidator/modules/vrs_utils.py create mode 100644 tests/test_vrs.py diff --git a/VariantValidator/modules/valoutput.py b/VariantValidator/modules/valoutput.py index e3e0d5a6..2c2ba131 100644 --- a/VariantValidator/modules/valoutput.py +++ b/VariantValidator/modules/valoutput.py @@ -1,6 +1,7 @@ import logging import json from VariantValidator.modules import lovd_api +from VariantValidator.modules.vrs_utils import HGVS_to_VRS logger = logging.getLogger(__name__) @@ -312,8 +313,31 @@ def lovd_syntax_check(self, variant): variant.lovd_corrections = lovd_corrections - - + def format_as_vrs(self): + """ + Format output into a set of VRS style outputs, does normalisation due + to HGVS->VRS differences. + """ + vrs_convert = HGVS_to_VRS() + vrs_output = {} + error_count = 0 + for variant in self.output_list: + if not variant.lose_vm: + variant.lose_vm = self.validator.lose_vm + out = vrs_convert.variant_validator_output_set_to_vrs(variant) + key = '' + if 'vrs_transcript_variant' in out: + key = out['vrs_transcript_variant']['id'] + elif 'vrs_intronic_genomic_variant' in out: + key = out['vrs_intronic_genomic_variant']['id'] + elif 'vrs_intergenic_genomic_variant' in out: + key = out['vrs_intergenic_genomic_variant']['id'] + else: + error_count = error_count + 1 + key = 'error_' + str(error_count) + # error state no output + vrs_output[key] = out + return vrs_output # # Copyright (C) 2016-2026 VariantValidator Contributors diff --git a/VariantValidator/modules/vrs_utils.py b/VariantValidator/modules/vrs_utils.py new file mode 100644 index 00000000..48761869 --- /dev/null +++ b/VariantValidator/modules/vrs_utils.py @@ -0,0 +1,872 @@ +""" +A set of functions for handling VRS style data, for now focusing on creating +VRS output from hgvs input. +The first function makes a complete VRS output for a given completed +variantValidator output variant, for not converting data into "aleles" +(hgvs variants) and Cis-Phased Blocks, protin is not yet handled (this probably +needs Terminus) Derivative Molecule and Adjacency are not yet handled (and would +probably be inputted and processed as hgvs for the latter, or not at all for the +former, anyway. + +Each of the following are then used to produce the VRS components. +""" +import base64 +import hashlib +import os +import copy +from configparser import ConfigParser +from biocommons.seqrepo import SeqRepo +from VariantValidator.modules.complex_descriptions import FEInterval +try: + from VariantValidator.settings import CONFIG_DIR +except ImportError: + CONFIG_DIR = None +#from vvhgvs.location import Interval + +class HGVS_to_VRS: + """ + A module for creating VRS output from HGVS input + To avoid overcomplicating the core VV object this code is kept independent + from VV, however it should normally only be used by VV instances. We also + provide facilities for setting alternative ID and seq data/length sources. + """ + def __init__(self,seq_repo = False,id_fetch = False, + ref_fetch_blocksize = 10, cache = False): + """ + Module init + set "seq_repo" to use a pre-existing seqrepo connection, for now this + is somewhat direct and is used for the id_fetch too by default, but + we are set up to change this in the future if needed + set id_fetch to use a substitute other than seqrepo as a fetch location + for the ID this could be the VVTA or VV database if needed, though + currently they only work for transcripts + set ref_fetch_blocksize to change the seqrepo fetch amount, given that + the source data is block compressed the time savings from smaller + than ten are probably not worth it even for data which nearly never + "rolls" the input seq + set cache to true or a dict to cache the results for this object + instance, (or use the existing dict as a cache). This cache is + intended to be used for a single user input set, which will usually + share lots of genomic & prot mappings. It does not currently limit + the result set size or anything else. If cache is set to false + variant_validator_output_set_to_vrs will partly overrule this to + create a per output-set cache to limit the re-normalisation of + already normalised genomic mappings. + """ + if seq_repo: + self.seq_repo = seq_repo + elif CONFIG_DIR: + config = ConfigParser() + config.read(CONFIG_DIR) + seq_repo_path = os.path.join(config["seqrepo"]["location"], + config["seqrepo"]["version"]) + self.seq_repo = SeqRepo(seq_repo_path) + else: + raise ValueError("HGVS_to_VRS conversion eitehr needs a VV " + "CONFIG_DIR or a already set up seq_repo") + if id_fetch: + self.id_fetch = id_fetch + else: + self.id_fetch = self.get_vrs_id_for_seq + self.ref_fetch_blocksize = ref_fetch_blocksize + if isinstance(cache,dict): + self.cache = cache + elif cache: + self.cache = {} + else: + self.cache = False + + self.sorted_vrs_digest_keys = { + "SequenceLocation":["end", "sequenceReference","start","type"], + "Allele":["location","state","type"], + "LiteralSequenceExpression":['sequence', 'type'], + "ReferenceLengthExpression":[ + 'length','repeatSubunitLength','type'], + "LengthExpression":['length','type'], + "SequenceReference":['refgetAccession', 'type'], + } + self.vrs_digest_prefixes = { + "SequenceLocation":'ga4gh:SL.', + "Allele":'ga4gh:VA.', + } + + def variant_validator_output_set_to_vrs(self,variant): + """" + Convert a set of VV equivalent results into a VRS format, dataset. + + VV has the following fields that may contain differing HGVS data: + hgvs_transcript_variant, + genome_context_intronic_sequence, + refseqgene_context_intronic_sequence, + hgvs_refseqgene_variant, + hgvs_lrg_transcript_variant, + hgvs_lrg_variant, + alt_genomic_loci, + primary_assembly_loci, + exonic_positions, + + rna_data gets used instead of hgvs_transcript_variant when mapping n/a + hgvs_predicted_protein_consequence is used for valid transcripts + To avoid un-needed churn when variant==variant and ID/VRS ID==ID/VRS ID + we skip/reuse. + The current output structure is: + { + "selected_assembly': variant.selected_assembly, + "submitted_variant': variant.original, + + "gene_ids":{} + "hgvs_transcript_variant":VRS (if available and not intronic) + "hgvs_refseqgene_variant": VRS likewise (also tagged with redundant + "hgvs_lrg_transcript_variant" ?) + "hgvs_lrg_variant": VRS# + "predicted prot consequence":VRS? + "primary_assembly_loci":{VRS set} + "alt_genomic_loci":[VRS list] + "exonic_positions": variant.exonic_positions, # if wrt transcript + "warnings":[warn list] # Set of VariantValidator warnings: + # 'lovd_messages' 'lovd_corrections' + # include bonus "VRSConversionWarning:" for: + # "intronic type variant descriptions not allowed + # in the VRS standard" + # also add "VRS variant data for ### between + # transcript locations so gene data has not been + # attached" for r type? + } + + Add annot_level to get annotation skipped or used for partial filtering? + """ + output = { + 'selected_assembly': variant.selected_assembly, + 'submitted_variant': variant.original, + 'warnings_and_messages': { + 'validation_warnings': variant.process_warnings(), + 'lovd_messages': variant.lovd_messages, + 'lovd_corrections': variant.lovd_corrections, + }, + # Do we want to skip these 2 even as empty fields for intergenic + # transcripts? + "gene_ids":variant.stable_gene_ids, + 'transcript_description': variant.description, + } + if variant.gene_symbol: + if not output["gene_ids"]: + output["gene_ids"] = {} + if isinstance(output["gene_ids"], dict): + output["gene_ids"]['current_symbol'] = variant.gene_symbol + # annotations is already JSON (eg below), and relatively redundant with + # the other data, should add here if we want it but skip for now. + # e.g. annotation set: '{"db_xref": {"select": false, "ncbigene": + # "7840", "ensemblgene": null, "hgnc": "HGNC:428", "CCDS": null}, + # "chromosome": "2", "map": "2p13.1", "note": + # "ALMS1 centrosome and basal body associated protein", "variant": "1", + # "refseq_select": false, "mane_select": false, "ensembl_select": false, + # "mane_plus_clinical": false} + + # abort on bad output_type_flag, or other failure flag can be one of: + # 'gene' 'warning' 'mitochondrial' or 'intergenic' + if output['warnings_and_messages']['validation_warnings'] == [""] or \ + variant.output_type_flag == 'warning': + return output + pos_warnings = \ + "VRSUncertainPositionWarning:VRS description conversion of hgvs "\ + "data with unknown locations is not fully supported, normalisation"\ + " is disabled and other data, e.g. ins vs delins data, may be lost." + + if variant.rna_data: + assert not variant.hgvs_transcript_variant + assert not variant.primary_assembly_loci + # :r. RNA versions are independent of :c. versions, supposed to be + # derived directly from RNA sequencing data, and as such skip + # genomic and standard cDNA variant based validation steps. + # We add a warning message about this. For now the default one, but + # fold the output into hgvs_transcript_variant. + # content :"usage_warnings", "rna_variant","translation" and + # 'translation_slr' + # tlr translation is skipped due to vrs code relying on single + # letter alphabet + output['vrs_transcript_variant'] = self.hgvs_single_var_to_vrs( + variant.rna_data['rna_variant'],variant) + if variant.rna_data['translation_slr'].posedit: + output['vrs_predicted_protein_consequence'] = \ + self.hgvs_single_var_to_vrs( + variant.rna_data['translation_slr']) + output['warnings_and_messages']['r_type_input'] = \ + variant.rna_data['usage_warnings'] + if isinstance(variant.rna_data['rna_variant'], FEInterval): + output['warnings_and_messages']['vrs_output_warnings'] = [ + pos_warnings] + return output + + # now check for intergenic and/or intronic + reset_cache = False + if self.cache is False: + self.cache = {} + reset_cache = True + # use output flag instead? + if variant.hgvs_transcript_variant and not ( + variant.hgvs_transcript_variant.type in ['c','n'] and + self._intronic(variant.hgvs_transcript_variant)): + output['vrs_transcript_variant'] = self.hgvs_single_var_to_vrs( + variant.hgvs_transcript_variant,variant) + if variant.hgvs_refseqgene_variant: + output['vrs_refseqgene_variant'] = self.hgvs_single_var_to_vrs( + variant.hgvs_refseqgene_variant,variant) + if variant.hgvs_predicted_protein_consequence and \ + variant.hgvs_predicted_protein_consequence['prot'] and \ + variant.hgvs_predicted_protein_consequence['prot'].posedit and \ + getattr( + variant.hgvs_predicted_protein_consequence['prot'].posedit, + 'edit',False): + output['vrs_predicted_protein_consequence'] = \ + self.hgvs_single_var_to_vrs( + variant.hgvs_predicted_protein_consequence['prot']) + # LRG is redundant due to VRS checksum based seq id, but do we want + # to add it as a alias in a comment for the output object? + # the RSG/LRG vrs_predicted_protein_consequence is also skipped, do + # we want to add this too? + if isinstance(variant.hgvs_transcript_variant, FEInterval): + output['warnings_and_messages']['vrs_output_warnings'] = [ + pos_warnings] + elif variant.hgvs_transcript_variant: + # add warning for intronic data + output['warnings_and_messages']['vrs_output_warnings'] = [ ( + "VRSIntronWarning: VRS does not handle mappings shared between" + " genomic and transcript reference sequences. As such only the " + "genomic mappings for this hgvs intronic transcript " + "variant are preserved.")] + if isinstance(variant.hgvs_transcript_variant, FEInterval): + output['warnings_and_messages']['vrs_output_warnings'].append( + pos_warnings) + + if variant.hgvs_genomic: + output['vrs_intronic_genomic_variant'] = \ + self.hgvs_single_var_to_vrs(variant.hgvs_genomic) + # RSG data + if variant.hgvs_refseqgene_variant: + output['vrs_refseqgene_variant'] = self.hgvs_single_var_to_vrs( + variant.hgvs_refseqgene_variant) + elif variant.hgvs_genomic or variant.hgvs_refseqgene_variant: + if variant.hgvs_genomic: + output['vrs_intergenic_genomic_variant'] = \ + self.hgvs_single_var_to_vrs(variant.hgvs_genomic) + # RSG data + if variant.hgvs_refseqgene_variant: + output['vrs_refseqgene_variant'] = self.hgvs_single_var_to_vrs( + variant.hgvs_refseqgene_variant) + if ( + variant.hgvs_genomic and + isinstance(variant.hgvs_genomic, FEInterval)) or ( + variant.hgvs_refseqgene_variant and + isinstance(variant.hgvs_refseqgene_variant, FEInterval)): + output['warnings_and_messages']['vrs_output_warnings'] = [ + pos_warnings] + + + # continue for universally (non :r. included data) + primary_assembly_loci = {} + if variant.primary_assembly_loci: + primary_assembly_loci = variant.primary_assembly_loci + output['VRS_mappings_for_primary_assemblies'] = {} + for gen in primary_assembly_loci: + if gen in ['hg19', 'hg38']: + continue + output['VRS_mappings_for_primary_assemblies'][gen] = \ + self.hgvs_single_var_to_vrs( + primary_assembly_loci[gen][ + 'hgvs_genomic_description']) + output['VRS_mappings_for_alt_genomic_loci'] = [] + alt_genomic_loci = [] + + if variant.alt_genomic_loci: + alt_genomic_loci = variant.alt_genomic_loci + output['VRS_mappings_for_alt_genomic_loci'] = [] + for loci in alt_genomic_loci: + gen_key = list(loci.keys())[0] + if gen_key in ['hg19', 'hg38']: + continue + output['VRS_mappings_for_alt_genomic_loci'].append( + self.hgvs_single_var_to_vrs( + loci[gen_key]['hgvs_genomic_description'] + )) + # Crudely re-set cache, relies on us not caching this object in odd + # ways later, but if we do that we probably need a full caching system + # not something this simple. + if reset_cache: + self.cache = False + + return output + + def _intronic(self,hgvs): + if isinstance(hgvs.posedit.pos,FEInterval): + if (hgvs.posedit.pos.start.start.offset or + hgvs.posedit.pos.start.end.offset or + hgvs.posedit.pos.end.start.offset or + hgvs.posedit.pos.end.end.offset): + return True + elif (hgvs.posedit.pos.start.offset or hgvs.posedit.pos.end.offset): + return True + return False + + def hgvs_single_var_to_vrs(self, hgvs_variant,variant = False): + "convert a hgvs with a single location to VRS, if possible" + # first detect intronic variants and abort for them + # VRS only does the genomic sequnce in this case + if not self.cache is False: + hgvs_key = str(hgvs_variant) + if hgvs_key in self.cache: + return self.cache[hgvs_key] + if hgvs_variant.type in ['c','n','t'] and self._intronic(hgvs_variant): + return None + if hgvs_variant.type == 'c': + # fix transcript coordinates to n + if not variant: + raise ValueError( + "Meta-variant with a variant mapper needed for c->n case") + if hgvs_variant.posedit.pos.uncertain: + ref_seq = hgvs_variant.posedit.edit.ref + # handle paired ambig ends + if isinstance(hgvs_variant.posedit.pos, FEInterval): + hgvs1 = copy.deepcopy(hgvs_variant) + hgvs1.posedit.pos = hgvs1.posedit.pos.start + hgvs1 = variant.lose_vm.c_to_n(hgvs1) + hgvs2 = copy.deepcopy(hgvs_variant) + hgvs2.posedit.pos = hgvs2.posedit.pos.end + hgvs2 = variant.lose_vm.c_to_n(hgvs2) + hgvs_variant.type = 'n' + hgvs_variant.posedit.pos.start = hgvs1.posedit.pos + hgvs_variant.posedit.pos.end = hgvs2.posedit.pos + else: + hgvs_variant = variant.lose_vm.c_to_n(hgvs_variant) + # prevent ref for unc pos from being reset (eg NNN>TTTGGGTTTGGG) + hgvs_variant.posedit.edit.ref = ref_seq + else: + hgvs_variant = variant.lose_vm.c_to_n(hgvs_variant) + + # From this point genomic and transcript VRS should handle the same + # For objects IDs using child objects as variables the "canonical" + # set of content is used for the ID + + # fetch vrs id for sequence + # Derive "VRS refrence" using ID e.g. + # { + # "type": "SequenceReference", + # "refgetAccession": "SQ.F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul", + # "label": "NC_000007.14" + #} + #GA4GH Digest Prefix:None Inherent:[‘refgetAccession’, ‘type’] + vrs_ref_seq = { + "type": "SequenceReference", + "refgetAccession":self.id_fetch(hgvs_variant.ac), + "label": str(hgvs_variant.ac)} + # Handle sequences with start-stop ranges, normalisation skipped + if hgvs_variant.posedit.pos.uncertain: + # could check for FEInterval but since this sets uncertain don't + # for now. Currently we do not attempt to handle VRS types more + # complex than LiteralSequenceExpression. + # Ranges are specified but in VRS 2.1, but not + # normalisation for range input so we don't for now. + if not isinstance(hgvs_variant.posedit.pos, FEInterval): + vrs_start = [ + int(hgvs_variant.posedit.pos.start.base)-1, + int(hgvs_variant.posedit.pos.end.base)] + vrs_end = [ + int(hgvs_variant.posedit.pos.start.base)-1, + int(hgvs_variant.posedit.pos.end.base)] + else: + vrs_start = [ + int(hgvs_variant.posedit.pos.start.start.base)-1, + int(hgvs_variant.posedit.pos.start.end.base)] + vrs_end = [ + int(hgvs_variant.posedit.pos.end.start.base)-1, + int(hgvs_variant.posedit.pos.end.end.base)] + vrs_loc = { + "type": "SequenceLocation", + "sequenceReference":vrs_ref_seq, + "start":vrs_start, + "end":vrs_end, + } + # if del set length expr, if dupe likewise, else handle as + # LiteralSequenceExpression + if hgvs_variant.posedit.edit.type in 'dup': + min_len = 2* max(vrs_end[0] - vrs_start[1],0) + max_len = 2*(vrs_end[1] - vrs_start[0]) + vrs_state = { + "type": "LengthExpression", + "length": [min_len,max_len]} + elif hgvs_variant.posedit.edit.alt: + # ins, sub, inv, or delins + vrs_state = { + "type": "LiteralSequenceExpression", + "sequence": hgvs_variant.posedit.edit.alt, + } + else: + # del bases represent the deleted ref length + # so length is length - ref length + min_len = max( + vrs_end[0] - vrs_start[1] - \ + len(hgvs_variant.posedit.edit.ref), + 0) + max_len = vrs_end[1] - vrs_start[0] - \ + len(hgvs_variant.posedit.edit.ref) + vrs_state = { + "type": "LengthExpression", + "length":[min_len,max_len]} + vrs_allele = { + "type": "Allele", + "location":vrs_loc, + "state":vrs_state,} + vrs_allele['id'] = self.vrs_flatten(vrs_allele,digest=True) + if not self.cache is False: + self.cache[hgvs_key] = vrs_allele + return vrs_allele + # Derive normalised start and stop + # Derive VRS sequence location from VRS sequence and normalised location + # start by converting hgvs (1 based ins special) to vrs (0 based) + # coordinates, also handle empty refs, this probably should not be an + # issue for us in normal use but still needs to be handled for the + # general case. + ref_seq = '' + alt_seq = '' + hgvs_type = hgvs_variant.posedit.edit.type + if hgvs_type == 'ins': + #may need to switch to {"type": "Number", "value": num} post VRS 2.1 + vrs_start = int(hgvs_variant.posedit.pos.start.base) + vrs_end = int(hgvs_variant.posedit.pos.end.base) -1 + ref_seq = '' + alt_seq = hgvs_variant.posedit.edit.alt + else: + vrs_start = int(hgvs_variant.posedit.pos.start.base) -1 + vrs_end = int(hgvs_variant.posedit.pos.end.base) + ref_seq = hgvs_variant.posedit.edit.ref + if not ref_seq: + ref_seq = self.seq_repo[hgvs_variant.ac][vrs_start:vrs_end] + if hgvs_type == 'dup': + alt_seq = ref_seq + ref_seq + elif hgvs_type == 'identity': + alt_seq = ref_seq + else: + alt_seq = hgvs_variant.posedit.edit.alt + + # To finish the creation of a VRS allele we now need to derive the + # state, which could be either a ReferenceLengthExpression or a + # LiteralSequenceExpression + + # If both the ref and alt disappears on down-normalisation (or variant + # type is otherwise is equal) ReferenceLengthExpression is used with + # repeatSubunitLength set to the total length + + # cheat shortcut for = variants + if ref_seq == alt_seq: + #{ "type": "Number", "value": 55} + vrs_loc = { + "type": "SequenceLocation", + "sequenceReference":vrs_ref_seq, + "start":vrs_start, + "end":vrs_end, + } + vrs_loc["id"] = self.vrs_flatten(vrs_loc,digest=True) + vrs_state = { + "type": "ReferenceLengthExpression", + "length": vrs_end - vrs_start, + "repeatSubunitLength": vrs_end - vrs_start + } + vrs_allele = { + "type": "Allele", + "location":vrs_loc, + "state":vrs_state,} + vrs_allele["id"] = self.vrs_flatten(vrs_allele,digest=True) + if not self.cache is False: + self.cache[hgvs_key] = vrs_allele + return vrs_allele + # need to pre zero base normalise start/end + full_ref, full_alt,min_ref,min_alt,vrs_start,vrs_end,derived = \ + self.normalise_outwards( + hgvs_variant.ac, + ref_seq,alt_seq, + vrs_start, + vrs_end) + vrs_loc = { + "type": "SequenceLocation", + "sequenceReference":vrs_ref_seq, + "start":vrs_start, + "end":vrs_end, + } + vrs_loc["id"] = self.vrs_flatten(vrs_loc,digest=True) + + #Do we want to "assert min_alt or min_ref"? This should only happen on + # == and be disallowed by the above code, so skip for now. + # If both ref and alt are full post down-normalisation + # LiteralSequenceExpression is the chosen type. + # if we have a indel even minimalised then this must be a + # LiteralSequenceExpression + # If we have a pure (un-ambiguous location) ins post up-normalisation + # LiteralSequenceExpression is the chosen output type, + # for the other extreme ReferenceLengthExpressions are used for del's + if full_alt and not full_ref:#ins not norm outwards + vrs_state = { + "type": "LiteralSequenceExpression", + "sequence": full_alt, + } + elif full_ref and not full_alt: # del not norm outwards + vrs_state = { + "type": "ReferenceLengthExpression", + "length": 0, + "repeatSubunitLength": vrs_end-vrs_start + } + # For other del or ins after down-normalisation, that expand to delins + # after up-normalisation, if the ref or alt can fully be recreated from + # the flank we use ReferenceLengthExpressions + elif derived:#full_ref and full_alt is implicit beyond this point + vrs_state = { + "type": "ReferenceLengthExpression", + "length": len(full_alt), + # one of these must be 0 length, so + works as well as an if + "repeatSubunitLength":len(min_ref+min_alt) + } + # otherwise we use a LiteralSequenceExpression + elif not derived: + vrs_state = { + "type": "LiteralSequenceExpression", + "sequence": full_alt, + } + vrs_allele = { + "type": "Allele", + "location":vrs_loc, + "state":vrs_state,} + vrs_allele['id'] = self.vrs_flatten(vrs_allele,digest=True) + if not self.cache is False: + self.cache[hgvs_key] = vrs_allele + return vrs_allele + + def vrs_flatten(self, vrs_dict, dict_keys = None, digest = False): + """ + Derive a canonical VRS object or it's digest ID from a dictionaried set + of VRS type inputs. + VRS demands not the contents of the fields concatenated somehow, but + instead a RFC 8785 Canonicalised JSON object containing just said fields + (with IDs), because we can't have simple things. + Keys must be sorted alphabetically, but we trust that any given + dict_keys are pre-sorted. + We also presume that the object type is accurate, and all numbers are + integers, this fits our current use case. + The official VRS code uses 'canoicaljson' as a dependency, but given the + limits on the allowed input, this is overkill. + """ + if dict_keys: + # force specific dict keys, used in development, + # may be removed later + keys = dict_keys + else: + keys = self.sorted_vrs_digest_keys[vrs_dict['type']] + json = '{' + for key in keys: + if isinstance(vrs_dict[key],int): + json = json + f'"{key}":{vrs_dict[key]},' + elif isinstance(vrs_dict[key],str): + json = json + f'"{key}":"{vrs_dict[key]}",' + elif isinstance(vrs_dict[key],list): + assert len(vrs_dict[key]) == 2 + # 1 must be int, other may be None, start id looks like + def ga4gh_digest(seq): + digest = hashlib.sha512(seq.encode('utf-8')).digest() + url_safe_di = base64.urlsafe_b64encode(digest[:24]).decode("utf-8") + return f"ga4gh:SQ.{url_safe_di}" + """ + # we could also use VVTA for non genomic if it is faster, but this may + # need a different translation for the url safe encoding + out = self.seq_repo.translate_alias(hgvs_seq_id) + for seq_id in out: + if seq_id.startswith('ga4gh:SQ.'): + return seq_id[6:] + return False + #Failure to find ID for sequence should trigger a KeyError in SeqRepo + + def _trim(self, ref_seq, alt_seq): + """ + Trim a VRS compliant ref and alt seq to match the VRS standard + Trim suffix (trim right) first then prefix (left), this is specified as + the correct trim order in the VRS standard + """ + # Early return if we can't trim (only ins or del) + if not ref_seq or not alt_seq: + return ref_seq, alt_seq, 0, 0 + # Early return for ==, is not classically normalised + if alt_seq == ref_seq: + # return empty to flag this as == + return '', '', 0, 0 + + loop_len = len(ref_seq) + # Early return shortcut for most common single base transition + if loop_len == 1 and len(alt_seq) == 1: + return ref_seq, alt_seq, 0, 0 + + # Finally start to trim possible, hgvs delins type + #Trim Right + loop_len = min(loop_len,len(alt_seq)) + alt_len = 0 + for i in range(-1,-loop_len-1,-1): + if ref_seq[i] == alt_seq[i]: + alt_len = i + else: + break + if alt_len: + ref_seq = ref_seq[:alt_len] + alt_seq = alt_seq[:alt_len] + sufix = alt_len + + # Early return if fully trimmed i.e. one seq is a subset of the other + loop_len = min(len(ref_seq),len(alt_seq)) + if not loop_len: + return ref_seq, alt_seq, 0, sufix + + #Trim Left + alt_len = 0 + for i in range(loop_len): + if ref_seq[i] == alt_seq[i]: + alt_len = i + 1 + else: + break + if alt_len is not None: + ref_seq = ref_seq[alt_len:] + alt_seq = alt_seq[alt_len:] + + return ref_seq, alt_seq, alt_len, sufix + + def _push_r(self,target_ac,start,ref_flank,roll_seq): + """ + Function for "pushing" a minimised differing sequence along a reference + this function acts by "rolling" the diff sequence right. Used to VRS + normalise sequence locations outwards to the full possible location + span. + """ + edge_found = False + curr_roll_pos = 0 + ref_chunk_start = 0 + roll_len = len(roll_seq) + fully_derived = False + flank_section = '' + nomalised_edge = start + curr_flank_idx = 0 + while ref_flank: + for curr_flank_idx, flank_char in enumerate(ref_flank): + if flank_char != roll_seq[curr_roll_pos]: + edge_found = True + break + curr_roll_pos = curr_roll_pos + 1 + if curr_roll_pos >= roll_len: + fully_derived = True + curr_roll_pos = 0 + if edge_found: + nomalised_edge = start + ref_chunk_start + curr_flank_idx + flank_section = flank_section + \ + ref_flank[:curr_flank_idx] + break + ref_chunk_start = ref_chunk_start + len(ref_flank) + flank_section = flank_section + ref_flank + ref_flank = self.seq_repo[target_ac][ + start + ref_chunk_start: + start + ref_chunk_start + self.ref_fetch_blocksize] + if fully_derived: + bases_non_derived = 0 + else: + bases_non_derived = roll_len-curr_roll_pos + if not edge_found: + if not ref_flank: + curr_flank_idx = 0 + nomalised_edge = start + ref_chunk_start + curr_flank_idx + + return flank_section,nomalised_edge,bases_non_derived + + def _push_l(self,target_ac,start,ref_flank,roll_seq): + """ + Function for "pushing" a minimised differing sequence along a reference + this function acts by "rolling" the diff sequence left. Used to VRS + normalise sequence locations outwards to the full possible location + span. + """ + edge_found = False + curr_roll_pos = 0 + ref_chunk_start = 0 + roll_len = len(roll_seq) + fully_derived = False + curr_flank_idx = 0 + roll_seq = roll_seq[::-1] + flank_section = '' + nomalised_edge = 0 # if we stop before an edge is found then we hit 0 + while ref_flank: + curr_flank_idx = 0 + for curr_flank_idx, flank_char in enumerate(reversed(ref_flank)): + if flank_char != roll_seq[curr_roll_pos]: + edge_found = True + break + curr_roll_pos = curr_roll_pos + 1 + if curr_roll_pos >= roll_len: + fully_derived = True + curr_roll_pos = 0 + if edge_found: + nomalised_edge = start - ref_chunk_start - curr_flank_idx + if curr_flank_idx : + flank_section = ref_flank[-curr_flank_idx:] + flank_section + break + ref_chunk_start = ref_chunk_start + len(ref_flank) + flank_section = ref_flank + flank_section + # existing end was truncated due to sequence length + if start - ref_chunk_start > 0: + ref_flank = self.seq_repo[target_ac][ + max(start-ref_chunk_start-self.ref_fetch_blocksize,0): + start - ref_chunk_start] + else: + ref_flank = '' + if fully_derived: + bases_non_derived = 0 + else: + bases_non_derived = roll_len-curr_roll_pos + return flank_section,nomalised_edge,bases_non_derived + + def normalise_outwards( + self, + target_ac, + ref_seq,alt_seq, + start,end): + """ + Normalise a variant, defined in 0 based coordinates, outwards to the + limits of its ambiguity e.g. a ins GC in GCGC becomes ref GCGC alt + GCGCGC. + + This is not 'gap/mapping normalisation' like what VV does inside the + hard left/right hgvs2vcf functions, and as such ignores things like + intron-exon boundaries, which are not mentioned in the VRS standard for + normalisation, thus if we want to handle these then they need to be + handled external to this normalisation step. + + This should otherwise be equivalent to fully pushing right and left, for + ambiguous locations, at the same time. + """ + if not ref_seq: + assert start == end + else: + assert len(ref_seq) == end - start, ref_seq+str(end)+' '+str(start) + if not alt_seq: + alt_seq = '' # for del alt seq may start as None + # We first minimise then re-normalise back to full, minimalisation is + # is needed to allow "rolling" for variants like del C ins CGC in a GC + # stretch. We may be able to skip this step with hgvs pre-normalise d + # input. + orig_ref = ref_seq + orig_alt = alt_seq + ref_seq, alt_seq, prefix, suffix = self._trim(ref_seq, alt_seq) + # VRS does not do any normalisation on == (which trim to '') + if not ref_seq and not alt_seq: + return orig_ref,orig_alt,orig_ref,orig_alt,start,end,0 + # suffix is negative so handle a such + start = start + prefix + end = end + suffix + # VRS also does not push/expand normalise indels (or base transitions) + if ref_seq and alt_seq: + return ref_seq, alt_seq, ref_seq, alt_seq,\ + start, end,0 + # we must now either have an ins or a del but not a delins + + # First grab seq + ref_left_flank = '' + ref_right_flank = '' + if len(ref_seq) < self.ref_fetch_blocksize*2: + # Even if we go past the end we only get an error if we exceed the + # limits of seqrepo's index, otherwise we silently truncate! + seq = self.seq_repo[target_ac][ + max(start - self.ref_fetch_blocksize,0): + end + self.ref_fetch_blocksize] + if start - self.ref_fetch_blocksize >= 0: + ref_left_flank = seq[:self.ref_fetch_blocksize] + ref_right_flank = seq[self.ref_fetch_blocksize+len(ref_seq):] + else: + # this is simpler since we can run relative to the start + ref_left_flank = seq[:start] + ref_right_flank = seq[end:] + else: + ref_left_flank = self.seq_repo[target_ac][ + max(start - self.ref_fetch_blocksize,0):start] + ref_right_flank = self.seq_repo[target_ac][ + end:end + self.ref_fetch_blocksize] + + # Roll in both directions, this uses a roll_length and roll_seq that is + # from either the seq and length of the ins, or the seq and length of + # the del. + roll_seq = ref_seq + if alt_seq: + roll_seq = alt_seq + ref_left_flank, start, bases_non_derived_l = self._push_l( + target_ac, + start, + ref_left_flank, + roll_seq) + ref_right_flank, end, bases_non_derived_r = self._push_r( + target_ac, + end, + ref_right_flank, + roll_seq) + fully_derived = True + if bases_non_derived_r + bases_non_derived_l > len(roll_seq): + fully_derived = False + + #rf = ref_left_flank + ref_seq + ref_right_flank + #lf = ref_left_flank + alt_seq + ref_right_flank + return ref_left_flank + ref_seq + ref_right_flank,\ + ref_left_flank + alt_seq + ref_right_flank,\ + ref_seq, alt_seq,\ + start, end, fully_derived + +# +# Copyright (C) 2016-2026 VariantValidator Contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# diff --git a/tests/test_vrs.py b/tests/test_vrs.py new file mode 100644 index 00000000..34fd9abd --- /dev/null +++ b/tests/test_vrs.py @@ -0,0 +1,3393 @@ +""" +This test set tests on HGVS to VRS output conversion module to check first the +internal functions and then the overall outputs. + +The extra test data, particularly test checksums, is taken from the known good +output given in the VRS documentation. +""" +import copy +import os +import unittest +import json +from configparser import ConfigParser + +import vvhgvs +from vvhgvs.location import Interval +from biocommons.seqrepo import SeqRepo + +from VariantValidator.modules.vrs_utils import HGVS_to_VRS +from VariantValidator.modules.complex_descriptions import FEInterval +from VariantValidator import Validator +from VariantValidator.modules.hgvs_utils import hgvs_obj_from_existing_edit,\ + _hgvs_offset_pos_from_str_in +from VariantValidator.settings import CONFIG_DIR + +vv = Validator() +vv.alt_aln_method = "splign" + + +class MockVVData(): + "Mock class should be == to VV wrt to VRS output production" + def __init__( + self, + selected_assembly = 'GRCh38', original = 'TestOrigVarText', + warnings = None, lovd_messages = None, lovd_corrections = None, + stable_gene_ids = None, description = '', gene_symbol = '', + output_type_flag = None, rna_data = None, + hgvs_transcript_variant = '', primary_assembly_loci = None, + alt_genomic_loci = None, hgvs_predicted_protein_consequence = None, + hgvs_refseqgene_variant = None, hgvs_genomic = None): + self.selected_assembly = selected_assembly + self.original = original # submitted_variant + self.warnings = [] + if self.warnings: + self.warnings = warnings + self.lovd_messages = [] + if lovd_messages: + self.lovd_messages = lovd_messages + self.lovd_corrections = [] + if lovd_corrections: + self.lovd_corrections = lovd_corrections + self.hgvs_genomic = hgvs_genomic + + ## should be set to null if intergenic + self.stable_gene_ids = [] + if stable_gene_ids: + self.stable_gene_ids = stable_gene_ids + + self.description = description # transcript_description + self.gene_symbol = gene_symbol + + # one of 'gene' 'warning' 'mitochondrial' or 'intergenic' + self.output_type_flag = output_type_flag + + + # now set individual hgvs datasets + + # rna_data should be set to null if not r type variant, or else a dict + # containing: 'rna_variant', 'translation_slr', and 'usage_warnings' + self.rna_data = rna_data + + # non r type transcript data + self.hgvs_transcript_variant = hgvs_transcript_variant + + # mappings for transcript variant or main genomic variant given + self.primary_assembly_loci = primary_assembly_loci + self.alt_genomic_loci = alt_genomic_loci + + # prot variation + self.hgvs_predicted_protein_consequence = \ + hgvs_predicted_protein_consequence + # RSG variation + self.hgvs_refseqgene_variant = hgvs_refseqgene_variant + # VM / data provider + self.hdp = vvhgvs.dataproviders.uta.connect(pooling=True) + self.lose_vm = vvhgvs.variantmapper.VariantMapper( + self.hdp, + replace_reference=True, + prevalidation_level=None + ) # Variant mapper + def process_warnings(self): + return self.warnings + +class TestVRSOutputTrim(unittest.TestCase): + """ + Tests for the internal vrs_util.py module, particularly the HGVS_to_VRS + object's trimming function which reduces a variant to its most minimal + sequence length of differing bases. + + Tests are for: + minimise totally, + no change, + minimise from partial, + minimise from whole region, + minimise from left, and + minimise from right + + """ + @classmethod + def setup_class(self): + self.vrs = HGVS_to_VRS() + + def test_vrs_trim_total(self): + ref_seq, alt_seq, prefix_bases_removed, sufix_bases_removed = \ + self.vrs._trim('AACAA','AACAA') + assert ref_seq == '' + assert alt_seq == '' + assert not prefix_bases_removed and not sufix_bases_removed + + def test_vrs_trim_no_change(self): + ref_seq, alt_seq, prefix_bases_removed, sufix_bases_removed = \ + self.vrs._trim('ACGT','CTG') + assert ref_seq == 'ACGT' + assert alt_seq == 'CTG' + assert not prefix_bases_removed and not sufix_bases_removed + + def test_vrs_trim_both_edge_partial(self): + ref_seq, alt_seq, prefix_bases_removed, sufix_bases_removed = \ + self.vrs._trim('AAAA','ACA') + assert ref_seq == 'AA' + assert alt_seq == 'C' + assert prefix_bases_removed == 1 + assert sufix_bases_removed == -1 + ref_seq, alt_seq, prefix_bases_removed, sufix_bases_removed = \ + self.vrs._trim('ACA','AAAA') + assert alt_seq == 'AA' + assert ref_seq == 'C' + assert prefix_bases_removed == 1 + assert sufix_bases_removed == -1 + + def test_vrs_trim_both_edge_total(self): + ref_seq, alt_seq, prefix_bases_removed, sufix_bases_removed = \ + self.vrs._trim('AAAA','AACAA') + assert ref_seq == '' + assert alt_seq == 'C' + assert prefix_bases_removed == 2 + assert sufix_bases_removed == -2 + ref_seq, alt_seq, prefix_bases_removed, sufix_bases_removed = \ + self.vrs._trim('AACAA','AAAA') + assert alt_seq == '' + assert ref_seq == 'C' + assert prefix_bases_removed == 2 + assert sufix_bases_removed == -2 + + def test_vrs_trim_from_left(self): + ref_seq, alt_seq, prefix_bases_removed, sufix_bases_removed = \ + self.vrs._trim('AAAA','CAA') + assert ref_seq == 'AA' + assert alt_seq == 'C' + assert prefix_bases_removed == 0 + assert sufix_bases_removed == -2 + ref_seq, alt_seq, prefix_bases_removed, sufix_bases_removed = \ + self.vrs._trim('CAA','AAAA') + assert ref_seq == 'C' + assert alt_seq == 'AA' + assert prefix_bases_removed == 0 + assert sufix_bases_removed == -2 + + def test_vrs_trim_from_right(self): + ref_seq, alt_seq, prefix_bases_removed, sufix_bases_removed = \ + self.vrs._trim('AAAA','AAC') + assert ref_seq == 'AA' + assert alt_seq == 'C' + assert prefix_bases_removed == 2 + assert sufix_bases_removed == 0 + ref_seq, alt_seq, prefix_bases_removed, sufix_bases_removed = \ + self.vrs._trim('AAC','AAAA') + assert ref_seq == 'C' + assert alt_seq == 'AA' + assert prefix_bases_removed == 2 + assert sufix_bases_removed == 0 + +class TestVRSOutputPush(unittest.TestCase): + """ + Tests for the internal vrs_util.py modules push left & right used in + normalisation of VRS input, the testes are: + normal push, + push already done, + push from full opposed end, + push with flank amount less than repeat unit, + push where flank fetch amount == repeat unit length, + push with flank fetch amount large enough to overlap transcript end, and + push with fetch end == end coordinate + known quirks: + ref_left_flank/ref_right_flank input needs to be set to something (or + we return as if we hit the end), for now, due to loop logic + push does not care (or know) about the difference between del an ins + push function data/key + returns: ref_(left/right)_flank, start, bases_non_derived + ref_(left/right)_flank, bases to add to the flank after roll + start, new start location + bases_non_derived are the no. of bases in the roll seq not in flank + input: target_ac, start,ref_flank, roll_seq + roll_seq is the minimised ins or del in question, + ref_(left/right)_flank is the current left/right flank, + roll_seq minimised (i.e. trimmed) version of input variant + """ + @classmethod + def setup_class(self): + self.vrs = HGVS_to_VRS() + self.longer_vrs = copy.copy(self.vrs) + self.longer_vrs.ref_fetch_blocksize = 10 + self.mid_vrs = copy.copy(self.vrs) + self.mid_vrs.ref_fetch_blocksize = 5 + self.short_vrs = copy.copy(self.vrs) + self.short_vrs.ref_fetch_blocksize = 2 + self.ex_short_vrs = copy.copy(self.vrs) + self.ex_short_vrs.ref_fetch_blocksize = 1 + # push left data: + # NM_003073.5 + # cds start 204, seq AGAAGAAGA start 1288:1297 + # flank 'TGAGATGG' 'AGAAGAAGA' 'TCCGCGAC' + # this has a hanging part repeat of either CA or GC depending VRS + # normalisation should get the whole +partial when hgvs does not + # NM_002111.8 + # cds start 145 seq: + # CA GCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA + # start 198:261 (or rather 196:261) + # with flank is: + # CAAGTCCTTC + # CA GCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA + # ACAGCCGCCA + # longer mid seq (not used atm) + # NM_001350922.2:c.240+14TGGG[2] NC_000010.10:g.128358789_128358796= + # start seq + # NM_001330096.1 + # rep CT stop 26 + # seq CTCTCTCTCTCTCTCTCTCTCTCTCT TTCTTTCTTTCCGATGGGGAAGAGAGGGT + # NM_001330112.1 + # rep GGGGCGGGTC stop 28 (or 20 for complete segments) + # seq (with flank) GGGGCGGGTCGGGGCGGGTC GGGGCGGG CCCACGGGCGGCCGGATTTG + # end seq + # NM_052928.3 + # rep GTTTT start 4375 len 4405 + # seq (w flank): + # ATAACACATATGCCTCCTTCTGAGTTGTTG GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT + # NM_002129.4 + # rep AT start 1408 len 1441 + # seq ATTAGCTAAATTGTTCCTCAGGTGTGTG T ATATATATATACATATATATATATATATATAT + def test_vrs_push_left_from_mid(self): + # push left from middle + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_l('NR_110010.2',27,'C','GC') + assert start == 25 + assert bases_non_derived == 0 + assert ref_left_flank == 'GC' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_l('NM_003073.5',1291,'AGA','AGA') + assert start == 1288 + assert bases_non_derived == 0 + assert ref_left_flank == 'AGA' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_l('NM_002111.8',202,'AG','CAG') + assert start == 196 + assert bases_non_derived == 0 + assert ref_left_flank == 'CAGCAG' + + def test_vrs_push_left_already_done(self): + # push left already done (from far push base) + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_l('NR_110010.2',25,'A','GC') + assert start == 25 + assert bases_non_derived == 2 + assert ref_left_flank == '' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_l('NM_003073.5',1288,'GG','AGA') + assert start == 1288 + assert bases_non_derived == 3 + assert ref_left_flank == '' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_l('NM_002111.8',196,'CAAGTCCTTC','CAG') + assert start == 196 + assert bases_non_derived == 3 + assert ref_left_flank == '' + + def test_vrs_push_left_from_full_right(self): + # push left from full right + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_l('NR_110010.2',32,'G','CG') + assert start == 25 + assert bases_non_derived == 0 + assert ref_left_flank == 'GCGCGCG' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_l('NM_003073.5',1297,'A','AGA') + assert start == 1288 + assert bases_non_derived == 0 + assert ref_left_flank == 'AGAAGAAGA' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_l('NM_002111.8',261,'CA','GCA') + assert start == 196 + assert bases_non_derived == 0 + assert ref_left_flank == \ + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA' + + def test_vrs_push_left_from_full_right_w_fetch(self): + # push left from full right (pre fetched flank less than needed, to force internal fetch) + ref_left_flank, start, bases_non_derived = \ + self.longer_vrs._push_l('NR_110010.2',32,'G','CG') + assert start == 25 + assert bases_non_derived == 0 + assert ref_left_flank == 'GCGCGCG' + ref_left_flank, start, bases_non_derived = \ + self.longer_vrs._push_l('NM_003073.5',1297,'AGA','AGA') + assert start == 1288 + assert bases_non_derived == 0 + assert ref_left_flank == 'AGAAGAAGA' + + def test_vrs_push_left_fetch_lt_repeat_size(self): + # push left given flank to fetch amount less than the size repeat unit (roll_seq) + ref_left_flank, start, bases_non_derived = \ + self.ex_short_vrs._push_l('NR_110010.2',32,'G','CG') + assert start == 25 + assert bases_non_derived == 0 + assert ref_left_flank == 'GCGCGCG' + ref_left_flank, start, bases_non_derived = \ + self.short_vrs._push_l('NM_003073.5',1297,'GA','AGA') + assert start == 1288 + assert bases_non_derived == 0 + assert ref_left_flank == 'AGAAGAAGA' + ref_left_flank, start, bases_non_derived = \ + self.short_vrs._push_l('NM_001330112.1',10,'GTC', 'GGGGCGGGTC') + assert start == 0 + assert bases_non_derived == 0 + assert ref_left_flank == 'GGGGCGGGTC' + + def test_vrs_push_left_fetch_eq_repeat_size(self): + # push left, flank to fetch amount == repeat unit length (roll_seq) + ref_left_flank, start, bases_non_derived = \ + self.short_vrs._push_l('NR_110010.2',32,'CG','CG') + assert start == 25 + assert bases_non_derived == 0 + assert ref_left_flank == 'GCGCGCG' + + def test_vrs_push_left_partial_roll(self): + # test partial roll + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_l('NR_110010.2',27,'C','GCTGC') + assert start == 25 + assert bases_non_derived == 3 + assert ref_left_flank == 'GC' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_l('NM_003073.5',1291,'AGA','AGACAGA') + assert start == 1288 + assert bases_non_derived == 4 + assert ref_left_flank == 'AGA' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_l('NM_002111.8',199,'CAG','CAGTCAG') + assert start == 196 + assert bases_non_derived == 4 + assert ref_left_flank == 'CAG' + + def test_vrs_push_left_hits_seq_start(self): + # test hits start + # NM_001330096.1 + # rep CT stop 26 + # seq CTCTCTCTCTCTCTCTCTCTCTCTCT TTCTTTCTTTCCGATGGGGAAGAGAGGGT + # NM_001330112.1 + # rep GGGGCGGGTC stop 28 (or 20 for complete segments) + # seq (with flank) GGGGCGGGTCGGGGCGGGTC GGGGCGGG CCCACGGGCGGCCGGATTTG + # test corner case roll fetch hits 0 and rolls to 0 + ref_left_flank, start, bases_non_derived = \ + self.short_vrs._push_l('NM_001330096.1',4,'CT','CT') + assert start == 0 + assert bases_non_derived == 0 + assert ref_left_flank == 'CTCT' + ref_left_flank, start, bases_non_derived = self.longer_vrs._push_l( + 'NM_001330112.1',20,'GGGGCGGGTC','GGGGCGGGTC') + assert start == 0 + assert bases_non_derived == 0 + assert ref_left_flank == 'GGGGCGGGTCGGGGCGGGTC' + # test corner cases: roll fetch crosses 0 (truncated final fetch) + # and rolls to 0 + ref_left_flank, start, bases_non_derived = \ + self.longer_vrs._push_l('NM_001330096.1',4,'CT','CT') + assert start == 0 + assert bases_non_derived == 0 + assert ref_left_flank == 'CTCT' + ref_left_flank, start, bases_non_derived = self.longer_vrs._push_l( + 'NM_001330112.1',20,'GGGTCGGGGCGGGTC','GGGGCGGGTC') + assert start == 0 + assert bases_non_derived == 0 + assert ref_left_flank == 'GGGGCGGGTCGGGGCGGGTC' + + # Push right data + # end seq + # NM_052928.3 + # rep GTTTT start 4375 len 4405 + # seq (w flank): + # ATAACACATATGCCTCCTTCTGAGTTGTTG GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT + # NM_002129.4 + # rep AT start 1408 len 1441 + # seq ATTAGCTAAATTGTTCCTCAGGTGTGTG T ATATATATATACATATATATATATATATATAT + # NR_110010.2 + # seq start 25:32 + # seq w flank ATGGTGGCGA GCGCGCG AGTGCAGAAG + # NM_003073.5 + # cds start 204 start 1288:1297 + # seq w flank TGAGATGG AGAAGAAGA TCCGCGAC + # this (NM_003073.5 rep) has a hanging partial repeat, either CA or GC + # depending, VRS normalisation should get whole +partial, hgvs does not + # NM_002111.8 + # cds start 145 start 198:261 or rather 196:261 + # seq with flank: + # CAAGTCCTTC + # CA GCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA + # ACAGCCGCCA + # longer mid seq (not used atm) + # NM_001350922.2:c.240+14TGGG[2] NC_000010.10:g.128358789_128358796= + # start seq + # NM_001330096.1 + # rep CT stop 26 + # seq CTCTCTCTCTCTCTCTCTCTCTCTCT TTCTTTCTTTCCGATGGGGAAGAGAGGGT + # NM_001330112.1 + # rep GGGGCGGGTC stop 28 (or 20 for complete segments) + # seq (with flank) GGGGCGGGTCGGGGCGGGTC GGGGCGGG CCCACGGGCGGCCGGATTTG + # end seq + # NM_052928.3 + # rep GTTTT start 4375 len 4405 + # seq (w flank): + # ATAACACATATGCCTCCTTCTGAGTTGTTG GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT + # NM_002129.4 + # rep AT start 1408 len 1441 + # seq ATTAGCTAAATTGTTCCTCAGGTGTGTG T ATATATATATAC ATATATATATATATATATAT + + def test_vrs_push_right_from_middle(self): + # push right from middle + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_r('NR_110010.2',27,'G','GC') + assert start == 32 + assert bases_non_derived == 0 + assert ref_left_flank == 'GCGCG' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_r('NM_003073.5',1291,'AGA','AGA') + assert start == 1297 + assert bases_non_derived == 0 + assert ref_left_flank == 'AGAAGA' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_r('NM_002111.8',202,'CA','CAG') + assert start == 261 + assert bases_non_derived == 0 + assert ref_left_flank == \ + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA' + + def test_vrs_push_right_already_done(self): + # push right already done (from far push base) + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_r('NR_110010.2',32,'A','GC') + assert start == 32 + assert bases_non_derived == 2 + assert ref_left_flank == '' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_r('NM_003073.5',1297,'TCCGCGAC','AGA') + assert start == 1297 + assert bases_non_derived == 3 + assert ref_left_flank == '' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_r('NM_002111.8',261,'ACAGCCGCCA','CAG') + assert start == 261 + assert bases_non_derived == 3 + assert ref_left_flank == '' + + def test_vrs_push_right_from_full_left(self): + # push right from full left + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_r('NR_110010.2',25,'G','GC') + assert start == 32 + assert bases_non_derived == 0 + assert ref_left_flank == 'GCGCGCG' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_r('NM_003073.5',1288,'A','AGA') + assert start == 1297 + assert bases_non_derived == 0 + assert ref_left_flank == 'AGAAGAAGA' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_r('NM_002111.8',196,'CA','CAG') + assert start == 261 + assert bases_non_derived == 0 + assert ref_left_flank == \ + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA' + + def test_vrs_push_right_from_full_left_w_forced_fetch(self): + # push right from full left (pre fetched flank less than needed, to force internal fetch) + ref_left_flank, start, bases_non_derived = \ + self.longer_vrs._push_r('NR_110010.2',25,'G','GC') + assert start == 32 + assert bases_non_derived == 0 + assert ref_left_flank == 'GCGCGCG' + ref_left_flank, start, bases_non_derived = \ + self.longer_vrs._push_r('NM_003073.5',1288,'AGA','AGA') + assert start == 1297 + assert bases_non_derived == 0 + assert ref_left_flank == 'AGAAGAAGA' + + def test_vrs_push_right_fetch_lt_repeat_unit(self): + # push right given flank to fetch amount less than the size repeat unit (roll_seq) + ref_left_flank, start, bases_non_derived = \ + self.ex_short_vrs._push_r('NR_110010.2',25,'G','GC') + assert start == 32 + assert bases_non_derived == 0 + assert ref_left_flank == 'GCGCGCG' + ref_left_flank, start, bases_non_derived = \ + self.short_vrs._push_r('NM_003073.5',1288,'AG','AGA') + assert start == 1297 + assert bases_non_derived == 0 + assert ref_left_flank == 'AGAAGAAGA' + ref_left_flank, start, bases_non_derived = \ + self.short_vrs._push_r('NM_001330112.1',0,'GG', 'GGGGCGGGTC') + assert start == 28 + assert bases_non_derived == 0 + assert ref_left_flank == 'GGGGCGGGTCGGGGCGGGTCGGGGCGGG' + + def test_vrs_push_right_fetch_eq_repeat_unit(self): + # push right, flank to fetch amount == repeat unit length (roll_seq) + ref_left_flank, start, bases_non_derived = \ + self.short_vrs._push_r('NR_110010.2',25,'GC','GC') + assert start == 32 + assert bases_non_derived == 0 + assert ref_left_flank == 'GCGCGCG' + + def test_vrs_push_right_partial_roll(self): + # test partial roll + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_r('NR_110010.2',27,'G','GCTGC') + assert start == 29 + assert bases_non_derived == 3 + assert ref_left_flank == 'GC' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_r('NM_003073.5',1288,'AGA','AGACAGA') + assert start == 1291 + assert bases_non_derived == 4 + assert ref_left_flank == 'AGA' + ref_left_flank, start, bases_non_derived = \ + self.vrs._push_r('NM_002111.8',196,'CAG','CAGTCAG') + assert start == 199 + assert bases_non_derived == 4 + assert ref_left_flank == 'CAG' + + def test_vrs_push_right_hits_end(self): + # test hits end + # NM_052928.3 + # rep GTTTT start 4375 len 4405 + # seq (w flank): + # ATAACACATATGCCTCCTTCTGAGTTGTTG GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT + # NM_002129.4 + # rep AT start 1408 len 1441 + # seq ATTAGCTAAATTGTTCCTCAGGTGTGTG T ATATATATATAC ATATATATATATATATATAT' + # test corner case roll fetch hits last base and rolls to last base + ref_left_flank, start, bases_non_derived = \ + self.mid_vrs._push_r('NM_052928.3',4375,'GTT','GTTTT') + assert start == 4405 + assert bases_non_derived == 0 + assert ref_left_flank == 'GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT' + ref_left_flank, start, bases_non_derived = \ + self.short_vrs._push_r('NM_002129.4',1421,'AT','AT') + assert start == 1441 + assert bases_non_derived == 0 + assert ref_left_flank == 'ATATATATATATATATATAT' + def test_vrs_push_right_hits_end_handle_last_fetch_gt_final_base(self): + # test corner case roll fetch crosses last base (truncated final fetch) + # and rolls to last base + ref_left_flank, start, bases_non_derived = \ + self.longer_vrs._push_r('NM_052928.3',4375,'GTT','GTTTT') + assert start == 4405 + assert bases_non_derived == 0 + assert ref_left_flank == 'GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT' + ref_left_flank, start, bases_non_derived = \ + self.longer_vrs._push_r('NM_002129.4',1421,'AT','AT') + assert start == 1441 + assert bases_non_derived == 0 + assert ref_left_flank == 'ATATATATATATATATATAT' + +class TestVRSOutputNorm(unittest.TestCase): + """ + Tests for the internal vrs_util.py module's HGVS_to_VRS object's + normalise_outwards function, which should minimise and then push outwards + (in both directions) test structure as for the push left/right above. + known quirks: + Because the first step is minimise, followed by rolling the seq in both + directions the actual minimised seq can differ in roll position + e.g. CG vs GC. But otherwise for the same underlying variant the + results should always be the same, + push function data/key: + returns: full_ref, full_alt,min_ref,min_alt,vrs_start,vrs_end,derived + full_ref: normalised (maximised) version of the ref + full_alt: normalised (maximised) version of the alt + min_ref & min_alt: minimised version of the same, (used along with + derived_state for variant type detection) + derived_state: True/False, is the variant derived from the + immediate flank (eg cnv/tandem repeat) + input: ref_ac, ref_seq,alt_seq, vrs_start, vrs_end + ref_ac is the accession that the change is relevant for + ref_seq is the original sequence of the region + alt_seq is the sequence of the variant in question + vrs_start & vrs_end are the 0 based VRS type coordinates of the ref + location + tests done: + Done for both del and ins: + expand left, expand right, expand both directions, expand already done, + expand flank fetch amount less than repeat unit, expand flank amount + same as repeat unit, expand flank fetch amount large enough to overlap + transcript start/end, expand flank amount == start/end coordinate from + current + + Also test early abort on == data. + """ + @classmethod + def setup_class(self): + self.vrs = HGVS_to_VRS() + self.ex_short_vrs = copy.copy(self.vrs) + self.ex_short_vrs.ref_fetch_blocksize = 1 + # NR_110010.2 + # seq start 25:32 + # seq with flank ATGGTGGCGA GCGCGCG AGTGCAGAAG + # NM_003073.5 + # cds start 204 start 1288:1297 + # seq with flank TGAGATGG AGAAGAAGA TCCGCGAC + # this has a hanging part repeat of either CA or GC depending + # VRS normalisation should get the whole +partial when hgvs does not + # NM_002111.8 + # cds start 145 start 198:261 or rather 196:261 + # seq with flank: + # CAAGTCCTTC + # CA GCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA + # ACAGCCGCCA + # longer mid seq (not used atm) + # NM_001350922.2:c.240+14TGGG[2] NC_000010.10:g.128358789_128358796= + # start seq + # NM_001330096.1 + # rep CT stop 26 + # seq CTCTCTCTCTCTCTCTCTCTCTCTCT TTCTTTCTTTCCGATGGGGAAGAGAGGGT + # NM_001330112.1 + # rep GGGGCGGGTC stop 28 (or 20 for complete segments) + # seq (with flank) GGGGCGGGTCGGGGCGGGTC GGGGCGGG CCCACGGGCGGCCGGATTTG + # end seq + # NM_052928.3 + # rep GTTTT start 4375 len 4405 + # seq (with flank): + # ATAACACATATGCCTCCTTCTGAGTTGTTG GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT + # NM_002129.4 + # rep AT start 1408 len 1441 + # seq ATTAGCTAAATTGTTCCTCAGGTGTGTG T ATATATATATACATATATATATATATATATAT + + # result set key: + # full_ref, full_alt, min_ref, min_alt, vrs_start, vrs_end, derived + self.NR_110010_2_ins_result_set = ( + 'GCGCGCG','GCGCGCGCG','','CG',25,32,True) + self.NR_110010_2_ins_result_set_end = ( + 'GCGCGCG','GCGCGCGCG','','GC',25,32,True) + self.NM_003073_5_ins_result_set = ( + 'AGAAGAAGA','AGAAGAAGAAGA','','AGA',1288,1297,True) + self.NM_002111_8_ins_result_set = ( + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA', + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA', + '','CAG',196,261,True) + self.NM_002111_8_ins_result_set_end = ( + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA', + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA', + '','GCA',196,261,True) + # start seq + # NM_001330096.1 + # rep CT stop 26 + # seq CTCTCTCTCTCTCTCTCTCTCTCTCT TTCTTTCTTTCCGATGGGGAAGAGAGGGT + # NM_001330112.1 + # rep GGGGCGGGTC stop 28 (or 20 for complete segments) + # seq (with flank) GGGGCGGGTCGGGGCGGGTC GGGGCGGG CCCACGGGCGGCCGGATTTG + # end seq + # NM_052928.3 + # rep GTTTT start 4375 len 4405 + # seq (w flank): + # ATAACACATATGCCTCCTTCTGAGTTGTTG GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT + # NM_002129.4 + # rep AT start 1408 len 1441 + # seq ATTAGCTAAATTGTTCCTCAGGTGTGTG T ATATATATATACATATATATATATATATATAT + self.res_set_start_NM_001330096_1 = ( + 'CTCTCTCTCTCTCTCTCTCTCTCTCT', + 'CTCTCTCTCTCTCTCTCTCTCTCTCTCT', + '','TC',0,26,True) + self.res_set_start_NM_001330096_1_end = ( + 'CTCTCTCTCTCTCTCTCTCTCTCTCT', + 'CTCTCTCTCTCTCTCTCTCTCTCTCTCT', + '','CT',0,26,True) + self.res_set_start_NM_001330112_1 = ( + 'GGGGCGGGTCGGGGCGGGTCGGGGCGGG', + 'GGGGCGGGTCGGGGCGGGTCGGGGCGGGTCGGGGCGGG', + '','GGGGCGGGTC',0,28,True) + self.res_set_start_NM_001330112_1_end = ( + 'GGGGCGGGTCGGGGCGGGTCGGGGCGGG', + 'GGGGCGGGTCGGGGCGGGTCGGGGCGGGTCGGGGCGGG', + '','TCGGGGCGGG',0,28,True) + + self.res_set_end_NM_052928_3 = ( + 'GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT', + 'GTTTTGTTTTGTTTTGTTTTGTTTTGTTTTGTTTT', + '','GTTTT',4375,4405,True) + self.res_set_end_NM_052928_3_end = ( + 'GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT', + 'GTTTTGTTTTGTTTTGTTTTGTTTTGTTTTGTTTT', + '','TTGTT' ,4375,4405,True) + self.res_set_end_NM_002129_4 = ( + 'ATATATATATATATATATAT', + 'ATATATATATATATATATATAT', + '','AT',1421,1441,True) + self.res_set_end_NM_002129_4_end = ( + 'ATATATATATATATATATAT', + 'ATATATATATATATATATATAT', + '','TA',1421,1441,True) + + # del + self.NR_110010_2_del_result_set = ( + 'GCGCGCG','GCGCG','CG','',25,32,True) + self.NR_110010_2_del_result_set_end = ( + 'GCGCGCG','GCGCG','GC','',25,32,True) + self.NM_003073_5_del_result_set = ( + 'AGAAGAAGA','AGAAGA','AGA','',1288,1297,True) + self.NM_002111_8_del_result_set = ( + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA', + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA', + 'CAG','',196,261,True) + self.NM_002111_8_del_result_set_end = ( + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA', + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA', + 'GCA','',196,261,True) + # start seq + # NM_001330096.1 + # rep CT stop 26 + # seq CTCTCTCTCTCTCTCTCTCTCTCTCT TTCTTTCTTTCCGATGGGGAAGAGAGGGT + # NM_001330112.1 + # rep GGGGCGGGTC stop 28 (or 20 for complete segments) + # seq (with flank) GGGGCGGGTCGGGGCGGGTC GGGGCGGG CCCACGGGCGGCCGGATTTG + # end seq + # NM_052928.3 + # rep GTTTT start 4375 len 4405 + # seq (with flank): + # ATAACACATATGCCTCCTTCTGAGTTGTTG GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT + # NM_002129.4 + # rep AT start 1408 len 1441 seq + # ATTAGCTAAATTGTTCCTCAGGTGTGTG T ATATATATATACATATATATATATATATATAT + self.res_set_d_start_NM_001330096_1 = ( + 'CTCTCTCTCTCTCTCTCTCTCTCTCT', + 'CTCTCTCTCTCTCTCTCTCTCTCT', + 'TC','',0,26,True) + self.res_set_d_start_NM_001330096_1_end = ( + 'CTCTCTCTCTCTCTCTCTCTCTCTCT', + 'CTCTCTCTCTCTCTCTCTCTCTCT', + 'CT','',0,26,True) + self.res_set_d_start_NM_001330112_1 = ( + 'GGGGCGGGTCGGGGCGGGTCGGGGCGGG', + 'GGGGCGGGTCGGGGCGGG', + 'GGGGCGGGTC','',0,28,True) + self.res_set_d_start_NM_001330112_1_end = ( + 'GGGGCGGGTCGGGGCGGGTCGGGGCGGG', + 'GGGGCGGGTCGGGGCGGG', + 'TCGGGGCGGG','',0,28,True) + + self.res_set_d_end_NM_052928_3 = ( + 'GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT', + 'GTTTTGTTTTGTTTTGTTTTGTTTT', + 'GTTTT', '', 4375,4405,True) + self.res_set_d_end_NM_052928_3_end = ( + 'GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT', + 'GTTTTGTTTTGTTTTGTTTTGTTTT', + 'TTGTT', '', 4375,4405,True) + self.res_set_d_end_NM_002129_4 = ( + 'ATATATATATATATATATAT', + 'ATATATATATATATATAT', + 'AT','',1421,1441,True) + self.res_set_d_end_NM_002129_4_end = ( + 'ATATATATATATATATATAT', + 'ATATATATATATATATAT', + 'TA','',1421,1441,True) + + def test_vrs_normalise_outwards(self): + # == return un-trimmed + res = self.vrs.normalise_outwards('NR_110010.2', 'CG','CG', 31,33) + NR_110010_2_eq_set = ('CG','CG','CG','CG',31,33,0) + assert res == NR_110010_2_eq_set + + def test_vrs_normalise_outwards_ex_left_ins(self): + # expand left ins + res = self.vrs.normalise_outwards('NR_110010.2', '','CG', 32,32) + assert res == self.NR_110010_2_ins_result_set + res = self.vrs.normalise_outwards('NR_110010.2', 'G','GCG', 31,32) + assert res == self.NR_110010_2_ins_result_set_end + res = self.vrs.normalise_outwards('NM_003073.5', '','AGA', 1297,1297) + assert res == self.NM_003073_5_ins_result_set + res = self.vrs.normalise_outwards( + 'NM_003073.5', 'AGA','AGAAGA', 1294,1297) + assert res == self.NM_003073_5_ins_result_set + res = self.vrs.normalise_outwards('NM_002111.8', '','GCA', 261,261) + assert res == self.NM_002111_8_ins_result_set_end + res = self.vrs.normalise_outwards('NM_002111.8', 'CA','CAGCA', 259,261) + assert res == self.NM_002111_8_ins_result_set + + def test_vrs_normalise_outwards_ex_right_ins(self): + # expand right ins + res = self.vrs.normalise_outwards('NR_110010.2', '','GC', 25,25) + assert res == self.NR_110010_2_ins_result_set_end + res = self.vrs.normalise_outwards('NR_110010.2', 'G','GCG', 25,26) + assert res == self.NR_110010_2_ins_result_set_end + res = self.vrs.normalise_outwards('NM_003073.5', '','AGA', 1288,1288) + assert res == self.NM_003073_5_ins_result_set + res = self.vrs.normalise_outwards( + 'NM_003073.5', 'AGA','AGAAGA', 1288,1291) + assert res == self.NM_003073_5_ins_result_set + res = self.vrs.normalise_outwards('NM_002111.8', '','CAG', 196,196) + assert res == self.NM_002111_8_ins_result_set + res = self.vrs.normalise_outwards('NM_002111.8', 'CA','CAGCA', 196,198) + assert res == self.NM_002111_8_ins_result_set + + def test_vrs_normalise_outwards_ex_both_ins(self): + # expand both directions ins + res = self.vrs.normalise_outwards('NR_110010.2', '','GC', 27,27) + assert res == self.NR_110010_2_ins_result_set_end + res = self.vrs.normalise_outwards('NR_110010.2', 'G','GCG', 27,28) + assert res == self.NR_110010_2_ins_result_set_end + res = self.vrs.normalise_outwards('NM_003073.5', '','AGA', 1291,1291) + assert res == self.NM_003073_5_ins_result_set + res = self.vrs.normalise_outwards( + 'NM_003073.5', 'AGA','AGAAGA', 1291,1294) + assert res == self.NM_003073_5_ins_result_set + res = self.vrs.normalise_outwards('NM_002111.8', '','CAG', 202,202) + assert res == self.NM_002111_8_ins_result_set + res = self.vrs.normalise_outwards('NM_002111.8', 'CA','CAGCA', 202,204) + assert res == self.NM_002111_8_ins_result_set + + def test_vrs_normalise_outwards_ex_done_ins(self): + # expand already done ins + res = self.vrs.normalise_outwards( + 'NR_110010.2', 'GCGCGCG','GCGCGCGCG', 25,32) + assert res == self.NR_110010_2_ins_result_set_end + res = self.vrs.normalise_outwards( + 'NM_003073.5', 'AGAAGAAGA','AGAAGAAGAAGA', 1288,1297) + assert res == self.NM_003073_5_ins_result_set + res = self.vrs.normalise_outwards( + 'NM_002111.8', + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA', + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA', + 196,261) + assert res == self.NM_002111_8_ins_result_set + + def test_vrs_normalise_outwards_ex_into_start_ins(self): + # expand into start ins + res = self.vrs.normalise_outwards('NM_001330096.1', '','CT', 24,24) + assert res == self.res_set_start_NM_001330096_1_end + res = self.vrs.normalise_outwards('NM_001330096.1', 'T','TCT', 23,24) + assert res == self.res_set_start_NM_001330096_1 + res = self.vrs.normalise_outwards( + 'NM_001330112.1', '','GGGGCGGGTC', 10,10) + assert res == self.res_set_start_NM_001330112_1 + res = self.vrs.normalise_outwards( + 'NM_001330112.1', 'TC','TCGGGGCGGGTC', 8,10) + assert res == self.res_set_start_NM_001330112_1_end + + def test_vrs_normalise_outwards_ex_into_start_done_ins(self): + # expand start already done ins + res = self.vrs.normalise_outwards( + 'NM_001330096.1', + 'CTCTCTCTCTCTCTCTCTCTCTCTCT', + 'CTCTCTCTCTCTCTCTCTCTCTCTCTCT', + 0,26) + assert res == self.res_set_start_NM_001330096_1_end + res = self.vrs.normalise_outwards( + 'NM_001330112.1', + 'GGGGCGGGTCGGGGCGGGTCGGGGCGGG', + 'GGGGCGGGTCGGGGCGGGTCGGGGCGGGTCGGGGCGGG', + 0,28) + assert res == self.res_set_start_NM_001330112_1 + + + def test_vrs_normalise_outwards_ex_into_end_ins(self): + # expand into end ins + res = self.vrs.normalise_outwards('NM_052928.3', '','GTTTT', 4380,4380) + assert res == self.res_set_end_NM_052928_3 + res = self.vrs.normalise_outwards( + 'NM_052928.3', 'TT','TTGTTTT', 4378,4380) + assert res == self.res_set_end_NM_052928_3_end + res = self.vrs.normalise_outwards('NM_002129.4', '','TA', 1422,1422) + assert res == self.res_set_end_NM_002129_4_end + res = self.vrs.normalise_outwards('NM_002129.4', 'TA','TATA', 1422,1424) + assert res == self.res_set_end_NM_002129_4_end + + def test_vrs_normalise_outwards_ex_into_end_done_ins(self): + # expand end already done ins + res = self.vrs.normalise_outwards( + 'NM_052928.3', + 'GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT', + 'GTTTTGTTTTGTTTTGTTTTGTTTTGTTTTGTTTT', + 4375,4405) + assert res == self.res_set_end_NM_052928_3 + res = self.vrs.normalise_outwards( + 'NM_002129.4', + 'ATATATATATATATATATAT', + 'ATATATATATATATATATATAT', + 1421,1441) + assert res == self.res_set_end_NM_002129_4 + + + ##DEL SET## + def test_vrs_normalise_outwards_ex_left_del(self): + # expand left del + res = self.vrs.normalise_outwards('NR_110010.2', 'CG','', 30,32) + assert res == self.NR_110010_2_del_result_set + res = self.vrs.normalise_outwards('NR_110010.2', 'GCG','G', 29,32) + assert res == self.NR_110010_2_del_result_set_end + res = self.vrs.normalise_outwards('NM_003073.5', 'AGA','', 1294,1297) + assert res == self.NM_003073_5_del_result_set + res = self.vrs.normalise_outwards( + 'NM_003073.5', 'AGAAGA','AGA', 1291,1297) + assert res == self.NM_003073_5_del_result_set + res = self.vrs.normalise_outwards('NM_002111.8', 'GCA','', 258,261) + assert res == self.NM_002111_8_del_result_set_end + res = self.vrs.normalise_outwards('NM_002111.8', 'CAGCA','CA', 256,261) + assert res == self.NM_002111_8_del_result_set + + def test_vrs_normalise_outwards_ex_left_del_flank_sepfetch(self): + # repeat left shift with self.ex_short_vrs to test flank fetch with: + # minimised ref length > 2*flank fetch + # this is used as the cut-off for fetching flanks separately + res = self.ex_short_vrs.normalise_outwards( + 'NM_003073.5', 'AGA','', 1294,1297) + assert res == self.NM_003073_5_del_result_set + res = self.ex_short_vrs.normalise_outwards( + 'NM_003073.5', 'AGAAGA','AGA', 1291,1297) + assert res == self.NM_003073_5_del_result_set + res = self.ex_short_vrs.normalise_outwards( + 'NM_002111.8', 'GCA','', 258,261) + assert res == self.NM_002111_8_del_result_set_end + res = self.ex_short_vrs.normalise_outwards( + 'NM_002111.8', 'CAGCA','CA', 256,261) + assert res == self.NM_002111_8_del_result_set + + def test_vrs_normalise_outwards_ex_right_del(self): + # expand right del + res = self.vrs.normalise_outwards('NR_110010.2', 'GC','', 25,27) + assert res == self.NR_110010_2_del_result_set_end + res = self.vrs.normalise_outwards('NR_110010.2', 'GCG','G', 25,28) + assert res == self.NR_110010_2_del_result_set_end + res = self.vrs.normalise_outwards('NM_003073.5', 'AGA','', 1288,1291) + assert res == self.NM_003073_5_del_result_set + res = self.vrs.normalise_outwards( + 'NM_003073.5', 'AGAAGA','AGA', 1288,1294) + assert res == self.NM_003073_5_del_result_set + res = self.vrs.normalise_outwards('NM_002111.8', 'CAG','', 196,199) + assert res == self.NM_002111_8_del_result_set + res = self.vrs.normalise_outwards('NM_002111.8', 'CAGCA','CA', 196,201) + assert res == self.NM_002111_8_del_result_set + + def test_vrs_normalise_outwards_ex_right_del_flank_sepfetch(self): + # repeat left shift with self.ex_short_vrs to test flank fetch with: + # minimised ref length > 2*flank fetch + # this is used as the cut-off for fetching flanks separately + res = self.ex_short_vrs.normalise_outwards( + 'NM_003073.5', 'AGA','', 1288,1291) + assert res == self.NM_003073_5_del_result_set + res = self.ex_short_vrs.normalise_outwards( + 'NM_003073.5', 'AGAAGA','AGA', 1288,1294) + assert res == self.NM_003073_5_del_result_set + res = self.ex_short_vrs.normalise_outwards( + 'NM_002111.8', 'CAG','', 196,199) + assert res == self.NM_002111_8_del_result_set + res = self.ex_short_vrs.normalise_outwards( + 'NM_002111.8', 'CAGCA','CA', 196,201) + assert res == self.NM_002111_8_del_result_set + + def test_vrs_normalise_outwards_ex_both_del(self): + # expand both directions del + res = self.vrs.normalise_outwards('NR_110010.2', 'GC','', 27,29) + assert res == self.NR_110010_2_del_result_set_end + res = self.vrs.normalise_outwards('NR_110010.2', 'GCG','G', 27,30) + assert res == self.NR_110010_2_del_result_set_end + res = self.vrs.normalise_outwards('NM_003073.5', 'AGA','', 1291,1294) + assert res == self.NM_003073_5_del_result_set + res = self.vrs.normalise_outwards('NM_003073.5', 'AGAAGA','AGA', 1291,1297) + assert res == self.NM_003073_5_del_result_set + res = self.vrs.normalise_outwards('NM_002111.8', 'CAG','', 202,205) + assert res == self.NM_002111_8_del_result_set + res = self.vrs.normalise_outwards('NM_002111.8', 'CAGCA','CA', 202,207) + assert res == self.NM_002111_8_del_result_set + + def test_vrs_normalise_outwards_ex_done_del(self): + # expand already done del + res = self.vrs.normalise_outwards( + 'NR_110010.2', 'GCGCGCG','GCGCG', 25,32) + assert res == self.NR_110010_2_del_result_set_end + res = self.vrs.normalise_outwards( + 'NM_003073.5', 'AGAAGAAGA','AGAAGA', 1288,1297) + assert res == self.NM_003073_5_del_result_set + res = self.vrs.normalise_outwards( + 'NM_002111.8', + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA', + 'CAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCAGCA', + 196,261) + assert res == self.NM_002111_8_del_result_set + + def test_vrs_normalise_outwards_ex_into_start_del(self): + # expand into start del + res = self.vrs.normalise_outwards('NM_001330096.1', 'CT','', 22,24) + assert res == self.res_set_d_start_NM_001330096_1_end + res = self.vrs.normalise_outwards('NM_001330096.1', 'TCT','T', 21,24) + assert res == self.res_set_d_start_NM_001330096_1 + res = self.vrs.normalise_outwards( + 'NM_001330112.1', 'GGGGCGGGTC','', 10,20) + assert res == self.res_set_d_start_NM_001330112_1 + res = self.vrs.normalise_outwards( + 'NM_001330112.1', 'TCGGGGCGGGTC', 'TC', 8,20) + assert res == self.res_set_d_start_NM_001330112_1_end + + def test_vrs_normalise_outwards_ex_into_start_done_del(self): + # expand start already done del + res = self.vrs.normalise_outwards( + 'NM_001330096.1', + 'CTCTCTCTCTCTCTCTCTCTCTCTCT', + 'CTCTCTCTCTCTCTCTCTCTCTCT', + 0,26) + assert res == self.res_set_d_start_NM_001330096_1_end + res = self.vrs.normalise_outwards( + 'NM_001330112.1', + 'GGGGCGGGTCGGGGCGGGTCGGGGCGGG', + 'GGGGCGGGTCGGGGCGGG', + 0,28) + assert res == self.res_set_d_start_NM_001330112_1 + + def test_vrs_normalise_outwards_ex_into_end_del(self): + # expand into end del + res = self.vrs.normalise_outwards('NM_052928.3', 'GTTTT','', 4380,4385) + assert res == self.res_set_d_end_NM_052928_3 + res = self.vrs.normalise_outwards( + 'NM_052928.3', 'TTGTTTT','TT', 4378,4385) + assert res == self.res_set_d_end_NM_052928_3_end + res = self.vrs.normalise_outwards('NM_002129.4', 'TA','', 1422,1424) + assert res == self.res_set_d_end_NM_002129_4_end + res = self.vrs.normalise_outwards('NM_002129.4', 'TATA','TA', 1422,1426) + assert res == self.res_set_d_end_NM_002129_4_end + + def test_vrs_normalise_outwards_ex_into_end_done_del(self): + # expand end already done del + res = self.vrs.normalise_outwards( + 'NM_052928.3', + 'GTTTTGTTTTGTTTTGTTTTGTTTTGTTTT', + 'GTTTTGTTTTGTTTTGTTTTGTTTT', + 4375,4405) + assert res == self.res_set_d_end_NM_052928_3 + res = self.vrs.normalise_outwards( + 'NM_002129.4', + 'ATATATATATATATATATAT', + 'ATATATATATATATATAT', + 1421,1441) + assert res == self.res_set_d_end_NM_002129_4 + +class TestVRSOutputVRSObjectIDs(unittest.TestCase): + """ + Tests for the internal vrs_util.py module, particularly the HGVS_to_VRS + object functions required for fetching and making VRS IDs. + + A good number of these tests take their required output from the VRS docs + (current as of VRS 2.1). + tests: + Test VRS IDs for all data types with specified id's in the VRS docs, + using stored VRS data as input + Test that the VRS id generation code produces the expected output for + the given input. + """ + @classmethod + def setup_class(self): + self.vrs = HGVS_to_VRS() + + def test_vrs_seq_ids_bad_id(self): + # test 1 bad ID to check that it asserts in some way + with self.assertRaises(KeyError): + self.vrs.id_fetch('LIVE_PARROT') + + def test_vrs_seq_ids(self): + """ + Test the basic fetching of VRS sequence IDs using given hgvs ID. + The VRS id_fetch() calls get_vrs_id_for_seq() to get the base id + via SeqRepo, this might change in the future, but we want to test the + id_fetch in use for this, not the base function, as the id_fetch + function in use is the target of these tests. + """ + # the internal set of sequences used: + # NR_110010.2, NM_003073.5, NM_002111.8, NM_001350922.2, + # NM_001330096.1, NM_001330112.1, NM_052928.3, and NM_002129.4 + vrs_seq_id = self.vrs.id_fetch('NR_110010.2') + assert vrs_seq_id == 'SQ.Ksaa229gzGC4Uc3ytCixai4vziud-MKi' + vrs_seq_id = self.vrs.id_fetch('NM_003073.5') + assert vrs_seq_id == 'SQ.pL2lXe9Qjg9ME_5vP0o85MgTUIqTAklc' + vrs_seq_id = self.vrs.id_fetch('NM_002111.8') + assert vrs_seq_id == 'SQ.4qBhnA470l_6xfLWJyi8WkOKeW6u5KJJ' + vrs_seq_id = self.vrs.id_fetch('NM_001350922.2') + assert vrs_seq_id == 'SQ.OgH_wLQV1lYs_Z107fts3xI9CuarHG7_' + vrs_seq_id = self.vrs.id_fetch('NM_001330096.1') + assert vrs_seq_id == 'SQ.Xzqbjy7R3hDF81IInpgNugs7N3c0qk2M' + vrs_seq_id = self.vrs.id_fetch('NM_001330112.1') + assert vrs_seq_id == 'SQ.L_1dwsmKTbVRGrbiF4p6FrgaWkWKWycu' + vrs_seq_id = self.vrs.id_fetch('NM_052928.3') + assert vrs_seq_id == 'SQ.pGqCl6h3ASKqxNA6o0aLmx_6NIoQio9l' + vrs_seq_id = self.vrs.id_fetch('NM_002129.4') + assert vrs_seq_id == 'SQ.8rOzThqGEimTT1vXu8KEb73YqgPBMfWJ' + # sequence IDs found in the vrs documentation + # allele docs + vrs_seq_id = self.vrs.id_fetch('NC_000001.11') + assert vrs_seq_id == 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO' + # sequence docs and from: + # https://github.com/ga4gh/vrs/blob/2.0/validation/models.yaml + vrs_seq_id = self.vrs.id_fetch('NC_000007.14') + assert vrs_seq_id == "SQ.F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul" + + def test_vrs_flatten(self): + """ Test the VRS flattening logic for basic functionality and currently + unused but, in the future needed, behaviour """ + # Test flattening start stop spans + flat = self.vrs.vrs_flatten({ + "type": "SequenceLocation", + "sequenceReference": { + "type": "SequenceReference", + "refgetAccession": "SQ.F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul" + }, + "start": [44908820,44908821], + "end": [44908822,44908823] + }) + assert flat == \ + '{"end":"[44908822,44908823]","sequenceReference":{'\ + '"refgetAccession":"SQ.F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul","type":"'\ + 'SequenceReference"},"start":"[44908820,44908821]","type":'\ + '"SequenceLocation"}' + flat = self.vrs.vrs_flatten({ + "type": "SequenceLocation", + "sequenceReference": { + "type": "SequenceReference", + "refgetAccession": "SQ.F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul" + }, + "start": [None,44908821], + "end": [44908822,None] + }) + assert flat == \ + '{"end":"[44908822,null]","sequenceReference":{"refgetAccession":'\ + '"SQ.F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul","type":"SequenceReference"}'\ + ',"start":"[null,44908821]","type":"SequenceLocation"}' + # Test specified digest_keys + flat = self.vrs.vrs_flatten({ + "type": "SequenceReference", + "refgetAccession": "SQ.F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul", + "test":"bla" + }, dict_keys=["refgetAccession","test","type"]) + assert flat == \ + '{"refgetAccession":"SQ.F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul","test":'\ + '"bla","type":"SequenceReference"}' + + + def test_vrs_object_ids_assert_invalid_type(self): + # test type assert on flatten & hash on non valid data type + with self.assertRaises(TypeError): + vrs_eg_seq_loc = { + "type": "SequenceReference", + "refgetAccession": self.vrs + } + self.vrs.vrs_flatten(vrs_eg_seq_loc,digest=True) + + def test_vrs_object_ids_assert_fail_no_vrs_id_prefix(self): + # test value error on VRS type without VRS id prefix + with self.assertRaises(ValueError): + vrs_state = { + "type": "ReferenceLengthExpression", + "length": 11, + "repeatSubunitLength": 3 + } + self.vrs.vrs_flatten(vrs_state,digest=True) + + # now test working ids + def test_vrs_object_ids_seq_loc(self): + vrs_eg_seq_loc = { + "id": "ga4gh:SL.4t6JnYWqHwYw9WzBT_lmWBb3tLQNalkT", + "type": "SequenceLocation", + "sequenceReference": { + "type": "SequenceReference", + "refgetAccession": "SQ.F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul" + }, + "start": 44908821, + "end": 44908822 + } + vrs_id = self.vrs.vrs_flatten(vrs_eg_seq_loc,digest=True) + assert vrs_id == vrs_eg_seq_loc['id'] + + def test_vrs_object_ids_full_allele(self): + vrs_eg_allele = { + "id": "ga4gh:VA.Oop4kjdTtKcg1kiZjIJAAR3bp7qi4aNT", + "type": "Allele", + "expressions": [ + { + "syntax": "spdi", + "value": "NC_000001.11:40819438:CTCCTCCT:CTCCTCCTCCT" + } + ], + "location": { + "type": "SequenceLocation", + "sequenceReference": { + "type": "SequenceReference", + "refgetAccession": "SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO", + "residueAlphabet": "na", + "id": "NC_000001.11" + }, + "start": 40819438, + "end": 40819446 + }, + "state": { + "type": "ReferenceLengthExpression", + "length": 11, + "repeatSubunitLength": 3 + } + } + vrs_id = self.vrs.vrs_flatten(vrs_eg_allele,digest=True) + assert vrs_id == vrs_eg_allele['id'] + + def test_vrs_object_ids_allele_from_eg_page(self): + vrs_eg_allele_from_id_eg_page = { + "location": { + "type": "SequenceLocation", + "sequenceReference": { + "type": "SequenceReference", + "refgetAccession": "SQ.IIB53T8CNeJJdUqzn9V_JnRtQadwWCbl" + }, + "start": 44908821, + "end": 44908822 + }, + "state": { + "type": "LiteralSequenceExpression", + "sequence": "T" + }, + "type": "Allele" + } + #{"location":"wIlaGykfwHIpPY2Fcxtbx4TINbbODFVz","state": + # {"sequence":"T","type":"LiteralSequenceExpression"},"type":"Allele"} + # ga4gh:VA.0AePZIWZUNsUlQTamyLrjm2HWUw2opLt_ + # note that the _ on the end is not genarated even when running the + # sample code given by + # https://vrs.ga4gh.org/en/latest/conventions/computed_identifiers.html#digest-serialization + # *exactly* as-is, the rest of the ID is identical + vrs_id = self.vrs.vrs_flatten(vrs_eg_allele_from_id_eg_page,digest=True) + assert vrs_id == 'ga4gh:VA.0AePZIWZUNsUlQTamyLrjm2HWUw2opLt' + + def test_vrs_object_ids_b_moddels_notebook(self): + # test the ids from the basic models notebook + # https://github.com/ga4gh/vrs-python/blob/main/notebooks/getting_started/3_Basic_Models.ipynb + #'NM_002439.5'# SQ.Pw3Ch0x3XWD6ljsnIfmk_NERcZCI9sNM + vrs_eg_allele_from_basic_mod_nb = { + 'id': 'ga4gh:VA.5C67OBmCLuHPgDkCQj7EOMih58BS2Eor', + 'type': 'Allele', + 'location': {'type': 'SequenceLocation', + 'sequenceReference': {'type': 'SequenceReference', + 'refgetAccession': 'SQ.Pw3Ch0x3XWD6ljsnIfmk_NERcZCI9sNM'}, + 'start': 80656509, + 'end': 80656510}, + 'state': {'type': 'LiteralSequenceExpression', 'sequence': 'TT'}} + vrs_id_loc = self.vrs.vrs_flatten( + vrs_eg_allele_from_basic_mod_nb['location'],digest=True) + assert vrs_id_loc == 'ga4gh:SL.lGxOP1JRd4dysmrOVaskO5P_35DyCLnx' + vrs_id = self.vrs.vrs_flatten( + vrs_eg_allele_from_basic_mod_nb,digest=True) + assert vrs_id == 'ga4gh:VA.5C67OBmCLuHPgDkCQj7EOMih58BS2Eor' + + def test_vrs_object_ids_2_0_validation_models1(self): + # test id creation for Alleles and sequence loacations found in + # https://github.com/ga4gh/vrs/blob/2.0/validation/models.yaml + vrs_allele_valid_model_test_set_lit= {'type': 'Allele', + 'location': {'type': 'SequenceLocation', + 'sequenceReference': {'type': 'SequenceReference', + 'refgetAccession': 'SQ.IIB53T8CNeJJdUqzn9V_JnRtQadwWCbl'}, + 'start': 44908821, + 'end': 44908822}, + 'state': {'type': 'LiteralSequenceExpression', 'sequence': 'T'}} + vrs_id = self.vrs.vrs_flatten( + vrs_allele_valid_model_test_set_lit['location'],digest=True) + assert vrs_id == 'ga4gh:SL.wIlaGykfwHIpPY2Fcxtbx4TINbbODFVz' + vrs_id = self.vrs.vrs_flatten( + vrs_allele_valid_model_test_set_lit,digest=True) + assert vrs_id == 'ga4gh:VA.0AePZIWZUNsUlQTamyLrjm2HWUw2opLt' + + def test_vrs_object_ids_2_0_validation_models2(self): + # test id creation for Alleles and sequence loacations found in + # https://github.com/ga4gh/vrs/blob/2.0/validation/models.yaml + vrs_allele_valid_model_test_set_len= {'type': 'Allele', + 'location': {'type': 'SequenceLocation', + 'sequenceReference': {'type': 'SequenceReference', + 'refgetAccession': 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO'}, + 'start': 40819438, + 'end': 40819446}, + 'state': { + 'type': 'ReferenceLengthExpression', + 'length': 11, + 'repeatSubunitLength': 3}} + vrs_id = self.vrs.vrs_flatten( + vrs_allele_valid_model_test_set_len['location'],digest=True) + assert vrs_id == 'ga4gh:SL.nQGBuvRQOLEboA5TYtcz975fp_GulxbZ' + vrs_id = self.vrs.vrs_flatten( + vrs_allele_valid_model_test_set_len,digest=True) + assert vrs_id == 'ga4gh:VA.Oop4kjdTtKcg1kiZjIJAAR3bp7qi4aNT' + + def test_vrs_object_ids_notebook_ex_allele_translator1(self): + # https://github.com/ga4gh/vrs-python/blob/main/notebooks/getting_started/4_Exploring_the_AlleleTranslator.ipynb + hgvs_out_lit = {'type': 'Allele', + 'location': {'type': 'SequenceLocation', + 'sequenceReference': {'type': 'SequenceReference', + 'refgetAccession': 'SQ.aUiQCzCPZ2d0csHbMSbh2NzInhonSXwI'}, + 'start': 80656488, + 'end': 80656489}, + 'state': {'type': 'LiteralSequenceExpression', 'sequence': 'T'}} + vrs_id = self.vrs.vrs_flatten(hgvs_out_lit['location'],digest=True) + assert vrs_id == 'ga4gh:SL.JiLRuuyS5wefF_6-Vw7m3Yoqqb2YFkss' + vrs_id = self.vrs.vrs_flatten(hgvs_out_lit,digest=True) + assert vrs_id == 'ga4gh:VA.ebezGL6HoAhtGJyVnB_mE5BH18ntKev4' + + def test_vrs_object_ids_notebook_ex_allele_translator2(self): + # https://github.com/ga4gh/vrs-python/blob/main/notebooks/getting_started/4_Exploring_the_AlleleTranslator.ipynb + hgvs_out_lit2 = {'type': 'Allele', + 'location': {'type': 'SequenceLocation', + 'sequenceReference': {'type': 'SequenceReference', + 'refgetAccession': 'SQ.vbjOdMfHJvTjK_nqvFvpaSKhZillW0SX'}, + 'start': 79952307, + 'end': 79952308}, + 'state': {'type': 'LiteralSequenceExpression', 'sequence': 'T'}} + vrs_id = self.vrs.vrs_flatten(hgvs_out_lit2['location'],digest=True) + assert vrs_id == 'ga4gh:SL.Y-itBtqe9IwbxyL4EVZ4T_X9TUsdbJ22' + vrs_id = self.vrs.vrs_flatten(hgvs_out_lit2,digest=True) + assert vrs_id == 'ga4gh:VA.hEyB1sGiQrdrPFIq4u4CF17uAuUs2Wvx' + +class TestVRSOutputVRSObjects(unittest.TestCase): + """ + Tests for the internal vrs_util.py module, particularly the HGVS_to_VRS + object function hgvs_single_var_to_vrs, which is the the basic core + hgvs->vrs function. + + Starts with standard input as from + https://github.com/ga4gh/vrs/blob/2.0/validation/models.yaml + then moves to extra tests on shortcuts and error/null return states. + + """ + @classmethod + def setup_class(self): + self.vrs = HGVS_to_VRS() + + def test_vrs_object_output_NC_000019_10_g_44908822C_T_set(self): + + # allele test + # hgvs in NC_000019.10:g44908822_44908821C>T + hgvs = vv.hp.parse('NC_000019.10:g.44908822C>T') + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'LiteralSequenceExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 44908822 + assert hgvs_allele['location']['start'] == 44908821 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.IIB53T8CNeJJdUqzn9V_JnRtQadwWCbl' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.wIlaGykfwHIpPY2Fcxtbx4TINbbODFVz' + assert hgvs_allele['state']['sequence'] == 'T' + assert hgvs_allele['id'] == 'ga4gh:VA.0AePZIWZUNsUlQTamyLrjm2HWUw2opLt' + + def test_vrs_object_output_NC_000001_11_g_40819439_40819446delCTCCTCCTinsCTCCTCCTCCT(self): + # No need to test the 'canonical' JSON serilised form as this, is not available as output, + # and should be redundant with the checksum ID test + # hgvs in "NC_000001.11:40819439_40819446delCTCCTCCTinsCTCCTCCTCCT + hgvs = vv.hp.parse( + "NC_000001.11:g.40819439_40819446delCTCCTCCTinsCTCCTCCTCCT") + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'ReferenceLengthExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 40819446 + assert hgvs_allele['location']['start'] == 40819438 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.nQGBuvRQOLEboA5TYtcz975fp_GulxbZ' + assert hgvs_allele['state']['length'] == 11 + assert hgvs_allele['state']['repeatSubunitLength'] == 3 + assert hgvs_allele['id'] == 'ga4gh:VA.Oop4kjdTtKcg1kiZjIJAAR3bp7qi4aNT' + + def test_vrs_object_output_NC_000005_10_g_80656489C_T(self): + # test for good output from hgvs input matching + # https://github.com/ga4gh/vrs-python/blob/main/notebooks/getting_started/4_Exploring_the_AlleleTranslator.ipynb + #From HGVS + hgvs = vv.hp.parse("NC_000005.10:g.80656489C>T") + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'LiteralSequenceExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 80656489 + assert hgvs_allele['location']['start'] == 80656488 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.aUiQCzCPZ2d0csHbMSbh2NzInhonSXwI' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.JiLRuuyS5wefF_6-Vw7m3Yoqqb2YFkss' + assert hgvs_allele['state']['sequence'] == 'T' + assert hgvs_allele['id'] == 'ga4gh:VA.ebezGL6HoAhtGJyVnB_mE5BH18ntKev4' + + def test_vrs_object_output_NC_000005_9_g_79952308C_T(self): + hgvs = vv.hp.parse("NC_000005.9:g.79952308C>T") + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'LiteralSequenceExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 79952308 + assert hgvs_allele['location']['start'] == 79952307 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.vbjOdMfHJvTjK_nqvFvpaSKhZillW0SX' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.Y-itBtqe9IwbxyL4EVZ4T_X9TUsdbJ22' + assert hgvs_allele['state']['sequence'] == 'T' + assert hgvs_allele['id'] == 'ga4gh:VA.hEyB1sGiQrdrPFIq4u4CF17uAuUs2Wvx' + + + def test_vrs_obj_out_norm_identity_diff_input_start(self): + """ + test VRS IDs are the same for multiple hgvs inputs that should + normalise to the same output VRS start hgvs input. + """ + # hgvs in "NC_000001.11:40819439_40819446delCTCCTCCTinsCTCCTCCTCCT + hgvs = vv.hp.parse("NC_000001.11:g.40819438_40819439insCTC") + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'ReferenceLengthExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 40819446 + assert hgvs_allele['location']['start'] == 40819438 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.nQGBuvRQOLEboA5TYtcz975fp_GulxbZ' + assert hgvs_allele['state']['length'] == 11 + assert hgvs_allele['state']['repeatSubunitLength'] == 3 + assert hgvs_allele['id'] == 'ga4gh:VA.Oop4kjdTtKcg1kiZjIJAAR3bp7qi4aNT' + + def test_vrs_obj_out_norm_identity_diff_input_end(self): + """ + test VRS IDs are the same for multiple hgvs inputs that should + normalise to the same output VRS end hgvs input. + """ + # hgvs in "NC_000001.11:40819439_40819446delCTCCTCCTinsCTCCTCCTCCT + hgvs = vv.hp.parse("NC_000001.11:g.40819446_40819447insCCT") + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'ReferenceLengthExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 40819446 + assert hgvs_allele['location']['start'] == 40819438 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.nQGBuvRQOLEboA5TYtcz975fp_GulxbZ' + assert hgvs_allele['state']['length'] == 11 + assert hgvs_allele['state']['repeatSubunitLength'] == 3 + assert hgvs_allele['id'] == 'ga4gh:VA.Oop4kjdTtKcg1kiZjIJAAR3bp7qi4aNT' + + def test_vrs_obj_out_norm_identity_diff_input_mid(self): + """ + test VRS IDs are the same for multiple hgvs inputs that should + normalise to the same output VRS middle hgvs input. + """ + # hgvs in "NC_000001.11:40819439_40819446delCTCCTCCTinsCTCCTCCTCCT + hgvs = vv.hp.parse("NC_000001.11:g.40819443_40819444insCCT") + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'ReferenceLengthExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 40819446 + assert hgvs_allele['location']['start'] == 40819438 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.nQGBuvRQOLEboA5TYtcz975fp_GulxbZ' + assert hgvs_allele['state']['length'] == 11 + assert hgvs_allele['state']['repeatSubunitLength'] == 3 + assert hgvs_allele['id'] == 'ga4gh:VA.Oop4kjdTtKcg1kiZjIJAAR3bp7qi4aNT' + + def test_vrs_obj_out_norm_identity_diff_set_dup(self): + """ + test VRS IDs are the same for multiple hgvs inputs that should + normalise to the same output VRS middle hgvs input. + Dup, like ins also has a separate code path from del/delins/subtitue + test this as well. + """ + # hgvs in "NC_000001.11:40819439_40819446delCTCCTCCTinsCTCCTCCTCCT + hgvs = vv.hp.parse("NC_000001.11:g.40819442_40819444dup") + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'ReferenceLengthExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 40819446 + assert hgvs_allele['location']['start'] == 40819438 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.nQGBuvRQOLEboA5TYtcz975fp_GulxbZ' + assert hgvs_allele['state']['length'] == 11 + assert hgvs_allele['state']['repeatSubunitLength'] == 3 + assert hgvs_allele['id'] == 'ga4gh:VA.Oop4kjdTtKcg1kiZjIJAAR3bp7qi4aNT' + + def test_vrs_object_output_part_ambig_pos(self): + """ + test variant with partially ambiguous positions + eg ACCA to ACGCCA could be an ins either before (of CG) or after + (of GC) the fist C + so VRS expands to cover the first C + hgvs inputs should be normalised 3' in this case, but this also should + work either way + done for TCCT >TCGCCT and TCCT >TCCGCT in fully expanded delins + both + directions + """ + hgvs = vv.hp.parse("NC_000001.11:g.40819444_40819445delCCinsCCGC") + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'LiteralSequenceExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 40819445 + assert hgvs_allele['location']['start'] == 40819444 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.UhfMqgh_jnDrVNWaFx-_ksYssV1-9Hr3' + assert hgvs_allele['state']['sequence'] == 'CGC' + assert hgvs_allele['state']['type'] == 'LiteralSequenceExpression' + assert hgvs_allele['id'] == 'ga4gh:VA.r-IMwTjeSjYhwqkaIX7tloTeeMnuEGYy' + + hgvs = vv.hp.parse("NC_000001.11:g.40819445delCinsCGC") + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'LiteralSequenceExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 40819445 + assert hgvs_allele['location']['start'] == 40819444 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.UhfMqgh_jnDrVNWaFx-_ksYssV1-9Hr3' + assert hgvs_allele['state']['sequence'] == 'CGC' + assert hgvs_allele['state']['type'] == 'LiteralSequenceExpression' + assert hgvs_allele['id'] == 'ga4gh:VA.r-IMwTjeSjYhwqkaIX7tloTeeMnuEGYy' + + + # extra tests VRS varified, outside of the examples? + + def test_vrs_object_output_eq_shortcut(self): + "hgvs_single_var_to_vrs on == variant to test internal shortcut" + hgvs = vv.hp.parse("NC_000001.11:g.40819444_40819445=") + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'ReferenceLengthExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 40819445 + assert hgvs_allele['location']['start'] == 40819443 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.zxbyl3-hn1MHmcZYF0N8WWaAzNZcryGk' + assert hgvs_allele['state']['length'] == 2 + assert hgvs_allele['state']['repeatSubunitLength'] == 2 + assert hgvs_allele['state']['type'] == 'ReferenceLengthExpression' + + def test_vrs_object_output_fail_with_no_mapper_for_c_input(self): + # Test C type works, and failes propperly with mapper missing + with self.assertRaises(ValueError): + self.vrs.hgvs_single_var_to_vrs( + vv.hp.parse('NM_015120.4:c.35T>C')) + + def test_vrs_object_output_for_c_input(self): + hgvs_allele = self.vrs.hgvs_single_var_to_vrs( + vv.hp.parse('NM_015120.4:c.35T>C'), + variant = MockVVData()) + assert hgvs_allele == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.mq1Saz29534GzZ6T_jCfUjsqNlifC7pZ', + 'label': 'NM_015120.4' + }, + 'start': 145, + 'end': 146, + 'id': 'ga4gh:SL.x_rdZXUcg7V8Ae5pPVv0OS-faxKyzNQb' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'C' + }, + 'id': 'ga4gh:VA.x8gKEBS-s638D8uuuQXVJKIgCDcuQJCW' + } + + def test_vrs_object_output_no_norm_shortcut_ins(self): + # test that ins and del that don't at all normalise outwards + # use the shortcut correctly + hgvs = vv.hp.parse("NC_000001.11:g.40819444_40819445insG") + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'LiteralSequenceExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 40819444 + assert hgvs_allele['location']['start'] == 40819444 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.aBJ1_py6od8pI4OWHQegaTxdAsszkbBy' + assert hgvs_allele['state']['sequence'] == 'G' + assert hgvs_allele['state']['type'] == 'LiteralSequenceExpression' + assert hgvs_allele['id'] == 'ga4gh:VA.i_1aVFr-IQvl8bXoIX_UjwAU6_IwAmCD' + + def test_vrs_object_output_no_norm_shortcut_del(self): + hgvs = vv.hp.parse("NC_000001.11:g.40819444_40819445delCC") + hgvs_allele = self.vrs.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'ReferenceLengthExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 40819445 + assert hgvs_allele['location']['start'] == 40819443 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.zxbyl3-hn1MHmcZYF0N8WWaAzNZcryGk' + assert hgvs_allele['state']['repeatSubunitLength'] == 2 + assert hgvs_allele['state']['length'] == 0 + assert hgvs_allele['state']['type'] == 'ReferenceLengthExpression' + assert hgvs_allele['id'] == 'ga4gh:VA.gpJtRDCma3r-7snQ90wFU7i-hRPuJKGh' + + def test_vrs_object_output_intronic_null_result(self): + # test null result on intronic input + hgvs_allele = self.vrs.hgvs_single_var_to_vrs( + # not valid but works for test + vv.hp.parse('NM_015120.4:c.35-6T>C'), + variant = MockVVData()) + assert hgvs_allele is None + + def test_vrs_object_output_cache(self): + # test cached = type + vrs_cached = copy.copy(self.vrs) + vrs_cached.cache = {} + hgvs = vv.hp.parse("NC_000001.11:g.40819444_40819445=") + hgvs_allele = vrs_cached.hgvs_single_var_to_vrs(hgvs) + assert hgvs_allele['type'] == 'Allele' + assert hgvs_allele['state']['type'] == 'ReferenceLengthExpression' + assert hgvs_allele['location']['type'] == 'SequenceLocation' + assert hgvs_allele['location']['sequenceReference'][ + 'type'] == 'SequenceReference' + assert hgvs_allele['location']['end'] == 40819445 + assert hgvs_allele['location']['start'] == 40819443 + assert hgvs_allele['location']['sequenceReference'][ + 'refgetAccession'] == 'SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO' + assert hgvs_allele['location'][ + 'id'] == 'ga4gh:SL.zxbyl3-hn1MHmcZYF0N8WWaAzNZcryGk' + assert hgvs_allele['state']['length'] == 2 + assert hgvs_allele['state']['repeatSubunitLength'] == 2 + assert hgvs_allele['state']['type'] == 'ReferenceLengthExpression' + assert vrs_cached.cache["NC_000001.11:g.40819444_40819445="] == \ + hgvs_allele + +class TestVRSOutputFromDummyVV(unittest.TestCase): + """ + Test the generation of VRS output from dummy VV result objects, unlike raw + VV this relies objects this does not rely on VV working consistently with + previous results. Unfortunately this also means that this test set may need + to be changed in the case of VV alterations/improvements. + + See also the direct VV using tests below in the case of failures. + """ + + @classmethod + def setup_class(self): + self.vrs = HGVS_to_VRS() + # core set of dummy input + self.dummy_vv_output = MockVVData( + selected_assembly='GRCh38', + original = 'test_input', + warnings=[], + lovd_messages = [], + lovd_corrections = [], + stable_gene_ids = { + 'hgnc_id': 'HGNC:TST', + 'entrez_gene_id': '0000', + 'ucsc_id': 'uc0tst.1', + 'omim_id': ['0000000']}, + gene_symbol = 'Dummy_S', + description = "Text transcript description",) + + def test_vv_output_to_vrs_output_val_err(self): + # test set 1: test that we trap and return variants with corrections + # without VRS data + # 1a test variant.warnings == ['Validation error'] response + err_fail = copy.copy(self.dummy_vv_output) + err_fail.warnings=['Validation error'] + output = self.vrs.variant_validator_output_set_to_vrs(err_fail) + assert output['selected_assembly'] == err_fail.selected_assembly + assert output['submitted_variant'] == err_fail.original + assert output['warnings_and_messages'] == { + 'validation_warnings': ['Validation error'], + 'lovd_messages': [], + 'lovd_corrections': [] + } + assert output['gene_ids']['hgnc_id'] == 'HGNC:TST' + assert output['gene_ids']['entrez_gene_id'] == '0000' + assert output['gene_ids']['ucsc_id'] == 'uc0tst.1' + assert output['gene_ids']['omim_id'] == ['0000000'] + assert output['gene_ids']['current_symbol'] == 'Dummy_S' + assert output['transcript_description'] == err_fail.description + + def test_vv_output_to_vrs_output_warn_type(self): + # 1b test variant.output_type_flag = 'warning' + err_warn = copy.copy(self.dummy_vv_output) + err_warn.output_type_flag = 'warning' + output = self.vrs.variant_validator_output_set_to_vrs(err_warn) + assert output['selected_assembly'] == err_warn.selected_assembly + assert output['submitted_variant'] == err_warn.original + assert output['warnings_and_messages'] == { + 'validation_warnings': [], + 'lovd_messages': [], + 'lovd_corrections': [] + } + assert output['gene_ids']['hgnc_id'] == 'HGNC:TST' + assert output['gene_ids']['entrez_gene_id'] == '0000' + assert output['gene_ids']['ucsc_id'] == 'uc0tst.1' + assert output['gene_ids']['omim_id'] == ['0000000'] + assert output['gene_ids']['current_symbol'] == 'Dummy_S' + assert output['transcript_description'] == err_warn.description + + def test_vv_output_to_vrs_output_warn_for_working_type(self): + # test warnings in output for variants with output_type_flag and w/o + err_warn = copy.copy(self.dummy_vv_output) + err_warn.output_type_flag = 'warning' + err_warn.warnings=['VV_Warn'] + err_warn.lovd_messages = ['LO_Warn'] + err_warn.lovd_corrections = ['LO_corr'] + output = self.vrs.variant_validator_output_set_to_vrs(err_warn) + assert output['selected_assembly'] == err_warn.selected_assembly + assert output['submitted_variant'] == err_warn.original + assert output['warnings_and_messages'] == { + 'validation_warnings': ['VV_Warn'], + 'lovd_messages': ['LO_Warn'], + 'lovd_corrections': ['LO_corr'] + } + assert output['gene_ids']['hgnc_id'] == 'HGNC:TST' + assert output['gene_ids']['entrez_gene_id'] == '0000' + assert output['gene_ids']['ucsc_id'] == 'uc0tst.1' + assert output['gene_ids']['omim_id'] == ['0000000'] + assert output['gene_ids']['current_symbol'] == 'Dummy_S' + assert output['transcript_description'] == err_warn.description + + def test_vv_output_to_vrs_output_gene_symbol(self): + # test that gene symbols get handled regardless of stable gene id state + no_gene_id = copy.copy(self.dummy_vv_output) + no_gene_id.stable_gene_ids = None + output = self.vrs.variant_validator_output_set_to_vrs(no_gene_id) + assert output['gene_ids'] == {'current_symbol': 'Dummy_S'} + no_gene_id.gene_symbol = None + output = self.vrs.variant_validator_output_set_to_vrs(no_gene_id) + assert output['gene_ids'] is None + + # seq tests done using data from input test 1 + def test_vv_output_to_vrs_output_r_type(self): + # test 2: test that we catch and return the simpler r type input + # 2a test for working output to valid input + rna_test = copy.copy(self.dummy_vv_output) + rna_test.rna_data = { + 'rna_variant':vv.hp.parse('NM_015120.4:c.35T>C'), + 'translation_slr':vv.hp.parse('NP_055935.4:p.(L12P)'), + 'usage_warnings':"RNA warning"} + vrs_out = self.vrs.variant_validator_output_set_to_vrs(rna_test) + assert vrs_out['vrs_transcript_variant'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.mq1Saz29534GzZ6T_jCfUjsqNlifC7pZ', + 'label': 'NM_015120.4' + }, + 'start': 145, + 'end': 146, + 'id': 'ga4gh:SL.x_rdZXUcg7V8Ae5pPVv0OS-faxKyzNQb' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'C' + }, + 'id': 'ga4gh:VA.x8gKEBS-s638D8uuuQXVJKIgCDcuQJCW' + } + assert vrs_out['vrs_predicted_protein_consequence'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.ggxH8oENu3WDprE6Z_Qg1HvOiT0d8UNa', + 'label': 'NP_055935.4' + }, + 'start': 11, + 'end': 12, + 'id': 'ga4gh:SL.qOpPs9dwgqK6MuH3CTQUDoRYbi9IWwSh' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'P' + }, + 'id': 'ga4gh:VA.cJPulDXE1G73sZS4dHaZX_27-c4pNjYo' + } + + def test_vv_output_to_vrs_output_r_type_missmatch1(self): + # 2b test that r type variant causes assertions on + # hgvs_transcript_variant or primary_assembly_loci being set + # (we don't check object type) + rna_test_bad_1 = copy.copy(self.dummy_vv_output) + rna_test_bad_1.rna_data = { + 'rna_variant':vv.hp.parse('NM_015120.4:c.35T>C'), + 'translation_slr':vv.hp.parse('NP_055935.4:p.(L12P)'), + 'usage_warnings':"RNA warning"} + rna_test_bad_1.hgvs_transcript_variant = 1 + with self.assertRaises(Exception): + self.vrs.variant_validator_output_set_to_vrs(rna_test_bad_1) + + def test_vv_output_to_vrs_output_r_type_missmatch2(self): + rna_test_bad_2 = copy.copy(self.dummy_vv_output) + rna_test_bad_2.rna_data = { + 'rna_variant':vv.hp.parse('NM_015120.4:c.35T>C'), + 'translation_slr':vv.hp.parse('NP_055935.4:p.(L12P)'), + 'usage_warnings':"RNA warning"} + rna_test_bad_2.primary_assembly_loci = 2 + with self.assertRaises(Exception): + self.vrs.variant_validator_output_set_to_vrs(rna_test_bad_2) + + def test_vv_output_to_vrs_output_working_exonic(self): + # test 3: test for working responses to c/n type input that is exonic + c_test = copy.copy(self.dummy_vv_output) + c_test.hgvs_transcript_variant = vv.hp.parse('NM_015120.4:c.35T>C') + hgvs_hg37 = vv.hp.parse('NC_000002.11:g.73613031delinsCGGA') + hgvs_hg38 = vv.hp.parse('NC_000002.12:g.73385903delinsCGGA') + c_test.primary_assembly_loci = { + 'hg19': { + 'hgvs_genomic_description': hgvs_hg37, + 'vcf': { + 'chr': 'chr2', 'pos': '73613031', 'ref': 'T', 'alt': 'CGGA'} + }, + 'hg38': { + 'hgvs_genomic_description': hgvs_hg37, + 'vcf': { + 'chr': 'chr2', 'pos': '73385903', 'ref': 'T', 'alt': 'CGGA'} + }, + 'grch37' : { + 'hgvs_genomic_description': hgvs_hg37, + 'vcf': { + 'chr': '2', 'pos': '73613031', 'ref': 'T', 'alt': 'CGGA'} + }, + 'grch38': { + 'hgvs_genomic_description': hgvs_hg38, + 'vcf': { + 'chr': '2', 'pos': '73385903', 'ref': 'T', 'alt': 'CGGA'} + } + } + c_test.hgvs_predicted_protein_consequence = { + 'prot':vv.hp.parse('NP_055935.4:p.(L12P)')} + vrs_out = self.vrs.variant_validator_output_set_to_vrs(c_test) + assert vrs_out['vrs_transcript_variant'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.mq1Saz29534GzZ6T_jCfUjsqNlifC7pZ', + 'label': 'NM_015120.4' + }, + 'start': 145, + 'end': 146, + 'id': 'ga4gh:SL.x_rdZXUcg7V8Ae5pPVv0OS-faxKyzNQb' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'C' + }, + 'id': 'ga4gh:VA.x8gKEBS-s638D8uuuQXVJKIgCDcuQJCW' + } + assert vrs_out['vrs_predicted_protein_consequence'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.ggxH8oENu3WDprE6Z_Qg1HvOiT0d8UNa', + 'label': 'NP_055935.4' + }, + 'start': 11, + 'end': 12, + 'id': 'ga4gh:SL.qOpPs9dwgqK6MuH3CTQUDoRYbi9IWwSh' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'P' + }, + 'id': 'ga4gh:VA.cJPulDXE1G73sZS4dHaZX_27-c4pNjYo' + } + assert vrs_out['VRS_mappings_for_primary_assemblies'] == { + 'grch37': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.9KdcA9ZpY1Cpvxvg8bMSLYDUpsX6GDLO', + 'label': 'NC_000002.11' + }, + 'start': 73613030, + 'end': 73613031, + 'id': 'ga4gh:SL.IzA6GY6mvDiJ1EzMbe1Cxv6jyhg4u8t7' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'CGGA' + }, + 'id': 'ga4gh:VA.rKc_HxeylEKYQzYo_e_kY1lqYLF6Ch5m' + }, + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.pnAqCRBrTsUoBghSD1yp_jXWSmlbdh4g', + 'label': 'NC_000002.12' + }, + 'start': 73385902, + 'end': 73385903, + 'id': 'ga4gh:SL.ndPBndyntJTTRHuU6tYpgbN6TUsRv94B' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'CGGA' + }, + 'id': 'ga4gh:VA.eDiJCZTUpLf1UQHaUFFP1UaPsHfbOjyC' + } + } + def test_vv_output_to_vrs_output_working_exonic_w_rsg(self): + c_test = copy.copy(self.dummy_vv_output) + c_test.hgvs_transcript_variant = vv.hp.parse('NM_015120.4:c.35T>C') + c_test.hgvs_refseqgene_variant = vv.hp.parse('NG_011690.1:g.5146T>C') + hgvs_hg37 = vv.hp.parse('NC_000002.11:g.73613031delinsCGGA') + hgvs_hg38 = vv.hp.parse('NC_000002.12:g.73385903delinsCGGA') + c_test.primary_assembly_loci = { + 'hg19': {'hgvs_genomic_description': hgvs_hg37}, + 'hg38': {'hgvs_genomic_description': hgvs_hg37}, + 'grch37' : {'hgvs_genomic_description': hgvs_hg37}, + 'grch38': {'hgvs_genomic_description': hgvs_hg38} + } + c_test.hgvs_predicted_protein_consequence = { + 'prot':vv.hp.parse('NP_055935.4:p.(L12P)')} + vrs_out = self.vrs.variant_validator_output_set_to_vrs(c_test) + assert vrs_out['vrs_transcript_variant'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.mq1Saz29534GzZ6T_jCfUjsqNlifC7pZ', + 'label': 'NM_015120.4' + }, + 'start': 145, + 'end': 146, + 'id': 'ga4gh:SL.x_rdZXUcg7V8Ae5pPVv0OS-faxKyzNQb' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'C' + }, + 'id': 'ga4gh:VA.x8gKEBS-s638D8uuuQXVJKIgCDcuQJCW' + } + assert vrs_out['vrs_predicted_protein_consequence'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.ggxH8oENu3WDprE6Z_Qg1HvOiT0d8UNa', + 'label': 'NP_055935.4' + }, + 'start': 11, + 'end': 12, + 'id': 'ga4gh:SL.qOpPs9dwgqK6MuH3CTQUDoRYbi9IWwSh' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'P' + }, + 'id': 'ga4gh:VA.cJPulDXE1G73sZS4dHaZX_27-c4pNjYo' + } + assert vrs_out['VRS_mappings_for_primary_assemblies'] == { + 'grch37': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.9KdcA9ZpY1Cpvxvg8bMSLYDUpsX6GDLO', + 'label': 'NC_000002.11' + }, + 'start': 73613030, + 'end': 73613031, + 'id': 'ga4gh:SL.IzA6GY6mvDiJ1EzMbe1Cxv6jyhg4u8t7' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'CGGA' + }, + 'id': 'ga4gh:VA.rKc_HxeylEKYQzYo_e_kY1lqYLF6Ch5m' + }, + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.pnAqCRBrTsUoBghSD1yp_jXWSmlbdh4g', + 'label': 'NC_000002.12' + }, + 'start': 73385902, + 'end': 73385903, + 'id': 'ga4gh:SL.ndPBndyntJTTRHuU6tYpgbN6TUsRv94B' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'CGGA' + }, + 'id': 'ga4gh:VA.eDiJCZTUpLf1UQHaUFFP1UaPsHfbOjyC' + } + } + assert vrs_out['vrs_refseqgene_variant'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.r47Jw6neAxxIQgeyZLmdPrqFN4S6h7hy', + 'label': 'NG_011690.1'}, + 'start': 5145, + 'end': 5146, + 'id': 'ga4gh:SL.3CEnQ3JJo2jQbraJ0UHIO-lqBbR4xs15'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'C'}, + 'id': 'ga4gh:VA.TjhuWhUG-PJapHphK3r0HWyU5b9CgGG_'} + + def test_vv_output_to_vrs_output_intronic_main_chr(self): + # test 4: test for working responses to c/n type input that is intronic + # 4a intronic matches main genomic ref, taken from input test 7 + c_int_test = copy.copy(self.dummy_vv_output) + c_int_test.hgvs_transcript_variant = vv.hp.parse( + 'NM_000548.4:c.138+821del') + c_int_test.genome_context_intronic_sequence = copy.copy( + c_int_test.hgvs_transcript_variant) + c_int_test.genome_context_intronic_sequence.rel_ac = 'NC_000016.9' + c_int_test.hgvs_predicted_protein_consequence = { + 'prot': vv.hp.parse('NP_000539.2:p.?')} + + hgvs_hg37 = vv.hp.parse('NC_000016.9:g.2099575del') + hgvs_hg38 = vv.hp.parse('NC_000016.10:g.2049574del') + c_int_test.primary_assembly_loci = { + 'hg19':{'hgvs_genomic_description': hgvs_hg37}, + 'hg38':{'hgvs_genomic_description': hgvs_hg38}, + 'grch37':{'hgvs_genomic_description': hgvs_hg37}, + 'grch38':{'hgvs_genomic_description': hgvs_hg38}} + c_int_test.hgvs_genomic = copy.copy(hgvs_hg37) + + vrs_out = self.vrs.variant_validator_output_set_to_vrs(c_int_test) + warnings_and_messages = { + 'validation_warnings': [], + 'lovd_messages': [], + 'lovd_corrections': [], + 'vrs_output_warnings': [ + 'VRSIntronWarning: VRS does not handle mappings shared between ' + 'genomic and transcript reference sequences. As such only the ' + 'genomic mappings for this hgvs intronic transcript variant are' + ' preserved.']} + vrs_intronic_genomic_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.W6wLoIFOn4G7cjopxPxYNk2lcEqhLQFb', + 'label': 'NC_000016.9'}, + 'start': 2099572, + 'end': 2099575, + 'id': 'ga4gh:SL.OSRCAmPHLKeVCIccBJw2PE2fjBBZVO5g'}, + 'state': { + 'type': 'ReferenceLengthExpression', + 'length': 2, 'repeatSubunitLength': 1}, + 'id': 'ga4gh:VA.R4sVDvRvhoB_h0TTNcxbRkEkqL8Iwzww'} + VRS_mappings_for_primary_assemblies = { + 'grch37': vrs_intronic_genomic_variant, + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.yC_0RBj3fgBlvgyAuycbzdubtLxq-rE0', + 'label': 'NC_000016.10'}, + 'start': 2049571, + 'end': 2049574, + 'id': 'ga4gh:SL.LBYz9IIcNSR00VhmUb06RRpf9WQKh_xN'}, + 'state': { + 'type': 'ReferenceLengthExpression', + 'length': 2, + 'repeatSubunitLength': 1}, + 'id': 'ga4gh:VA.swtaZ9h8m_gc6bDXPtNAALWSRBquhDcs'}} + assert vrs_out['warnings_and_messages']['vrs_output_warnings'] == \ + warnings_and_messages['vrs_output_warnings'] + assert vrs_out['warnings_and_messages'] == warnings_and_messages + assert vrs_out['vrs_intronic_genomic_variant'] == \ + vrs_intronic_genomic_variant + assert vrs_out['VRS_mappings_for_primary_assemblies'] == \ + VRS_mappings_for_primary_assemblies + + def test_vv_output_to_vrs_output_intronic_alt_chr_match(self): + # 4b intronic matches alt genomic ref, adapted from input test 189 + # also tests RSG+ intronic RSG + c_int_alt = copy.copy(self.dummy_vv_output) + c_int_alt.hgvs_transcript_variant = vv.hp.parse( + 'NM_012309.4:c.913-5058G>A') + # orig 'genome_context_intronic_sequence' = \ + # 'NC_000011.10(NM_012309.4):c.913-5058G>A' + # we switch to NW_004070871.1 which would normally only turn up in + # output if used in input to test such input handling + c_int_alt.hgvs_transcript_variant.rel_ac = 'NW_004070871.1' + c_int_alt.refseqgene_context_intronic_sequence = vv.hp.parse( + 'NM_012309.4:c.913-5058G>A') + c_int_alt.refseqgene_context_intronic_sequence.rel_ac = 'NG_042866.1' + c_int_alt.hgvs_refseqgene_variant = vv.hp.parse( + 'NG_042866.1:g.149464G>A') + c_int_alt.hgvs_predicted_protein_consequence = { + 'prot': vv.hp.parse('NP_036441.2:p.?')} + hgvs_hg37_alt = vv.hp.parse('NW_004070871.1:g.574546C>T') + c_int_alt.alt_genomic_loci = [{ + 'grch37': { + 'hgvs_genomic_description': hgvs_hg37_alt, + }}, { + 'hg19': { + 'hgvs_genomic_description': hgvs_hg37_alt, + }}] + hgvs_hg37 = vv.hp.parse('NC_000011.10:g.71080333C>T') + c_int_alt.primary_assembly_loci = { + 'hg38': {'hgvs_genomic_description': hgvs_hg37}, + 'grch38': {'hgvs_genomic_description': hgvs_hg37}} + c_int_alt.hgvs_genomic = hgvs_hg37_alt + vrs_out = self.vrs.variant_validator_output_set_to_vrs(c_int_alt) + warnings_and_messages = { + 'validation_warnings': [], + 'lovd_messages': [], + 'lovd_corrections': [], + 'vrs_output_warnings': [ + 'VRSIntronWarning: VRS does not handle mappings shared ' + 'between genomic and transcript reference sequences. As ' + 'such only the genomic mappings for this hgvs intronic ' + 'transcript variant are preserved.']} + VRS_mappings_for_primary_assemblies = { + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.2NkFm8HK88MqeNkCgj78KidCAXgnsfV1', + 'label': 'NC_000011.10'}, + 'start': 71080332, + 'end': 71080333, + 'id': 'ga4gh:SL.s01bDxQQkHA1YafVKjPKqmPdy_BCgwWp'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'T'}, + 'id': 'ga4gh:VA.Exskx3X6aUlO_msp7n5jyBYUDkiHG2Xw'}} + VRS_mappings_for_alt_genomic_loci = [{ + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.EZG3Q7xvM2XQ-HQTX5w1BEEmWo-AGqtB', + 'label': 'NW_004070871.1'}, + 'start': 574545, + 'end': 574546, + 'id': 'ga4gh:SL.c9V2DUbRs4P15CWnthnRRgptVohYcy__'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'T'}, + 'id': 'ga4gh:VA.BzVOTYucxGEVNsslkihaJ_kgH9GN5Tro'}] + vrs_intronic_genomic_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.EZG3Q7xvM2XQ-HQTX5w1BEEmWo-AGqtB', + 'label': 'NW_004070871.1'}, + 'start': 574545, + 'end': 574546, + 'id': 'ga4gh:SL.c9V2DUbRs4P15CWnthnRRgptVohYcy__'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'T'}, + 'id': 'ga4gh:VA.BzVOTYucxGEVNsslkihaJ_kgH9GN5Tro'} + assert vrs_out['warnings_and_messages'] == warnings_and_messages + assert vrs_out['VRS_mappings_for_primary_assemblies'] == \ + VRS_mappings_for_primary_assemblies + assert vrs_out['VRS_mappings_for_alt_genomic_loci'] == \ + VRS_mappings_for_alt_genomic_loci + assert vrs_out['vrs_intronic_genomic_variant'] == \ + vrs_intronic_genomic_variant + + def test_vv_output_to_vrs_output_intergenic_type_data(self): + # test 5 g type input (intergenic/psudo-intergenic variant input) + # taken from input test 1 + intergenic_vv_res = copy.copy(self.dummy_vv_output) + # submitted_variant NC_000017.10:g.48279242G>T + intergenic_vv_res.hgvs_refseqgene_variant = vv.hp.parse( + 'NG_007400.1:g.4759C>A') + hgvs_hg37 = vv.hp.parse('NC_000017.10:g.48279242G>T') + hgvs_hg38 = vv.hp.parse('NC_000017.11:g.50201881G>T') + intergenic_vv_res.hgvs_genomic = hgvs_hg38 + intergenic_vv_res.primary_assembly_loci = { + 'hg19': {'hgvs_genomic_description': hgvs_hg37}, + 'hg38': {'hgvs_genomic_description': hgvs_hg38}, + 'grch37': {'hgvs_genomic_description': hgvs_hg37}, + 'grch38': {'hgvs_genomic_description': hgvs_hg38}} + vrs_out = self.vrs.variant_validator_output_set_to_vrs(intergenic_vv_res) + warnings_and_messages = { + 'validation_warnings': [], + 'lovd_messages': [], + 'lovd_corrections': []} + vrs_refseqgene_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.QHD1sq0MO0ekfVC3dyTMD76jTTuJdHzC', + 'label': 'NG_007400.1'}, + 'start': 4758, + 'end': 4759, + 'id': 'ga4gh:SL.Zo3m-4l8X4ec1R9WJoFNnRZJpsf1vxD-'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'A'}, + 'id': 'ga4gh:VA.T6RVcuFrzTqxCS16D3stnBB8uaf86LoT'} + VRS_mapping_for_grch37 = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.AjWXsI7AkTK35XW9pgd3UbjpC3MAevlz', + 'label': 'NC_000017.10'}, + 'start': 48279241, + 'end': 48279242, + 'id': 'ga4gh:SL.BTi2Q9MAcw-eIZjsascBF4kPH7JFHiwK'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'T'}, + 'id': 'ga4gh:VA.Mu21lzIQgKp1Bzq0-E-DiVNNVS1GeF9l'} + VRS_mapping_for_grch38 = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.dLZ15tNO1Ur0IcGjwc3Sdi_0A6Yf4zm7', + 'label': 'NC_000017.11'}, + 'start': 50201880, + 'end': 50201881, + 'id': 'ga4gh:SL.4sKmwc3lN8mn6DxNKFmV_piFt6zW4nkD'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'T'}, + 'id': 'ga4gh:VA.VMA149-dG25GHnAHd76yLApQ_5lMS2-4'} + VRS_mappings_for_primary_assemblies = { + 'grch37': VRS_mapping_for_grch37, + 'grch38': VRS_mapping_for_grch38} + assert vrs_out['vrs_intergenic_genomic_variant'] == \ + VRS_mapping_for_grch38 + assert vrs_out['VRS_mappings_for_alt_genomic_loci'] == [] + assert vrs_out['warnings_and_messages'] == warnings_and_messages + assert vrs_out['VRS_mappings_for_primary_assemblies'] == \ + VRS_mappings_for_primary_assemblies + assert vrs_out['vrs_refseqgene_variant'] == vrs_refseqgene_variant + + def test_vv_output_to_vrs_output_intronic(self): + # test variant.hgvs_refseqgene_variant used for output with transcript data + ex_vv_res = copy.copy(self.dummy_vv_output) + ex_vv_res.hgvs_transcript_variant = vv.hp.parse( + 'NM_000548.3:c.138+821del') + ex_vv_res.hgvs_refseqgene_variant = vv.hp.parse( + 'NG_005895.1:g.5269del') + hgvs_hg37 = vv.hp.parse('NC_000016.9:g.2099575del') + ex_vv_res.selected_assembly = 'GRCh37' + ex_vv_res.primary_assembly_loci = { + 'hg19': {'hgvs_genomic_description': hgvs_hg37}, + 'grch37': {'hgvs_genomic_description': hgvs_hg37}} + vrs_out = self.vrs.variant_validator_output_set_to_vrs(ex_vv_res) + print(vrs_out) + assert vrs_out['VRS_mappings_for_primary_assemblies'] == { 'grch37': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.W6wLoIFOn4G7cjopxPxYNk2lcEqhLQFb', + 'label': 'NC_000016.9'}, + 'start': 2099572, + 'end': 2099575, + 'id': 'ga4gh:SL.OSRCAmPHLKeVCIccBJw2PE2fjBBZVO5g'}, + 'state': { + 'type': 'ReferenceLengthExpression', + 'length': 2, + 'repeatSubunitLength': 1}, + 'id': 'ga4gh:VA.R4sVDvRvhoB_h0TTNcxbRkEkqL8Iwzww'}} + + assert vrs_out['vrs_refseqgene_variant'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.Zuvuz5j8xEuvGT-iyjeCPW96y9eJ-2Rt', + 'label': 'NG_005895.1'}, + 'start': 5266, + 'end': 5269, + 'id': 'ga4gh:SL.7lSDh7A9L3fdqd91RzMX5JcLXzKoRL4m'}, + 'state': { + 'type': 'ReferenceLengthExpression', + 'length': 2, 'repeatSubunitLength': 1}, + 'id': 'ga4gh:VA.3-TnA4NFzRb1rPNumnbdwe6vzFK93Dy3'} + +class TestVRSOutputRangeTypeFromVV(unittest.TestCase): + """ + Test VRS range response to VV uncertain location input types. + """ + @classmethod + def setup_class(self): + self.vrs = HGVS_to_VRS() + # core set of dummy input + self.dummy_vv_output = MockVVData( + selected_assembly='GRCh38', + original = 'test_input', + warnings=[], + lovd_messages = [], + lovd_corrections = [], + stable_gene_ids = { + 'hgnc_id': 'HGNC:TST', + 'entrez_gene_id': '0000', + 'ucsc_id': 'uc0tst.1', + 'omim_id': ['0000000']}, + gene_symbol = 'Dummy_S', + description = "Text transcript description",) + def _two_span_var(self, ac, var_type, s1, e1, s2, e2, edit): + if var_type in ['g','m','o']: + edit = vv.hp.parse_dna_edit(edit) + else: + edit = vv.hp.parse_rna_edit(edit) + o_start_pos, start_end = _hgvs_offset_pos_from_str_in( + s1, + None, + ref_type=var_type, + end=e1) + start = Interval(start=o_start_pos,end=start_end) + end_start, o_end_pos = _hgvs_offset_pos_from_str_in( + s2, + None, + ref_type=var_type, + end=e2) + end = Interval(start=end_start,end=o_end_pos) + full_obj = hgvs_obj_from_existing_edit( + ac, + var_type, + FEInterval( + start=start, + end=end), + edit) + return full_obj + + # possible data for additional tests? + #SequenceLocation (redundant data == to below skipped) + # in: + # end: [44908822, 44908922] + # start: [44908721, 44908821] + # out: + # ga4gh_digest: 8-sGv9AY7GJT6QVgqbxhMXFNamnWcFJu + # ga4gh_identify: ga4gh:SL.8-sGv9AY7GJT6QVgqbxhMXFNamnWcFJu + # ga4gh_serialize: '{"end":[44908822,44908922],"sequenceReference":{"refgetAccession":"SQ.F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul","type":"SequenceReference"},"start":[44908721,44908821],"type":"SequenceLocation"}' + # - name: "SequenceLocation w/Definite and Indefinite Ranges" + # in: + # end: [44908822, null] + # start: [44908721, 44908821] + # out: + # ga4gh_digest: XQAXpesghmuDHziAcDCAmESBOPKTBhwD + # ga4gh_identify: ga4gh:SL.XQAXpesghmuDHziAcDCAmESBOPKTBhwD + # ga4gh_1_3_identify: ga4gh:VSL.zGh9Zy42Zu9R0sbyB1rXsxd33BIyiORk + # ga4gh_serialize: '{"end":[44908822,null],"sequenceReference":{"refgetAccession":"SQ.F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul","type":"SequenceReference"},"start":[44908721,44908821],"type":"SequenceLocation"}' + # ga4gh_1_3_serialize: '{"interval":{"end":{"comparator":">=","type":"IndefiniteRange","value":44908822},"start":{"max":44908821,"min":44908721,"type":"DefiniteRange"},"type":"SequenceInterval"},"sequence_id":"F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul","type":"SequenceLocation"}' + # - name: "SequenceLocation w/more Definite and Indefinite Ranges" + # in: + # end: [null, 44908822] + # start: [44908721, 44908821] + # out: + # ga4gh_digest: OYplG0vkUojmK2hDejylSykx-np3HPFP + # ga4gh_identify: ga4gh:SL.OYplG0vkUojmK2hDejylSykx-np3HPFP + # ga4gh_serialize: '{"end":[null,44908822],"sequenceReference":{"refgetAccession":"SQ.F-LrLMe1SRpfUZHkQmvkVKFEGaoDeHul","type":"SequenceReference"},"start":[44908721,44908821],"type":"SequenceLocation"}' + + def test_vrs_range_unc_del(self): + # test unknown range input for del ins and dup + # 'NM_001377405.1:c.(4_246)delN[15]' + # "NC_000003.12:g.(63912602_63912844)delNNNNNNNNNNNNNNN" + tx_unk_var = vv.hp.parse('NM_001377405.1:c.(4_246)delNNNNNNNNNNNNNNN') + gen_unk_var = vv.hp.parse( + "NC_000003.12:g.(63912602_63912844)delNNNNNNNNNNNNNNN") + unc_test = copy.copy(self.dummy_vv_output) + unc_test.hgvs_transcript_variant = tx_unk_var + unc_test.primary_assembly_loci = { + 'hg38': { + 'hgvs_genomic_description': gen_unk_var, + }, + 'grch38': { + 'hgvs_genomic_description': gen_unk_var, + } + } + vrs_out = self.vrs.variant_validator_output_set_to_vrs(unc_test) + warnings_and_messages = { + 'validation_warnings': [], + 'lovd_messages': [], + 'lovd_corrections': []} + assert vrs_out['warnings_and_messages'] == warnings_and_messages + vrs_transcript_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.xVHtmFKoGeLwmbDXLl_nxw0q4c2FRMYg', + 'label': 'NM_001377405.1'}, + 'start': [405, 648], + 'end': [405, 648], + 'id': 'ga4gh:SL.wV0h-QOnWPUSj3T9ImdcphfFnqveZIMq'}, + 'state': { + 'type': 'LengthExpression', + 'length': [0,228]},# this would better be something like -15 + # but does not translate well into VRS + 'id': 'ga4gh:VA.EpZ3bKdZlmN7K4mmRaLPtXavbriZJCT5'} + assert vrs_out['vrs_transcript_variant'] == vrs_transcript_variant + VRS_mappings_for_primary_assemblies = { + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.Zu7h9AggXxhTaGVsy7h_EZSChSZGcmgX', + 'label': 'NC_000003.12'}, + 'start': [63912601,63912844], + 'end': [63912601,63912844], + 'id': 'ga4gh:SL.u2JpyMzpWPiqASuoO6yvXH_615hlTpRQ'}, + 'state': { + 'type': 'LengthExpression', + 'length': [0,228]},# as before this is the post-change state + # 15 bases del from unc pos maps badly + 'id': 'ga4gh:VA.Nt3i0Fc2JYErd93LznLg1G-l3hYTSN80'}} + assert vrs_out['VRS_mappings_for_primary_assemblies'] == \ + VRS_mappings_for_primary_assemblies + VRS_mappings_for_alt_genomic_loci = [] + assert vrs_out['VRS_mappings_for_alt_genomic_loci'] ==\ + VRS_mappings_for_alt_genomic_loci + + def test_vrs_range_unc_ins(self): + # now test ins delins dup and sub + # 'NM_001377405.1:c.(4_246)insTTTT' + # "NC_000003.12:g.(63912602_63912844)insTTT" + tx_unk_var = vv.hp.parse('NM_001377405.1:c.(4_246)insTTTT') + gen_unk_var = vv.hp.parse("NC_000003.12:g.(63912602_63912844)insTTTT") + unc_test = copy.copy(self.dummy_vv_output) + unc_test.hgvs_transcript_variant = tx_unk_var + unc_test.primary_assembly_loci = { + 'hg38': { + 'hgvs_genomic_description': gen_unk_var, + }, + 'grch38': { + 'hgvs_genomic_description': gen_unk_var, + } + } + vrs_out = self.vrs.variant_validator_output_set_to_vrs(unc_test) + vrs_transcript_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.xVHtmFKoGeLwmbDXLl_nxw0q4c2FRMYg', + 'label': 'NM_001377405.1'}, + 'start': [405, 648], + 'end': [405, 648], + 'id': 'ga4gh:SL.wV0h-QOnWPUSj3T9ImdcphfFnqveZIMq'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'TTTT'}, + 'id': 'ga4gh:VA.HLa3ZXHcJk2LPUFAX8d7qYE-NJcIVl0D'} + VRS_mappings_for_primary_assemblies = { + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.Zu7h9AggXxhTaGVsy7h_EZSChSZGcmgX', + 'label': 'NC_000003.12'}, + 'start': [63912601, 63912844], + 'end': [63912601, 63912844], + 'id': 'ga4gh:SL.u2JpyMzpWPiqASuoO6yvXH_615hlTpRQ'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'TTTT'}, + 'id': 'ga4gh:VA.KsKPm1k9vo1xoWj2AsKbzWrXzcKZ_rdG'}} + assert vrs_out['vrs_transcript_variant'] == vrs_transcript_variant + assert vrs_out['VRS_mappings_for_primary_assemblies'] ==\ + VRS_mappings_for_primary_assemblies + assert vrs_out['VRS_mappings_for_alt_genomic_loci'] == [] + + def test_vrs_range_unc_delins(self): + # 'NM_001377405.1:c.(4_246)delinsTTTT' + # "NC_000003.12:g.(63912602_63912844)delinsTTT" + tx_unk_var = vv.hp.parse('NM_001377405.1:c.(4_246)delinsTTTT') + gen_unk_var = vv.hp.parse( + "NC_000003.12:g.(63912602_63912844)delinsTTTT") + unc_test = copy.copy(self.dummy_vv_output) + unc_test.hgvs_transcript_variant = tx_unk_var + unc_test.primary_assembly_loci = { + 'hg38': { + 'hgvs_genomic_description': gen_unk_var, + }, + 'grch38': { + 'hgvs_genomic_description': gen_unk_var, + } + } + vrs_out = self.vrs.variant_validator_output_set_to_vrs(unc_test) + vrs_transcript_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.xVHtmFKoGeLwmbDXLl_nxw0q4c2FRMYg', + 'label': 'NM_001377405.1'}, + 'start': [405, 648], + 'end': [405, 648], + 'id': 'ga4gh:SL.wV0h-QOnWPUSj3T9ImdcphfFnqveZIMq'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'TTTT'}, + 'id': 'ga4gh:VA.HLa3ZXHcJk2LPUFAX8d7qYE-NJcIVl0D'} + VRS_mappings_for_primary_assemblies = { + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.Zu7h9AggXxhTaGVsy7h_EZSChSZGcmgX', + 'label': 'NC_000003.12'}, + 'start': [63912601, 63912844], + 'end': [63912601, 63912844], + 'id': 'ga4gh:SL.u2JpyMzpWPiqASuoO6yvXH_615hlTpRQ'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'TTTT'}, + 'id': 'ga4gh:VA.KsKPm1k9vo1xoWj2AsKbzWrXzcKZ_rdG'}} + assert vrs_out['vrs_transcript_variant'] == vrs_transcript_variant + assert vrs_out['VRS_mappings_for_primary_assemblies'] == \ + VRS_mappings_for_primary_assemblies + assert vrs_out['VRS_mappings_for_alt_genomic_loci'] == [] + + def test_vrs_range_unc_dup(self): + # 'NM_001377405.1:c.(4_246)dup' + # "NC_000003.12:g.(63912602_63912844)dup" + tx_unk_var = vv.hp.parse('NM_001377405.1:c.(4_246)dup') + gen_unk_var = vv.hp.parse("NC_000003.12:g.(63912602_63912844)dup") + unc_test = copy.copy(self.dummy_vv_output) + unc_test.hgvs_transcript_variant = tx_unk_var + unc_test.primary_assembly_loci = { + 'hg38': { + 'hgvs_genomic_description': gen_unk_var, + }, + 'grch38': { + 'hgvs_genomic_description': gen_unk_var, + } + } + vrs_out = self.vrs.variant_validator_output_set_to_vrs(unc_test) + vrs_transcript_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.xVHtmFKoGeLwmbDXLl_nxw0q4c2FRMYg', + 'label': 'NM_001377405.1'}, + 'start': [405, 648], + 'end': [405, 648], + 'id': 'ga4gh:SL.wV0h-QOnWPUSj3T9ImdcphfFnqveZIMq'}, + 'state': { + 'type': 'LengthExpression', + 'length': [0, 486]}, + 'id': 'ga4gh:VA.oqDJt3ITorI0hrvsS7LmBUx-mBtQZRS7'} + VRS_mappings_for_primary_assemblies = { + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.Zu7h9AggXxhTaGVsy7h_EZSChSZGcmgX', + 'label': 'NC_000003.12'}, + 'start': [63912601, 63912844], + 'end': [63912601, 63912844], + 'id': 'ga4gh:SL.u2JpyMzpWPiqASuoO6yvXH_615hlTpRQ'}, + 'state': { + 'type': 'LengthExpression', + 'length': [0, 486]}, + 'id': 'ga4gh:VA.gtya_8q_3eubU0Hijsw_bR1DNU3KoPo4'}} + assert vrs_out['vrs_transcript_variant'] == vrs_transcript_variant + assert vrs_out['VRS_mappings_for_primary_assemblies'] == \ + VRS_mappings_for_primary_assemblies + assert vrs_out['VRS_mappings_for_alt_genomic_loci'] == [] + + def test_vrs_range_unc_basechanage(self): + # 'NM_001377405.1:c.(4_246)C>G' # probably nonsense but we don't + # (can't) normalise so it works to test the code + # "NC_000003.12:g.(63912602_63912844)C>G" + tx_unk_var = vv.hp.parse('NM_001377405.1:c.(4_246)C>G') + gen_unk_var = vv.hp.parse("NC_000003.12:g.(63912602_63912844)C>G") + unc_test = copy.copy(self.dummy_vv_output) + unc_test.hgvs_transcript_variant = tx_unk_var + unc_test.primary_assembly_loci = { + 'hg38': { + 'hgvs_genomic_description': gen_unk_var, + }, + 'grch38': { + 'hgvs_genomic_description': gen_unk_var, + } + } + vrs_out = self.vrs.variant_validator_output_set_to_vrs(unc_test) + vrs_transcript_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.xVHtmFKoGeLwmbDXLl_nxw0q4c2FRMYg', + 'label': 'NM_001377405.1'}, + 'start': [405, 648], + 'end': [405, 648], + 'id': 'ga4gh:SL.wV0h-QOnWPUSj3T9ImdcphfFnqveZIMq'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'G'}, + 'id': 'ga4gh:VA.6r6uUT5Xopvs9mzMgnoLGnxn0UozdQ69'} + VRS_mappings_for_primary_assemblies = { + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.Zu7h9AggXxhTaGVsy7h_EZSChSZGcmgX', + 'label': 'NC_000003.12'}, + 'start': [63912601, 63912844], + 'end': [63912601, 63912844], + 'id': 'ga4gh:SL.u2JpyMzpWPiqASuoO6yvXH_615hlTpRQ'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'G'}, + 'id': 'ga4gh:VA.p4PEk1oAj25G_7C81VjYTNUmzxvgA1pu'}} + assert vrs_out['vrs_transcript_variant'] == vrs_transcript_variant + assert vrs_out['VRS_mappings_for_primary_assemblies'] == \ + VRS_mappings_for_primary_assemblies + assert vrs_out['VRS_mappings_for_alt_genomic_loci'] == [] + + # test range input with spans for each end + # taken from test_uncertain_3 + # 'NM_006138.4:c.(1_20)_(30_36)del' + # "grch38" 'NC_000011.10:g.(60061161_60061180)_(60061190_60061196)del' + + def test_vrs_range_unc_span_intronic(self): + # test that intronic detection works on span of span types + tx_unk_var_intr = self._two_span_var( + 'NM_006138.4','c','1','20','30','36','del') + tx_unk_var_intr.posedit.pos.start.start.offset = True + assert self.vrs._intronic(tx_unk_var_intr) + + def test_vrs_range_unc_span_del(self): + #'NM_006138.4:c.(1_20)_(30_36)del' + tx_unk_var = self._two_span_var( + 'NM_006138.4','c','1','20','30','36','del') + gen_unk_var = self._two_span_var( + 'NC_000011.10','g', + '60061161','60061180', + '60061190','60061196', + 'del') + unc_multi_end_test = copy.copy(self.dummy_vv_output) + unc_multi_end_test.hgvs_transcript_variant = tx_unk_var + unc_multi_end_test.primary_assembly_loci = { + 'hg38': { + 'hgvs_genomic_description': gen_unk_var, + }, + 'grch38': { + 'hgvs_genomic_description': gen_unk_var, + } + } + vrs_out = self.vrs.variant_validator_output_set_to_vrs(unc_multi_end_test) + vrs_transcript_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.kDpc3QS9BP_MPB7-8c32tLm_ofkpKuki', + 'label': 'NM_006138.4'}, + 'start': [128, 148], + 'end': [157, 164], + 'id': 'ga4gh:SL.VKym0ZocRoqcMIFALbtocM9G-JXI_NPq'}, + 'state': { + 'type': 'LengthExpression', + 'length': [9, 36]}, + 'id': 'ga4gh:VA._QbcvpURg3S57MKEBF5kNXJagBt8LD3Q'} + VRS_mappings_for_primary_assemblies = { + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.2NkFm8HK88MqeNkCgj78KidCAXgnsfV1', + 'label': 'NC_000011.10'}, + 'start': [60061160, 60061180], + 'end': [60061189, 60061196], + 'id': 'ga4gh:SL.3SYax-eEvXhMFCS3jWUrqpOQmBwbt5Xd'}, + 'state': { + 'type': 'LengthExpression', + 'length': [9, 36]}, + 'id': 'ga4gh:VA.BimsbJi2CRmY5zw8dTShvosXgdW4KQGf'}} + assert vrs_out['vrs_transcript_variant'] == vrs_transcript_variant + assert vrs_out['VRS_mappings_for_primary_assemblies'] == VRS_mappings_for_primary_assemblies + assert vrs_out['VRS_mappings_for_alt_genomic_loci'] == [] + + def test_vrs_range_unc_span_delins(self): + #'NM_006138.4:c.(1_30)_(20_36)delinsCCC' + tx_unk_var = self._two_span_var( + 'NM_006138.4','c','1','20','30','36','delinsCCC') + gen_unk_var = self._two_span_var( + 'NC_000011.10','g', + '60061161','60061180', + '60061190','60061196', + 'delinsCCC') + unc_multi_end_test = copy.copy(self.dummy_vv_output) + unc_multi_end_test.hgvs_transcript_variant = tx_unk_var + unc_multi_end_test.primary_assembly_loci = { + 'hg38': { + 'hgvs_genomic_description': gen_unk_var, + }, + 'grch38': { + 'hgvs_genomic_description': gen_unk_var, + } + } + vrs_out = self.vrs.variant_validator_output_set_to_vrs( + unc_multi_end_test) + vrs_transcript_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.kDpc3QS9BP_MPB7-8c32tLm_ofkpKuki', + 'label': 'NM_006138.4'}, + 'start': [128, 148], + 'end': [157, 164], + 'id': 'ga4gh:SL.VKym0ZocRoqcMIFALbtocM9G-JXI_NPq'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'CCC'}, + 'id': 'ga4gh:VA.hJy6_vMrmQUVQQzu3ZBmPboypcESYKmk'} + VRS_mappings_for_primary_assemblies = { + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.2NkFm8HK88MqeNkCgj78KidCAXgnsfV1', + 'label': 'NC_000011.10'}, + 'start': [60061160, 60061180], + 'end': [60061189, 60061196], + 'id': 'ga4gh:SL.3SYax-eEvXhMFCS3jWUrqpOQmBwbt5Xd'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'CCC'}, + 'id': 'ga4gh:VA.W4I8QnB7DOZzL5LFvdVLwWeEI0BdEWvX'}} + assert vrs_out['vrs_transcript_variant'] == vrs_transcript_variant + assert vrs_out['VRS_mappings_for_primary_assemblies'] == \ + VRS_mappings_for_primary_assemblies + assert vrs_out['VRS_mappings_for_alt_genomic_loci'] == [] + + def test_vrs_range_unc_span_ins(self): + #ins + #'NM_006138.4:c.(1_30)_(20_36)insCCC' + tx_unk_var = self._two_span_var( + 'NM_006138.4','c','1','30','20','36','insCCC') + gen_unk_var = self._two_span_var( + 'NC_000011.10','g', + '60061161','60061190', + '60061180','60061196', + 'insCCC') + unc_multi_end_test = copy.copy(self.dummy_vv_output) + unc_multi_end_test.hgvs_transcript_variant = tx_unk_var + unc_multi_end_test.primary_assembly_loci = { + 'hg38': { + 'hgvs_genomic_description': gen_unk_var, + }, + 'grch38': { + 'hgvs_genomic_description': gen_unk_var, + } + } + vrs_out = self.vrs.variant_validator_output_set_to_vrs( + unc_multi_end_test) + vrs_transcript_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.kDpc3QS9BP_MPB7-8c32tLm_ofkpKuki', + 'label': 'NM_006138.4'}, + 'start': [128, 158], + 'end': [147, 164], + 'id': 'ga4gh:SL.aXpOvtXB05WvJoNnaJEw8XCUJFKJfSRT'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'CCC'}, + 'id': 'ga4gh:VA.mTaxUtyrZx27pR7KgWFH392KzesV6PNm'} + VRS_mappings_for_primary_assemblies = { + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.2NkFm8HK88MqeNkCgj78KidCAXgnsfV1', + 'label': 'NC_000011.10'}, + 'start': [60061160, 60061190], + 'end': [60061179, 60061196], + 'id': 'ga4gh:SL.HJElyJSuBdeXx6zYpvI6kl2gRrmYYYAw'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'CCC'}, + 'id': 'ga4gh:VA.zqcJ7xJM5ucY5wAEEBf-VWcqnyRinBL8'}} + + assert vrs_out['vrs_transcript_variant'] == vrs_transcript_variant + assert vrs_out['VRS_mappings_for_primary_assemblies'] == \ + VRS_mappings_for_primary_assemblies + + + def test_vrs_range_unc_span_dup(self): + #'NM_006138.4:c.(1_30)_(20_36)dup' + tx_unk_var = self._two_span_var( + 'NM_006138.4','c','1','20','30','36','dup') + gen_unk_var = self._two_span_var( + 'NC_000011.10','g', + '60061161','60061180', + '60061190','60061196', + 'dup') + unc_multi_end_test = copy.copy(self.dummy_vv_output) + unc_multi_end_test.hgvs_transcript_variant = tx_unk_var + unc_multi_end_test.primary_assembly_loci = { + 'hg38': { + 'hgvs_genomic_description': gen_unk_var, + }, + 'grch38': { + 'hgvs_genomic_description': gen_unk_var, + } + } + vrs_out = self.vrs.variant_validator_output_set_to_vrs( + unc_multi_end_test) + vrs_transcript_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.kDpc3QS9BP_MPB7-8c32tLm_ofkpKuki', + 'label': 'NM_006138.4'}, + 'start': [128, 148], + 'end': [157, 164], + 'id': 'ga4gh:SL.VKym0ZocRoqcMIFALbtocM9G-JXI_NPq'}, + 'state': { + 'type': 'LengthExpression', + 'length': [18, 72]}, + 'id': 'ga4gh:VA.QldDqxFkU7wNQbB9QJvfiR3apKiWd25L'} + VRS_mappings_for_primary_assemblies = { + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.2NkFm8HK88MqeNkCgj78KidCAXgnsfV1', + 'label': 'NC_000011.10'}, + 'start': [60061160, 60061180], + 'end': [60061189, 60061196], + 'id': 'ga4gh:SL.3SYax-eEvXhMFCS3jWUrqpOQmBwbt5Xd'}, + 'state': { + 'type': 'LengthExpression', + 'length': [18, 72]}, + 'id': 'ga4gh:VA.6_mydc1dT4GgT4lH7hsj1kzL7CuyHi41'}} + assert vrs_out['vrs_transcript_variant'] == vrs_transcript_variant + assert vrs_out['VRS_mappings_for_primary_assemblies'] == \ + VRS_mappings_for_primary_assemblies + assert vrs_out['VRS_mappings_for_alt_genomic_loci'] == [] + + def test_vrs_range_unc_span_r_type_rna_var(self): + # test same for r. type variants + # prot is dummy but same type as expected + # note that r type are actually C or n type as far as biocommons.hgvs + # is concerned, r is used as similar to ":n.", but without caring about + # coding status + rna_test = copy.copy(self.dummy_vv_output) + tx_unk_var = self._two_span_var( + 'NM_006138.4','c','1','20','30','36','dup') + rna_test.rna_data = { + 'rna_variant':tx_unk_var, + 'translation_slr':vv.hp.parse('NP_055935.4:p.?'), + 'usage_warnings':"RNA warning"} + vrs_out = self.vrs.variant_validator_output_set_to_vrs(rna_test) + vrs_transcript_variant = { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.kDpc3QS9BP_MPB7-8c32tLm_ofkpKuki', + 'label': 'NM_006138.4'}, + 'start': [128, 148], + 'end': [157, 164], + 'id': 'ga4gh:SL.VKym0ZocRoqcMIFALbtocM9G-JXI_NPq'}, + 'state': { + 'type': 'LengthExpression', + 'length': [18, 72]}, + 'id': 'ga4gh:VA.QldDqxFkU7wNQbB9QJvfiR3apKiWd25L'} + print(vrs_out) + assert vrs_out['vrs_transcript_variant'] == vrs_transcript_variant + assert 'VRS_mappings_for_primary_assemblies' not in vrs_out + assert 'VRS_mappings_for_alt_genomic_loci' not in vrs_out + +class TestVRSObjectInit(unittest.TestCase): + """ + Test all remaining untested object init features, these are not normally + exposed by VV, so may be more subject to change than might otherwise be + expected. + """ + + def test_vrs_object_seqrepo(self): + # test setting SR loc + config = ConfigParser() + config.read(CONFIG_DIR) + seq_repo_path = os.path.join(config["seqrepo"]["location"], + config["seqrepo"]["version"]) + vrs = HGVS_to_VRS(seq_repo=SeqRepo(seq_repo_path)) + #do ID fetch based on set seqrepo as above + vrs_seq_id = vrs.id_fetch('NR_110010.2') + assert vrs_seq_id == 'SQ.Ksaa229gzGC4Uc3ytCixai4vziud-MKi' + + # there is an additional (for now untested fallback for when + # VariantValidator.settings import CONFIG_DIR + # fails, but testing this here is hard, and since VV can't work in this case + # is redundant + + def test_vrs_object_init_id_fetch(self): + # test setting ID fetch method + def _test_id_fetch(input_id): + if input_id == 'test_true': + return 'True' + return False + vrs = HGVS_to_VRS(id_fetch=_test_id_fetch) + vrs_seq_id = vrs.id_fetch('NR_110010.2') + assert vrs_seq_id is False + vrs_seq_id = vrs.id_fetch('test_true') + assert vrs_seq_id == 'True' + + def test_vrs_object_init_preset_cache(self): + # dummy set a pre-filled cache with normally used and filled with + # previous hgvs sourced VRS output, due to how common repeats VV output + # can be(multi transcript but same genomic), but with weird contents + # for testing. Test via hgvs_single_var_to_vrs(). + vrs = HGVS_to_VRS(cache = {'not_hgvs':{'not_vrs'}}) + vrs_out = vrs.hgvs_single_var_to_vrs('not_hgvs') + assert vrs_out == {'not_vrs'} + assert vrs.cache == {'not_hgvs':{'not_vrs'}} + + def test_vrs_object_init_normal_cache(self): + # test normal cache behaviour + vrs = HGVS_to_VRS(cache=True) + assert vrs.cache == {} + +class TestVRSObjectVV(unittest.TestCase): + """ + Tests for VV usage of the code working as intended, should mostly be a + simple shim on vv_output_to_vrs_output internally so not a complex test set + at the moment. + + Testing transcript, intronic transcript, genomic, and error type results + """ + @classmethod + def setup_class(self): + self.vv = Validator() + self.vv.testing = True + + def test_format_as_vrs_tx(self): + variant = 'NM_015120.4:c.35T>C' + results = self.vv.validate(variant, 'GRCh37', 'all') + resultsf = results.format_as_vrs() + print(results.format_as_dict()) + print(resultsf) + result = resultsf['ga4gh:VA.x8gKEBS-s638D8uuuQXVJKIgCDcuQJCW'] + assert result['selected_assembly'] == 'GRCh37' + assert result['submitted_variant'] == 'NM_015120.4:c.35T>C' + assert result['warnings_and_messages'] == { + 'validation_warnings': [], + 'lovd_messages': None, + 'lovd_corrections': None} + assert result['transcript_description'] == \ + 'Homo sapiens ALMS1 centrosome and basal body associated '\ + 'protein (ALMS1), transcript variant 1, mRNA' + assert result['gene_ids'] == { + 'hgnc_id': 'HGNC:428', + 'entrez_gene_id': '7840', + 'ensembl_gene_id': 'ENSG00000116127', + 'ucsc_id': 'uc032nrd.1', + 'omim_id': ['606844'], + 'ccds_ids': ['CCDS42697'], + 'current_symbol': 'ALMS1'} + assert result['vrs_transcript_variant'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.mq1Saz29534GzZ6T_jCfUjsqNlifC7pZ', + 'label': 'NM_015120.4' + }, + 'start': 145, + 'end': 146, + 'id': 'ga4gh:SL.x_rdZXUcg7V8Ae5pPVv0OS-faxKyzNQb' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'C' + }, + 'id': 'ga4gh:VA.x8gKEBS-s638D8uuuQXVJKIgCDcuQJCW' + } + assert result['vrs_refseqgene_variant'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.r47Jw6neAxxIQgeyZLmdPrqFN4S6h7hy', + 'label': 'NG_011690.1'}, + 'start': 5145, + 'end': 5146, + 'id': 'ga4gh:SL.3CEnQ3JJo2jQbraJ0UHIO-lqBbR4xs15'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'C'}, + 'id': 'ga4gh:VA.TjhuWhUG-PJapHphK3r0HWyU5b9CgGG_'} + + assert result['vrs_predicted_protein_consequence'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.ggxH8oENu3WDprE6Z_Qg1HvOiT0d8UNa', + 'label': 'NP_055935.4' + }, + 'start': 11, + 'end': 12, + 'id': 'ga4gh:SL.qOpPs9dwgqK6MuH3CTQUDoRYbi9IWwSh' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'P' + }, + 'id': 'ga4gh:VA.cJPulDXE1G73sZS4dHaZX_27-c4pNjYo' + } + assert result['VRS_mappings_for_primary_assemblies'] == { + 'grch37': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.9KdcA9ZpY1Cpvxvg8bMSLYDUpsX6GDLO', + 'label': 'NC_000002.11' + }, + 'start': 73613030, + 'end': 73613031, + 'id': 'ga4gh:SL.IzA6GY6mvDiJ1EzMbe1Cxv6jyhg4u8t7' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'CGGA' + }, + 'id': 'ga4gh:VA.rKc_HxeylEKYQzYo_e_kY1lqYLF6Ch5m' + }, + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.pnAqCRBrTsUoBghSD1yp_jXWSmlbdh4g', + 'label': 'NC_000002.12' + }, + 'start': 73385902, + 'end': 73385903, + 'id': 'ga4gh:SL.ndPBndyntJTTRHuU6tYpgbN6TUsRv94B' + }, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'CGGA' + }, + 'id': 'ga4gh:VA.eDiJCZTUpLf1UQHaUFFP1UaPsHfbOjyC' + } + } + assert result['VRS_mappings_for_alt_genomic_loci'] == [{ + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.8wk0aF1q6kFjIccOvc8bq3AJVuqWersd', + 'label': 'NW_025791766.1'}, + 'start': 55397, + 'end': 55398, + 'id': 'ga4gh:SL.vknogVomLiGh3q7IfPcZDAU9Kg6nSvyY'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'C'}, + 'id': 'ga4gh:VA.pdjaBvFWh8JgChnGQzOTt9s5zxhNudPS'}] + def test_format_as_vrs_tx_intronic(self): + # from test_variant5 + variant = 'NC_000023.10:g.33229673A>T' + results = self.vv.validate(variant, 'GRCh37', 'all').format_as_vrs() + print(results) + #'NM_000109.3:c.7+127703T>A' + result = results['ga4gh:VA.6LfJDlOTAm1QeCDWfbE0986oJwQ25NSd'] + assert result['selected_assembly'] == 'GRCh37' + assert result['submitted_variant'] == 'NC_000023.10:g.33229673A>T' + assert result['warnings_and_messages'] == { + 'validation_warnings': [], + 'lovd_messages': None, 'lovd_corrections': None, + 'vrs_output_warnings': [ + 'VRSIntronWarning: VRS does not handle mappings shared between ' + 'genomic and transcript reference sequences. As such only the ' + 'genomic mappings for this hgvs intronic transcript variant are' + ' preserved.']} + assert result['gene_ids']['hgnc_id'] == 'HGNC:2928' + assert result['gene_ids']['entrez_gene_id'] == '1756' + assert result['gene_ids']['ensembl_gene_id'] =='ENSG00000198947' + assert result['gene_ids']['ucsc_id'] == 'uc004dda.2' + assert result['transcript_description'] == \ + 'Homo sapiens dystrophin (DMD), transcript variant Dp427c, mRNA' + assert result['vrs_intronic_genomic_variant'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.v7noePfnNpK8ghYXEqZ9NukMXW7YeNsm', + 'label': 'NC_000023.10'}, + 'start': 33229672, + 'end': 33229673, + 'id': 'ga4gh:SL.WqDN0NCy9-Y7PVGwqvI0VtcogTVZCsM4'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'T'}, + 'id': 'ga4gh:VA.6LfJDlOTAm1QeCDWfbE0986oJwQ25NSd'} + assert result['VRS_mappings_for_primary_assemblies'] == { + 'grch37': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.v7noePfnNpK8ghYXEqZ9NukMXW7YeNsm', + 'label': 'NC_000023.10'}, + 'start': 33229672, + 'end': 33229673, + 'id': 'ga4gh:SL.WqDN0NCy9-Y7PVGwqvI0VtcogTVZCsM4'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'T'}, + 'id': 'ga4gh:VA.6LfJDlOTAm1QeCDWfbE0986oJwQ25NSd'}, + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.w0WZEvgJF0zf_P4yyTzjjv9oW1z61HHP', + 'label': 'NC_000023.11'}, + 'start': 33211555, + 'end': 33211556, + 'id': 'ga4gh:SL.wdORKcdcBA4pikHhob-AdKP-T-IlgKbV'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'T'}, + 'id': 'ga4gh:VA.I70ooeAvjS4QEfhx9ZYZI3ews44FjqiH'}} + def test_format_as_vrs_genomic(self): + # from test_variant16 + variant = 'NC_000017.10:g.48279242G>T' + results = self.vv.validate(variant, 'GRCh37', 'all').format_as_vrs() + print(results) + result = results['ga4gh:VA.Mu21lzIQgKp1Bzq0-E-DiVNNVS1GeF9l'] + assert result['selected_assembly'] == 'GRCh37' + assert result['submitted_variant'] == 'NC_000017.10:g.48279242G>T' + assert result['warnings_and_messages'] == { + 'validation_warnings': [ + 'TranscriptIdentificationWarning: No individual transcripts ' + 'have been identified that fully overlap the described ' + 'variation in the genomic sequence. Large variants might span ' + 'one or more genes and are currently only described at the ' + 'genome (g.) level.'], + 'lovd_messages': None, + 'lovd_corrections': None} + assert result['gene_ids'] == { + 'hgnc_id': 'HGNC:2197', + 'entrez_gene_id': '1277', + 'ensembl_gene_id': 'ENSG00000108821', + 'ucsc_id': 'uc002iqm.4', + 'omim_id': ['120150'], + 'ccds_ids': ['CCDS11561'], + 'current_symbol': 'COL1A1'} + assert result['transcript_description'] == '' + assert result['vrs_intergenic_genomic_variant'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.AjWXsI7AkTK35XW9pgd3UbjpC3MAevlz', + 'label': 'NC_000017.10'}, + 'start': 48279241, + 'end': 48279242, + 'id': 'ga4gh:SL.BTi2Q9MAcw-eIZjsascBF4kPH7JFHiwK'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'T'}, + 'id': 'ga4gh:VA.Mu21lzIQgKp1Bzq0-E-DiVNNVS1GeF9l'} + + assert result['vrs_refseqgene_variant'] == { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.QHD1sq0MO0ekfVC3dyTMD76jTTuJdHzC', + 'label': 'NG_007400.1'}, + 'start': 4758, + 'end': 4759, + 'id': 'ga4gh:SL.Zo3m-4l8X4ec1R9WJoFNnRZJpsf1vxD-'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'A'}, + 'id': 'ga4gh:VA.T6RVcuFrzTqxCS16D3stnBB8uaf86LoT'} + assert result['VRS_mappings_for_primary_assemblies'] == { + 'grch37': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.AjWXsI7AkTK35XW9pgd3UbjpC3MAevlz', + 'label': 'NC_000017.10'}, + 'start': 48279241, + 'end': 48279242, + 'id': 'ga4gh:SL.BTi2Q9MAcw-eIZjsascBF4kPH7JFHiwK'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'T'}, + 'id': 'ga4gh:VA.Mu21lzIQgKp1Bzq0-E-DiVNNVS1GeF9l'}, + 'grch38': { + 'type': 'Allele', + 'location': { + 'type': 'SequenceLocation', + 'sequenceReference': { + 'type': 'SequenceReference', + 'refgetAccession': 'SQ.dLZ15tNO1Ur0IcGjwc3Sdi_0A6Yf4zm7', + 'label': 'NC_000017.11'}, + 'start': 50201880, + 'end': 50201881, + 'id': 'ga4gh:SL.4sKmwc3lN8mn6DxNKFmV_piFt6zW4nkD'}, + 'state': { + 'type': 'LiteralSequenceExpression', + 'sequence': 'T'}, + 'id': 'ga4gh:VA.VMA149-dG25GHnAHd76yLApQ_5lMS2-4'}} + + def test_format_as_vrs_err_result(self): + variant = 'NM_007075.3:r.235_236insGCCCACCCACCTGCCAG' + results = self.vv.validate(variant, 'GRCh37', 'all').format_as_vrs() + assert 'error_1' in results + assert results['error_1'] == { + 'selected_assembly': 'GRCh37', + 'submitted_variant': 'NM_007075.3:r.235_236insGCCCACCCACCTGCCAG', + 'warnings_and_messages': { + 'validation_warnings': [ + 'The IUPAC RNA alphabet dictates that RNA variants must ' + 'use the character u in place of t'], + 'lovd_messages': None, + 'lovd_corrections': None}, + 'gene_ids': {}, + 'transcript_description': ''} + + def test_format_as_vrs_multi_err_result(self): + variants = ['NM_007075.3:r.235_236insGCCCACCCACCTGCCAG', + 'NC_000017.10:g.41232400_41236235del383'] + variants = json.dumps(variants) + results = self.vv.validate(variants, 'GRCh37', 'all').format_as_vrs() + assert 'error_1' in results + assert 'error_2' in results + assert results['error_1'] == { + 'selected_assembly': 'GRCh37', + 'submitted_variant': 'NM_007075.3:r.235_236insGCCCACCCACCTGCCAG', + 'warnings_and_messages': { + 'validation_warnings': + ['The IUPAC RNA alphabet dictates that RNA variants must ' + 'use the character u in place of t'], + 'lovd_messages': None, + 'lovd_corrections': None}, + 'gene_ids': {}, + 'transcript_description': ''} + assert results['error_2'] == { + 'selected_assembly': 'GRCh37', + 'submitted_variant': 'NC_000017.10:g.41232400_41236235del383', + 'warnings_and_messages': { + 'validation_warnings': [ + 'Length implied by coordinates must equal sequence deletion length', + 'Trailing digits are not permitted in HGVS variant descriptions', + 'Refer to http://varnomen.hgvs.org/recommendations/DNA/variant/'], + 'lovd_messages': None, + 'lovd_corrections': None}, + 'gene_ids': {}, + 'transcript_description': ''}