Repository navigation
Fix fragment parsing and placement bugs; neighbor lists, load balancing, ligand options - #28
Merged
Merged
Conversation
Parser (src/parse.c):
- Polarizable points: GAMESS writes the labels as 'CT',I1/I2/I3, so labels
from CT100 on are five characters long; the fixed 4-character skip read the
last label digit as x and dropped z. This misplaced the points of every
fragment with more than 99 polarizable points (chlorophylls, carotenoids)
and caused polarization catastrophes.
- New skip_ct_label() (skip "CT", blanks, index digits) for static, dynamic
and dipole-quadrupole polarizable points and LMO centroids. It also handles
negative coordinates that fill their F15.10 field and run into the label
("CT1-132.2805782867"), where the fixed skips ate the sign and the token
skip of the LMO centroids swallowed the coordinate.
- tok_label(): end an A8 label that runs into a negative number
("BO123124-132.2805782867").
- parse_dipquad_polarizable_pts(): store points at
n_dipquad_polarizable_pts - 1 (was n_dynamic_polarizable_pts - 1, out of
bounds when no dynamic points were read before).
- Projection basis/wavefunction keep their fixed widths (correct for the
GAMESS A8,A2 and I2,I3 formats); documented.
Dispersion (src/efp.c, src/disp.c): the LMO overlap arrays s/ds were left
uninitialized for pairs with a fragment without LMOs but were read for every
pair of dynamic polarizable points, also with Tang-Toennies damping
(segmentation fault). Initialize them to NULL and read them only when present.
Tests: parse_labels_1a/b (labels CT100+), parse_labels_2a/b (labels glued to
negative coordinates, all sections), parse_labels_3a/b (dipole-quadrupole
section), disp_no_lmo_1a/b (dispersion without LMOs). The b tests use
fragments generated from pentacene/water by tests/tools/
make_parse_test_fragments.py and must reproduce the energies of the a tests
with the library fragments; all b tests fail without these fixes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…toms
Center of mass (src/parse.c, src/efp.c):
- Fragments are placed as frag->x + R * p_lib, and frag->x is the center
of mass for the xyzabc and atoms coordinate types and for cutoffs and
periodic images. Library fragments written in another frame than the
center-of-mass frame of GAMESS (e.g. in MD coordinates) were therefore
misplaced with atom coordinates (all polarizable, dispersion and exchange-
repulsion points shifted by R * COM_lib), and cutoffs/periodic images were
computed from the internal origin instead of the molecule. center_fragment()
now moves every library fragment to its center-of-mass frame when it is
loaded. Placement with coord points is frame independent and unchanged.
- center_fragment() also records whether the axes are principal axes of
inertia; efp_get_frag_inertia() (used by MD and the Hessian, which assume
principal axes) warns otherwise. Fragments are not rotated, because that
would change the meaning of xyzabc Euler angles.
Bond mid-points (src/parse.c, src/elec.c): efp_update_elec_special() decoded
the atoms of "BO<i><j>" labels by string length, which is ambiguous ("BO1211"
is atoms 12 and 11 or 121 and 1), wrong for atom numbers above 99, and left
the indices uninitialized for longer labels. assign_multipole_atoms() now
finds the atom, or the atom pair of a bond mid-point, of every multipole point
when the fragment is loaded: label splits are checked against the coordinates
(linear in the number of points). Points that are neither move rigidly with
the fragment; before, they were not moved at all.
Tests:
- coord_com_1a/b: atom coordinates with a fragment whose frame is 500 bohr
away from the molecule (reproduces parse_labels_2a, which uses coord points).
- bo_labels_1a/b: 108-atom fragment with labels like BO10075, atom
coordinates vs coord points.
Both b tests fail without these changes.
- symm_2, symm_2full, symm_2pw: references updated. The internal origin of
xxii_exp_n2_*.efp is 14.1 bohr away from the molecule and was the fragment
center for the cutoff and periodic images; energies without cutoff are
unchanged, and symmetric and full calculations still agree.
- parse_labels_1a/b, disp_no_lmo_1a/b: smaller num_step_angle, as the center
of mass of pentacene_2x is 200 bohr away from the interacting molecule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MONOPOLES, DIPOLES, QUADRUPOLES, OCTUPOLES and SCREEN sections give one line per multipole point, and every line searched all points for its label: quadratic in the number of points. SCREEN3 lines did not even stop at the match. COORDINATES grew its arrays by one element per point. - find_multipole_pt(): open-addressing hash index label -> first point with that label (same result as the linear search, including duplicated labels), built once per fragment and freed after parsing. - parse_coordinates(): geometric growth of the atom and point arrays. A 493,567-point MM superfragment (point charges of a protein environment) plus a water molecule: 1519 s -> 2.0 s for the whole efpmd run, identical energy. Test parse_labels_4a/b: multipole and screening sections listed in reverse order relative to COORDINATES (h2o_shuffled.efp) must reproduce h2o_l. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
efp_add_stress() adds to one shared tensor with nine omp atomic updates per fragment pair. The stress tensor can only be read with gradients (efp_get_stress_tensor), but it was accumulated in every calculation; in a 151,550-fragment single point most of the electrostatics time was spent in these contended atomics. Skip it without gradients. Runs with gradients still accumulate it with atomics (a per-thread reduction would remove that contention too). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
For large systems the fragment pair bookkeeping dominated: the skip-list was a dense n_frag x n_frag byte array (23 GB for 151,550 fragments), and every pair loop (two-body terms, static and induced fields of every polarization iteration) tested all n_frag^2 pairs against it and against the cutoff. Skipped pairs (src/pairs.c): a default rule plus a hash map of the pairs set with efp_skip_fragments(). For symmetric crystals efp_set_symmlist() sets the rule "skip pairs without a symmetry-unique fragment" instead of n_frag^2 entries (same pairs as before, also replacing earlier explicit settings). efp->prepared replaces the use of the skip-list as "efp_prepare was called". Neighbor lists (src/pairs.c): with a cutoff, efp_compute() builds the list of fragments within the cutoff of every fragment with cell lists (open boundaries and orthogonal periodic boxes with at least 3 cells per direction; brute force otherwise), using the same minimum-image test as efp_skip_frag_pair(). The lists are sorted and valid only inside efp_compute(); the pair loops in compute_two_body_range() (same pair ownership as before), compute_two_body_crystal() and pol.c iterate over them and keep their own checks. Outside efp_compute() (e.g. efp_get_electric_field from QM codes) they loop over all fragments as before. Stress tensor: every thread accumulates its own padded copy during efp_compute(), summed before the MPI reduction, instead of nine omp atomic updates of one shared tensor per pair. This matters because efp_compute() currently always computes gradients (efp->do_gradient = 1). Checks: 3375 water box with 12 A cutoff (open) and 10 A cutoff (periodic 45 A cube): energies agree with the previous code to 6e-10, all 20250 gradient components to 1e-10 (printed precision); NPT pressures agree to 3e-11 relative. New tests nbr_1 (open boundaries) and nbr_2 (periodic, cell lists) with 512 water molecules and references from the previous code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
efp_compute() always set efp->do_gradient = 1, so every energy-only calculation also computed all gradients (e.g. the polarization gradient of every polarizable point against all point charges of an MM superfragment). Use the argument again. All tests pass; note that efp_get_gradient(), efp_get_point_charge_gradient(), efp_get_stress_tensor() and the atomic gradients now fail after efp_compute(efp, 0), as they are documented to; programs that ask for gradients after an energy-only call (to be checked for the Q-Chem interface) must request them in efp_compute(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With print >= 1, efpmd prints the wall time of reading the fragment parameters, creating/placing the fragments and printing the geometry, and efp_compute() prints the time of the neighbor lists, the two-body terms and the polarization phases (static field, induced dipoles, energy, gradient). The new efpmd option print_geometry (default true) turns off the geometry printout of large systems. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…igand In a pairwise analysis (enable_pairwise) only the interactions with the ligand are needed. With the new option pairwise_ligand_only (efp_opts and efpmd keyword, default off) the two-body terms (electrostatics, dispersion, exchange repulsion, charge penetration) are computed only for the fragment pairs with the ligand; polarization is computed for the whole system as before (static field from all fragments, all induced dipoles), so the pairwise polarization energies are unchanged. The total energy is then not the energy of the system (a note is printed), and gradients are refused. etest jobs compute the energy without gradients (only the energy is compared). Test pairwise_ligand_1: pairwise_1 with the option; the reference is the sum of the pairwise elec/disp/xr energies of pairwise_1 plus its polarization energy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With enable_cutoff, the pairs with the ligand (fragment opts.ligand >= 0) now use ligand_cutoff (efp_opts and efpmd keyword; 0 = swf_cutoff) and all other pairs swf_cutoff. This applies to the two-body terms and to the polarization fields between the ligand and the other fragments, so the interaction with the ligand can be converged with distance while the static and induced fields in the environment stay at a smaller cutoff. Every pair keeps one cutoff for all its interactions, so the polarization problem stays symmetric. Exchange repulsion keeps xr_cutoff. efp_pair_cutoff() gives the cutoff of a pair; efp_skip_frag_pair(), efp_make_swf() and the neighbor lists use it (the ligand is tested explicitly). ligand_cutoff requires enable_cutoff and a ligand fragment, not with symmetry, and must fit in the periodic box. Tests: - ligand_cutoff_1 (gradient test): 4 water molecules on a line with swf_cutoff 10 and ligand_cutoff 16 have the same pairs and switching factors as a standard run with swf_cutoff 16, which gives the reference. - ligand_cutoff_2: 512 water molecules (cell lists), swf_cutoff 7 and ligand_cutoff 12; reference from standard runs: E(7 A) minus the ligand pairwise energies at 7 A plus those at 12 A. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With print_pairwise_geometry false, the pairwise energy printout gives one header line and the energies per fragment and the ligand geometry once, instead of the coordinates of the fragment and of the ligand for every fragment (2.6 GB for 151,550 fragments). Default true (unchanged output). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The static field, the ligand field, the induced dipole iterations and the polarization gradient were parallelized over fragments, so all polarizable points of one fragment were computed by one thread. A fragment interacting with very many others (e.g. the ligand with a large ligand_cutoff) then serialized the calculation: with ligand_cutoff 100 A in a 151,550-fragment system the induced dipoles took 536 s. The loops now run over the polarizable points of the fragment range (efp->pol_pt_frag maps a point to its fragment); gradient contributions were already accumulated atomically. The polarization energy loop, which adds to per-fragment pairwise energies, stays per fragment. Energies and all 20250 gradient components of a 3375 water box (12 A cutoff, and 10 A cutoff in a periodic box) agree with the previous code to 6e-10 and 1e-10 (printed precision). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unks The two-body loop gave every fragment's pairs to one thread. Two cases serialized the calculation: - a fragment with very many partners (the ligand with a large ligand_cutoff owns ~75,000 pairs); - pairs with a very large fragment (an MM superfragment of 10^5 - 10^6 point charges with every inner fragment: 71 min in a 30 A EFP/MM run). With neighbor lists the loop now runs over work items of up to PAIR_BLOCK (256) partners of one fragment, with the same pair ownership. The body of the loop is compute_pair(), unchanged. The electrostatics of pairs with more than HEAVY_PAIR_POINTS (10^6) multipole point pairs is computed after the pair loop in chunks of HEAVY_PAIR_CHUNK (2048) points of the larger fragment (efp_frag_frag_elec_range(), the point range version of efp_frag_frag_elec()), shared among threads; their pairwise energies are accumulated atomically. Other terms of such pairs (normally absent for MM fragments) and pairs with a special fragment stay in the pair loop. Energies and all 20250 gradient components of a 3375 water box (12 A cutoff, and 10 A cutoff in a periodic box) agree with the previous code to 6e-10 and 1e-10. New test heavy_pair_1: an MM fragment of 15000 charges (mm_box.efp) with pentacene and water, pairwise; reference from the code before chunking; the analytic gradient is unchanged (checked separately). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
n_symm_frag() counted the fragments of the class of fragment i for i = 0 ... nsymm_frag - 1 instead of class i + 1, so the multiplicities that scale the two-body and polarization energies of symmetric crystals were wrong unless the first fragments of the input were one per class in class order. symm_2 starts with two fragments of the same class but has 32 fragments in both classes, which hid the error. Tests symm_classes_a/b: symm_2 without 8 fragments of the second class (32 + 24) in two orders with the same symmetry-unique fragments; before the fix they gave -5.0784182113 and -4.4584671219. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compute_two_body_crystal(), compute_elec_field_crystal(), compute_id_crystal() and compute_energy_crystal() were serial. They now use OpenMP like the regular path: the two-body terms over blocks of partners of the symmetry-unique fragments, the static field and the induced dipoles over the polarizable points of the symmetry-unique fragments, the broadcast of the induced dipoles over the fragments (each written by one thread, its unique fragment from its symmetry class), and the pairwise parts over the other fragment of each pair. The class arrays were not freed in compute_elec_field_crystal(), compute_id_crystal() (every polarization iteration) and compute_energy_crystal(); they are now. symm_1, symm_2, symm_2full, symm_2pw, symm_2pw_printout and symm_classes_a give the same energies as before to all printed digits, and all 384 pairwise/lattice energy lines of the pairwise tests are identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The library and efpmd are compiled as strict C99 (C_EXTENSIONS OFF), where the POSIX clock_gettime() and struct timespec are not declared on Linux; the Intel CI build failed (GCC only warned). The timers are now in src/timer.c (efp_wall_time) and efpmd/src/timer.c (efpmd_wall_time), which use omp_get_wtime() with OpenMP, QueryPerformanceCounter() on Windows, and otherwise clock_gettime() with _POSIX_C_SOURCE requested in that file only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The conda-forge psi4 1.11 build (py310hb493b4b_1) allows einsums >=1.1.5,<1.2, but einsums 1.1.6 lacks einsums::random_engine, so "import psi4" failed in the MKL jobs (also on master). Checked on Linux: psi4 1.11 imports with einsums 1.1.5 and fails with 1.1.6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There is no win-64 einsums package; psi4 on Windows does not depend on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
|
Three commits added after the CI run on this PR:
All 7 jobs pass now (ctest and pytest on Linux GCC/Intel/Torch, macOS Clang, Windows MinGW). 🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug fixes, new options and performance work found while running EFP on a 151,550-fragment photosystem I model (polarization did not converge on master). Full details are in
CHANGELOG.md.Bug fixes
CT123). Affected every fragment with >99 polarizable points and caused polarization catastrophes.CT1-132.28…,BO123124-132.28…) lost the sign or the coordinate.coord atoms/xyzabc, and cutoffs/PBC used the frame origin instead of the molecule.n_symm_frag) were wrong unless the input started with one fragment per class.New options
ligand_cutoff,pairwise_ligand_only,print_geometry,print_pairwise_geometry(efpmd keywords /efp_optsfields), andTIMINGlines withprint 1. Website documentation: libefp2/libefp2.github.io#31.Behavior changes reviewers should check
coord pointsplacement unchanged). MD/Hessian warn for non-principal-axes frames.efp_compute(efp, do_gradient)now honorsdo_gradient(it was forced to 1). Gradient getters fail after an energy-only call. This may matter for interfaces (Q-Chem, Psi4) that read gradients afterefp_compute(efp, 0).etestcomputes the energy without gradients.symm_2,symm_2full,symm_2pwreference energies changed (-2.4694268198 → -5.5786738865): thexxii_exp_n2_*frame origin is 14 bohr from the molecule and was used as the fragment center for the cutoff. Energies without cutoff are unchanged.Performance
Linear-time parsing of large fragments (493k-point MM fragment: 25 min → 2 s); sparse skip list and cell-list neighbor lists instead of the dense n_frag² skip list; per-thread stress tensor; OpenMP load balancing (polarization over points, two-body over partner blocks, chunked heavy MM pairs); parallel symmetric-crystal path. Example, 151,550 fragments on 64 threads: 15 Å cutoff single point 53 s; EFP/MM with a 30 Å boundary 2.5 min (was 1 h 12 min).
Testing
torch_*tests (master: 172/172).🤖 Generated with Claude Code