lhallee commited on
Commit
447c1a4
·
verified ·
1 Parent(s): 743b0f1

Upload cif_writer.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. cif_writer.py +126 -126
cif_writer.py CHANGED
@@ -1,126 +1,126 @@
1
- from pathlib import Path
2
- from typing import List, Optional
3
-
4
- import numpy as np
5
- import torch
6
-
7
- from .minimal_structures import ProteinStructureTemplate
8
-
9
-
10
- def _confidence_per_atom(
11
- plddt: Optional[torch.Tensor],
12
- atom_to_residue: List[int],
13
- num_atoms: int,
14
- sample_index: int,
15
- ) -> np.ndarray:
16
- if plddt is None:
17
- return np.ones((num_atoms,), dtype=np.float32) * 100.0
18
-
19
- values = plddt.detach().cpu()
20
- if values.ndim == 1:
21
- values = values.unsqueeze(0)
22
- assert values.ndim == 2, "Expected pLDDT with shape [samples, tokens/atoms]."
23
- assert sample_index < values.shape[0], "sample_index out of range for pLDDT."
24
-
25
- selected = values[sample_index]
26
- if selected.shape[0] == num_atoms:
27
- return (selected.numpy() * 100.0).astype(np.float32)
28
-
29
- num_residues = max(atom_to_residue) + 1
30
- if selected.shape[0] == num_residues:
31
- expanded = np.zeros((num_atoms,), dtype=np.float32)
32
- selected_np = selected.numpy()
33
- for atom_idx, residue_idx in enumerate(atom_to_residue):
34
- expanded[atom_idx] = selected_np[residue_idx] * 100.0
35
- return expanded
36
-
37
- return np.ones((num_atoms,), dtype=np.float32) * 100.0
38
-
39
-
40
- def write_cif(
41
- structure_template: ProteinStructureTemplate,
42
- atom_coords: torch.Tensor,
43
- atom_mask: torch.Tensor,
44
- output_path: str,
45
- plddt: Optional[torch.Tensor] = None,
46
- sample_index: int = 0,
47
- ) -> str:
48
- coords = atom_coords.detach().cpu()
49
- if coords.ndim == 2:
50
- coords = coords.unsqueeze(0)
51
- assert coords.ndim == 3, "Expected coordinates with shape [samples, atoms, 3]."
52
- assert sample_index < coords.shape[0], "sample_index out of range."
53
- selected_coords_tensor = coords[sample_index]
54
- all_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor))
55
- assert not torch.any(all_non_finite), (
56
- "CIF export received non-finite coordinates. "
57
- f"Non-finite count: {int(all_non_finite.sum().item())}"
58
- )
59
- selected_coords = selected_coords_tensor.numpy()
60
-
61
- mask = atom_mask.detach().cpu()
62
- if mask.ndim == 2:
63
- mask = mask[0]
64
- assert mask.ndim == 1, "Expected atom mask with shape [atoms]."
65
- assert mask.shape[0] == selected_coords.shape[0], "Atom mask/coord size mismatch."
66
- assert torch.any(mask > 0), "Atom mask has no valid atoms for CIF export."
67
- valid_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor[mask > 0]))
68
- assert not torch.any(valid_non_finite), (
69
- "CIF export has non-finite coordinates in unmasked atoms. "
70
- f"Non-finite count: {int(valid_non_finite.sum().item())}"
71
- )
72
-
73
- b_iso = _confidence_per_atom(
74
- plddt=plddt,
75
- atom_to_residue=structure_template.atom_residue_index,
76
- num_atoms=structure_template.num_atoms,
77
- sample_index=sample_index,
78
- )
79
- assert b_iso.shape[0] == structure_template.num_atoms
80
-
81
- lines = [
82
- "data_boltz2_prediction",
83
- "#",
84
- "loop_",
85
- "_atom_site.group_PDB",
86
- "_atom_site.id",
87
- "_atom_site.type_symbol",
88
- "_atom_site.label_atom_id",
89
- "_atom_site.label_comp_id",
90
- "_atom_site.label_asym_id",
91
- "_atom_site.label_seq_id",
92
- "_atom_site.Cartn_x",
93
- "_atom_site.Cartn_y",
94
- "_atom_site.Cartn_z",
95
- "_atom_site.occupancy",
96
- "_atom_site.B_iso_or_equiv",
97
- "_atom_site.pdbx_PDB_model_num",
98
- ]
99
-
100
- atom_id = 1
101
- for idx in range(structure_template.num_atoms):
102
- if mask[idx] <= 0:
103
- continue
104
-
105
- residue_idx = structure_template.atom_residue_index[idx]
106
- residue_name = structure_template.residue_names[residue_idx]
107
- atom_name = structure_template.atom_names[idx]
108
- element = structure_template.atom_elements[idx]
109
- chain_id = structure_template.atom_chain_id[idx]
110
- x_val, y_val, z_val = selected_coords[idx].tolist()
111
- b_factor = float(b_iso[idx])
112
-
113
- line = (
114
- f"ATOM {atom_id} {element} {atom_name} {residue_name} {chain_id} "
115
- f"{residue_idx + 1} {x_val:.3f} {y_val:.3f} {z_val:.3f} 1.00 {b_factor:.2f} 1"
116
- )
117
- lines.append(line)
118
- atom_id += 1
119
-
120
- lines.append("#")
121
- text = "\n".join(lines) + "\n"
122
-
123
- out_path = Path(output_path)
124
- out_path.parent.mkdir(parents=True, exist_ok=True)
125
- out_path.write_text(text, encoding="utf-8")
126
- return str(out_path)
 
