uniaf3 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
uniaf3/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Unified data processing for AlphaFold3-like models."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("uniaf3")
@@ -0,0 +1,119 @@
1
+ """Adapters to convert between UniAF3Config and model-specific configs.
2
+
3
+ Each model has a ``to_*`` and ``from_*`` function pair in its own module under
4
+ ``uniaf3.adapters``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ from uniaf3.adapters.alphafold3 import from_alphafold3, to_alphafold3
12
+ from uniaf3.adapters.alphafold3_server import (
13
+ from_alphafold3_server,
14
+ to_alphafold3_server,
15
+ )
16
+ from uniaf3.adapters.boltz import from_boltz, to_boltz
17
+ from uniaf3.adapters.chai import from_chai, to_chai
18
+ from uniaf3.adapters.protenix import from_protenix, to_protenix
19
+ from uniaf3.schema import (
20
+ AF3Config,
21
+ AF3ServerConfig,
22
+ AnyConfig,
23
+ AnyConfigList,
24
+ BoltzConfig,
25
+ ChaiConfig,
26
+ ProtenixConfig,
27
+ UniAF3Config,
28
+ )
29
+
30
+ __all__ = [
31
+ "from_alphafold3",
32
+ "from_alphafold3_server",
33
+ "from_boltz",
34
+ "from_chai",
35
+ "from_protenix",
36
+ "from_uniaf3",
37
+ "to_alphafold3",
38
+ "to_alphafold3_server",
39
+ "to_boltz",
40
+ "to_chai",
41
+ "to_protenix",
42
+ "to_uniaf3",
43
+ ]
44
+
45
+
46
+ def to_uniaf3(
47
+ conf: AnyConfig, *, msa_dir: str | Path = "."
48
+ ) -> UniAF3Config | list[UniAF3Config]:
49
+ """Convert any supported model config to UniAF3Config.
50
+
51
+ Args:
52
+ conf: A config object from any supported model format.
53
+ msa_dir: Directory to save MSA files (used by Boltz and Chai).
54
+
55
+ Returns:
56
+ The equivalent UniAF3Config.
57
+
58
+ Raises:
59
+ TypeError: If the config type is not recognized.
60
+
61
+ """
62
+ if isinstance(conf, UniAF3Config):
63
+ return conf
64
+ if isinstance(conf, AF3ServerConfig):
65
+ return from_alphafold3_server(conf)
66
+ if isinstance(conf, AF3Config):
67
+ return from_alphafold3(conf)
68
+ if isinstance(conf, BoltzConfig):
69
+ return from_boltz(conf, msa_dir=msa_dir)
70
+ if isinstance(conf, ChaiConfig):
71
+ return from_chai(conf, msa_dir=msa_dir)
72
+ if isinstance(conf, ProtenixConfig):
73
+ return from_protenix(conf)
74
+ raise TypeError(f"Unsupported config type: {type(conf)}")
75
+
76
+
77
+ def from_uniaf3(
78
+ conf: UniAF3Config | list[UniAF3Config],
79
+ target: type[AnyConfig],
80
+ *,
81
+ name: str = "uniaf3_job",
82
+ msa_dir: str | Path = ".",
83
+ strict: bool = False,
84
+ ) -> AnyConfig | AnyConfigList:
85
+ """Convert a UniAF3Config to a specific model config.
86
+
87
+ Args:
88
+ conf: The UniAF3Config to convert.
89
+ target: The target config class.
90
+ name: Job name for models that require one (AF3, Protenix).
91
+ msa_dir: Directory to save MSA CSV files (used by Boltz and Chai).
92
+ strict: If True, raise errors for unsupported features.
93
+
94
+ Returns:
95
+ The target model config.
96
+
97
+ Raises:
98
+ TypeError: If the target type is not recognized.
99
+
100
+ """
101
+ if target is UniAF3Config:
102
+ return conf
103
+ if target is AF3Config:
104
+ if isinstance(conf, list):
105
+ return [to_alphafold3(c, name=name, strict=strict) for c in conf]
106
+ return to_alphafold3(conf, name=name, strict=strict)
107
+ if target is AF3ServerConfig:
108
+ return to_alphafold3_server(conf, name=name, strict=strict)
109
+ if target is BoltzConfig:
110
+ if isinstance(conf, list):
111
+ return [to_boltz(c, msa_dir=msa_dir, strict=strict) for c in conf]
112
+ return to_boltz(conf, msa_dir=msa_dir, strict=strict)
113
+ if target is ChaiConfig:
114
+ if isinstance(conf, list):
115
+ return [to_chai(c, msa_dir=msa_dir, strict=strict) for c in conf]
116
+ return to_chai(conf, msa_dir=msa_dir, strict=strict)
117
+ if target is ProtenixConfig:
118
+ return to_protenix(conf, name=name, strict=strict)
119
+ raise TypeError(f"Unsupported target type: {target}")
@@ -0,0 +1,23 @@
1
+ """Shared helpers for adapter modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+
7
+
8
+ def ensure_list(val: str | list[str]) -> list[str]:
9
+ """Normalize id field to a list."""
10
+ return val if isinstance(val, list) else [val]
11
+
12
+
13
+ def warn_lossy_conversion(msg: str):
14
+ """Emit a warning for lossy conversion behavior."""
15
+ warnings.warn(f"Lossy conversion: {msg}", UserWarning, stacklevel=3)
16
+
17
+
18
+ def err_unsupported_feature(strict: bool, msg: str):
19
+ """Help handle unsupported features based on the strict flag."""
20
+ if strict:
21
+ raise ValueError(msg)
22
+ else:
23
+ warnings.warn(f"Skipping unsupported feature: {msg}", UserWarning, stacklevel=3)
@@ -0,0 +1,356 @@
1
+ """Adapter for converting between UniAF3Config and AlphaFold3 config."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uniaf3.adapters._helpers import (
6
+ err_unsupported_feature,
7
+ warn_lossy_conversion,
8
+ )
9
+ from uniaf3.schema.alphafold3 import (
10
+ AF3DNA,
11
+ AF3RNA,
12
+ AF3BondedAtom,
13
+ AF3Config,
14
+ AF3Ligand,
15
+ AF3NucleotideModification,
16
+ AF3Protein,
17
+ AF3ProteinModification,
18
+ AF3SequenceEntry,
19
+ AF3Template,
20
+ )
21
+ from uniaf3.schema.base import (
22
+ Atom,
23
+ AuxiliaryParams,
24
+ CovalentBond,
25
+ Glycan,
26
+ Ligand,
27
+ Polymer,
28
+ PolymerType,
29
+ ProteinSeq,
30
+ SequenceModification,
31
+ StructuralTemplate,
32
+ UniAF3Config,
33
+ )
34
+
35
+
36
+ def to_alphafold3(
37
+ config: UniAF3Config,
38
+ name: str = "uniaf3_job",
39
+ strict: bool = False,
40
+ ) -> AF3Config:
41
+ """Convert a UniAF3Config to an AlphaFold3 config.
42
+
43
+ Args:
44
+ config: UniAF3Config pydantic object.
45
+ name: Job name for the AF3 config.
46
+ strict: If True, raise errors when encountering unsupported features.
47
+ If False, skip unsupported features with warnings.
48
+
49
+ """
50
+ sequences: list[AF3SequenceEntry] = []
51
+ for seq in config.sequences:
52
+ if isinstance(seq, Glycan):
53
+ # TODO: AF3 does not have a native glycan type. Glycans must be
54
+ # represented as multi-CCD ligands. This requires knowing the
55
+ # component CCD codes, which the chai_str notation may not directly
56
+ # map to.
57
+ err_unsupported_feature(
58
+ strict,
59
+ f"Glycans are not directly supported in AF3: {seq}",
60
+ )
61
+ continue
62
+
63
+ if isinstance(seq, ProteinSeq):
64
+ mods = (
65
+ [
66
+ AF3ProteinModification(ptmType=m.ccd, ptmPosition=m.position)
67
+ for m in seq.modifications
68
+ ]
69
+ if seq.modifications
70
+ else None
71
+ )
72
+ protein = AF3Protein(
73
+ id=seq.id,
74
+ sequence=seq.sequence,
75
+ modifications=mods,
76
+ description=seq.description,
77
+ unpairedMsaPath=seq.unpaired_msa,
78
+ pairedMsaPath=seq.paired_msa,
79
+ )
80
+ # Templates
81
+ if seq.templates:
82
+ af3_templates = []
83
+ for tmpl in seq.templates:
84
+ if tmpl.query_chains or tmpl.template_chains:
85
+ warn_lossy_conversion(
86
+ "UniAF3Config.sequences[*].templates.{query_chains,template_chains} are not represented by AF3Config.sequences[*].protein.templates."
87
+ )
88
+ if (
89
+ tmpl.boltz_enable_force
90
+ or tmpl.boltz_template_threshold is not None
91
+ ):
92
+ warn_lossy_conversion(
93
+ "UniAF3Config.sequences[*].templates.{boltz_enable_force,boltz_template_threshold} are not represented by AF3Config.sequences[*].protein.templates."
94
+ )
95
+ # TODO: extract a single chain from the template structure
96
+ af3_templates.append(
97
+ AF3Template(
98
+ mmcifPath=tmpl.path,
99
+ queryIndices=tmpl.query_idx or [],
100
+ templateIndices=tmpl.template_idx or [],
101
+ )
102
+ )
103
+ protein.templates = af3_templates
104
+ sequences.append(AF3SequenceEntry(protein=protein))
105
+
106
+ elif isinstance(seq, Polymer):
107
+ if seq.polymer_type == PolymerType.Protein:
108
+ mods = (
109
+ [
110
+ AF3ProteinModification(ptmType=m.ccd, ptmPosition=m.position)
111
+ for m in seq.modifications
112
+ ]
113
+ if seq.modifications
114
+ else None
115
+ )
116
+ protein = AF3Protein(
117
+ id=seq.id,
118
+ sequence=seq.sequence,
119
+ modifications=mods,
120
+ description=seq.description,
121
+ )
122
+ sequences.append(AF3SequenceEntry(protein=protein))
123
+ elif seq.polymer_type == PolymerType.DNA:
124
+ mods = (
125
+ [
126
+ AF3NucleotideModification(
127
+ modificationType=m.ccd, basePosition=m.position
128
+ )
129
+ for m in seq.modifications
130
+ ]
131
+ if seq.modifications
132
+ else None
133
+ )
134
+ dna = AF3DNA(
135
+ id=seq.id,
136
+ sequence=seq.sequence,
137
+ modifications=mods,
138
+ description=seq.description,
139
+ )
140
+ sequences.append(AF3SequenceEntry(dna=dna))
141
+ elif seq.polymer_type == PolymerType.RNA:
142
+ mods = (
143
+ [
144
+ AF3NucleotideModification(
145
+ modificationType=m.ccd, basePosition=m.position
146
+ )
147
+ for m in seq.modifications
148
+ ]
149
+ if seq.modifications
150
+ else None
151
+ )
152
+ rna = AF3RNA(
153
+ id=seq.id,
154
+ sequence=seq.sequence,
155
+ modifications=mods,
156
+ description=seq.description,
157
+ )
158
+ sequences.append(AF3SequenceEntry(rna=rna))
159
+
160
+ elif isinstance(seq, Ligand):
161
+ if seq.ccd:
162
+ lig = AF3Ligand(
163
+ id=seq.id,
164
+ ccdCodes=seq.ccd,
165
+ description=seq.description,
166
+ )
167
+ elif seq.smiles:
168
+ lig = AF3Ligand(
169
+ id=seq.id,
170
+ smiles=seq.smiles,
171
+ description=seq.description,
172
+ )
173
+ else:
174
+ continue
175
+ sequences.append(AF3SequenceEntry(ligand=lig))
176
+
177
+ # Bonded atom pairs (only covalent bonds)
178
+ bonded_atom_pairs: list[tuple[AF3BondedAtom, AF3BondedAtom]] = []
179
+ if config.covalent_bonds:
180
+ for r in config.covalent_bonds:
181
+ # AF3 requires atom names to be given
182
+ if r.atom1.atom_name is None or r.atom2.atom_name is None:
183
+ err_unsupported_feature(
184
+ strict,
185
+ f"AF3 bondedAtomPairs require atom names, but got: {r}",
186
+ )
187
+ continue
188
+ a1: AF3BondedAtom = (
189
+ r.atom1.chain_id,
190
+ r.atom1.residue_idx,
191
+ r.atom1.atom_name,
192
+ )
193
+ a2: AF3BondedAtom = (
194
+ r.atom2.chain_id,
195
+ r.atom2.residue_idx,
196
+ r.atom2.atom_name,
197
+ )
198
+ bonded_atom_pairs.append((a1, a2))
199
+
200
+ if config.contact_restraints or config.pocket_restraints:
201
+ err_unsupported_feature(
202
+ strict,
203
+ "AF3 does not support contact or pocket restraints.",
204
+ )
205
+
206
+ return AF3Config(
207
+ name=name,
208
+ modelSeeds=config.aux.seeds,
209
+ sequences=sequences,
210
+ bondedAtomPairs=bonded_atom_pairs or None,
211
+ )
212
+
213
+
214
+ def from_alphafold3(config: AF3Config) -> UniAF3Config:
215
+ """Convert an AlphaFold3 config to a UniAF3Config.
216
+
217
+ Args:
218
+ config: AF3Config pydantic object.
219
+
220
+ Returns:
221
+ A UniAF3Config.
222
+
223
+ """
224
+ sequences: list[Polymer | ProteinSeq | Ligand | Glycan] = []
225
+ if config.name:
226
+ warn_lossy_conversion(
227
+ f"AF3Config.name ('{config.name}') is not represented in UniAF3Config."
228
+ )
229
+ if config.userCCD is not None or config.userCCDPath is not None:
230
+ warn_lossy_conversion(
231
+ "AF3Config.{userCCD,userCCDPath} are not represented in UniAF3Config."
232
+ )
233
+
234
+ for entry in config.sequences:
235
+ if entry.protein is not None:
236
+ p = entry.protein
237
+ mods = (
238
+ [
239
+ SequenceModification(ccd=m.ptmType, position=m.ptmPosition)
240
+ for m in p.modifications
241
+ ]
242
+ if p.modifications
243
+ else None
244
+ )
245
+ if p.unpairedMsa is not None or p.pairedMsa is not None:
246
+ warn_lossy_conversion(
247
+ "AF3Config.sequences[*].protein.{unpairedMsa,pairedMsa} are not imported; UniAF3 maps only file-based MSA paths."
248
+ )
249
+
250
+ templates = None
251
+ if p.templates:
252
+ if any(
253
+ t.mmcif is not None and t.mmcifPath is None for t in p.templates
254
+ ):
255
+ warn_lossy_conversion(
256
+ "AF3Config.sequences[*].protein.templates[*].mmcif is not preserved; only mmcifPath maps to UniAF3 templates.path."
257
+ )
258
+ templates = [
259
+ StructuralTemplate(
260
+ path=t.mmcifPath or "",
261
+ query_idx=t.queryIndices,
262
+ template_idx=t.templateIndices,
263
+ )
264
+ for t in p.templates
265
+ ]
266
+
267
+ seq = ProteinSeq(
268
+ polymer_type=PolymerType.Protein,
269
+ id=p.id,
270
+ sequence=p.sequence,
271
+ modifications=mods,
272
+ description=p.description,
273
+ unpaired_msa=p.unpairedMsaPath,
274
+ paired_msa=p.pairedMsaPath,
275
+ templates=templates,
276
+ )
277
+ sequences.append(seq)
278
+
279
+ elif entry.dna is not None:
280
+ d = entry.dna
281
+ mods = (
282
+ [
283
+ SequenceModification(
284
+ ccd=m.modificationType, position=m.basePosition
285
+ )
286
+ for m in d.modifications
287
+ ]
288
+ if d.modifications
289
+ else None
290
+ )
291
+ seq = Polymer(
292
+ polymer_type=PolymerType.DNA,
293
+ id=d.id,
294
+ sequence=d.sequence,
295
+ modifications=mods,
296
+ description=d.description,
297
+ )
298
+ sequences.append(seq)
299
+
300
+ elif entry.rna is not None:
301
+ r = entry.rna
302
+ mods = (
303
+ [
304
+ SequenceModification(
305
+ ccd=m.modificationType, position=m.basePosition
306
+ )
307
+ for m in r.modifications
308
+ ]
309
+ if r.modifications
310
+ else None
311
+ )
312
+ seq = Polymer(
313
+ polymer_type=PolymerType.RNA,
314
+ id=r.id,
315
+ sequence=r.sequence,
316
+ modifications=mods,
317
+ description=r.description,
318
+ )
319
+ sequences.append(seq)
320
+
321
+ elif entry.ligand is not None:
322
+ lg = entry.ligand
323
+ lig = Ligand(
324
+ id=lg.id,
325
+ ccd=lg.ccdCodes,
326
+ smiles=lg.smiles,
327
+ description=lg.description,
328
+ )
329
+ sequences.append(lig)
330
+
331
+ # Bonded atom pairs → covalent bonds
332
+ covalent_bonds: list[CovalentBond] = []
333
+ if config.bondedAtomPairs:
334
+ for a1, a2 in config.bondedAtomPairs:
335
+ covalent_bonds.append(
336
+ CovalentBond(
337
+ atom1=Atom(
338
+ chain_id=a1[0],
339
+ residue_idx=a1[1],
340
+ atom_name=a1[2],
341
+ residue_name=None,
342
+ ),
343
+ atom2=Atom(
344
+ chain_id=a2[0],
345
+ residue_idx=a2[1],
346
+ atom_name=a2[2],
347
+ residue_name=None,
348
+ ),
349
+ )
350
+ )
351
+
352
+ return UniAF3Config(
353
+ sequences=sequences,
354
+ covalent_bonds=covalent_bonds or None,
355
+ aux=AuxiliaryParams(seeds=config.modelSeeds),
356
+ )