Skip to content

Commit f26a8ff

Browse files
committed
Merge Nextflow ModelPDB and UniProt parser fixes
2 parents 14841f9 + b7d0e56 commit f26a8ff

6 files changed

Lines changed: 338 additions & 11 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,8 @@ AF-0001234567890125
619619
AF-0001234567890126
620620
```
621621

622+
The ModelPDB step also requires provider metadata. By default the workflow reads this from `input/provider.json`; override it with `--provider_json <path>` if your provider file is elsewhere.
623+
622624
**Example input.txt:**
623625
```bash
624626
# Create the input list file

tests/test_uniprot_extract_subset.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
parse_alt_products,
44
parse_de_sections,
55
parse_var_seq,
6+
shard_key_for_target,
7+
stable_shard_for_accession,
8+
stable_shard_for_target,
69
)
710

811

@@ -98,3 +101,50 @@ def test_parse_var_seq_multiline_note_preserves_full_replacement() -> None:
98101
varseqs = parse_var_seq(lines)
99102

100103
assert varseqs["VSP_060194"] == (1056, 1097, "RWEDRLRPGVRDQPGQHSKIPIF")
104+
105+
106+
def test_parse_var_seq_single_position() -> None:
107+
lines = [
108+
"FT VAR_SEQ 339",
109+
"FT /note=\"L -> MPIARLNSAPLNSHFWRPVWGASPSSV (in isoform 2)\"",
110+
"FT /id=\"VSP_053046\"",
111+
]
112+
113+
varseqs = parse_var_seq(lines)
114+
115+
assert varseqs["VSP_053046"] == (339, 339, "MPIARLNSAPLNSHFWRPVWGASPSSV")
116+
117+
118+
def test_build_entry_payload_keeps_isoform_with_single_position_varseq() -> None:
119+
lines = [
120+
"ID Q1LVW0_TEST Reviewed; 1021 AA.",
121+
"AC Q1LVW0; Q1MTD0;",
122+
"DE RecName: Full=Ankyrin repeat and BTB domain-containing protein 4;",
123+
"CC -!- ALTERNATIVE PRODUCTS:",
124+
"CC IsoId=Q1LVW0-1; Sequence=Displayed;",
125+
"CC IsoId=Q1LVW0-2; Sequence=VSP_053045, VSP_053046;",
126+
"FT VAR_SEQ 1..338",
127+
"FT /note=\"Missing (in isoform 2)\"",
128+
"FT /id=\"VSP_053045\"",
129+
"FT VAR_SEQ 339",
130+
"FT /note=\"L -> MPIARLNSAPLNSHFWRPVWGASPSSV (in isoform 2)\"",
131+
"FT /id=\"VSP_053046\"",
132+
"SQ SEQUENCE 1021 AA; 0 MW; 0000000000000000 CRC64;",
133+
f" {'A' * 338}{'L'}{'B' * (1021 - 339)}",
134+
]
135+
136+
rows = build_entry_payload(lines, ["Q1LVW0", "Q1MTD0"], "2025_04", reviewed=True)
137+
by_ac = {row["primary_ac"]: row for row in rows}
138+
139+
assert "Q1LVW0-2" in by_ac
140+
assert by_ac["Q1LVW0-2"]["is_isoform"] is True
141+
assert by_ac["Q1LVW0-2"]["length"] == 709
142+
143+
144+
def test_isoform_targets_route_by_canonical_shard_key() -> None:
145+
isoform = "Q1LVW0-2"
146+
canonical = "Q1LVW0"
147+
148+
assert shard_key_for_target(isoform) == canonical
149+
assert stable_shard_for_target(isoform, 8) == stable_shard_for_accession(canonical, 8)
150+
assert stable_shard_for_target(isoform, 64) == stable_shard_for_accession(canonical, 64)

