Performance

June 4, 2026 · View on GitHub

pyconfind ships two interchangeable contact-degree backends:

  • python — pure NumPy + SciPy cKDTree, the reference implementation.
  • numba — a JIT-compiled, multi-threaded kernel (the default when Numba is installed; pip install pyconfind[fast]).

Both produce results identical to ~1e-15 (far below the 1e-6 print precision), so output stays byte-identical to the C++ reference either way. The pure-Python backend already beats the hand-tuned C++ binary; the Numba backend is faster again on top of that.

When pyconfind[fast] is installed, three numba kernels run regardless of the backend= choice — they accelerate steps that are not contact-degree computation and are hand-scalarized to be bit-equivalent to the original NumPy code:

  • structure._dihedrals_kernel — batched phi/psi/omega computation
  • geometry._place_batch_kernel — IC placement of all rotamers per call
  • contacts_numba — the contact-degree kernel itself (this is what backend="numba" selects; backend="python" falls back to NumPy/SciPy for the contact step only)

Numbers

Per-structure analysis time vs. sequence length, rotamer library pre-loaded and excluded from every measurement (the realistic batch case). Eleven structures spanning ~88-555 residues, measured on the same machine. C++ library-load wall time (~6.6 s on the bench machine) was measured separately via confind --pL and subtracted from each C++ data point so it isn't counted twice.

Full analysis (native_only=False)

StructureResiduesnumpynumbaC++ (analysis only)numba vs C++
AF-A1L190-F1882.74 s1.18 s8.62 s7.3×
AF-A6NNB3-F11324.44 s1.93 s14.41 s7.5×
AF-A6NI61-F12219.32 s3.37 s30.46 s9.0×
1AB924213.10 s3.85 s39.31 s10.2×
AF-A1L3X0-F128114.00 s4.49 s44.55 s9.9×
1C0835019.66 s5.96 s64.20 s10.8×
1B0R37521.93 s6.42 s69.33 s10.8×
1BWU43024.92 s6.99 s76.40 s10.9×
1AVG44225.42 s7.35 s81.05 s11.0×
1C0448825.13 s7.44 s71.94 s9.7×
1BQL55533.16 s9.76 s105.74 s10.8×

Native-only (native_only=True)

StructureResiduesnumpynumba
AF-A1L190-F1880.098 s0.066 s
AF-A6NNB3-F11320.144 s0.069 s
AF-A6NI61-F12210.263 s0.127 s
1AB92420.310 s0.145 s
AF-A1L3X0-F12810.390 s0.239 s
1C083500.515 s0.234 s
1B0R3750.651 s0.303 s
1BWU4300.554 s0.262 s
1AVG4420.736 s0.347 s
1C044880.600 s0.322 s
1BQL5550.784 s0.359 s

Median speedups (numba backend, library pre-loaded everywhere):

  • numba vs C++ (full mode): 10.2× (range 7.3-11.0×)
  • numba vs numpy (full mode): 3.4×
  • native_only=True vs full mode (numba): ~26× faster again — every structure in this set finishes in under 0.36 s, smaller ones in ~0.07 s.

The IC numba kernel (this branch) takes the full-mode numba backend from ~7.8× → ~10.2× over C++ and lifts native-only numba by another ~+25% median on top of the phi/psi gains. Raw data: docs/timing_results.json.

Where the time goes

The two costs are rotamer building (IC placement + backbone-clash pruning) and the per-pair contact-degree computation. The latter scales ~O(N²) in residue count (mitigated by the CA-distance cutoff).

The pure-Python contact path was first made fast by:

  1. Vectorizing the inner atom-neighbor loop with cKDTree.sparse_distance_matrix + NumPy scatter-adds, replacing a ~30M-iteration Python loop (1UBQ contact step: 13.7 s → 4.8 s).
  2. Hoisting per-position constants (bounding boxes, weight sums) out of the per-pair loop and replacing np.cross in the IC builder with direct component arithmetic.

The Numba backend (contacts_numba.py) then replaces the contact computation with a JIT-compiled, multi-threaded kernel — ~4.6× faster than the already-optimized Python contact step (470-residue structure: 10.9 s → 2.4 s), making rotamer building the new dominant cost.

A second numba kernel (structure._dihedrals_kernel) batches the phi/psi/omega computation across all positions, replacing what used to be a per-position np.cross/np.dot loop. This matters most for native_only=True runs, where rotamer building is cheap and the dihedral pass would otherwise be a sizable fraction of the call. The dihedral arithmetic is hand-scalarized over the length-3 vectors so it is bit-equivalent to the original — the byte-identity goldens are the canary.

A third numba kernel (geometry._place_batch_kernel) handles the inner IC placement for every rotamer at every position. The backbone-clash prune then compares the placed coordinates against a hard 2.0 Å threshold; because that threshold turns infinitesimal FP perturbations into a discrete yes/no decision, this kernel was held back until we could verify it preserves byte-identity. The kernel is hand-scalarized over the length-3 vectors and uses fastmath=False, which makes its arithmetic bit-equivalent to the original NumPy form. Validation:

  • All in-repo byte-identity goldens (1CRN, 1UBQ, 1EJG, 5TRU bio-assembly 1) pass.
  • A wider sweep over 77 real structures (mixed PDB + AlphaFold DB, 88-876 residues) shows 77/77 outputs byte-identical to the pure-NumPy IC builder on main — no new mismatches against the C++ reference, even on the insertion-code structures documented in docs/stress_test_results.md.

The contact-degree backend remains safe to accelerate for a different reason: floating-point reordering there only perturbs the 6th+ decimal of a degree, never a discrete decision.

Reproduce

python scripts/benchmark_v2.py \
    --rLib original-source/confind-msl/rotlibs \
    --cpp  original-source/confind-msl/mslib/bin/confind \
    --tiny tests/data/structures/1CRN.pdb \
    --out  docs/timing_results.json \
    <structure.pdb> [<structure2.pdb> ...]

python scripts/plot_timing_vs_length.py

--tiny is the small reference PDB used to measure the C++ library-load overhead (subtracted from each C++ data point so the comparison is fair).