1
+ from pathlib import Path
2
+ from typing import List, Optional
3
+
4
+ import numpy as np
5
+ import torch
6
+
7
+ from .minimal_structures import ProteinStructureTemplate
8
+
9
+
10
+ def _confidence_per_atom(
11
+ plddt: Optional[torch.Tensor],
12
+ atom_to_residue: List[int],
13
+ num_atoms: int,
14
+ sample_index: int,
15
+ ) -> np.ndarray:
16
+ if plddt is None:
17
+ return np.ones((num_atoms,), dtype=np.float32) * 100.0
18
+
19
+ values = plddt.detach().cpu()
20
+ if values.ndim == 1:
21
+ values = values.unsqueeze(0)
22
+ assert values.ndim == 2, "Expected pLDDT with shape [samples, tokens/atoms]."
23
+ assert sample_index < values.shape[0], "sample_index out of range for pLDDT."
24
+
25
+ selected = values[sample_index]
26
+ if selected.shape[0] == num_atoms:
27
+ return (selected.numpy() * 100.0).astype(np.float32)
28
+
29
+ num_residues = max(atom_to_residue) + 1
30
+ if selected.shape[0] == num_residues:
31
+ expanded = np.zeros((num_atoms,), dtype=np.float32)
32
+ selected_np = selected.numpy()
33
+ for atom_idx, residue_idx in enumerate(atom_to_residue):
34
+ expanded[atom_idx] = selected_np[residue_idx] * 100.0
35
+ return expanded
36
+
37
+ return np.ones((num_atoms,), dtype=np.float32) * 100.0
38
+
39
+
40
+ def write_cif(
41
+ structure_template: ProteinStructureTemplate,
42
+ atom_coords: torch.Tensor,
43
+ atom_mask: torch.Tensor,
44
+ output_path: str,
45
+ plddt: Optional[torch.Tensor] = None,
46
+ sample_index: int = 0,
47
+ ) -> str:
48
+ coords = atom_coords.detach().cpu()
49
+ if coords.ndim == 2:
50
+ coords = coords.unsqueeze(0)
51
+ assert coords.ndim == 3, "Expected coordinates with shape [samples, atoms, 3]."
52
+ assert sample_index < coords.shape[0], "sample_index out of range."
53
+ selected_coords_tensor = coords[sample_index]
54
+ all_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor))
55
+ assert not torch.any(all_non_finite), (
56
+ "CIF export received non-finite coordinates. "
57
+ f"Non-finite count: {int(all_non_finite.sum().item())}"
58
+ )
59
+ selected_coords = selected_coords_tensor.numpy()
60
+
61
+ mask = atom_mask.detach().cpu()
62
+ if mask.ndim == 2:
63
+ mask = mask[0]
64
+ assert mask.ndim == 1, "Expected atom mask with shape [atoms]."
65
+ assert mask.shape[0] == selected_coords.shape[0], "Atom mask/coord size mismatch."
66
+ assert torch.any(mask > 0), "Atom mask has no valid atoms for CIF export."
67
+ valid_non_finite = torch.logical_not(torch.isfinite(selected_coords_tensor[mask > 0]))
68
+ assert not torch.any(valid_non_finite), (
69
+ "CIF export has non-finite coordinates in unmasked atoms. "
70
+ f"Non-finite count: {int(valid_non_finite.sum().item())}"
71
+ )
72
+
73
+ b_iso = _confidence_per_atom(
74
+ plddt=plddt,
75
+ atom_to_residue=structure_template.atom_residue_index,
76
+ num_atoms=structure_template.num_atoms,
77
+ sample_index=sample_index,
78
+ )
79
+ assert b_iso.shape[0] == structure_template.num_atoms
80
+
81
+ lines = [
82
+ "data_boltz2_prediction",
83
+ "#",
84
+ "loop_",
85
+ "_atom_site.group_PDB",
86
+ "_atom_site.id",
87
+ "_atom_site.type_symbol",
88
+ "_atom_site.label_atom_id",
89
+ "_atom_site.label_comp_id",
90
+ "_atom_site.label_asym_id",
91
+ "_atom_site.label_seq_id",
92
+ "_atom_site.Cartn_x",
93
+ "_atom_site.Cartn_y",
94
+ "_atom_site.Cartn_z",
95
+ "_atom_site.occupancy",
96
+ "_atom_site.B_iso_or_equiv",
97
+ "_atom_site.pdbx_PDB_model_num",
98
+ ]
99
+
100
+ atom_id = 1
101
+ for idx in range(structure_template.num_atoms):
102
+ if mask[idx] <= 0:
103
+ continue
104
+
105
+ residue_idx = structure_template.atom_residue_index[idx]
106
+ residue_name = structure_template.residue_names[residue_idx]
107
+ atom_name = structure_template.atom_names[idx]
108
+ element = structure_template.atom_elements[idx]
109
+ chain_id = structure_template.atom_chain_id[idx]
110
+ x_val, y_val, z_val = selected_coords[idx].tolist()
111
+ b_factor = float(b_iso[idx])
112
+
113
+ line = (
114
+ f"ATOM {atom_id} {element} {atom_name} {residue_name} {chain_id} "
115
+ f"{residue_idx + 1} {x_val:.3f} {y_val:.3f} {z_val:.3f} 1.00 {b_factor:.2f} 1"
116
+ )
117
+ lines.append(line)
118
+ atom_id += 1
119
+
120
+ lines.append("#")
121
+ text = "\n".join(lines) + "\n"
122
+
123
+ out_path = Path(output_path)
124
+ out_path.parent.mkdir(parents=True, exist_ok=True)
125
+ out_path.write_text(text, encoding="utf-8")
126
+ return str(out_path)