Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ce36a0f
update for deploy only on version tags;
nickjhathaway Jun 4, 2026
1d22c18
default in the argparse was still mhap_id but we changed the default …
nickjhathaway Jun 5, 2026
374e14e
small fixes for mutation of input data for filter_pmo_by_target_ids;
nickjhathaway Jun 29, 2026
6ad954b
update the hashes because now reads_by_stage is now properly exported…
nickjhathaway Jun 29, 2026
5ea5a2e
fix for when exporting a header for bed locations didn't add a newlin…
nickjhathaway Jun 29, 2026
c71bbfd
update so pseudocigar is properly constructed during the building fun…
nickjhathaway Jun 29, 2026
aa1c22f
start and end were flipped when setting location for forward primer l…
nickjhathaway Jun 29, 2026
fb80099
fix for the change in the schema to qpcr name in the schema which was…
nickjhathaway Jun 29, 2026
e45234f
two bugs in combine_pmos fixed;
nickjhathaway Jun 29, 2026
8b32cda
fixed a left over old schema version where all fields were copies ove…
nickjhathaway Jun 29, 2026
6bb0564
small fixes for handling minimum format; added more robust unittests …
nickjhathaway Jun 29, 2026
0ced495
Merge pull request #80 from PlasmoGenEpi/hotfix/update_man_deploy_git…
nickjhathaway Jul 21, 2026
e7aa09d
Merge pull request #81 from PlasmoGenEpi/hotfix/update_output_name_in…
nickjhathaway Jul 21, 2026
f50e531
Merge pull request #83 from PlasmoGenEpi/hotfix/fix_small_bugs
nickjhathaway Jul 21, 2026
cc821c1
Merge pull request #86 from PlasmoGenEpi/hotfix/update_combine_pmos
nickjhathaway Jul 21, 2026
1b8bf8a
Merge pull request #82 from PlasmoGenEpi/hotfix/fix_filtering_pmos
nickjhathaway Jul 21, 2026
804fce5
Merge pull request #87 from PlasmoGenEpi/hotfix/various_bug_fixes
nickjhathaway Jul 21, 2026
3361846
change ps_chrom_col to pseduocigar_chrom_col
nickjhathaway Jul 21, 2026
ddef2d0
Merge pull request #85 from PlasmoGenEpi/hotfix/fix_pseudocigar_handling
nickjhathaway Jul 21, 2026
4f5e775
Skip blank target_attributes cells when building panel info
kathrynmurie Oct 3, 2026
dd1d770
skip blanks in add_plate_info
kathrynmurie Oct 3, 2026
a6a6570
Rename host_subject_id to host_subject_name for v1.1.0 schema
kathrynmurie Oct 3, 2026
e442d97
Add PMOUpdater methods for nested fields from long-format tables
kathrynmurie Oct 3, 2026
608baf7
Clarify pseudocigar_chrom_col docs when no full location is set
kathrynmurie Oct 3, 2026
ecad11d
Merge pull request #95 from PlasmoGenEpi/hotfix/pseudocigar_chrom_docs
nickjhathaway Oct 7, 2026
4e9b10d
Merge pull request #90 from PlasmoGenEpi/hotfix/target_attributes_blank
nickjhathaway Oct 7, 2026
bfdc0b3
Merge pull request #91 from PlasmoGenEpi/hotfix/blank_plate_infos
nickjhathaway Oct 7, 2026
ea91c96
Merge pull request #93 from PlasmoGenEpi/feature/nested_field_updaters
nickjhathaway Oct 7, 2026
ce1cb53
Merge pull request #92 from PlasmoGenEpi/hotfix/host_subject_name
nickjhathaway Oct 7, 2026
b617a9a
Bump version to v1.2.0
kathrynmurie Oct 7, 2026
25b7d90
Merge pull request #96 from PlasmoGenEpi/release/v1.2.0
kathrynmurie Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
tags:
- 'v*.*.*'
- 'test*'
workflow_dispatch:

