Skip to content

Rebase/softcore v2 onto main#110

Open
David-Araripe wants to merge 124 commits into
mainfrom
rebase/softcore-v2-onto-main
Open

Rebase/softcore v2 onto main#110
David-Araripe wants to merge 124 commits into
mainfrom
rebase/softcore-v2-onto-main

Conversation

@David-Araripe

Copy link
Copy Markdown
Collaborator

Rebase softcore-methods-v2 onto main

Brings the feature/softcore-methods-v2 line of work up to date on top of main (124 commits). This is a broad update spanning the MD engine, the QligFEP setup layer, perturbation-network generation, system preparation, and the test suite. Highlights below, grouped by theme.

Soft-core methods

Adds a selectable soft-core scheme via --softcore-method {standard, beutler_coul, gapsys} (engine constants SC_STANDARD, SC_BEUTLER_COUL, SC_GAPSYS), threaded from the CLI through the FEP-file writer into all 15 nonbonded routines of the engine:

  • standard — soft-core on LJ only (default; prior behaviour).
  • beutler_coul — extends soft-core to Coulomb via a modified effective distance, with the Beutler-1994 (1-λ)² offset scaling.
  • gapsys — force-based linearization below a critical radius for both LJ and Coulomb, matched to Gapsys–Seeliger–de Groot (2012) Eqs. 1.1–1.4.

Mirrored into md_test.f90, initialized at FEP setup (softcore_init_gapsys), and covered by new tests.

Important

All three methods are in place and selectable, but the way Q builds dual-topology perturbations means none of them are actually triggered at runtime. The code paths exist, are threaded through the engine, and are exercised by tests — but Q's per-state Hamiltonian mixing with the dual-topology DUM convention leaves the soft-core offset structurally dormant: it evaluates to zero on the DUM (vanishing) side, and QligFEP's asymmetric [softcore] block disables it on the real side, so the offset never modifies the forces seen by the integrator. The three methods therefore produce statistically equivalent trajectories for routine congeneric FEP.

Perturbation-network generation

  • New qkonnektor CLI (konnektor-based networks) alongside the existing qlomap, with a RestraintScorer that combines LOMAP + RestraintSetter for better edge quality.
  • Network flexibility: --central_ligand (star networks), redundant-MST and n-edges network types, --self-solve (repair alignment/topology), --exp-key (standardized experimental-value naming), --separate_charges.
  • Ligand aligners reworked: rdkit-based aligner added; old GlobalLigandAligner renamed to fkcombuLigandAligner (clearer), with scaffold_lock, atom/bond-type options, and an options() inspector. Tiered BiasedConformerGenerator with topology-only MCS default.
  • Fixed the ΔG sign convention in qlomap/qkonnektor to match Q's thermodynamic cycle.

Template system refactor (partial migration)

Moves input-file generation off the legacy replacements-dictionary approach toward render-based templates in templates/:

  • MD, qprep, and qfep inputs now render from templates/ with structured string formatting.
  • The neq-related generators were migrated to the new system as well.
  • Not a full deprecation — the replacements path still exists; this PR moves the codebase toward the template approach rather than removing the old one. Golden-file "identical generation" tests lock the rendered output.

Charge-changing perturbations

  • Two charge-perturbation methods: co-alchemical water and counter-ion placement on the water leg to match the protein-leg total charge (ion-type aware, per force field).
  • New counter-ion placement module + wall-restraint support in the templates; wall restraints fixed for both co-alchemical waters and counter-ions.

System preparation robustness (qprep / pdb2amber)

  • pdb2amber + AMBER14sb hardened across many targets: proline handling, ATOM/HETATM-only parsing, TER-sensitivity.
  • Out-of-sphere neutralization: C-/N-terminal amino acids, insertion-code matching, DNA nucleotide removal; added DNA parameters and better DNA renaming.
  • Disulfide detection improved (dual cutoffs, CYX handling, HG removal, respect manual bonds).
  • qprep now parses the whole protein and reports all problematic atom/residue names instead of exiting on the first; unified execution/error handling between qprep_cli and QligFEP; hydrogen-placement optimization; POPC lipid charge-groups for sphere exclusion.

