From c71bbfde77612dbebd53e1cec6d17c7d9d99649e Mon Sep 17 00:00:00 2001 From: Nicholas Hathaway Date: Mon, 29 Jun 2026 13:28:27 -0700 Subject: [PATCH] update so pseudocigar is properly constructed during the building functions; updated unittests; --- src/pmotools/pmo_builder/mhap_table_to_pmo.py | 99 ++++++++++++++++++- .../test_mhap_table_to_pmo.py | 71 +++++++------ .../test_schema_validation_integration.py | 8 ++ 3 files changed, 140 insertions(+), 38 deletions(-) diff --git a/src/pmotools/pmo_builder/mhap_table_to_pmo.py b/src/pmotools/pmo_builder/mhap_table_to_pmo.py index be0ae34..70e4a57 100644 --- a/src/pmotools/pmo_builder/mhap_table_to_pmo.py +++ b/src/pmotools/pmo_builder/mhap_table_to_pmo.py @@ -26,6 +26,13 @@ def mhap_table_to_pmo( masking_delim: str = ",", microhaplotype_name_col: str | None = None, pseudocigar_col: str | None = None, + pseudocigar_chrom_col: str | None = None, + pseudocigar_start_col: str | None = None, + pseudocigar_end_col: str | None = None, + pseudocigar_ref_seq_col: str | None = None, + pseudocigar_strand_col: str | None = None, + pseudocigar_genome_id: int | None = None, + pseudocigar_generation_description_col: str | None = None, quality_col: str | None = None, additional_representative_mhap_cols: list | None = None, additional_mhap_detected_cols: list | None = None, @@ -73,6 +80,20 @@ def mhap_table_to_pmo( :type microhaplotype_name_col: str, optional :param pseudocigar_col: the name of the column containing a pseudocigar for the microhaplotype :type pseudocigar_col: str, optional + :param pseudocigar_chrom_col: column for the pseudocigar ref_loc chromosome; defaults to chrom_col + :type pseudocigar_chrom_col: str, optional + :param pseudocigar_start_col: column for the pseudocigar ref_loc start (required if pseudocigar_col is set) + :type pseudocigar_start_col: str, optional + :param pseudocigar_end_col: column for the pseudocigar ref_loc end (required if pseudocigar_col is set) + :type pseudocigar_end_col: str, optional + :param pseudocigar_ref_seq_col: optional column for the pseudocigar ref_loc reference sequence + :type pseudocigar_ref_seq_col: str, optional + :param pseudocigar_strand_col: optional column for the pseudocigar ref_loc strand + :type pseudocigar_strand_col: str, optional + :param pseudocigar_genome_id: genome id for the pseudocigar ref_loc; defaults to genome_id + :type pseudocigar_genome_id: int, optional + :param pseudocigar_generation_description_col: optional column describing how the pseudocigar was generated + :type pseudocigar_generation_description_col: str, optional :param quality_col: the name of the column containing the ANSI FASTQ per-base quality score for this sequence :type quality_col: str, optional :param additional_representative_mhap_cols: additional columns to add to the representative microhaplotypes table @@ -100,6 +121,13 @@ def mhap_table_to_pmo( masking_delim=masking_delim, microhaplotype_name_col=microhaplotype_name_col, pseudocigar_col=pseudocigar_col, + pseudocigar_chrom_col=pseudocigar_chrom_col, + pseudocigar_start_col=pseudocigar_start_col, + pseudocigar_end_col=pseudocigar_end_col, + pseudocigar_ref_seq_col=pseudocigar_ref_seq_col, + pseudocigar_strand_col=pseudocigar_strand_col, + pseudocigar_genome_id=pseudocigar_genome_id, + pseudocigar_generation_description_col=pseudocigar_generation_description_col, quality_col=quality_col, additional_representative_mhap_cols=additional_representative_mhap_cols, ) @@ -160,6 +188,13 @@ def create_representative_microhaplotype_dict( masking_delim: str = ",", microhaplotype_name_col: str | None = None, pseudocigar_col: str | None = None, + pseudocigar_chrom_col: str | None = None, + pseudocigar_start_col: str | None = None, + pseudocigar_end_col: str | None = None, + pseudocigar_ref_seq_col: str | None = None, + pseudocigar_strand_col: str | None = None, + pseudocigar_genome_id: int | None = None, + pseudocigar_generation_description_col: str | None = None, quality_col: str | None = None, additional_representative_mhap_cols: list[str] | None = None, ): @@ -198,6 +233,20 @@ def create_representative_microhaplotype_dict( :type microhaplotype_name_col: str, optional :param pseudocigar_col: the name of the column containing a pseudocigar for the microhaplotype :type pseudocigar_col: str, optional + :param pseudocigar_chrom_col: column for the pseudocigar ref_loc chromosome; defaults to chrom_col + :type pseudocigar_chrom_col: str, optional + :param pseudocigar_start_col: column for the pseudocigar ref_loc start (required if pseudocigar_col is set) + :type pseudocigar_start_col: str, optional + :param pseudocigar_end_col: column for the pseudocigar ref_loc end (required if pseudocigar_col is set) + :type pseudocigar_end_col: str, optional + :param pseudocigar_ref_seq_col: optional column for the pseudocigar ref_loc reference sequence + :type pseudocigar_ref_seq_col: str, optional + :param pseudocigar_strand_col: optional column for the pseudocigar ref_loc strand + :type pseudocigar_strand_col: str, optional + :param pseudocigar_genome_id: genome id for the pseudocigar ref_loc; defaults to genome_id + :type pseudocigar_genome_id: int, optional + :param pseudocigar_generation_description_col: optional column describing how the pseudocigar was generated + :type pseudocigar_generation_description_col: str, optional :param quality_col: the name of the column containing the ANSI FASTQ per-base quality score for this sequence :type quality_col: str, optional :param additional_representative_mhap_cols: additional columns to add to the representative microhaplotypes table @@ -211,6 +260,21 @@ def create_representative_microhaplotype_dict( microhaplotype_table, additional_representative_mhap_cols ) + # the pseudocigar ref_loc shares the chromosome / genome with the + # microhaplotype location by default, but each may use its own column + ps_chrom_col = pseudocigar_chrom_col if pseudocigar_chrom_col else chrom_col + ps_genome_id = ( + pseudocigar_genome_id if pseudocigar_genome_id is not None else genome_id + ) + if pseudocigar_col and not ( + ps_chrom_col and pseudocigar_start_col and pseudocigar_end_col + ): + raise ValueError( + "pseudocigar_col is set, so a Pseudocigar ref_loc must be " + "constructable: set a chromosome (pseudocigar_chrom_col or " + "chrom_col), pseudocigar_start_col, and pseudocigar_end_col." + ) + def get_if_present(row, col): return row[col] if col and pd.notna(row[col]) else None @@ -263,6 +327,12 @@ def warn_if_duplicated_seqs(df, target_col, seq_col): alt_annotations_col, microhaplotype_name_col, pseudocigar_col, + ps_chrom_col, + pseudocigar_start_col, + pseudocigar_end_col, + pseudocigar_ref_seq_col, + pseudocigar_strand_col, + pseudocigar_generation_description_col, quality_col, ] masking_cols = [ @@ -319,7 +389,34 @@ def warn_if_duplicated_seqs(df, target_col, seq_col): if val := get_if_present(row, microhaplotype_name_col): mhap["microhaplotype_name"] = val if val := get_if_present(row, pseudocigar_col): - mhap["pseudo_cigar"] = val + if not ( + pd.notna(row[ps_chrom_col]) + and pd.notna(row[pseudocigar_start_col]) + and pd.notna(row[pseudocigar_end_col]) + ): + raise ValueError( + f"pseudocigar present for target {target}, seq " + f"{row[seq_col]} but its ref_loc chrom/start/end " + "is missing" + ) + ref_loc = { + "genome_id": ps_genome_id, + "chrom": row[ps_chrom_col], + "start": row[pseudocigar_start_col], + "end": row[pseudocigar_end_col], + } + if pseudocigar_ref_seq_col and pd.notna(row[pseudocigar_ref_seq_col]): + ref_loc["ref_seq"] = row[pseudocigar_ref_seq_col] + if pseudocigar_strand_col and pd.notna(row[pseudocigar_strand_col]): + ref_loc["strand"] = row[pseudocigar_strand_col] + pseudocigar = {"pseudocigar_seq": val, "ref_loc": ref_loc} + if pseudocigar_generation_description_col and pd.notna( + row[pseudocigar_generation_description_col] + ): + pseudocigar["pseudocigar_generation_description"] = row[ + pseudocigar_generation_description_col + ] + mhap["pseudocigar"] = pseudocigar if val := get_if_present(row, quality_col): mhap["quality"] = val if additional_representative_mhap_cols: diff --git a/tests/test_pmo_builder/test_mhap_table_to_pmo.py b/tests/test_pmo_builder/test_mhap_table_to_pmo.py index fad711b..b50903c 100644 --- a/tests/test_pmo_builder/test_mhap_table_to_pmo.py +++ b/tests/test_pmo_builder/test_mhap_table_to_pmo.py @@ -510,52 +510,49 @@ def test_create_representative_microhaplotype_dict_with_ad_cols(self): mhap_table_ad_cols = self.small_mhap_table.copy() mhap_table_ad_cols["adcol1"] = "this" mhap_table_ad_cols["adcol2"] = "that" - mhap_table_ad_cols["cig"] = "cigs" rep_dict_with_ad_cols = copy.deepcopy(self.small_representative_dict) - rep_dict_with_ad_cols["targets"][0]["microhaplotypes"][0]["adcol1"] = "this" - rep_dict_with_ad_cols["targets"][0]["microhaplotypes"][0]["adcol2"] = "that" - rep_dict_with_ad_cols["targets"][0]["microhaplotypes"][0][ - "pseudo_cigar" - ] = "cigs" - rep_dict_with_ad_cols["targets"][0]["microhaplotypes"][1]["adcol1"] = "this" - rep_dict_with_ad_cols["targets"][0]["microhaplotypes"][1]["adcol2"] = "that" - rep_dict_with_ad_cols["targets"][0]["microhaplotypes"][1][ - "pseudo_cigar" - ] = "cigs" - rep_dict_with_ad_cols["targets"][0]["microhaplotypes"][2]["adcol1"] = "this" - rep_dict_with_ad_cols["targets"][0]["microhaplotypes"][2]["adcol2"] = "that" - rep_dict_with_ad_cols["targets"][0]["microhaplotypes"][2][ - "pseudo_cigar" - ] = "cigs" - rep_dict_with_ad_cols["targets"][1]["microhaplotypes"][0]["adcol1"] = "this" - rep_dict_with_ad_cols["targets"][1]["microhaplotypes"][0]["adcol2"] = "that" - rep_dict_with_ad_cols["targets"][1]["microhaplotypes"][0][ - "pseudo_cigar" - ] = "cigs" - rep_dict_with_ad_cols["targets"][1]["microhaplotypes"][1]["adcol1"] = "this" - rep_dict_with_ad_cols["targets"][1]["microhaplotypes"][1]["adcol2"] = "that" - rep_dict_with_ad_cols["targets"][1]["microhaplotypes"][1][ - "pseudo_cigar" - ] = "cigs" - rep_dict_with_ad_cols["targets"][2]["microhaplotypes"][0]["adcol1"] = "this" - rep_dict_with_ad_cols["targets"][2]["microhaplotypes"][0]["adcol2"] = "that" - rep_dict_with_ad_cols["targets"][2]["microhaplotypes"][0][ - "pseudo_cigar" - ] = "cigs" - rep_dict_with_ad_cols["targets"][2]["microhaplotypes"][1]["adcol1"] = "this" - rep_dict_with_ad_cols["targets"][2]["microhaplotypes"][1]["adcol2"] = "that" - rep_dict_with_ad_cols["targets"][2]["microhaplotypes"][1][ - "pseudo_cigar" - ] = "cigs" + for target in rep_dict_with_ad_cols["targets"]: + for mhap in target["microhaplotypes"]: + mhap["adcol1"] = "this" + mhap["adcol2"] = "that" actual = create_representative_microhaplotype_dict( mhap_table_ad_cols, additional_representative_mhap_cols=["adcol1", "adcol2"], - pseudocigar_col="cig", ) self.assertEqual(actual, rep_dict_with_ad_cols) + def test_create_representative_microhaplotype_dict_with_pseudocigar(self): + tbl = self.small_mhap_table.copy() + tbl["chrom"] = "chr1" + tbl["start"] = 100 + tbl["end"] = 150 + tbl["cig"] = "8M" + actual = create_representative_microhaplotype_dict( + tbl, + chrom_col="chrom", + start_col="start", + end_col="end", + pseudocigar_col="cig", + pseudocigar_start_col="start", + pseudocigar_end_col="end", + ) + first = actual["targets"][0]["microhaplotypes"][0] + # pseudocigar is a Pseudocigar object with a GenomicLocation ref_loc; + # the chromosome is reused from chrom_col and genome_id defaults to 0 + self.assertEqual(first["pseudocigar"]["pseudocigar_seq"], "8M") + self.assertEqual( + first["pseudocigar"]["ref_loc"], + {"genome_id": 0, "chrom": "chr1", "start": 100, "end": 150}, + ) + + def test_pseudocigar_without_ref_loc_columns_raises(self): + tbl = self.small_mhap_table.copy() + tbl["cig"] = "8M" + with self.assertRaises(ValueError): + create_representative_microhaplotype_dict(tbl, pseudocigar_col="cig") + @patch( "pmotools.pmo_builder.mhap_table_to_pmo.create_representative_microhaplotype_dict" ) diff --git a/tests/test_pmo_builder/test_schema_validation_integration.py b/tests/test_pmo_builder/test_schema_validation_integration.py index 766bca5..5f88b9b 100644 --- a/tests/test_pmo_builder/test_schema_validation_integration.py +++ b/tests/test_pmo_builder/test_schema_validation_integration.py @@ -191,6 +191,8 @@ def test_full_1_0_0_toy_pmo_validates_against_schema(): umis_col="umis", microhaplotype_name_col="microhap_name", pseudocigar_col="pseudocigar", + pseudocigar_start_col="start", + pseudocigar_end_col="end", quality_col="quality", masking_seq_start_col="mask_start", masking_seq_segment_size_col="mask_segment", @@ -467,6 +469,9 @@ def test_slimer_1_1_0_toy_pmo_validates_against_schema(): "umis": [10], "microhap_name": ["mh1"], "pseudocigar": ["8M"], + "pseudocigar_chrom": ["chr1"], + "pseudocigar_start": [120], + "pseudocigar_end": [127], "quality": ["ABCD"], "mask_start": ["1"], "mask_segment": ["2"], @@ -480,6 +485,9 @@ def test_slimer_1_1_0_toy_pmo_validates_against_schema(): umis_col="umis", microhaplotype_name_col="microhap_name", pseudocigar_col="pseudocigar", + pseudocigar_chrom_col="pseudocigar_chrom", + pseudocigar_start_col="pseudocigar_start", + pseudocigar_end_col="pseudocigar_end", quality_col="quality", masking_seq_start_col="mask_start", masking_seq_segment_size_col="mask_segment",