permissions:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pmotools"
version = "v1.1.0"
version = "v1.2.0"
description = "Tools for building and analyzing PMO files"
readme = "README.md"
authors = [
Expand Down
69 changes: 39 additions & 30 deletions src/pmotools/pmo_builder/metatable_to_pmo.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def library_sample_info_table_to_pmo(
library_prep_plate_position_col,
meta_json,
copy_contents,
"specimen_name",
"library_sample_name",
"library_prep_plate_info",
)
meta_json = add_parasite_density_info(
Expand All @@ -166,7 +166,7 @@ def library_sample_info_table_to_pmo(
meta_json,
copy_contents,
"library_sample_name",
entry_name="parasite_density_info",
entry_name="qpcr_parasite_density_info",
)
# listify columns that contain values that could be list, are delimited by the argument list_values_library_values_delimiter
primitives = (int, float, str, bool, complex)
Expand Down Expand Up @@ -212,7 +212,7 @@ def specimen_info_table_to_pmo(
has_travel_out_six_month_col: str = None,
host_age_col: str = None,
host_sex_col: str = None,
host_subject_id: str = None,
host_subject_name_col: str = None,
lat_lon_col: str = None,
parasite_density_col: str = None,
parasite_density_method_col: str = None,
Expand Down Expand Up @@ -281,8 +281,8 @@ def specimen_info_table_to_pmo(
:type host_age_col: str, optional
:param host_sex_col: if the specimen is from a person, the sex of that person
:type host_sex_col: str, optional
:param host_subject_id: ID for the individual a specimen was collected from
:type host_subject_id: str, optional
:param host_subject_name_col: identifier for the individual a specimen was collected from
:type host_subject_name_col: str, optional
:param lat_lon_col: latitude and longitude of the collection site
:type lat_lon_col: str, optional
:param parasite_density_col: the parasite density in parasites per microliter
Expand Down Expand Up @@ -353,7 +353,7 @@ def specimen_info_table_to_pmo(
geo_admin3_col: "geo_admin3",
host_age_col: "host_age",
host_sex_col: "host_sex",
host_subject_id: "host_subject_id",
host_subject_name_col: "host_subject_name",
lat_lon_col: "lat_lon",
specimen_accession_col: "specimen_accession",
specimen_type_col: "specimen_type",
Expand Down Expand Up @@ -392,7 +392,7 @@ def specimen_info_table_to_pmo(
geo_admin3_col,
host_age_col,
host_sex_col,
host_subject_id,
host_subject_name_col,
lat_lon_col,
specimen_accession_col,
specimen_type_col,
Expand Down Expand Up @@ -487,14 +487,22 @@ def check_columns_exist(df, columns):
)


def _is_blank(value):
if value is None:
return True
if isinstance(value, str):
return not value.strip()
return bool(pd.isna(value))


def add_plate_info(
plate_col_col,
plate_name_col,
plate_row_col,
plate_position_col,
meta_json,
df,
specimen_name_col,
match_col,
entry_name="plate_info",
):
if all(
Expand All @@ -516,38 +524,39 @@ def add_plate_info(
plate_row_col = "plate_row"
plate_col_col = "plate_col"

try:
df[plate_row_col] = (
df[plate_position_col].str.extract(r"(?i)^([A-H])")[0].str.upper()
)
df[plate_col_col] = (
df[plate_position_col]
.str.extract(r"(?i)^[A-H]0*([1-9]|1[0-2])$")[0]
.astype(int)
)
except (AttributeError, ValueError, IndexError, KeyError) as e:
positions = df[plate_position_col]
blank = positions.map(_is_blank)
parsed = (
positions[~blank]
.astype(str)
.str.strip()
.str.extract(r"(?i)^([A-H])0*([1-9]|1[0-2])$")
)
if parsed.isna().any().any():
raise ValueError(
f"Values in '{plate_position_col}' must start with a single letter A-H/a-h followed by number 1-12."
) from e
)
df[plate_row_col] = None
df.loc[~blank, plate_row_col] = parsed[0].str.upper()
df[plate_col_col] = pd.Series(pd.NA, index=df.index, dtype="Int64")
df.loc[~blank, plate_col_col] = parsed[1].astype(int)

for row in meta_json:
content_row = df[df[specimen_name_col] == row[specimen_name_col]]
content_row = df[df[match_col] == row[match_col]]
plate_name_val = content_row[plate_name_col].iloc[0] if plate_name_col else None
plate_row_val = (
content_row[plate_row_col].iloc[0].upper() if plate_row_col else None
)
plate_row_val = content_row[plate_row_col].iloc[0] if plate_row_col else None
plate_col_val = content_row[plate_col_col].iloc[0] if plate_col_col else None
if plate_col_val is not None and not pd.isna(plate_col_val):
if not _is_blank(plate_col_val):
try:
plate_col_val = int(plate_col_val)
except (TypeError, ValueError):
plate_col_val = plate_col_val
plate_info = {}
if plate_name_val:
if not _is_blank(plate_name_val):
plate_info["plate_name"] = plate_name_val
if plate_row_val:
plate_info["plate_row"] = plate_row_val
if plate_col_val is not None and not pd.isna(plate_col_val):
if not _is_blank(plate_row_val):
plate_info["plate_row"] = str(plate_row_val).strip().upper()
if not _is_blank(plate_col_val):
plate_info["plate_col"] = plate_col_val

if plate_info:
Expand All @@ -560,7 +569,7 @@ def add_parasite_density_info(
parasite_density_method_col,
meta_json,
df,
specimen_name_col,
match_col,
entry_name,
):
density_method_pairs = []
Expand Down Expand Up @@ -608,7 +617,7 @@ def add_parasite_density_info(

# Add parasite density info to meta_json
for row in meta_json:
content_row = df[df[specimen_name_col] == row[specimen_name_col]]
content_row = df[df[match_col] == row[match_col]]
density_infos = []
for density_col, method_col in density_method_pairs:
density_val = content_row[density_col].iloc[0] if density_col else None
Expand Down
102 changes: 101 additions & 1 deletion src/pmotools/pmo_builder/mhap_table_to_pmo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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. chrom_col can only be set together with start_col and end_col, so use pseudocigar_chrom_col when there is no full microhaplotype location
: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
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
):
Expand Down Expand Up @@ -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. chrom_col can only be set together with start_col and end_col, so use pseudocigar_chrom_col when there is no full microhaplotype location
: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
Expand All @@ -211,6 +260,24 @@ 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
pseudocigar_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 (
pseudocigar_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 together with start_col and end_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

Expand Down Expand Up @@ -263,6 +330,12 @@ def warn_if_duplicated_seqs(df, target_col, seq_col):
alt_annotations_col,
microhaplotype_name_col,
pseudocigar_col,
pseudocigar_chrom_col,
pseudocigar_start_col,
pseudocigar_end_col,
pseudocigar_ref_seq_col,
pseudocigar_strand_col,
pseudocigar_generation_description_col,
quality_col,
]
masking_cols = [
Expand Down Expand Up @@ -319,7 +392,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[pseudocigar_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[pseudocigar_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:
Expand Down
12 changes: 7 additions & 5 deletions src/pmotools/pmo_builder/panel_information_to_pmo.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,11 @@ def build_target_info_dict(
if self.gene_name_col:
target_dict["gene_name"] = row[self.gene_name_col]
if self.target_attributes_col:
target_dict["target_attributes"] = row[
self.target_attributes_col
].split(self.target_attributes_col_delimiter)
target_attributes = row[self.target_attributes_col]
if pd.notna(target_attributes) and str(target_attributes).strip():
target_dict["target_attributes"] = str(target_attributes).split(
self.target_attributes_col_delimiter
)
if self.additional_target_info_cols:
for col in self.additional_target_info_cols:
value = row[col]
Expand Down Expand Up @@ -332,8 +334,8 @@ def build_target_info_dict(
fwd_primer_dict["location"] = {
"genome_id": genome_id,
"chrom": row[chrom_col],
"end": int(row[forward_primers_start_col]),
"start": int(row[forward_primers_end_col]),
"start": int(row[forward_primers_start_col]),
"end": int(row[forward_primers_end_col]),
}
if strand_col and pd.notna(row[strand_col]):
fwd_primer_dict["location"]["strand"] = row[strand_col]
Expand Down
Loading
Loading