MD engine — minimization, performance, MPI

  • Energy minimization support in the engine with new [MD] parameters; eq1 default updated to the new protocol.
  • Pair-list construction sped up: O(N) linear exclusion scans replaced with O(log k) CSR binary search; cross-molecule charge-group pairs skip the exclusion scan.

Notable bugfixes

  • Lambda values: fixed rounding that wrote e.g. 0.988/0.013 instead of the intended values.
  • polarized vs polarised: golden files corrected to the spelling the engine actually reads (polarised was silently ignored, defaulting the parameters).
  • Fixed 1-4 epsilon (column 5) for hand-added AMBER14sb atom types.
  • Fixed qprep.f90 positional-argument bug; various UnboundLocalError/NameError/duplicate-logging fixes.

Charges, dependencies, tests

  • OpenFF latest iteration supported; NAGL-assigned partial charges as default; custom --nagl-model passthrough.
  • Local execution of QligFEP enabled.
  • Expanded test suite: RestraintSetter golden restraints, template identical-generation tests, qprep tests, softcore-method tests, conftest fixtures.

Notes for reviewers

  • The diff is large (~109k insertions) but heavily weighted by data/params/tutorial assets (benchmark ligand SDFs, DNA parameters, updated Tyk2 tutorial) — the code change is a smaller subset.
  • Out of scope / follow-on: the consolidation of the standalone qdyn_neq engine into qdyn/qdynp is a separate branch (refactor/consolidate-neq-engine) that will be rebased onto main and opened as its own PR after this one lands.

…optimal bond geometries from auto-added hydrogens
…ns, solute.

Now, setting the FEP run to 2fs will apply shake to all hydrogens in the simulation. Solvent SHAKE is set to off in case solvent contains other solvents like POPC.

shake_hydrogens already handles shake settings for HOH molecules.
- add generate_golden_restraints.py to generate restraints compatible with QligFEP v2.1.0
…ame to avoid conflict with python package name (avoid conflicts when importing from pytest directory)
…oduction runs.

This commit includes the additon of a "templates" subpackage that will handle the input files instead of the old INPUTS/.

An example on how to display the rendered MD files was added to templates.__init__
…ized atom type, parse the whole protein file and tell all the problematic atom/residue names
…igfep. Qprep input sare now passed as positional arguments allowing us to use subprocess for command execution.

qprep positional arguments are supported commit 8d67120
    1. use the qprep & qfep templates from the templates/ directory
    2. dynamicall remove out-of-sphere molecules that aren't part of the system (avoid unecessary calculation of bonded energies with the MD engine)

Also added tests for this fragment filtering step along with two protein structures that serve as test cases
…n pair list construction

Build compressed sparse row (CSR) lookup tables from listexlong and
list14long at startup. Each atom gets a sorted list of its long-range
exclusion/1-4 partners, enabling binary search instead of scanning the
full list for every atom pair.

Replaces linear scans in all 8 nbpplis2*/nbpplist* routines plus
nbpp_count and the monitor group 1-4 check.

Benchmarks (5000 steps, eq1):
- CDK2 protein: 109s -> 96s (1.13x speedup)
- P2Y1 GPCR membrane (LRF): 488s -> 179s (2.73x speedup)
- Tyk2 water: 70s -> 70s (no regression)
Bit-for-bit identical energies on all three systems.
…ing a single approach, the method now attempts to generate the conformer in a tirered approach:

1. ConstrainedEmbed with element-strict MCS
2. ConstrainedEmbed with topology-only MCS (only if it differs from #1)
3. coordMap embed with topology-only MCS positions
4. Unconstrained embed + centroid translation
…x alingment issues and repair the network and edge topology
…routine (out-of-sphere charges shouldn't be present)
…ffset.

It applies Beutler 1994 Eq. 7's (1-lambda_s)^2
modulation to the offset itself per state, so that:

  lambda_s = 1: offset = 0    -> standard LJ + Coulomb
  lambda_s = 0: offset = full -> bounded V at r=0 for cross-state evaluation
  intermediate: offset = full * (1 - lambda_s)^2

Scaling the offset (post-quadratic), not the user's max-V target, preserves
endpoint behavior - target-scaling would propagate through the quadratic in
populate_sc_lookup and leave a non-vanishing offset at lambda=1.

The Coulomb term uses the s=p=6 case of Beutler's generalized Eq. 9 so LJ
and Coulomb can share a single offset table; this is documented in the
softcore_scale_beutler docstring.

QligFEP now writes symmetric (20, 20) alphas when softcore_method=beutler_coul
so the modulation can protect both endpoints; the legacy asymmetric (0, 20)
pattern is preserved for back compatibility.

Refactor: the sc_lookup fill loop is extracted from qatom_load_fep into
populate_sc_lookup so softcore_scale_beutler can reuse it.
…1.1-1.4

The previous SC_GAPSYS implementation had three correctness bugs against the
source manuscript (Gapsys, Seeliger, de Groot, JCTC 2012, 8, 2373) and its
Supporting Information §S1:

1. Lambda inversion in softcore_init_gapsys. The paper formulas assume
   lambda_state increases as the state disappears. Q's convention is the
   opposite (EQ(istate)%lambda = 1 means coupled/real, = 0 means
   decoupled/ghost), so r_sc must scale with (1 - lambda_state)^(1/6),
   not lambda_state^(1/6). The previous code applied no softcore at the
   ghost endpoint exactly when it was needed -- the root cause of
   intra-ligand Q-Q endpoint overflow under Gapsys.

2. Endpoint-skip cycle (if lambda_st < 1e-10 cycle) compounded #1 by
   leaving the lookup arrays at zero for the ghost state. Removed; the
   corrected formula handles the endpoint naturally.

3. Runtime V(r), F(r) inside r < r_sc did not match SI Eqs. 1.1-1.4. The
   previous Q form was continuous but qualitatively wrong (F(0) = 0,
   force increasing with r). Paper requires F(0) = 168 C12/r_sc^13 -
   48 C6/r_sc^7 and force monotonically decreasing with r (paper §2.3).
   Replaced with the SI's exact a*r^2 - b*r + c form for LJ and Coulomb
   via two new pure helpers gapsys_eval_lj and gapsys_eval_q in
   qatom.f90, called from each of the ~15 Gapsys runtime sites in md.f90
   and md_test.f90.

Also addressed:

- Exposed sigma_Q as a tunable parameter (FEP keyword gapsys_sigma_q,
  default 1.0 per paper). The previously-existing gapsys_sigma_lj is
  unrelated to the paper's sigma_Q -- it is the LJ sigma fallback for
  dummy atoms with C6 = C12 = 0.

- Q-protein and Q-water Coulomb radius now uses the full paper Eq. 5
  factor (1 + sigma_Q * |q_i*q_j|) at runtime where q_j is in scope.
  Previously precomputation used |q_i| only because partner charges are
  per-atom, not per-type. Restructured: precompute
  alpha_Q*(1-lambda)^(1/6) per (q-atom, state) as
  gapsys_q_lambda_factor (2D), multiply by the partner-charge factor at
  each nonbonded runtime site. Old gapsys_q_rsc (3D) removed.

- R*-to-sigma conversion under arithmetic mixing. AMBER14sb.prm and
  CHARMM36.prm store R* (R_min/2), not sigma/2. Q's standard LJ formula
  absorbs the R_min-vs-sigma factor cleanly because it consumes R_min^12
  and 2*R_min^6 directly, but Gapsys's r_sc formula is in terms of
  sigma (zero-crossing radius). The arithmetic branch of
  softcore_init_gapsys now divides the summed R* values by 2^(1/6) to
  obtain sigma_pair. Without this, r_sc was ~12% too large under AMBER
  and CHARMM. The geometric branch is unaffected -- (C12/C6)^(1/6)
  extracts sigma_pair directly from physical C12/C6.

- MPI broadcast of all four Gapsys scalars (alpha_lj, alpha_q, sigma_lj,
  sigma_q) plus the new gapsys_q_lambda_factor 2D array. The previous
  block broadcast only the 3D lookup tables, not the scalars, so
  non-master ranks held stale defaults.

- qligfep.py comment updated: the asymmetric (0,20)/(20,0) [softcore]
  alpha pattern for Gapsys is now redundant rather than load-bearing.
  The (1-lambda)^(1/6) formula already produces r_sc = 0 at the coupled
  endpoint, so the sc_lookup gate is a no-op there. Pattern kept for
  backward compatibility.

Out of scope (deferred to follow-up):

- qfep TI estimator. SI Eqs. 1.5-1.6 give the full dH/dlambda including
  the chain-rule term from the lambda-dependence of r_sc(lambda). qfep
  currently approximates dH/dlambda from per-state V_1, V_2 only,
  dropping that contribution. BAR and OS use per-state V directly and
  are now fully paper-faithful; TI awaits a follow-up.

Files changed:
  src/q6/qatom.f90       new gapsys_sigma_q param + parsing; rewritten
                         softcore_init_gapsys with (1-lambda)^(1/6)
                         convention, R*-to-sigma conversion, and
                         gapsys_q_lambda_factor replacing gapsys_q_rsc;
                         new pure helpers gapsys_eval_lj and
                         gapsys_eval_q implementing SI Eqs. 1.1-1.4.
  src/q6/md.f90          all Gapsys runtime sites call the new helpers
                         and apply the partner-charge factor at
                         runtime; MPI block updated.
  src/q6/md_test.f90     mirrored md.f90 changes.
  src/QligFEP/qligfep.py comment-only update on the [softcore] writer.
…al water and ions on the water leg to match the protein leg total charge; still to be tested)
…lambda 0.988 and 0.013 to be written, for example
…Changes include:

pdb2amber / atom renaming:
- drop the unconditional H1->H / H2->H mappings that corrupted real NH3+ N-termini
  (ARG/GLU/GLN/THR/LEU/GLY); add context-aware lone-backbone-H handling instead
- NME cap methyl: map HH31/HH32/HH33 -> H1/H2/H3
- ASN sidechain HD1/HD2 -> HD21/HD22
- generalize ion renaming and make the lookup case-insensitive; add Ca -> CAL
  (factor_xa ships calcium as residue "Ca" / atom "CA1")

Force field (AMBER14sb):
- add {SEP} (phosphoserine) library entry, generated with qtools/QLib from
  AmberTools phosaa14SB.lib (same source as TPO); net charge -2.00. All atom types
  and bonded params were already present from the TPO import.
- add {CAL} ion + CAL atom type, Li & Merz 2013 CM set for TIP3P
  (frcmod.ions234lm_126_tip3p, R*=1.649, eps=0.10592870) -- the set MAG/ZIN use.

FF/README.md:
- fix the atom_types column-5 rule: it is the 1-4 epsilon (eps/2), not sigma/2.
The [atom_types] column 5 is the 1-4 Lennard-Jones well depth (epsilon2&3):
qprep parses it into bvdw(3) (prep.f90), md sqrt's it (md.f90), and every 1-4
pair uses it via LJcod=3 (calc_nb.f90, md.f90), giving eps_pair_14 =
sqrt(col5_i*col5_j) in the arithmetic LJ formula. AMBER scales 1-4 vdW by
1/SCNB = 1/2 at the same R*, so column 5 must be eps/2.

31 hand-added types stored R*/2 there instead (inheriting the wrong rule the FF
README used to state): 8 phospho-AA (OP OQ OR OT OV OX OZ CG), 20 POPC Lipid21
(lowercase), 3 ions (CHL MAG ZIN). Their normal LJ (col1/col3) matches the AMBER
source exactly (frcmod.phosaa14SB, parm10.dat) -- only col5 was mistyped. This
made phospho-AA and POPC intra-molecular 1-4 vdW several-fold too strong; the
ions were unaffected (monatomic, no 1-4 pairs).
The Neutralizer identified residues to rewrite by (chain_id,
residue_seq_number) only. Two residues can share that pair via an insertion
code -- classically a C-terminal NME cap numbered 167A on top of residue 167.
Neutralizing residue 167 then also rewrote the cap (renamed it / stripped
atoms), so qprep could not build it and excluded the whole protein
(complexnotexcluded.pdb came out water-only).

Add insertion_code to the match mask in all three rewrite paths (general
neutralization, N-terminal, C-terminal) and carry it into the residue-info
dict. Restores preparation for the 14 IndustryBenchmarks targets with capped
residues sharing chain+seq via insertion codes (back to 60/60).

Test: TestNeutralizerInsertionCode in test_qprep_cli.py.
…rameters. Golden files are changed to state "polarized", the actual spelling used by src/q6/md.f90. The word "polarised" was silently ignored, setting the parameters to default regardless of input value
…ng in the same exact coordinate is now checked in a while loop instead of a single increment of 0.001 to the coordinate
…f the legacy `replacements` dictionary-based approach
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant