File size: 15,104 Bytes
3e62986
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
// Copyright 2024 DeepMind Technologies Limited
//
// AlphaFold 3 source code is licensed under CC BY-NC-SA 4.0. To view a copy of
// this license, visit https://creativecommons.org/licenses/by-nc-sa/4.0/
//
// To request access to the AlphaFold 3 model parameters, follow the process set
// out at https://github.com/google-deepmind/alphafold3. You may only use these
// if received directly from Google. Use is subject to terms of use available at
// https://github.com/google-deepmind/alphafold3/blob/main/WEIGHTS_TERMS_OF_USE.md

#include <cstddef>
#include <iterator>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "absl/algorithm/container.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/string_view.h"
#include "absl/types/span.h"
#include "alphafold3/parsers/cpp/cif_dict_lib.h"
#include "alphafold3/structure/cpp/mmcif_struct_conn.h"

namespace alphafold3 {

namespace {

struct AtomId {
  absl::string_view chain_id;
  absl::string_view res_id_1;
  absl::string_view res_id_2;
  absl::string_view atom_name;
  absl::string_view alt_id;

  friend bool operator==(const AtomId&, const AtomId&) = default;
  template <typename H>
  friend H AbslHashValue(H h, const AtomId& m) {
    return H::combine(std::move(h), m.chain_id, m.res_id_1, m.res_id_2,
                      m.atom_name, m.alt_id);
  }
};

using StringArrayRef = absl::Span<const std::string>;
using BondIndexByAtom = absl::flat_hash_map<AtomId, std::vector<std::size_t>>;
using BondAtomIndices = std::vector<std::size_t>;

// Returns whether each container is the same size.
template <typename C, typename... Cs>
bool AreSameSize(const C& c, const Cs&... cs) {
  return ((c.size() == cs.size()) && ...);
}

struct ColumnSpec {
  absl::string_view chain_id_col;
  absl::string_view res_id_1_col;
  absl::string_view res_id_2_col;
  absl::string_view atom_name_col;
  std::optional<absl::string_view> alt_id_col;  // Not used by OpenMM.
};

class AtomColumns {
 public:
  static absl::StatusOr<AtomColumns> Create(const CifDict& mmcif,
                                            const ColumnSpec& column_spec) {
    StringArrayRef chain_id = mmcif[column_spec.chain_id_col];
    StringArrayRef res_id_1 = mmcif[column_spec.res_id_1_col];
    StringArrayRef res_id_2 = mmcif[column_spec.res_id_2_col];
    StringArrayRef atom_name = mmcif[column_spec.atom_name_col];
    if (!AreSameSize(chain_id, res_id_1, res_id_2, atom_name)) {
      return absl::InvalidArgumentError(absl::StrCat(
          "Atom columns are not the same size. ",                       //
          "len(", column_spec.chain_id_col, ")=", chain_id.size(),      //
          ", len(", column_spec.res_id_1_col, ")=", res_id_1.size(),    //
          ", len(", column_spec.res_id_2_col, ")=", res_id_2.size(),    //
          ", len(", column_spec.atom_name_col, ")=", atom_name.size(),  //
          "."));
    }
    if (column_spec.alt_id_col.has_value()) {
      StringArrayRef alt_id = mmcif[*column_spec.alt_id_col];
      if (!AreSameSize(alt_id, chain_id)) {
        return absl::InvalidArgumentError(absl::StrCat(
            "Atom columns are not the same size. ",                   //
            "len(", column_spec.chain_id_col, ")=", chain_id.size(),  //
            ", len(", *column_spec.alt_id_col, ")=", alt_id.size(),   //
            "."));
      }
      return AtomColumns(chain_id, res_id_1, res_id_2, atom_name, alt_id,
                         column_spec);
    } else {
      return AtomColumns(chain_id, res_id_1, res_id_2, atom_name, std::nullopt,
                         column_spec);
    }
  }

  inline std::size_t size() const { return size_; }

  absl::string_view GetNormalizedAltId(const std::size_t index) const {
    constexpr absl::string_view kFullStop = ".";
    if (alt_id_.has_value()) {
      absl::string_view alt_id = (*alt_id_)[index];
      return alt_id == "?" ? kFullStop : alt_id;
    } else {
      return kFullStop;
    }
  }