tests/test_uniprot_metadata.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
from __future__ import annotations
2+
3+
from uniprot.scripts.extract_subset import parse_de_sections, parse_organism
4+
from uniprot.scripts.export_model_metadata import (
5+
ManifestRow,
6+
ModelMetadataRow,
7+
build_record,
8+
)
9+
10+
11+
def test_parse_de_sections_preserves_square_brackets_in_names() -> None:
12+
lines = [
13+
"DE RecName: Full=Acyl-[acyl-carrier-protein]--UDP-N-acetylglucosamine O-acyltransferase;",
14+
]
15+
16+
full_names, short_names = parse_de_sections(lines)
17+
18+
assert full_names == [
19+
"Acyl-[acyl-carrier-protein]--UDP-N-acetylglucosamine O-acyltransferase"
20+
]
21+
assert short_names == []
22+
23+
24+
def test_parse_de_sections_preserves_semicolons_inside_names() -> None:
25+
lines = [
26+
"DE RecName: Full=nitrite reductase (cytochrome; ammonia-forming) {ECO:0000256|ARBA:ARBA00011887};",
27+
]
28+
29+
full_names, short_names = parse_de_sections(lines)
30+
31+
assert full_names == ["nitrite reductase (cytochrome; ammonia-forming)"]
32+
assert short_names == []
33+
34+
35+
def test_parse_de_sections_uses_submitted_names_as_full_name_fallback() -> None:
36+
lines = [
37+
"DE SubName: Full=Methyl-coenzyme M reductase alpha {ECO:0000313|EMBL:ABW73421.1};",
38+
"DE Flags: Fragment;",
39+
]
40+
41+
full_names, short_names = parse_de_sections(lines)
42+
43+
assert full_names == ["Methyl-coenzyme M reductase alpha"]
44+
assert short_names == []
45+
46+
47+
def test_parse_de_sections_keeps_recommended_names_before_submitted_names() -> None:
48+
lines = [
49+
"DE RecName: Full=Curated recommended name;",
50+
"DE SubName: Full=Submitted fallback name {ECO:0000313|EMBL:ABC123.1};",
51+
]
52+
53+
full_names, short_names = parse_de_sections(lines)
54+
55+
assert full_names == ["Curated recommended name", "Submitted fallback name"]
56+
assert short_names == []
57+
58+
59+
def test_parse_organism_preserves_parenthetical_text_in_scientific_name() -> None:
60+
organism, common_names, synonyms = parse_organism("Escherichia coli (strain K12).")
61+
62+
assert organism == "Escherichia coli (strain K12)"
63+
assert common_names == ["strain K12"]
64+
assert synonyms == []
65+
66+
67+
def test_build_record_adds_model_level_complex_fields() -> None:
68+
config = {
69+
"latestVersion": 1,
70+
"allVersions": [1],
71+
"providerId": "AF-TEST",
72+
"entityType": "protein",
73+
}
74+
manifest_rows = [
75+
ManifestRow(
76+
model_entity_id="AF-0000000000000001",
77+
entity_id="1",
78+
chain_id="A",
79+
uniprot_ac="P11111",
80+
sequence_start=None,
81+
sequence_end=None,
82+
is_fragment=False,
83+
is_isoform=False,
84+
entity_type="protein",
85+
average_plddt=90.0,
86+
fraction_plddt_very_low=0.0,
87+
fraction_plddt_low=0.0,
88+
fraction_plddt_confident=0.2,
89+
fraction_plddt_very_high=0.8,
90+
),
91+
ManifestRow(
92+
model_entity_id="AF-0000000000000001",
93+
entity_id="2",
94+
chain_id="B",
95+
uniprot_ac="Q22222",
96+
sequence_start=None,
97+
sequence_end=None,
98+
is_fragment=False,
99+
is_isoform=False,
100+
entity_type="protein",
101+
average_plddt=88.0,
102+
fraction_plddt_very_low=0.0,
103+
fraction_plddt_low=0.1,
104+
fraction_plddt_confident=0.2,
105+
fraction_plddt_very_high=0.7,
106+
),
107+
]
108+
entry_map = {
109+
"P11111": {
110+
"primary_ac": "P11111",
111+
"entry_name": "PROT1_TEST",
112+
"protein_full_names": ["Protein one"],
113+
"protein_short_names": None,
114+
"gene_names": "GENE1",
115+
"gene_synonyms": None,
116+
"gene_ordered_locus_names": None,
117+
"gene_orf_names": None,
118+
"organism": "Organismus exampleus",
119+
"organism_common_names": None,
120+
"organism_synonyms": None,
121+
"taxid": 1111,
122+
"sequence_version_date": "2024-01-01",
123+
"sequence": "ACDE",
124+
"is_uniprot_reference_proteome": True,
125+
"reviewed": True,
126+
},
127+
"Q22222": {
128+
"primary_ac": "Q22222",
129+
"entry_name": "PROT2_TEST",
130+
"protein_full_names": ["Protein two"],
131+
"protein_short_names": None,
132+
"gene_names": "GENE2",
133+
"gene_synonyms": None,
134+
"gene_ordered_locus_names": None,
135+
"gene_orf_names": None,
136+
"organism": "Organismus exampleus",
137+
"organism_common_names": None,
138+
"organism_synonyms": None,
139+
"taxid": 1111,
140+
"sequence_version_date": "2024-01-01",
141+
"sequence": "FGHI",
142+
"is_uniprot_reference_proteome": True,
143+
"reviewed": True,
144+
},
145+
}
146+
model_metadata = {
147+
"AF-0000000000000001": ModelMetadataRow(
148+
iptm=0.91,
149+
average_plddt=89.3,
150+
complex_name=None,
151+
is_am_data=False,
152+
)
153+
}
154+
155+
record = build_record(
156+
"AF-0000000000000001",
157+
config,
158+
manifest_rows,
159+
entry_map,
160+
model_metadata,
161+
)
162+
163+
assert record["assemblyType"] == "Hetero"
164+
assert record["oligomericState"] == "dimer"
165+
assert record["oligomericStateDescription"] == "Heterodimer"
166+
assert record["complexComposition"] == ["P11111_1", "Q22222_1"]

uniprot/scripts/export_model_metadata.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,15 @@ def derive_oligomeric_state(subunit_count: int) -> str:
421421
return OLIGOMERIC_STATE_MAP.get(subunit_count, "oligomer")
422422

423423

424+
def derive_oligomeric_state_description(
425+
assembly_type: Optional[str],
426+
oligomeric_state: Optional[str],
427+
) -> Optional[str]:
428+
if not assembly_type or not oligomeric_state:
429+
return None
430+
return f"{assembly_type}{oligomeric_state}"
431+
432+
424433
def aggregate_metric(rows: Sequence[ManifestRow], attr: str) -> Optional[float]:
425434
values = [getattr(row, attr) for row in rows if getattr(row, attr) is not None]
426435
if not values:
@@ -617,13 +626,23 @@ def build_record(
617626
)
618627

619628
is_complex = len(manifest_rows) > 1
629+
complex_composition = (
630+
[f"{component.accession}_{count}" for component, count in zip(components, component_counts)]
631+
if is_complex
632+
else None
633+
)
620634

621635
assembly_type = None
622636
oligomeric_state = None
637+
oligomeric_state_description = None
623638
if is_complex:
624639
unique_accessions = {row.uniprot_ac for row in manifest_rows if row.uniprot_ac}
625640
assembly_type = "Hetero" if len(unique_accessions) > 1 else "Homo"
626641
oligomeric_state = derive_oligomeric_state(len(manifest_rows))
642+
oligomeric_state_description = derive_oligomeric_state_description(
643+
assembly_type,
644+
oligomeric_state,
645+
)
627646

628647
model_meta = model_metadata.get(model_entity_id)
629648
iptm_value = model_meta.iptm if (is_complex and model_meta) else None
@@ -680,6 +699,10 @@ def consistent_or_none(values: Sequence[Optional[int]]) -> Optional[int]:
680699
if is_complex and assembly_type and oligomeric_state:
681700
record["assemblyType"] = assembly_type
682701
record["oligomericState"] = oligomeric_state
702+
if complex_composition:
703+
record["complexComposition"] = complex_composition
704+
if oligomeric_state_description:
705+
record["oligomericStateDescription"] = oligomeric_state_description
683706
record["uniprotAccession"] = accessions
684707
record["uniprotDescription"] = descriptions
685708
record["isUniProtReferenceProteome"] = is_uniprot_reference_proteome

0 commit comments

Comments
 (0)