Backlog
Backlog
Tier D: per-bond/per-angle SQL lookups for the AceDRG tables
The current SQLite backend (coot-utils/acedrg-sqlite-tables.cc) only
accelerates loading: prefetch_for_molecule() pulls whole hash-buckets
from acedrg.sqlite and feeds them into gemmi’s in-memory caches through
the insert_bond_row()/insert_angle_row() seam (gemmi PR #438), after
which gemmi’s search_bond_multilevel()/search_angle_multilevel() walk
those maps exactly as before. Bucket granularity is the ceiling: the
aromatic (sp²-C, sp²-C, in-ring) bucket alone is ~159k rows in the CCP4-9
COD data, so the measured 17× speedup on trivial molecules degrades to
~2-5× on real aromatic ligands (ATP 3.3×, A1DET 2.0×). The selectivity
that would shrink the fetch lives in the COD atom-type and neighbour
columns, which only exist mid-fill_bond — not at prefetch time.
Tier D replaces the walk itself: at each fallback level (12 for bonds, 6
for angles incl. the 5D wildcard), issue one indexed SELECT returning ≤1
row (~10 µs) instead of scanning the bucket. ~530 queries ≈ 5 ms per
molecule against the current 1.5-5 s. Detailed implementation plan (May
2026, written pre-split for the gemmi fork):
/hard-disk-partition-2/files/claude-play/notes/2026-05-31-acedrg-per-bond-sql.md.
What survives from that plan unchanged: the per-level SQL statements and
prepared-statement cache, the level semantics (incl. the 5D wildcard LIKE
trick and the ordered-map tie-break), the index pre-flight (Task 1), the
byte-identical 16-monomer harness discipline, and the open question about
where min_observations_* thresholds apply.
What the coot/gemmi split changes: the plan’s Tasks 3-7 all edit gemmi’s
acedrg_tables.cpp, which we no longer patch. The per-level lookup code
moves into coot-utils/acedrg-sqlite-tables.cc; the dispatch inside
search_*_multilevel() needs a NEW gemmi seam, since the fine-grained
COD atom types (a1_type_f etc.) are computed inside gemmi mid-search
and coot cannot reproduce them:
- Proposed seam (superseding the per-level idea — full spec in the next section): whole-function delegation. Gemmi defines plain-data
AcedrgBondSearchArgs/AcedrgAngleSearchArgsstructs and one optionalstd::functionhook per search function; the hook is authoritative when set, byte-identical fallback when unset. SQLite-free — same upstreamability argument as the #438 insert-row seam. - Coot implements the hook with the plan’s prepared-statement SQL against
acedrg.sqlite(built bycoot-make-acedrg-sqlite; verify the indexes there cover the per-level WHERE clauses — plan Task 1). - Once verified byte-identical,
prefetch_for_molecule()becomes dead weight for bonds/angles and is dropped (plan Task 7); HRS-backed levels (bond 9-11, angle 6) stay in-memory — those tables are small and eagerly loaded. - Fallback duplication of the ladder wholly inside coot (no seam) was considered and rejected: it would fork several hundred lines of intricate fallback logic and still couldn’t get the mid-search atom types.
Sequencing: (1) design + prototype the search seam on a gemmi fork branch, (2) propose it upstream alongside nudging #437/#438, (3) implement the coot-side lookups against the agreed signature, (4) harness, timing, drop prefetch. Real-world payoff on this box: pyrogen per-dictionary time in the tautomer pipeline (44 dictionaries/run) is dominated by exactly this prefetch cost.
Tier D hand-off spec: the AceDRG search seam (gemmi side)
Hand-off spec for the gemmi session (2026-09-14). Defines the seam gemmi
must grow so that coot can implement per-bond/per-angle SQL lookups
(Tier D above) without gemmi depending on SQLite. Written against
/home/paule/Projects/gemmi/fork-5/gemmi, branch master, commit
2e4998e9 (upstream official-master merged; includes the
insert_bond_row/insert_angle_row seam of gemmi PR #438). All
file:line references below are to that tree.
The old (pre-split) implementation plan
/hard-disk-partition-2/files/claude-play/notes/2026-05-31-acedrg-per-bond-sql.md
has its Tasks 3-7 superseded by this spec; its SQL/index/harness material
is still useful to the coot side.
Design decision: whole-function delegation, not per-level hooks
The May plan proposed per-level lookup hooks. That no longer fits: the
current search_bond_multilevel() (src/acedrg_tables.cpp:2509) entangles
control flow with in-memory map presence — the start_level gating
(:2633-2643), the in-ring Y/N fallback probe against
bond_full_4prefix_keys_ (:2534-2547), and the has_a1_class_only
post-search override branch (:2837-2867). search_angle_multilevel()
(:3040) likewise has the same-hash property-swap retry loop (:3106-3122)
and the 5D relaxed match (:3192-3211). Per-level hooks would need probe
hooks for all of these.
Instead: gemmi grows ONE optional hook per search function. When the hook is set, the search function computes its derived keys exactly as today, calls the hook, and returns the hook’s answer. All fallback semantics (levels, gating, probes, overrides, thresholds) become the hook implementer’s responsibility, verified byte-identical by the harness. When the hook is unset — or declines — behaviour is bit-identical to today. This keeps the gemmi patch small, dependency-free, and upstreamable on the same argument as PR #438.
Gemmi-side deliverable
1. Two plain-data args structs (public, in acedrg_tables.hpp). Field values are exactly the locals the search functions already compute.
// Arguments handed to an external bond-search hook. Atoms are already
// ordered by order_two_atoms() (hash, then cod_main, then id — see
// src/acedrg_tables.cpp:1215); "1"/"2" below mean left/right in that
// order, matching the bond-table column convention.
struct AcedrgBondSearchArgs {
int ha1 = 0, ha2 = 0; // hashing values, ordered
std::string hybr_comb; // "SP2_SP2" etc., two names sorted, '_'-joined
bool same_ring = false; // are_in_same_ring(a1, a2) — RAW, pre-probe
std::string a1_nb2, a2_nb2; // nb2_symb
std::string a1_nb, a2_nb; // nb1nb2_sp
std::string a1_type, a2_type; // cod_main
std::string a1_class, a2_class; // cod_class_no_charge
int num_th = 3; // min_observations_bond, or 1 for As/Ge pairs
};
// Arguments for an external angle-search hook. ha1 is the CENTER atom's
// hash; flanks are ordered by order_two_atoms() (min = "2", max = "3"),
// matching the angle-table column convention.
struct AcedrgAngleSearchArgs {
int ha1 = 0, ha2 = 0, ha3 = 0; // center, flank_min, flank_max
int ring_val = 0; // angle_ring_size(center, fmin, fmax)
std::string center_hybr; // hybridization_to_string(center) — for the 5D relaxed match
std::string value_key; // cat(ring_val, ':', hc_'_'-joined-sorted-flank-hybrs)
std::string a1_root, a2_root, a3_root; // cod_root
std::string a1_nb2, a2_nb2, a3_nb2; // nb2_symb
std::string a1_nb, a2_nb, a3_nb; // nb_symb
std::string a1_type, a2_type, a3_type; // cod_main
int min_obs = 3; // min_obs_eff (see :3091)
};
2. Two hook members + setters on AcedrgTables.
// External search hooks (e.g. a SQL backend living outside gemmi).
// Contract: return true when the hook is AUTHORITATIVE — `out` then
// carries the complete search result, including the no-match sentinel
// (bond: out.level == -1; angle: default CodStats with NaN value, and
// *out_level untouched i.e. left at the caller's initial value).
// Return false to decline (backend unavailable, error): gemmi then
// falls through to the normal in-memory walk.
std::function<bool(const AcedrgBondSearchArgs&, CodStats& out)>
external_bond_search;
std::function<bool(const AcedrgAngleSearchArgs&, CodStats& out,
int* out_level)> external_angle_search;
Public members (like verbose) or private with setters — implementer’s
choice; public members are simpler and match the class’s existing style.
3. Dispatch points.
search_bond_multilevel()— after the derived-key block ends at src/acedrg_tables.cpp:2565 (num_thincluding the As/Ge exception) and before the verbose print/key construction at :2567. BuildAcedrgBondSearchArgsfrom the localsha1, ha2, hybr_comb, the raware_in_same_ring(a1, a2)result (NOT the possibly-swappedin_ringstring — move the dispatch above the probe at :2534, or re-derive),a1_nb2 … a2_class,num_th. Thenif (external_bond_search) { CodStats out; if (external_bond_search(args, out)) return out; }.search_angle_multilevel()— after the type strings at :3076, before the verbose print at :3078.min_obsfield =min_obs_effper :3091.if (external_angle_search) { CodStats out; if (external_angle_search(args, out, out_level)) return out; }.- No other call sites change.
search_bond_hrs,search_bond_en, metal bonds, torsions, prot-hydr distances all stay in-memory (their tables are small and eagerly loaded).
4. Acceptance criteria (gemmi side).
- Hooks unset ⇒ zero behaviour change (byte-identical
gemmi drgoutput on the 16-monomer harness, ASCII backend). - A trivial test hook that returns false ⇒ still byte-identical.
- Compiles without sqlite3 anywhere in gemmi.
- Commit in the style of
70087d54(the insert-row seam), PR-able upstream alongside #438.
Semantics inventory (what the coot-side hook must reproduce)
The hook must return exactly what the in-memory walk would. Read the two functions in full before implementing; the inventory below is the map, not the territory.
Bond (src/acedrg_tables.cpp:2509-2878):
- In-ring probe (:2533-2547):
in_ring= same_ring ? “Y” : “N”, but if no rows exist for(ha1, ha2, hybr_comb, in_ring)and rows exist for the flipped value, flip it. In SQL: two EXISTS probes. - Exact-codClass pre-check (:2581-2599): row at (full 10-part key + a1_class + a2_class) with
count >= num_threturns as level 0. If the a1_class node exists but the a2_class node doesn’t, sethas_a1_class_only— it changes the endgame (below). start_levelgating (:2633-2643): computed from key presence (has_a2_type ⇒ 1, has_a1_type ⇒ 2, a2_nb ⇒ 4, a1_nb ⇒ 5, a2_nb2 ⇒ 7, a1_nb2 ⇒ 8, in_ring ⇒ 10, hybr ⇒ 11). Presence = EXISTS probes in SQL. Note gating is not merely an optimisation: starting at level N skips the more-specific levels even if their queries would coincidentally aggregate something.- Levels (loop :2654-2835), with the containers they walk:
- 0:
bond_idx_1d_[key_8][a1_type][a2_type]—.front()of the vector (FIRST-LOADED row: SQL must preserve/order by insertion rowid). Threshold:vs.count >= num_th(observation count). - 1: fronts of every a2_type group under a1_type, PLUS fronts of (other-a1_type, ==a2_type) groups; aggregate. Threshold: number of contributing entries
>= num_th. - 2: fronts of (a1_type != given, a2_type == given) groups; aggregate; entry-count threshold.
- 3: all rows at
bond_idx_2d_[key_4][a1_nb2][a2_nb2][a1_nb][a2_nb]; aggregate; entry-count = row count. - 4: all rows under a1_nb (any a2_nb) plus rows under (other a1_nb, ==a2_nb); aggregate.
- 5: only rows under (other a1_nb, ==a2_nb); aggregate.
- 6: every row under
[key_4][a1_nb2][a2_nb2]; aggregate. - 7: every row under
[key_4][a1_nb2](any a2_nb2 …) plus rows under (other a1_nb2, ==a2_nb2); aggregate. - 8: only rows under (other a1_nb2, ==a2_nb2); aggregate.
- 9/10/11:
bond_hasp_2d_[key_4]/bond_hasp_1d_[ha1|ha2|hybr]/bond_hasp_0d_[ha1|ha2]vectors; aggregate; threshold is mere presence.
- 0:
- Endgame (:2837-2870): normally return the first level that met threshold. But when
has_a1_class_only: prefer exact (a1_type, a2_type) front ifcount >= num_th(as level 0), else exact (a1_nb, a2_nb) aggregate regardless of threshold (as level 3), else the matched level. No match at all ⇒level = -1sentinel. - Aggregation formula (
aggregate_stats, :2359-2384): weighted meanSUM(v*n)/SUM(n); pooled sigmasqrt(|SUM((n-1)*s² + n*v²) − mean*SUM(v*n)| / (N−1))(divide by N when N==1); count =SUM(n). Single-entry input returns the entry unchanged. Expressible as SQL SUMs with the final arithmetic in C++.
Angle (src/acedrg_tables.cpp:3040-3226):
- All levels are flat compound-key lookups returning
.front()(first-loaded row) — no aggregation anywhere. Thresholdfront().count >= min_obsat levels 1D-5D; 6D is presence-only. - Same-hash retry (:3106-3122): when ha1==ha2 or ha1==ha3, levels 1D-4D are tried a second time with the center’s property columns swapped with the equal-hash flank’s. The 5D/6D levels are OUTSIDE the retry loop.
- Levels and keys: 1D = 16-part key (reported level 0), 2D = 13-part (1), 3D = 10-part (2), 4D = 7-part (3), 5D =
[ha1][ha2][ha3][value_key](4), 6D =ha1|ha2|ha3(6). Level 5 is never reported. - 5D relaxed match (:3192-3211): ONLY when the exact value_key is absent (an under-threshold exact entry falls through without relaxing). Scan value_keys in LEXICOGRAPHIC order (std::map order — SQL: ORDER BY value_key) for the first with prefix
ring_val:whose first ‘_‘-delimited hybr token equalscenter_hybr; try only that one, then stop. - Miss ⇒ default
CodStats()(NaN value), out_level untouched.
Cross-cutting:
- front() everywhere means the SQLite DB must preserve per-key row order matching the ASCII load order (rowid ASC from the existing builder is exactly that — verify
coot-make-acedrg-sqliteinserts in file order). - Level numbers matter downstream:
fill_bond(:2480) treatslevel >= 9like HRS (count-only acceptance) and returns the level as the approx-level; get them exactly right. - Verification: the 16-monomer byte-identical harness (
/hard-disk-partition-2/files/claude-play/experiments/compare_sqlite_vs_ascii.py) is the gate at every step, exactly as in the May plan. Timing targets also as there (A1DET from ~5.6 s to under ~1 s).
Tier D open questions
- Public members vs setters for the hooks (cosmetic; decide in gemmi session).
- Whether coot precomputes per-level aggregate tables at DB build time (turns aggregation levels into single-row SELECTs) or aggregates at query time with SQL SUMs. Query-time is simpler and the aggregation levels are rarely reached; start there.
- The May plan’s open question about threshold semantics is answered by the code read: bond levels 1-8 threshold on contributing-entry count, level 0 on observation count, 9-11 on presence; angles threshold on the front row’s observation count. The inventory above is authoritative as of commit 2e4998e9 — re-verify if gemmi master moves.
Replace quick_protein_donor_acceptors with a dictionary-aware donor/acceptor lookup
coot::quick_protein_donor_acceptors (geometry/protein-donor-acceptors.{hh,cc})
is a hard-coded (residue-name, atom-name) → hb_type map for the standard amino
acids only. It is used by the environment-distances display (“Constructor G” in
coords/Bond_lines.cc, ~line 3000) to decide whether a close contact is drawn as
a hydrogen bond (purple) or a plain contact.
Because it knows nothing about ligands, any non-protein atom pair
fails the lookup and gets the fallback colour — so ligand H-bonds are never
properly classified (e.g. the t3ND4 lactam O27 ↔ ASP main-chain O pair,
acceptor + acceptor, 2026-09-13). The replacement should get hb types from the
dictionaries: protein_geometry::get_h_bond_type(atom_name, monomer_name,
imol_enc) already maps _chem_comp_atom.type_energy → hb_t via the energy
lib, and protein_geom_p is in scope at the call site. The quick table could
remain as a fast path for protein atoms, or go entirely.
Quick-table defects all fixed 2026-09-13: THR “ OG1”, THR in the main-chain list, HOH “ O “ = HB_BOTH (explicit entry after the loop, overriding the loop’s blanket HB_ACCEPTOR), and n_res_types now comes from std::size(l). Consider #include <iterator> for std::size portability (GCC’s <iostream> provides it transitively; libc++ may not).
Speed up average-map generation by checking the grids
on_make_an_average_map_ok_button_clicked() (src/glade-callbacks-main-window.cc)
currently uses the general coot::util::average_map() route, which does not
require the contributing maps to share a grid sampling. It could be speeded up
by first checking that the grids of the contributing maps match, and if so using
the regen_map route (as regen_map_py() in src/c-interface-maps.cc does),
which averages in place and presumes matching grids.
Add property histograms behind the Layla QED desirability curves
The Layla QED tab draws the 8 ADS desirability curves d(x) (layla/ui.cpp,
draw_desirability_curve(); parameters in layla/qed.cpp). At some later stage
we would like to draw, behind each curve, the histogram of that property over
the reference set of 771 oral drugs from Bickerton et al. 2012 (as in Fig. 1 of
the paper) — light-blue bars under the curve.
Where the histogram data is: the paper’s supplementary Excel workbook
NIHMS50746-supplement-QEDExampleExcel.xlsx, sheet “Desirability Functions”,
773 rows = header + 771 drugs. Columns: A=Drug name, then the raw per-drug
properties B=MW, C=ALOGP, D=HBA, E=HBD, F=PSA, G=ROTB, H=AROM, I=ALERTS
(J-Q are the individual desirabilities, R/S the QED scores). This column order
matches the QED::QEDPropName enum. Histogramming columns B-I reproduces the
Fig. 1 bars. (The SuppInfo PDF and the main-text PDF do NOT contain this raw
data — only the fitted ADS parameters in Suppl. Table 2.)
To ship the bars we’d need to bake the 771-drug values (or precomputed bin counts per property) into the code/data, since we can’t depend on the Excel file at runtime.