  AtomId GetAtom(const std::size_t index) const {
    return {.chain_id = chain_id_[index],
            .res_id_1 = res_id_1_[index],
            .res_id_2 = res_id_2_[index],
            .atom_name = atom_name_[index],
            .alt_id = GetNormalizedAltId(index)};
  }

  std::string GetAtomString(const std::size_t index) const {
    std::string alt_id_col;
    if (column_spec_.alt_id_col.has_value()) {
      alt_id_col = *column_spec_.alt_id_col;
    } else {
      alt_id_col = "default label_alt_id";
    }
    return absl::StrCat(
        column_spec_.chain_id_col, "=", chain_id_[index], ", ",    //
        column_spec_.res_id_1_col, "=", res_id_1_[index], ", ",    //
        column_spec_.res_id_2_col, "=", res_id_2_[index], ", ",    //
        column_spec_.atom_name_col, "=", atom_name_[index], ", ",  //
        alt_id_col, "=", GetNormalizedAltId(index));               //
  }

 private:
  AtomColumns(StringArrayRef chain_id, StringArrayRef res_id_1,
              StringArrayRef res_id_2, StringArrayRef atom_name,
              std::optional<StringArrayRef> alt_id,
              const ColumnSpec& column_spec)
      : chain_id_(chain_id),
        res_id_1_(res_id_1),
        res_id_2_(res_id_2),
        atom_name_(atom_name),
        alt_id_(alt_id),
        column_spec_(column_spec),
        size_(chain_id.size()) {}
  StringArrayRef chain_id_;
  StringArrayRef res_id_1_;
  StringArrayRef res_id_2_;
  StringArrayRef atom_name_;
  std::optional<StringArrayRef> alt_id_;
  ColumnSpec column_spec_;
  std::size_t size_;
};

// Adds the atom index to any rows in the bond table involving that atom.
absl::Status FillInBondsForAtom(const BondIndexByAtom& bond_index_by_atom,
                                const AtomId& atom,
                                const std::size_t atom_index,
                                BondAtomIndices& bond_atom_indices) {
  if (auto bond_index_it = bond_index_by_atom.find(atom);
      bond_index_it != bond_index_by_atom.end()) {
    for (std::size_t bond_index : bond_index_it->second) {
      if (bond_index < 0 || bond_index >= bond_atom_indices.size()) {
        return absl::OutOfRangeError(
            absl::StrCat("Bond index out of range: ", bond_index));
      }
      bond_atom_indices[bond_index] = atom_index;
    }
  }
  return absl::OkStatus();
}

// Checks that the CifDict has all of the columns in the column spec.
bool HasAllColumns(const CifDict& mmcif, const ColumnSpec& columns) {
  return mmcif.Contains(columns.chain_id_col) &&
         mmcif.Contains(columns.res_id_1_col) &&
         mmcif.Contains(columns.res_id_2_col) &&
         mmcif.Contains(columns.atom_name_col) &&
         (!columns.alt_id_col.has_value() ||
          mmcif.Contains(*columns.alt_id_col));
}

// Fully specified ptnr1 atom.
constexpr ColumnSpec kStructConnPtnr1ColumnsFull{
    .chain_id_col = "_struct_conn.ptnr1_label_asym_id",
    .res_id_1_col = "_struct_conn.ptnr1_auth_seq_id",
    .res_id_2_col = "_struct_conn.pdbx_ptnr1_PDB_ins_code",
    .atom_name_col = "_struct_conn.ptnr1_label_atom_id",
    .alt_id_col = "_struct_conn.pdbx_ptnr1_label_alt_id",
};

// Fully specified ptnr2 atom.
constexpr ColumnSpec kStructConnPtnr2ColumnsFull{
    .chain_id_col = "_struct_conn.ptnr2_label_asym_id",
    .res_id_1_col = "_struct_conn.ptnr2_auth_seq_id",
    .res_id_2_col = "_struct_conn.pdbx_ptnr2_PDB_ins_code",
    .atom_name_col = "_struct_conn.ptnr2_label_atom_id",
    .alt_id_col = "_struct_conn.pdbx_ptnr2_label_alt_id",
};

// Columns used by OpenMM for ptnr1 atoms.
constexpr ColumnSpec kStructConnPtnr1OpenMM{
    .chain_id_col = "_struct_conn.ptnr1_label_asym_id",
    .res_id_1_col = "_struct_conn.ptnr1_label_seq_id",
    .res_id_2_col = "_struct_conn.ptnr1_label_comp_id",
    .atom_name_col = "_struct_conn.ptnr1_label_atom_id",
    .alt_id_col = std::nullopt,
};

// Columns used by OpenMM for ptnr2 atoms.
constexpr ColumnSpec kStructConnPtnr2OpenMM{
    .chain_id_col = "_struct_conn.ptnr2_label_asym_id",
    .res_id_1_col = "_struct_conn.ptnr2_label_seq_id",
    .res_id_2_col = "_struct_conn.ptnr2_label_comp_id",
    .atom_name_col = "_struct_conn.ptnr2_label_atom_id",
    .alt_id_col = std::nullopt,
};

// Fully specified atom sites.
constexpr ColumnSpec kAtomSiteColumnsFull{
    .chain_id_col = "_atom_site.label_asym_id",
    .res_id_1_col = "_atom_site.auth_seq_id",
    .res_id_2_col = "_atom_site.pdbx_PDB_ins_code",
    .atom_name_col = "_atom_site.label_atom_id",
    .alt_id_col = "_atom_site.label_alt_id",
};

// Atom site columns used to match OpenMM _struct_conn tables.
constexpr ColumnSpec kAtomSiteColumnsOpenMM{
    .chain_id_col = "_atom_site.label_asym_id",
    .res_id_1_col = "_atom_site.label_seq_id",
    .res_id_2_col = "_atom_site.label_comp_id",
    .atom_name_col = "_atom_site.label_atom_id",
    .alt_id_col = "_atom_site.label_alt_id",
};

}  // namespace

absl::StatusOr<std::pair<BondAtomIndices, BondAtomIndices>> GetBondAtomIndices(
    const CifDict& mmcif, absl::string_view model_id) {
  ColumnSpec ptnr1_columns, ptnr2_columns, atom_site_columns;

  if (HasAllColumns(mmcif, kStructConnPtnr1ColumnsFull) &&
      HasAllColumns(mmcif, kStructConnPtnr2ColumnsFull)) {
    ptnr1_columns = kStructConnPtnr1ColumnsFull;
    ptnr2_columns = kStructConnPtnr2ColumnsFull;
    atom_site_columns = kAtomSiteColumnsFull;
  } else {
    ptnr1_columns = kStructConnPtnr1OpenMM;
    ptnr2_columns = kStructConnPtnr2OpenMM;
    atom_site_columns = kAtomSiteColumnsOpenMM;
  }

  absl::StatusOr<AtomColumns> ptnr1_atoms =
      AtomColumns::Create(mmcif, ptnr1_columns);
  if (!ptnr1_atoms.ok()) {
    return ptnr1_atoms.status();
  }
  absl::StatusOr<AtomColumns> ptnr2_atoms =
      AtomColumns::Create(mmcif, ptnr2_columns);
  if (!ptnr2_atoms.ok()) {
    return ptnr2_atoms.status();
  }
  StringArrayRef struct_conn_id = mmcif["_struct_conn.id"];
  if (!AreSameSize(struct_conn_id, *ptnr1_atoms, *ptnr2_atoms)) {
    return absl::InvalidArgumentError(absl::StrCat(
        "Invalid '_struct_conn.' loop. ",                  //
        "len(id) = ", struct_conn_id.size(), ", ",         //
        "len(ptnr1_atoms) = ", ptnr1_atoms->size(), ", ",  //
        "len(ptnr2_atoms) = ", ptnr2_atoms->size(), "."    //
        ));
  }

  absl::StatusOr<AtomColumns> atoms =
      AtomColumns::Create(mmcif, atom_site_columns);
  if (!atoms.ok()) {
    return atoms.status();
  }
  StringArrayRef atom_site_id = mmcif["_atom_site.id"];
  StringArrayRef atom_site_model_id = mmcif["_atom_site.pdbx_PDB_model_num"];
  if (!AreSameSize(atom_site_id, atom_site_model_id, *atoms)) {
    return absl::InvalidArgumentError(absl::StrCat(
        "Invalid '_atom_site.' loop. ",                                //
        "len(id)= ", atom_site_id.size(), ", ",                        //
        "len(pdbx_PDB_model_num)= ", atom_site_model_id.size(), ", ",  //
        "len(atoms)= ", atoms->size(), "."));                          //
  }

  // Build maps from atom ID tuples to the rows in _struct_conn where that
  // atom appears (NB could be multiple).
  const std::size_t struct_conn_size = struct_conn_id.size();
  BondIndexByAtom ptnr1_rows_by_atom(struct_conn_size);
  BondIndexByAtom ptnr2_rows_by_atom(struct_conn_size);
  for (std::size_t i = 0; i < struct_conn_size; ++i) {
    ptnr1_rows_by_atom[ptnr1_atoms->GetAtom(i)].push_back(i);
    ptnr2_rows_by_atom[ptnr2_atoms->GetAtom(i)].push_back(i);
  }

  // Allocate two output arrays with one element per row in struct_conn, where
  // each element will be the index of that atom in the atom_site table.
  // Fill the arrays with atom_site_size, which is an invalid value, so that
  // we can check at the end that each atom has been found.
  const std::size_t atom_site_size = atom_site_id.size();
  BondAtomIndices ptnr1_atom_indices(struct_conn_size, atom_site_size);
  BondAtomIndices ptnr2_atom_indices(struct_conn_size, atom_site_size);

  bool model_id_ecountered = false;
  absl::flat_hash_set<absl::string_view> seen_alt_ids;
  for (std::size_t atom_i = 0; atom_i < atom_site_size; ++atom_i) {
    if (atom_site_model_id[atom_i] != model_id) {
      if (!model_id_ecountered) {
        continue;
      } else {
        // Models are contiguous so once we see a different model ID after
        // encountering our model ID then we can exit early.
        break;
      }
    } else {
      model_id_ecountered = true;
    }
    AtomId atom = atoms->GetAtom(atom_i);
    seen_alt_ids.insert(atom.alt_id);

    if (auto fill_in_bonds_status1 = FillInBondsForAtom(
            ptnr1_rows_by_atom, atom, atom_i, ptnr1_atom_indices);
        !fill_in_bonds_status1.ok()) {
      return fill_in_bonds_status1;
    }
    if (auto fill_in_bonds_status2 = FillInBondsForAtom(
            ptnr2_rows_by_atom, atom, atom_i, ptnr2_atom_indices);
        !fill_in_bonds_status2.ok()) {
      return fill_in_bonds_status2;
    }
  }
  // The seen_alt_ids check is a workaround for a known PDB issue: some mmCIFs
  // (2evw, 2g0v, 2g0x, 2g0z, 2g10, 2g11, 2g12, 2g14, 2grz, 2ntw as of 2024)
  // have multiple models and they set different whole-chain altloc in each
  // model. The bond table however doesn't distinguish between models, so there
  // are bonds that are valid only for some models. E.g. 2grz has model 1 with
  // chain A with altloc A, and model 2 with chain A with altloc B. The bonds
  // table lists a bond for each of these.

  // Check that a ptnr1 atom was found for every bond.
  if (auto row_it = absl::c_find(ptnr1_atom_indices, atom_site_size);
      row_it != ptnr1_atom_indices.end()) {
    if (seen_alt_ids.size() > 1 || seen_alt_ids.contains(".") ||
        seen_alt_ids.contains("?")) {
      std::size_t i = std::distance(ptnr1_atom_indices.begin(), row_it);
      return absl::InvalidArgumentError(
          absl::StrCat("Error parsing \"", mmcif.GetDataName(), "\". ",
                       "Cannot find atom for bond ID ", struct_conn_id[i], ": ",
                       ptnr1_atoms->GetAtomString(i)));
    }
  }

  // Check that a ptnr2 atom was found for every bond.
  if (auto row_it = absl::c_find(ptnr2_atom_indices, atom_site_size);
      row_it != ptnr2_atom_indices.end()) {
    if (seen_alt_ids.size() > 1 || seen_alt_ids.contains(".") ||
        seen_alt_ids.contains("?")) {
      std::size_t i = std::distance(ptnr2_atom_indices.begin(), row_it);
      return absl::InvalidArgumentError(
          absl::StrCat("Error parsing \"", mmcif.GetDataName(), "\". ",
                       "Cannot find atom for bond ID ", struct_conn_id[i], ": ",
                       ptnr2_atoms->GetAtomString(i)));
    }
  }

  if (!model_id_ecountered) {
    return absl::InvalidArgumentError(absl::StrCat(
        "Error parsing \"", mmcif.GetDataName(), "\". model_id \"", model_id,
        "\" not found in _atom_site.pdbx_PDB_model_num."));
  }

  return std::make_pair(std::move(ptnr1_atom_indices),
                        std::move(ptnr2_atom_indices));
}

}  // namespace alphafold3