From e5600d1228d19ddbb254d4580035801eba4db22c Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 9 Mar 2026 15:29:57 +0000 Subject: [PATCH 1/8] Fix GLYCAM force-field parsing issues. --- corelib/src/libs/SireIO/grotop.cpp | 193 +++++++++++++++++++++++- corelib/src/libs/SireIO/grotop.h | 10 ++ corelib/src/libs/SireMM/amberparams.cpp | 28 ++-- tests/io/test_amberprm.py | 76 ++++++++++ tests/io/test_grotop.py | 101 +++++++++++++ 5 files changed, 385 insertions(+), 23 deletions(-) create mode 100644 tests/io/test_amberprm.py diff --git a/corelib/src/libs/SireIO/grotop.cpp b/corelib/src/libs/SireIO/grotop.cpp index ef84a1e70..20bd805ea 100644 --- a/corelib/src/libs/SireIO/grotop.cpp +++ b/corelib/src/libs/SireIO/grotop.cpp @@ -1617,6 +1617,7 @@ GroMolType::GroMolType(const GroMolType &other) : nme(other.nme), warns(other.warns), atms0(other.atms0), atms1(other.atms1), first_atoms0(other.first_atoms0), first_atoms1(other.first_atoms1), bnds0(other.bnds0), bnds1(other.bnds1), angs0(other.angs0), angs1(other.angs1), dihs0(other.dihs0), dihs1(other.dihs1), cmaps0(other.cmaps0), cmaps1(other.cmaps1), + explicit_pairs(other.explicit_pairs), ffield0(other.ffield0), ffield1(other.ffield1), nexcl0(other.nexcl0), nexcl1(other.nexcl1), is_perturbable(other.is_perturbable) { @@ -1646,6 +1647,7 @@ GroMolType &GroMolType::operator=(const GroMolType &other) dihs1 = other.dihs1; cmaps0 = other.cmaps0; cmaps1 = other.cmaps1; + explicit_pairs = other.explicit_pairs; ffield0 = other.ffield0; ffield1 = other.ffield1; nexcl0 = other.nexcl0; @@ -1663,6 +1665,7 @@ bool GroMolType::operator==(const GroMolType &other) const first_atoms0 == other.first_atoms0 and first_atoms1 == other.first_atoms1 and bnds0 == other.bnds0 and bnds1 == other.bnds1 and angs0 == other.angs0 and angs1 == other.angs1 and dihs0 == other.dihs0 and dihs1 == other.dihs1 and cmaps0 == other.cmaps0 and cmaps1 == other.cmaps1 and + explicit_pairs == other.explicit_pairs and nexcl0 == other.nexcl0 and nexcl1 == other.nexcl1 and is_perturbable == other.is_perturbable; } @@ -2483,6 +2486,19 @@ QHash GroMolType::cmaps(bool is_lambda1) const return cmaps0; } +/** Add an explicit 1-4 pair (from a [pairs] funct=2 line) with given + * coulomb and LJ scale factors */ +void GroMolType::addExplicitPair(const BondID &pair, double cscl, double ljscl) +{ + explicit_pairs.insert(pair, qMakePair(cscl, ljscl)); +} + +/** Return the explicit 1-4 pair scale factors (from [pairs] funct=2) */ +QHash> GroMolType::explicitPairs() const +{ + return explicit_pairs; +} + /** Sanitise all of the CMAP terms - this sets the string equal to "1", * as the information contained previously has already been read */ @@ -3699,7 +3715,7 @@ static QStringList writeCMAPTypes(const QHash &cmap_para /** Internal function used to convert a Gromacs Moltyp to a set of lines */ static QStringList writeMolType(const QString &name, const GroMolType &moltype, const Molecule &mol, - bool uses_parallel) + bool uses_parallel, int combining_rules = 2) { QStringList lines; @@ -4755,6 +4771,33 @@ static QStringList writeMolType(const QString &name, const GroMolType &moltype, return; } + // Get LJ and charge properties for writing funct=2 explicit pairs. + AtomLJs ljs; + AtomCharges charges; + bool has_ljs = false; + bool has_charges = false; + + // Determine the combining rules from the forcefield (default to arithmetic = 2). + const int local_combining_rules = combining_rules; + + try + { + ljs = mol.property("LJ").asA(); + has_ljs = true; + } + catch (...) + { + } + + try + { + charges = mol.property("charge").asA(); + has_charges = true; + } + catch (...) + { + } + // A set of recorded 1-4 pairs. QSet> recorded_pairs; @@ -4787,10 +4830,57 @@ static QStringList writeMolType(const QString &name, const GroMolType &moltype, const auto s = scl.get(idx0, idx1); - if (not((s.coulomb() == 0 and s.lj() == 0) or (s.coulomb() == 1 and s.lj() == 1))) + if (s.coulomb() == 0 and s.lj() == 0) { - // This is a non-default pair. - scllines.append(QString("%1 %2 1").arg(idx0 + 1, 6).arg(idx1 + 1, 6)); + // Fully excluded: don't write. + } + else if (s.coulomb() == 1 and s.lj() == 1) + { + // Full 1-4 interaction (e.g. GLYCAM with SCNB=1.0, SCEE=1.0). + // Must write as funct=2 with explicit LJ parameters because + // funct=1 with gen-pairs would apply fudgeLJ and reduce the + // interaction, and not listing the pair would give zero interaction. + if (has_ljs and has_charges) + { + const auto cgidx0 = molinfo.cgAtomIdx(idx0); + const auto cgidx1 = molinfo.cgAtomIdx(idx1); + + const auto &lj0 = ljs.at(cgidx0); + const auto &lj1 = ljs.at(cgidx1); + + LJParameter lj_ij; + if (local_combining_rules == 2) + lj_ij = lj0.combineArithmetic(lj1); + else + lj_ij = lj0.combineGeometric(lj1); + + const double qi = + charges.at(cgidx0).to(mod_electron); + const double qj = + charges.at(cgidx1).to(mod_electron); + + scllines.append( + QString("%1 %2 2 1.0 %3 %4 %5 %6") + .arg(idx0 + 1, 6) + .arg(idx1 + 1, 6) + .arg(qi, 11, 'f', 6) + .arg(qj, 11, 'f', 6) + .arg(lj_ij.sigma().to(nanometer), 18, 'e', 11) + .arg(lj_ij.epsilon().to(kJ_per_mol), 18, 'e', 11)); + } + else + { + // Fall back to funct=1; the energy will be wrong if + // fudgeLJ != 1.0, but we have no LJ parameters to use. + scllines.append( + QString("%1 %2 1").arg(idx0 + 1, 6).arg(idx1 + 1, 6)); + } + } + else + { + // Standard partial 1-4 (e.g. fudgeQQ/fudgeLJ): write as funct=1. + scllines.append( + QString("%1 %2 1").arg(idx0 + 1, 6).arg(idx1 + 1, 6)); } } } @@ -7836,6 +7926,72 @@ QStringList GroTop::processDirectives(const QMap &taglocs, const Q }; // function that extracts all of the information from the 'cmap' lines + // function that extracts explicit 1-4 pair scale factors from the 'pairs' lines. + // funct=1 pairs are standard (use global fudge_qq/fudge_lj) and are handled + // automatically by gen-pairs, so we only need to store funct=2 explicit pairs. + // funct=2 format: ai aj 2 fudgeQQ qi qj sigma epsilon + // The LJ scale is 1.0 for funct=2 because sigma/epsilon are the full combined values. + auto addPairsTo = [&](GroMolType &moltype, int linenum) + { + QStringList lines = getDirectiveLines(linenum); + + for (const auto &line : lines) + { + const auto words = line.split(" "); + + if (words.count() < 3) + { + moltype.addWarning(QObject::tr("Cannot extract pair information " + "from the line '%1' as it should contain at least three words.") + .arg(line)); + continue; + } + + bool ok0, ok1, ok2; + + int atm0 = words[0].toInt(&ok0); + int atm1 = words[1].toInt(&ok1); + int funct = words[2].toInt(&ok2); + + if (not(ok0 and ok1 and ok2)) + { + moltype.addWarning(QObject::tr("Cannot extract pair information " + "from the line '%1' as the first three words need to be integers.") + .arg(line)); + continue; + } + + if (funct == 1) + { + // Standard pair: uses global fudge_qq/fudge_lj. + // The gen-pairs mechanism already handles these, so no explicit storage needed. + continue; + } + else if (funct == 2) + { + // Explicit pair: ai aj 2 fudgeQQ qi qj sigma epsilon + // The fudgeQQ is the coulomb scale factor; LJ params are used directly (lj_scl = 1.0). + double cscl = fudge_qq; // default to global fudge_qq if not specified + if (words.count() > 3) + { + bool ok; + double val = words[3].toDouble(&ok); + if (ok) + cscl = val; + } + + moltype.addExplicitPair(BondID(AtomNum(atm0), AtomNum(atm1)), cscl, 1.0); + } + else + { + moltype.addWarning(QObject::tr("Unsupported pair function type %1 in line '%2'. " + "Only funct=1 and funct=2 are supported.") + .arg(funct) + .arg(line)); + } + } + }; + auto addCMAPsTo = [&](GroMolType &moltype, int linenum) { QStringList lines = getDirectiveLines(linenum); @@ -7917,9 +8073,13 @@ QStringList GroTop::processDirectives(const QMap &taglocs, const Q addCMAPsTo(moltype, linenum); } + for (auto linenum : moltag.values("pairs")) + { + addPairsTo(moltype, linenum); + } + // now print out warnings for any lines that are missed... - const QStringList missed_tags = {"pairs", - "pairs_nb", + const QStringList missed_tags = {"pairs_nb", "exclusions", "contraints", "settles", @@ -8618,6 +8778,27 @@ GroTop::PropsAndErrors GroTop::getBondProperties(const MoleculeInfo &molinfo, co else { CLJNBPairs nbpairs(conn, CLJScaleFactor(fudge_qq, fudge_lj)); + + // Override with any explicitly specified [pairs] funct=2 entries. + // These carry their own fudgeQQ (coulomb scale) and use lj_scl=1.0 + // (sigma/epsilon in funct=2 are the full combined values, not scaled by fudgeLJ). + const auto explicit_pairs = moltype.explicitPairs(); + for (auto it = explicit_pairs.constBegin(); it != explicit_pairs.constEnd(); ++it) + { + const auto &pair = it.key(); + const auto &scl = it.value(); + try + { + AtomIdx idx0 = molinfo.atomIdx(pair.atom0()); + AtomIdx idx1 = molinfo.atomIdx(pair.atom1()); + nbpairs.set(idx0, idx1, CLJScaleFactor(scl.first, scl.second)); + } + catch (...) + { + // atom not found — skip silently (already warned during parsing) + } + } + props.setProperty("intrascale", nbpairs); } } diff --git a/corelib/src/libs/SireIO/grotop.h b/corelib/src/libs/SireIO/grotop.h index 2cbf2c41a..679de9182 100644 --- a/corelib/src/libs/SireIO/grotop.h +++ b/corelib/src/libs/SireIO/grotop.h @@ -43,6 +43,7 @@ #include "SireMM/cmapparameter.h" #include +#include SIRE_BEGIN_HEADER @@ -247,6 +248,9 @@ namespace SireIO QMultiHash dihedrals(bool is_lambda1 = false) const; QHash cmaps(bool is_lambda1 = false) const; + void addExplicitPair(const SireMol::BondID &pair, double cscl, double ljscl); + QHash> explicitPairs() const; + bool isWater(bool is_lambda1 = false) const; QStringList settlesLines(bool is_lambda1 = false) const; @@ -296,6 +300,12 @@ namespace SireIO SireMM::MMDetail ffield0; SireMM::MMDetail ffield1; + /** Explicit 1-4 pair scale factors from [pairs] funct=2 lines. + * Key: the atom pair. Value: (coulomb_scl, lj_scl) where + * coulomb_scl comes from the fudgeQQ column and lj_scl = 1.0 + * (since funct=2 LJ parameters are used directly, not further scaled). */ + QHash> explicit_pairs; + /** The number of excluded atoms */ qint64 nexcl0; qint64 nexcl1; diff --git a/corelib/src/libs/SireMM/amberparams.cpp b/corelib/src/libs/SireMM/amberparams.cpp index b102966f4..2aa147da0 100644 --- a/corelib/src/libs/SireMM/amberparams.cpp +++ b/corelib/src/libs/SireMM/amberparams.cpp @@ -1505,7 +1505,10 @@ QStringList AmberParams::validateAndFix() { const auto s = group_pairs.get(i, j); - if ((not(s.coulomb() == 0 or s.coulomb() == 1)) or (not(s.lj() == 0 or s.lj() == 1))) + // Process any non-zero 1-4 pair that isn't purely excluded (0,0). + // This includes both partial-scaling pairs (e.g. 0.833, 0.5 for standard AMBER) + // and full-interaction pairs (1.0, 1.0 for GLYCAM SCNB=1.0/SCEE=1.0). + if (not(s.coulomb() == 0.0 and s.lj() == 0.0)) { const auto atm0 = molinfo.atomIdx(CGAtomIdx(CGIdx(icg), Index(i))); const auto atm3 = molinfo.atomIdx(CGAtomIdx(CGIdx(jcg), Index(j))); @@ -2587,24 +2590,15 @@ void AmberParams::getAmberNBsFrom(const CLJNBPairs &nbpairs, const FourAtomFunct // extract the nb14 term from exc_atoms auto nbscl = nbpairs.get(nb14pair.atom0(), nb14pair.atom1()); - if (nbscl.coulomb() != 1.0 or nbscl.lj() != 1.0) + if (nbscl.coulomb() != 0.0 or nbscl.lj() != 0.0) { - if (nbscl.coulomb() != 0.0 or nbscl.lj() != 0.0) - { - // add them to the list of 14 scale factors - new_nb14s.insert(nb14pair, AmberNB14(nbscl.coulomb(), nbscl.lj())); + // add them to the list of 14 scale factors. + // This handles both standard AMBER (e.g. 0.833, 0.5) and + // GLYCAM-style (1.0, 1.0) where SCEE=1.0 and SCNB=1.0. + new_nb14s.insert(nb14pair, AmberNB14(nbscl.coulomb(), nbscl.lj())); - // and remove them from the excluded atoms list - exc_atoms.set(nb14pair.atom0(), nb14pair.atom1(), CLJScaleFactor(0)); - } - else - { - const auto tscl = nbpairs.get(nb14pair.atom0(), nb14pair.atom1()); - } - } - else - { - const auto tscl = nbpairs.get(nb14pair.atom0(), nb14pair.atom1()); + // and remove them from the excluded atoms list + exc_atoms.set(nb14pair.atom0(), nb14pair.atom1(), CLJScaleFactor(0)); } } } diff --git a/tests/io/test_amberprm.py b/tests/io/test_amberprm.py new file mode 100644 index 000000000..30b8138ab --- /dev/null +++ b/tests/io/test_amberprm.py @@ -0,0 +1,76 @@ +import sire as sr + +import pytest + + +def test_glycam(tmpdir): + """Test that a topology using the GLYCAM force field (SCEE=1.0, SCNB=1.0) + round-trips correctly through AMBER prm7 format. + + GLYCAM uses full 1-4 interactions (no scaling), so SCEE=1.0 and SCNB=1.0 + for glycan dihedrals. The protein dihedrals use standard AMBER scaling + (SCEE=1.2, SCNB=2.0). Before the fix, CLJScaleFactor(1.0, 1.0) pairs were + silently dropped when building AmberParams, so glycan dihedrals were written + with SCEE=0 and SCNB=0, giving zero 1-4 interactions. + """ + + # Load the GLYCAM topology and coordinates. + mols = sr.load_test_files("glycam.top", "glycam.gro") + + # Write to AMBER prm7 + rst7 format. + d = tmpdir.mkdir("test_glycam_amber") + f = sr.save(mols, d.join("glycam_out"), format=["PRM7", "RST7"]) + + # Parse SCEE_SCALE_FACTOR and SCNB_SCALE_FACTOR from the written prm7. + # Format: 5 values per line, 16 chars each (AmberFormat FLOAT 5 16 8). + scee_values = [] + scnb_values = [] + reading = None + + with open(f[0], "r") as fh: + for line in fh: + if line.startswith("%FLAG SCEE_SCALE_FACTOR"): + reading = "scee" + continue + elif line.startswith("%FLAG SCNB_SCALE_FACTOR"): + reading = "scnb" + continue + elif line.startswith("%FLAG") or line.startswith("%FORMAT"): + if line.startswith("%FLAG"): + reading = None + continue + if reading == "scee": + scee_values.extend(float(x) for x in line.split()) + elif reading == "scnb": + scnb_values.extend(float(x) for x in line.split()) + + assert len(scee_values) > 0, "No SCEE_SCALE_FACTOR entries found" + assert len(scnb_values) > 0, "No SCNB_SCALE_FACTOR entries found" + + # The system has both glycan (SCEE=1.0, SCNB=1.0) and protein + # (SCEE=1.2, SCNB=2.0) dihedrals. Both values must be present. + # Before the fix, all glycan entries would be 0.0. + assert any( + v == pytest.approx(1.0) for v in scee_values + ), "No SCEE=1.0 entries found; GLYCAM glycan dihedrals were not written correctly" + assert any( + v == pytest.approx(1.2, rel=1e-3) for v in scee_values + ), "No SCEE=1.2 entries found; standard AMBER protein dihedrals are missing" + assert any( + v == pytest.approx(1.0) for v in scnb_values + ), "No SCNB=1.0 entries found; GLYCAM glycan dihedrals were not written correctly" + assert any( + v == pytest.approx(2.0, rel=1e-3) for v in scnb_values + ), "No SCNB=2.0 entries found; standard AMBER protein dihedrals are missing" + + # SCEE/SCNB=0.0 is valid and expected — it marks dihedrals that share + # terminal atoms with another dihedral and should not contribute a 1-4 term. + # The pre-fix bug was that ALL glycan dihedral entries were 0.0 because + # CLJScaleFactor(1.0, 1.0) pairs were silently dropped. Having both 1.0 and + # 1.2 present (checked above) confirms the fix is working correctly. + + # Reload and verify the energy is self-consistent after the AMBER roundtrip. + # Before the fix, glycan 1-4 pairs had SCEE=0/SCNB=0, giving zero 1-4 + # interactions and a different energy. + mols2 = sr.load(f) + assert mols2.energy().value() == pytest.approx(mols.energy().value(), rel=1e-3) diff --git a/tests/io/test_grotop.py b/tests/io/test_grotop.py index d1399d475..cf2e51f6e 100644 --- a/tests/io/test_grotop.py +++ b/tests/io/test_grotop.py @@ -375,3 +375,104 @@ def test_grotop_water(tmpdir, water_model): is_settles = True if line.startswith("[ vsite3 ]"): is_vsite = True + + +def test_glycam(tmpdir): + """Test that a topology using the GLYCAM force field (SCEE=1.0, SCNB=1.0) + is read and written correctly. + + GLYCAM uses full 1-4 interactions (no scaling), unlike standard AMBER + which uses SCEE=1.2, SCNB=2.0. In GROMACS this is represented by funct=2 + pairs with explicit LJ parameters rather than funct=1 which would apply + the global fudgeLJ/fudgeQQ scaling. + """ + + # Load the GLYCAM topology and coordinates. + mols = sr.load_test_files("glycam.top", "glycam.gro") + + # The system contains a protein (PROA) and a glycan (CARB). + # Find them by molecule name. + glycan = mols["molname CARB"] + protein = mols["molname PROA"] + + # The glycan should use GLYCAM-style full 1-4 interactions. + # Its LJ property should be readable and non-trivial. + glycan_lj = glycan.property("LJ") + assert glycan_lj is not None + + protein_lj = protein.property("LJ") + assert protein_lj is not None + + # Write the whole system to GROMACS topology + coordinate files. + d = tmpdir.mkdir("test_glycam") + f = sr.save(mols, d.join("glycam_out"), format=["GroTop", "Gro87"]) + + # Parse the written file to verify the [pairs] sections are correct. + # The glycan (CARB) molecule type must use funct=2 (explicit LJ pairs, + # fudgeQQ=1.0) because its 1-4 interactions are unscaled. + # The protein (PROA) molecule type uses funct=1 (standard AMBER scaling). + current_moltype = None + expect_moltype_name = False + in_pairs = False + carb_funct2_count = 0 + carb_funct1_count = 0 + proa_funct1_count = 0 + + with open(f[0], "r") as fh: + for line in fh: + stripped = line.strip() + if not stripped or stripped.startswith(";"): + continue + if stripped == "[ moleculetype ]": + expect_moltype_name = True + in_pairs = False + continue + if expect_moltype_name: + current_moltype = stripped.split()[0] + expect_moltype_name = False + continue + if stripped.startswith("["): + in_pairs = stripped == "[ pairs ]" + continue + if in_pairs: + words = stripped.split() + if len(words) >= 3: + funct = int(words[2]) + if current_moltype == "CARB": + if funct == 2: + carb_funct2_count += 1 + else: + carb_funct1_count += 1 + elif current_moltype == "PROA": + if funct == 1: + proa_funct1_count += 1 + + # The CARB glycan uses GLYCAM (SCEE=1.0, SCNB=1.0), so the vast majority + # of its 1-4 pairs must be funct=2. The original topology has exactly one + # funct=1 pair (atoms 8-14), which is preserved as funct=1. + assert carb_funct2_count > 0, "No funct=2 pairs found for CARB glycan" + assert ( + carb_funct2_count > carb_funct1_count + ), "CARB glycan has more funct=1 pairs than funct=2; GLYCAM scaling not applied" + + # Protein pairs must use funct=1 (standard AMBER scaling). + assert proa_funct1_count > 0, "No funct=1 pairs found for PROA protein" + + # Reload and verify the energy is self-consistent after the GROMACS roundtrip. + # Before the fix, funct=2 pairs were written as funct=1, which would apply + # fudgeLJ=0.5 to the glycan 1-4 LJ interactions and give a different energy. + mols2 = sr.load(f, show_warnings=False) + glycan2 = mols2["molname CARB"] + + glycan_lj2 = glycan2.property("LJ") + + # Check a representative atom (index 1, type Oh) whose sigma and epsilon + # survive the roundtrip within floating-point formatting precision. + assert glycan_lj2[1].sigma().value() == pytest.approx( + glycan_lj[1].sigma().value(), rel=1e-3 + ) + assert glycan_lj2[1].epsilon().value() == pytest.approx( + glycan_lj[1].epsilon().value(), rel=1e-3 + ) + + assert mols2.energy().value() == pytest.approx(mols.energy().value(), rel=1e-3) From 718cbaf6e9f3c40f9e19835a050a940006e15172 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 9 Mar 2026 15:41:21 +0000 Subject: [PATCH 2/8] Handle perturbable topologies. --- corelib/src/libs/SireIO/grotop.cpp | 15300 +++++++++++++-------------- doc/source/changelog.rst | 2 + 2 files changed, 7180 insertions(+), 8122 deletions(-) diff --git a/corelib/src/libs/SireIO/grotop.cpp b/corelib/src/libs/SireIO/grotop.cpp index 20bd805ea..8d4a4fefe 100644 --- a/corelib/src/libs/SireIO/grotop.cpp +++ b/corelib/src/libs/SireIO/grotop.cpp @@ -46,11 +46,11 @@ #include "SireMM/atomljs.h" #include "SireMM/cljnbpairs.h" +#include "SireMM/cmapfunctions.h" #include "SireMM/fouratomfunctions.h" #include "SireMM/internalff.h" #include "SireMM/threeatomfunctions.h" #include "SireMM/twoatomfunctions.h" -#include "SireMM/cmapfunctions.h" #include "SireBase/booleanproperty.h" #include "SireBase/parallel.h" @@ -82,265 +82,195 @@ using namespace SireStream; static const RegisterMetaType r_groatom(NO_ROOT); -QDataStream &operator<<(QDataStream &ds, const GroAtom &atom) -{ - writeHeader(ds, r_groatom, 3); +QDataStream &operator<<(QDataStream &ds, const GroAtom &atom) { + writeHeader(ds, r_groatom, 3); - SharedDataStream sds(ds); + SharedDataStream sds(ds); - sds << atom.atmname << atom.resname << atom.chainname << atom.atmtyp << atom.bndtyp << atom.atmnum << atom.resnum - << atom.chggrp << atom.chg.to(mod_electron) << atom.mss.to(g_per_mol); + sds << atom.atmname << atom.resname << atom.chainname << atom.atmtyp + << atom.bndtyp << atom.atmnum << atom.resnum << atom.chggrp + << atom.chg.to(mod_electron) << atom.mss.to(g_per_mol); - return ds; + return ds; } -QDataStream &operator>>(QDataStream &ds, GroAtom &atom) -{ - VersionID v = readHeader(ds, r_groatom); +QDataStream &operator>>(QDataStream &ds, GroAtom &atom) { + VersionID v = readHeader(ds, r_groatom); - if (v == 3) - { - SharedDataStream sds(ds); + if (v == 3) { + SharedDataStream sds(ds); - double chg, mass; + double chg, mass; - sds >> atom.atmname >> atom.resname >> atom.chainname >> atom.atmtyp >> atom.bndtyp >> atom.atmnum >> - atom.resnum >> atom.chggrp >> chg >> mass; + sds >> atom.atmname >> atom.resname >> atom.chainname >> atom.atmtyp >> + atom.bndtyp >> atom.atmnum >> atom.resnum >> atom.chggrp >> chg >> mass; - atom.chg = chg * mod_electron; - atom.mss = mass * g_per_mol; - } - else if (v == 2) - { - SharedDataStream sds(ds); + atom.chg = chg * mod_electron; + atom.mss = mass * g_per_mol; + } else if (v == 2) { + SharedDataStream sds(ds); - double chg, mass; + double chg, mass; - sds >> atom.atmname >> atom.resname >> atom.atmtyp >> atom.bndtyp >> atom.atmnum >> atom.resnum >> - atom.chggrp >> chg >> mass; + sds >> atom.atmname >> atom.resname >> atom.atmtyp >> atom.bndtyp >> + atom.atmnum >> atom.resnum >> atom.chggrp >> chg >> mass; - atom.chg = chg * mod_electron; - atom.mss = mass * g_per_mol; - } - else if (v == 1) - { - SharedDataStream sds(ds); + atom.chg = chg * mod_electron; + atom.mss = mass * g_per_mol; + } else if (v == 1) { + SharedDataStream sds(ds); - double chg, mass; + double chg, mass; - sds >> atom.atmname >> atom.resname >> atom.atmtyp >> atom.atmnum >> atom.resnum >> atom.chggrp >> chg >> mass; + sds >> atom.atmname >> atom.resname >> atom.atmtyp >> atom.atmnum >> + atom.resnum >> atom.chggrp >> chg >> mass; - atom.bndtyp = atom.atmtyp; - atom.chg = chg * mod_electron; - atom.mss = mass * g_per_mol; - } - else - throw version_error(v, "1,2", r_groatom, CODELOC); + atom.bndtyp = atom.atmtyp; + atom.chg = chg * mod_electron; + atom.mss = mass * g_per_mol; + } else + throw version_error(v, "1,2", r_groatom, CODELOC); - return ds; + return ds; } /** Constructor */ -GroAtom::GroAtom() : atmnum(-1), resnum(-1), chggrp(-1), chg(0), mss(0) -{ -} +GroAtom::GroAtom() : atmnum(-1), resnum(-1), chggrp(-1), chg(0), mss(0) {} /** Copy constructor */ GroAtom::GroAtom(const GroAtom &other) - : atmname(other.atmname), resname(other.resname), chainname(other.chainname), atmtyp(other.atmtyp), - bndtyp(other.bndtyp), atmnum(other.atmnum), resnum(other.resnum), chggrp(other.chggrp), chg(other.chg), - mss(other.mss) -{ -} + : atmname(other.atmname), resname(other.resname), + chainname(other.chainname), atmtyp(other.atmtyp), bndtyp(other.bndtyp), + atmnum(other.atmnum), resnum(other.resnum), chggrp(other.chggrp), + chg(other.chg), mss(other.mss) {} /** Destructor */ -GroAtom::~GroAtom() -{ -} +GroAtom::~GroAtom() {} /** Copy assignment operator */ -GroAtom &GroAtom::operator=(const GroAtom &other) -{ - if (this != &other) - { - atmname = other.atmname; - resname = other.resname; - chainname = other.chainname; - atmtyp = other.atmtyp; - bndtyp = other.bndtyp; - atmnum = other.atmnum; - resnum = other.resnum; - chggrp = other.chggrp; - chg = other.chg; - mss = other.mss; - } - - return *this; +GroAtom &GroAtom::operator=(const GroAtom &other) { + if (this != &other) { + atmname = other.atmname; + resname = other.resname; + chainname = other.chainname; + atmtyp = other.atmtyp; + bndtyp = other.bndtyp; + atmnum = other.atmnum; + resnum = other.resnum; + chggrp = other.chggrp; + chg = other.chg; + mss = other.mss; + } + + return *this; } /** Comparison operator */ -bool GroAtom::operator==(const GroAtom &other) const -{ - return atmname == other.atmname and resname == other.resname and chainname == other.chainname and - atmtyp == other.atmtyp and bndtyp == other.bndtyp and atmnum == other.atmnum and resnum == other.resnum and - chggrp == other.chggrp and chg == other.chg and mss == other.mss; +bool GroAtom::operator==(const GroAtom &other) const { + return atmname == other.atmname and resname == other.resname and + chainname == other.chainname and atmtyp == other.atmtyp and + bndtyp == other.bndtyp and atmnum == other.atmnum and + resnum == other.resnum and chggrp == other.chggrp and + chg == other.chg and mss == other.mss; } /** Comparison operator */ -bool GroAtom::operator!=(const GroAtom &other) const -{ - return not operator==(other); +bool GroAtom::operator!=(const GroAtom &other) const { + return not operator==(other); } -const char *GroAtom::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); +const char *GroAtom::typeName() { + return QMetaType::typeName(qMetaTypeId()); } -const char *GroAtom::what() const -{ - return GroAtom::typeName(); -} +const char *GroAtom::what() const { return GroAtom::typeName(); } -QString GroAtom::toString() const -{ - if (isNull()) - return QObject::tr("GroAtom::null"); - else - return QObject::tr("GroAtom( name() = %1, number() = %2 )").arg(atmname).arg(atmnum); +QString GroAtom::toString() const { + if (isNull()) + return QObject::tr("GroAtom::null"); + else + return QObject::tr("GroAtom( name() = %1, number() = %2 )") + .arg(atmname) + .arg(atmnum); } /** Return whether or not this atom is null */ -bool GroAtom::isNull() const -{ - return operator==(GroAtom()); -} +bool GroAtom::isNull() const { return operator==(GroAtom()); } /** Return the name of the atom */ -AtomName GroAtom::name() const -{ - return AtomName(atmname); -} +AtomName GroAtom::name() const { return AtomName(atmname); } /** Return the number of the atom */ -AtomNum GroAtom::number() const -{ - return AtomNum(atmnum); -} +AtomNum GroAtom::number() const { return AtomNum(atmnum); } /** Return the name of the residue that contains this atom */ -ResName GroAtom::residueName() const -{ - return ResName(resname); -} +ResName GroAtom::residueName() const { return ResName(resname); } /** Return the number of the residue that contains this atom */ -ResNum GroAtom::residueNumber() const -{ - return ResNum(resnum); -} +ResNum GroAtom::residueNumber() const { return ResNum(resnum); } /** Return the name of the chain that contains this atom. This will be an empty name if a chain isn't specified */ -ChainName GroAtom::chainName() const -{ - return ChainName(chainname); -} +ChainName GroAtom::chainName() const { return ChainName(chainname); } /** Return the charge group of this atom */ -qint64 GroAtom::chargeGroup() const -{ - return chggrp; -} +qint64 GroAtom::chargeGroup() const { return chggrp; } /** Return the atom type of this atom */ -QString GroAtom::atomType() const -{ - return atmtyp; -} +QString GroAtom::atomType() const { return atmtyp; } -/** Return the bond type of this atom. This is normally the same as the atom type */ -QString GroAtom::bondType() const -{ - return bndtyp; -} +/** Return the bond type of this atom. This is normally the same as the atom + * type */ +QString GroAtom::bondType() const { return bndtyp; } /** Return the charge on this atom */ -SireUnits::Dimension::Charge GroAtom::charge() const -{ - return chg; -} +SireUnits::Dimension::Charge GroAtom::charge() const { return chg; } /** Return the mass of this atom */ -SireUnits::Dimension::MolarMass GroAtom::mass() const -{ - return mss; -} +SireUnits::Dimension::MolarMass GroAtom::mass() const { return mss; } /** Set the name of this atom */ -void GroAtom::setName(const QString &name) -{ - atmname = name; -} +void GroAtom::setName(const QString &name) { atmname = name; } /** Set the number of this atom */ -void GroAtom::setNumber(qint64 number) -{ - if (number >= 0) - atmnum = number; +void GroAtom::setNumber(qint64 number) { + if (number >= 0) + atmnum = number; } /** Set the name of the residue containing this atom */ -void GroAtom::setResidueName(const QString &name) -{ - resname = name; -} +void GroAtom::setResidueName(const QString &name) { resname = name; } /** Set the number of the residue containing this atom */ -void GroAtom::setResidueNumber(qint64 number) -{ - resnum = number; -} +void GroAtom::setResidueNumber(qint64 number) { resnum = number; } /** Set the name of the chain containing this atom */ -void GroAtom::setChainName(const QString &name) -{ - chainname = name; -} +void GroAtom::setChainName(const QString &name) { chainname = name; } /** Set the charge group of this atom */ -void GroAtom::setChargeGroup(qint64 grp) -{ - if (grp >= 0) - chggrp = grp; +void GroAtom::setChargeGroup(qint64 grp) { + if (grp >= 0) + chggrp = grp; } /** Set the atom type and bond type of this atom. To set the bond type separately, you need to set it after calling this function */ -void GroAtom::setAtomType(const QString &atomtype) -{ - atmtyp = atomtype; - bndtyp = atomtype; +void GroAtom::setAtomType(const QString &atomtype) { + atmtyp = atomtype; + bndtyp = atomtype; } /** Set the bond type of this atom */ -void GroAtom::setBondType(const QString &bondtype) -{ - bndtyp = bondtype; -} +void GroAtom::setBondType(const QString &bondtype) { bndtyp = bondtype; } /** Set the charge on this atom */ -void GroAtom::setCharge(SireUnits::Dimension::Charge charge) -{ - chg = charge; -} +void GroAtom::setCharge(SireUnits::Dimension::Charge charge) { chg = charge; } /** Set the mass of this atom */ -void GroAtom::setMass(SireUnits::Dimension::MolarMass mass) -{ - if (mass.value() >= 0) - mss = mass; +void GroAtom::setMass(SireUnits::Dimension::MolarMass mass) { + if (mass.value() >= 0) + mss = mass; } //////////////// @@ -349,1871 +279,1606 @@ void GroAtom::setMass(SireUnits::Dimension::MolarMass mass) static const RegisterMetaType r_gromoltyp(NO_ROOT); -QDataStream &operator<<(QDataStream &ds, const GroMolType &moltyp) -{ - writeHeader(ds, r_gromoltyp, 3); +QDataStream &operator<<(QDataStream &ds, const GroMolType &moltyp) { + writeHeader(ds, r_gromoltyp, 3); - SharedDataStream sds(ds); + SharedDataStream sds(ds); - sds << moltyp.nme << moltyp.warns << moltyp.atms0 << moltyp.atms1 << moltyp.bnds0 << moltyp.bnds1 << moltyp.angs0 - << moltyp.angs1 << moltyp.dihs0 << moltyp.dihs1 - << moltyp.cmaps0 << moltyp.cmaps1 << moltyp.first_atoms0 << moltyp.first_atoms1 << moltyp.ffield0 - << moltyp.ffield1 << moltyp.nexcl0 << moltyp.nexcl1 << moltyp.is_perturbable; + sds << moltyp.nme << moltyp.warns << moltyp.atms0 << moltyp.atms1 + << moltyp.bnds0 << moltyp.bnds1 << moltyp.angs0 << moltyp.angs1 + << moltyp.dihs0 << moltyp.dihs1 << moltyp.cmaps0 << moltyp.cmaps1 + << moltyp.first_atoms0 << moltyp.first_atoms1 << moltyp.ffield0 + << moltyp.ffield1 << moltyp.nexcl0 << moltyp.nexcl1 + << moltyp.is_perturbable; - return ds; + return ds; } -QDataStream &operator>>(QDataStream &ds, GroMolType &moltyp) -{ - VersionID v = readHeader(ds, r_gromoltyp); - - if (v == 1 or v == 2 or v == 3) - { - SharedDataStream sds(ds); +QDataStream &operator>>(QDataStream &ds, GroMolType &moltyp) { + VersionID v = readHeader(ds, r_gromoltyp); - sds >> moltyp.nme >> moltyp.warns >> moltyp.atms0 >> moltyp.atms1 >> moltyp.bnds0 >> moltyp.bnds1 >> - moltyp.angs0 >> moltyp.angs1 >> moltyp.dihs0 >> moltyp.dihs1; + if (v == 1 or v == 2 or v == 3) { + SharedDataStream sds(ds); - if (v == 3) - { - sds >> moltyp.cmaps0 >> moltyp.cmaps1; - } - else - { - moltyp.cmaps0 = QHash(); - moltyp.cmaps1 = QHash(); - } + sds >> moltyp.nme >> moltyp.warns >> moltyp.atms0 >> moltyp.atms1 >> + moltyp.bnds0 >> moltyp.bnds1 >> moltyp.angs0 >> moltyp.angs1 >> + moltyp.dihs0 >> moltyp.dihs1; - sds >> moltyp.first_atoms0 >> moltyp.first_atoms1; + if (v == 3) { + sds >> moltyp.cmaps0 >> moltyp.cmaps1; + } else { + moltyp.cmaps0 = QHash(); + moltyp.cmaps1 = QHash(); + } - if (v == 2) - sds >> moltyp.ffield0 >> moltyp.ffield1; - else - { - moltyp.ffield0 = MMDetail(); - moltyp.ffield1 = MMDetail(); - } + sds >> moltyp.first_atoms0 >> moltyp.first_atoms1; - sds >> moltyp.nexcl0 >> moltyp.nexcl1 >> moltyp.is_perturbable; + if (v == 2) + sds >> moltyp.ffield0 >> moltyp.ffield1; + else { + moltyp.ffield0 = MMDetail(); + moltyp.ffield1 = MMDetail(); } - else - throw version_error(v, "1,2,3", r_gromoltyp, CODELOC); - return ds; + sds >> moltyp.nexcl0 >> moltyp.nexcl1 >> moltyp.is_perturbable; + } else + throw version_error(v, "1,2,3", r_gromoltyp, CODELOC); + + return ds; } /** Constructor */ GroMolType::GroMolType() : nexcl0(3), nexcl1(3), // default to 3 as this is normal for most molecules is_perturbable(false) // default to a non-perturbable molecule -{ -} +{} + +/** Return the ID string for the cmap atom types 'atm0' 'atm1' 'atm2' 'atm3' + 'atm4'. This creates the string 'atm0;atm1;atm2;atm3;atm4' or + 'atm4;atm3;atm2;atm1;atm0' depending on which of the atoms is lower. The ';' + character is used as a separator as it cannot be in the atom names, as it is + used as a comment character in the Gromacs Top file */ +static QString get_cmap_id(const QString &atm0, const QString &atm1, + const QString &atm2, const QString &atm3, + const QString &atm4, int func_type) { + if ((atm0 < atm4) or (atm0 == atm4 and atm1 <= atm3)) { + return QString("%1;%2;%3;%4;%5;%6") + .arg(atm0, atm1, atm2, atm3, atm4) + .arg(func_type); + } else { + return QString("%1;%2;%3;%4;%5;%6") + .arg(atm4, atm3, atm2, atm1, atm0) + .arg(func_type); + } +} + +static QList cmap_id_to_atomtypes(const QString &cmap_id) { + // split the string by the ';' character + QStringList parts = cmap_id.split(";"); + + if (parts.size() != 6) { + throw SireError::incompatible_error( + QObject::tr("Invalid CMAP ID '%1'. Expected format: " + "'atm0;atm1;atm2;atm3;atm4;func_type'") + .arg(cmap_id), + CODELOC); + } -/** Return the ID string for the cmap atom types 'atm0' 'atm1' 'atm2' 'atm3' 'atm4'. This - creates the string 'atm0;atm1;atm2;atm3;atm4' or 'atm4;atm3;atm2;atm1;atm0' depending on which - of the atoms is lower. The ';' character is used as a separator - as it cannot be in the atom names, as it is used as a comment - character in the Gromacs Top file */ -static QString get_cmap_id(const QString &atm0, const QString &atm1, const QString &atm2, - const QString &atm3, const QString &atm4, int func_type) -{ - if ((atm0 < atm4) or (atm0 == atm4 and atm1 <= atm3)) - { - return QString("%1;%2;%3;%4;%5;%6").arg(atm0, atm1, atm2, atm3, atm4).arg(func_type); - } - else - { - return QString("%1;%2;%3;%4;%5;%6").arg(atm4, atm3, atm2, atm1, atm0).arg(func_type); - } + // return the first 5 parts as a list of atom types + return parts.mid(0, 5); } -static QList cmap_id_to_atomtypes(const QString &cmap_id) -{ - // split the string by the ';' character - QStringList parts = cmap_id.split(";"); +static QString cmap_to_string(const CMAPParameter &cmap) { + // format is "1 nRows nCols param param param..." + QStringList params; + params.append(QString("%1 %2").arg(cmap.nRows(), 2).arg(cmap.nColumns(), 2)); - if (parts.size() != 6) - { - throw SireError::incompatible_error(QObject::tr("Invalid CMAP ID '%1'. Expected format: 'atm0;atm1;atm2;atm3;atm4;func_type'") - .arg(cmap_id), - CODELOC); + // write as 10 values per line + // (this is the format used by Gromacs) + const auto vals = cmap.grid().toColumnMajorVector(); + + QStringList line; + + for (int i = 0; i < vals.size(); ++i) { + line.append(QString::number(vals[i], 'f', 8)); + + if (line.count() == 10) { + params.append(line.join(" ")); + line.clear(); } + } - // return the first 5 parts as a list of atom types - return parts.mid(0, 5); + if (not line.isEmpty()) { + params.append(line.join(" ")); + } + + return params.join("\\\n"); } -static QString cmap_to_string(const CMAPParameter &cmap) -{ - // format is "1 nRows nCols param param param..." - QStringList params; - params.append(QString("%1 %2").arg(cmap.nRows(), 2).arg(cmap.nColumns(), 2)); +static CMAPParameter string_to_cmap(QString params) { + params = params.trimmed().replace("\\\n", " "); - // write as 10 values per line - // (this is the format used by Gromacs) - const auto vals = cmap.grid().toColumnMajorVector(); + QStringList parts = params.split(" ", Qt::SkipEmptyParts); - QStringList line; + if (parts.size() < 3) { + throw SireError::incompatible_error( + QObject::tr("Invalid CMAP parameter string '%1'. Expected format: '1 " + "nRows nCols param param ...'") + .arg(params), + CODELOC); + } - for (int i = 0; i < vals.size(); ++i) - { - line.append(QString::number(vals[i], 'f', 8)); + bool ok_rows, ok_cols; - if (line.count() == 10) - { - params.append(line.join(" ")); - line.clear(); - } - } + int nRows = parts[0].toInt(&ok_rows); + int nCols = parts[1].toInt(&ok_cols); - if (not line.isEmpty()) - { - params.append(line.join(" ")); + if (!ok_rows || !ok_cols || nRows <= 0 || nCols <= 0) { + throw SireError::incompatible_error( + QObject::tr("Invalid CMAP parameter string '%1'. " + "Expected positive integers for nRows and nCols.") + .arg(params), + CODELOC); + } + + if (parts.size() != 2 + nRows * nCols) { + throw SireError::incompatible_error( + QObject::tr("Invalid CMAP parameter string '%1'. Expected %2 " + "parameters, got %3.") + .arg(params) + .arg(2 + nRows * nCols) + .arg(parts.size()), + CODELOC); + } + + QVector grid(nRows * nCols); + + for (int i = 0; i < nRows * nCols; ++i) { + bool ok; + double value = parts[2 + i].toDouble(&ok); + + if (!ok) { + throw SireError::incompatible_error( + QObject::tr("Invalid CMAP parameter string '%1'. " + "Expected floating-point values for parameters.") + .arg(params), + CODELOC); } - return params.join("\\\n"); + grid[i] = value; + } + + return CMAPParameter( + Array2D::fromColumnMajorVector(grid, nRows, nCols)); } -static CMAPParameter string_to_cmap(QString params) +/** Construct from the passed molecule */ +GroMolType::GroMolType(const SireMol::Molecule &mol, const PropertyMap &map) + : nexcl0(3), + nexcl1(3), // default to '3' as this is normal for most molecules + is_perturbable(false) // default to a non-perturbable molecule { - params = params.trimmed().replace("\\\n", " "); + if (mol.nAtoms() == 0) + return; - QStringList parts = params.split(" ", Qt::SkipEmptyParts); + // Try to see if this molecule is perturbable. + try { + is_perturbable = mol.property(map["is_perturbable"]).asABoolean(); + } catch (...) { + } - if (parts.size() < 3) - { - throw SireError::incompatible_error(QObject::tr( - "Invalid CMAP parameter string '%1'. Expected format: '1 nRows nCols param param ...'") - .arg(params), - CODELOC); - } + // Perturbable molecule. + if (is_perturbable) { + // For perturbable molecules we don't user the user PropertyMap to extract + // properties since the naming must be consistent for the properties at + // lambda = 0 and lambda = 1, e.g. "charge0" and "charge1". + + // get the name either from the molecule name or the name of the first + // residue + nme = mol.name(); - bool ok_rows, ok_cols; + if (nme.isEmpty()) { + nme = mol.residue(ResIdx(0)).name(); + } - int nRows = parts[0].toInt(&ok_rows); - int nCols = parts[1].toInt(&ok_cols); + // replace any strings in the name with underscores + nme = nme.simplified().replace(" ", "_"); - if (!ok_rows || !ok_cols || nRows <= 0 || nCols <= 0) - { - throw SireError::incompatible_error(QObject::tr("Invalid CMAP parameter string '%1'. " - "Expected positive integers for nRows and nCols.") - .arg(params), - CODELOC); + // get the forcefields for this molecule + try { + ffield0 = mol.property(map["forcefield0"]).asA(); + } catch (...) { + warns.append(QObject::tr("Cannot find a valid MM forcefield for this " + "molecule at lambda = 0!")); + } + try { + ffield1 = mol.property(map["forcefield1"]).asA(); + } catch (...) { + warns.append(QObject::tr("Cannot find a valid MM forcefield for this " + "molecule at lambda = 1!")); } - if (parts.size() != 2 + nRows * nCols) - { - throw SireError::incompatible_error(QObject::tr( - "Invalid CMAP parameter string '%1'. Expected %2 parameters, got %3.") - .arg(params) - .arg(2 + nRows * nCols) - .arg(parts.size()), - CODELOC); + const auto molinfo = mol.info(); + + bool uses_parallel = true; + if (map["parallel"].hasValue()) { + uses_parallel = map["parallel"].value().asA().value(); } - QVector grid(nRows * nCols); + // get information about all atoms in this molecule + auto extract_atoms = [&](bool is_lambda1) { + if (is_lambda1) + atms1 = QVector(molinfo.nAtoms()); + else + atms0 = QVector(molinfo.nAtoms()); - for (int i = 0; i < nRows * nCols; ++i) - { - bool ok; - double value = parts[2 + i].toDouble(&ok); + AtomMasses masses; + AtomElements elements; + AtomCharges charges; + AtomIntProperty groups; + AtomStringProperty atomtypes; + AtomStringProperty bondtypes; - if (!ok) - { - throw SireError::incompatible_error(QObject::tr("Invalid CMAP parameter string '%1'. " - "Expected floating-point values for parameters.") - .arg(params), - CODELOC); - } + bool has_mass(false), has_elem(false), has_chg(false), has_type(false), + has_bondtype(false); - grid[i] = value; - } + try { + if (is_lambda1) + masses = mol.property(map["mass1"]).asA(); + else + masses = mol.property(map["mass0"]).asA(); + has_mass = true; + } catch (...) { + } + + if (not has_mass) { + try { + if (is_lambda1) + elements = mol.property(map["element1"]).asA(); + else + elements = mol.property(map["element0"]).asA(); + has_elem = true; + } catch (...) { + } + } + + try { + if (is_lambda1) + charges = mol.property(map["charge1"]).asA(); + else + charges = mol.property(map["charge0"]).asA(); + has_chg = true; + } catch (...) { + } - return CMAPParameter(Array2D::fromColumnMajorVector(grid, nRows, nCols)); -} + try { + if (is_lambda1) + atomtypes = mol.property(map["atomtype1"]).asA(); + else + atomtypes = mol.property(map["atomtype0"]).asA(); + has_type = true; + } catch (...) { + } -/** Construct from the passed molecule */ -GroMolType::GroMolType(const SireMol::Molecule &mol, const PropertyMap &map) - : nexcl0(3), nexcl1(3), // default to '3' as this is normal for most molecules - is_perturbable(false) // default to a non-perturbable molecule -{ - if (mol.nAtoms() == 0) + try { + if (is_lambda1) + bondtypes = mol.property(map["bondtype1"]).asA(); + else + bondtypes = mol.property(map["bondtype0"]).asA(); + has_bondtype = true; + } catch (...) { + } + + if (not(has_chg and has_type and (has_elem or has_mass))) { + warns.append( + QObject::tr( + "Cannot find valid charge, atomtype and (element or mass) " + "properties for the molecule. These are needed! " + "has_charge=%1, has_atomtype=%2, has_mass=%3, has_element=%4") + .arg(has_chg) + .arg(has_type) + .arg(has_mass) + .arg(has_elem)); return; + } - // Try to see if this molecule is perturbable. - try - { - is_perturbable = mol.property(map["is_perturbable"]).asABoolean(); - } - catch (...) - { - } + // run through the atoms in AtomIdx order + auto extract_atom = [&](int iatm, bool is_lambda1) { + AtomIdx i(iatm); - // Perturbable molecule. - if (is_perturbable) - { - // For perturbable molecules we don't user the user PropertyMap to extract - // properties since the naming must be consistent for the properties at - // lambda = 0 and lambda = 1, e.g. "charge0" and "charge1". + const auto cgatomidx = molinfo.cgAtomIdx(i); + const auto residx = molinfo.parentResidue(i); - // get the name either from the molecule name or the name of the first - // residue - nme = mol.name(); + QString chainname; - if (nme.isEmpty()) - { - nme = mol.residue(ResIdx(0)).name(); + if (molinfo.isWithinChain(residx)) { + chainname = molinfo.name(molinfo.parentChain(residx)).value(); } - // replace any strings in the name with underscores - nme = nme.simplified().replace(" ", "_"); - - // get the forcefields for this molecule - try - { - ffield0 = mol.property(map["forcefield0"]).asA(); - } - catch (...) - { - warns.append(QObject::tr("Cannot find a valid MM forcefield for this molecule at lambda = 0!")); - } - try - { - ffield1 = mol.property(map["forcefield1"]).asA(); - } - catch (...) - { - warns.append(QObject::tr("Cannot find a valid MM forcefield for this molecule at lambda = 1!")); - } + // atom numbers have to count up sequentially from 1 + int atomnum = i + 1; + QString atomnam = molinfo.name(i); - const auto molinfo = mol.info(); + // assuming that residues are in the same order as the atoms + int resnum = residx + 1; + QString resnam = molinfo.name(residx); - bool uses_parallel = true; - if (map["parallel"].hasValue()) - { - uses_parallel = map["parallel"].value().asA().value(); + // people like to preserve the residue numbers of ligands and + // proteins. This is very challenging for the gromacs topology, + // as it would force a different topology for every solvent molecule, + // so deciding on the difference between protein/ligand and solvent + // is tough. Will preserve the residue number if the number of + // residues is greater than 1 and the number of atoms is greater + // than 32 (so octanol is a solvent) + if (molinfo.nResidues() > 1 or molinfo.nAtoms() > 32) { + resnum = molinfo.number(residx).value(); } - // get information about all atoms in this molecule - auto extract_atoms = [&](bool is_lambda1) - { - if (is_lambda1) - atms1 = QVector(molinfo.nAtoms()); - else - atms0 = QVector(molinfo.nAtoms()); - - AtomMasses masses; - AtomElements elements; - AtomCharges charges; - AtomIntProperty groups; - AtomStringProperty atomtypes; - AtomStringProperty bondtypes; - - bool has_mass(false), has_elem(false), has_chg(false), has_type(false), has_bondtype(false); - - try - { - if (is_lambda1) - masses = mol.property(map["mass1"]).asA(); - else - masses = mol.property(map["mass0"]).asA(); - has_mass = true; - } - catch (...) - { - } + // Just use the atom number as the charge group for perturbable + // molecules. + int group = atomnum; - if (not has_mass) - { - try - { - if (is_lambda1) - elements = mol.property(map["element1"]).asA(); - else - elements = mol.property(map["element0"]).asA(); - has_elem = true; - } - catch (...) - { - } - } + auto charge = charges[cgatomidx]; - try - { - if (is_lambda1) - charges = mol.property(map["charge1"]).asA(); - else - charges = mol.property(map["charge0"]).asA(); - has_chg = true; - } - catch (...) - { - } + SireUnits::Dimension::MolarMass mass; - try - { - if (is_lambda1) - atomtypes = mol.property(map["atomtype1"]).asA(); - else - atomtypes = mol.property(map["atomtype0"]).asA(); - has_type = true; - } - catch (...) - { - } + if (has_mass) { + mass = masses[cgatomidx]; + } else { + mass = elements[cgatomidx].mass(); + } - try - { - if (is_lambda1) - bondtypes = mol.property(map["bondtype1"]).asA(); - else - bondtypes = mol.property(map["bondtype0"]).asA(); - has_bondtype = true; - } - catch (...) - { - } + if (mass < 0) { + // not allowed to have a negative mass + mass = 1.0 * g_per_mol; + } - if (not(has_chg and has_type and (has_elem or has_mass))) - { - warns.append(QObject::tr("Cannot find valid charge, atomtype and (element or mass) " - "properties for the molecule. These are needed! " - "has_charge=%1, has_atomtype=%2, has_mass=%3, has_element=%4") - .arg(has_chg) - .arg(has_type) - .arg(has_mass) - .arg(has_elem)); - return; - } + QString atomtype = atomtypes[cgatomidx]; - // run through the atoms in AtomIdx order - auto extract_atom = [&](int iatm, bool is_lambda1) - { - AtomIdx i(iatm); + if (is_lambda1) { + auto &atom = atms1[i]; - const auto cgatomidx = molinfo.cgAtomIdx(i); - const auto residx = molinfo.parentResidue(i); + atom.setName(atomnam); + atom.setNumber(atomnum); + atom.setResidueName(resnam); + atom.setResidueNumber(resnum); + atom.setChainName(chainname); + atom.setChargeGroup(group); + atom.setCharge(charge); + atom.setMass(mass); + atom.setAtomType(atomtype); - QString chainname; + if (has_bondtype) { + atom.setBondType(bondtypes[cgatomidx]); + } else { + atom.setBondType(atomtype); + } + } else { + auto &atom = atms0[i]; - if (molinfo.isWithinChain(residx)) - { - chainname = molinfo.name(molinfo.parentChain(residx)).value(); - } + atom.setName(atomnam); + atom.setNumber(atomnum); + atom.setResidueName(resnam); + atom.setResidueNumber(resnum); + atom.setChainName(chainname); + atom.setChargeGroup(group); + atom.setCharge(charge); + atom.setMass(mass); + atom.setAtomType(atomtype); - // atom numbers have to count up sequentially from 1 - int atomnum = i + 1; - QString atomnam = molinfo.name(i); - - // assuming that residues are in the same order as the atoms - int resnum = residx + 1; - QString resnam = molinfo.name(residx); - - // people like to preserve the residue numbers of ligands and - // proteins. This is very challenging for the gromacs topology, - // as it would force a different topology for every solvent molecule, - // so deciding on the difference between protein/ligand and solvent - // is tough. Will preserve the residue number if the number of - // residues is greater than 1 and the number of atoms is greater - // than 32 (so octanol is a solvent) - if (molinfo.nResidues() > 1 or molinfo.nAtoms() > 32) - { - resnum = molinfo.number(residx).value(); - } + if (has_bondtype) { + atom.setBondType(bondtypes[cgatomidx]); + } else { + atom.setBondType(atomtype); + } + } + }; - // Just use the atom number as the charge group for perturbable molecules. - int group = atomnum; + if (uses_parallel) { + tbb::parallel_for(tbb::blocked_range(0, molinfo.nAtoms()), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + extract_atom(i, is_lambda1); + } + }); + } else { + for (int i = 0; i < molinfo.nAtoms(); ++i) { + extract_atom(i, is_lambda1); + } + } + }; - auto charge = charges[cgatomidx]; + // get all of the bonds in this molecule + auto extract_bonds = [&](bool is_lambda1) { + bool has_conn(false), has_funcs(false); - SireUnits::Dimension::MolarMass mass; + TwoAtomFunctions funcs; + Connectivity conn; - if (has_mass) - { - mass = masses[cgatomidx]; - } - else - { - mass = elements[cgatomidx].mass(); - } + const auto R = InternalPotential::symbols().bond().r(); - if (mass < 0) - { - // not allowed to have a negative mass - mass = 1.0 * g_per_mol; - } + try { + if (is_lambda1) + funcs = mol.property(map["bond1"]).asA(); + else + funcs = mol.property(map["bond0"]).asA(); + has_funcs = true; + } catch (...) { + } + + try { + conn = mol.property(map["connectivity"]).asA(); + has_conn = true; + } catch (...) { + } + + // get the bond potentials first + if (has_funcs) { + for (const auto &bond : funcs.potentials()) { + AtomIdx atom0 = molinfo.atomIdx(bond.atom0()); + AtomIdx atom1 = molinfo.atomIdx(bond.atom1()); + + if (atom0 > atom1) + qSwap(atom0, atom1); + + if (is_lambda1) + bnds1.insert(BondID(atom0, atom1), GromacsBond(bond.function(), R)); + else + bnds0.insert(BondID(atom0, atom1), GromacsBond(bond.function(), R)); + } + } + + // now fill in any missing bonded atoms with null bonds + if (has_conn) { + for (const auto &bond : conn.getBonds()) { + AtomIdx atom0 = molinfo.atomIdx(bond.atom0()); + AtomIdx atom1 = molinfo.atomIdx(bond.atom1()); + + if (atom0 > atom1) + qSwap(atom0, atom1); + + BondID b(atom0, atom1); + + if (is_lambda1) { + if (not bnds1.contains(b)) + bnds1.insert(b, + GromacsBond(5)); // function 5 is a simple connection + } else { + if (not bnds0.contains(b)) + bnds0.insert(b, + GromacsBond(5)); // function 5 is a simple connection + } + } + } + }; - QString atomtype = atomtypes[cgatomidx]; - - if (is_lambda1) - { - auto &atom = atms1[i]; - - atom.setName(atomnam); - atom.setNumber(atomnum); - atom.setResidueName(resnam); - atom.setResidueNumber(resnum); - atom.setChainName(chainname); - atom.setChargeGroup(group); - atom.setCharge(charge); - atom.setMass(mass); - atom.setAtomType(atomtype); - - if (has_bondtype) - { - atom.setBondType(bondtypes[cgatomidx]); - } - else - { - atom.setBondType(atomtype); - } - } - else - { - auto &atom = atms0[i]; - - atom.setName(atomnam); - atom.setNumber(atomnum); - atom.setResidueName(resnam); - atom.setResidueNumber(resnum); - atom.setChainName(chainname); - atom.setChargeGroup(group); - atom.setCharge(charge); - atom.setMass(mass); - atom.setAtomType(atomtype); - - if (has_bondtype) - { - atom.setBondType(bondtypes[cgatomidx]); - } - else - { - atom.setBondType(atomtype); - } - } - }; - - if (uses_parallel) - { - tbb::parallel_for(tbb::blocked_range(0, molinfo.nAtoms()), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - extract_atom(i, is_lambda1); - } }); - } - else - { - for (int i = 0; i < molinfo.nAtoms(); ++i) - { - extract_atom(i, is_lambda1); - } - } - }; + // get all of the angles in this molecule + auto extract_angles = [&](bool is_lambda1) { + bool has_funcs(false); - // get all of the bonds in this molecule - auto extract_bonds = [&](bool is_lambda1) - { - bool has_conn(false), has_funcs(false); + ThreeAtomFunctions funcs; - TwoAtomFunctions funcs; - Connectivity conn; + const auto theta = InternalPotential::symbols().angle().theta(); - const auto R = InternalPotential::symbols().bond().r(); + try { + if (is_lambda1) + funcs = mol.property(map["angle1"]).asA(); + else + funcs = mol.property(map["angle0"]).asA(); + has_funcs = true; + } catch (...) { + } + + if (has_funcs) { + for (const auto &angle : funcs.potentials()) { + AtomIdx atom0 = molinfo.atomIdx(angle.atom0()); + AtomIdx atom1 = molinfo.atomIdx(angle.atom1()); + AtomIdx atom2 = molinfo.atomIdx(angle.atom2()); + + if (atom0 > atom2) + qSwap(atom0, atom2); + + if (is_lambda1) { + angs1.insert(AngleID(atom0, atom1, atom2), + GromacsAngle(angle.function(), theta)); + } else { + angs0.insert(AngleID(atom0, atom1, atom2), + GromacsAngle(angle.function(), theta)); + } + } + } + }; - try - { - if (is_lambda1) - funcs = mol.property(map["bond1"]).asA(); - else - funcs = mol.property(map["bond0"]).asA(); - has_funcs = true; - } - catch (...) - { - } + // get all of the dihedrals in this molecule + auto extract_dihedrals = [&](bool is_lambda1) { + bool has_funcs(false); - try - { - conn = mol.property(map["connectivity"]).asA(); - has_conn = true; - } - catch (...) - { - } + FourAtomFunctions funcs; - // get the bond potentials first - if (has_funcs) - { - for (const auto &bond : funcs.potentials()) - { - AtomIdx atom0 = molinfo.atomIdx(bond.atom0()); - AtomIdx atom1 = molinfo.atomIdx(bond.atom1()); - - if (atom0 > atom1) - qSwap(atom0, atom1); - - if (is_lambda1) - bnds1.insert(BondID(atom0, atom1), GromacsBond(bond.function(), R)); - else - bnds0.insert(BondID(atom0, atom1), GromacsBond(bond.function(), R)); - } - } + const auto phi = InternalPotential::symbols().dihedral().phi(); + const auto theta = InternalPotential::symbols().improper().theta(); - // now fill in any missing bonded atoms with null bonds - if (has_conn) - { - for (const auto &bond : conn.getBonds()) - { - AtomIdx atom0 = molinfo.atomIdx(bond.atom0()); - AtomIdx atom1 = molinfo.atomIdx(bond.atom1()); + try { + if (is_lambda1) + funcs = mol.property(map["dihedral1"]).asA(); + else + funcs = mol.property(map["dihedral0"]).asA(); + has_funcs = true; + } catch (...) { + } - if (atom0 > atom1) - qSwap(atom0, atom1); + if (has_funcs) { + for (const auto &dihedral : funcs.potentials()) { + AtomIdx atom0 = molinfo.atomIdx(dihedral.atom0()); + AtomIdx atom1 = molinfo.atomIdx(dihedral.atom1()); + AtomIdx atom2 = molinfo.atomIdx(dihedral.atom2()); + AtomIdx atom3 = molinfo.atomIdx(dihedral.atom3()); - BondID b(atom0, atom1); + if (atom0 > atom3) { + qSwap(atom0, atom3); + qSwap(atom1, atom2); + } - if (is_lambda1) - { - if (not bnds1.contains(b)) - bnds1.insert(b, GromacsBond(5)); // function 5 is a simple connection - } - else - { - if (not bnds0.contains(b)) - bnds0.insert(b, GromacsBond(5)); // function 5 is a simple connection - } - } - } - }; + // get all of the dihedral terms (could be a lot) + auto parts = GromacsDihedral::construct(dihedral.function(), phi); - // get all of the angles in this molecule - auto extract_angles = [&](bool is_lambda1) - { - bool has_funcs(false); + DihedralID dihid(atom0, atom1, atom2, atom3); - ThreeAtomFunctions funcs; + for (const auto &part : parts) { + if (is_lambda1) + dihs1.insert(dihid, part); + else + dihs0.insert(dihid, part); + } + } + } - const auto theta = InternalPotential::symbols().angle().theta(); + bool has_imps(false); - try - { - if (is_lambda1) - funcs = mol.property(map["angle1"]).asA(); - else - funcs = mol.property(map["angle0"]).asA(); - has_funcs = true; - } - catch (...) - { - } + FourAtomFunctions imps; - if (has_funcs) - { - for (const auto &angle : funcs.potentials()) - { - AtomIdx atom0 = molinfo.atomIdx(angle.atom0()); - AtomIdx atom1 = molinfo.atomIdx(angle.atom1()); - AtomIdx atom2 = molinfo.atomIdx(angle.atom2()); + try { + if (is_lambda1) + imps = mol.property(map["improper1"]).asA(); + else + imps = mol.property(map["improper0"]).asA(); + has_imps = true; + } catch (...) { + } - if (atom0 > atom2) - qSwap(atom0, atom2); + if (has_imps) { + for (const auto &improper : imps.potentials()) { + AtomIdx atom0 = molinfo.atomIdx(improper.atom0()); + AtomIdx atom1 = molinfo.atomIdx(improper.atom1()); + AtomIdx atom2 = molinfo.atomIdx(improper.atom2()); + AtomIdx atom3 = molinfo.atomIdx(improper.atom3()); - if (is_lambda1) - { - angs1.insert(AngleID(atom0, atom1, atom2), GromacsAngle(angle.function(), theta)); - } - else - { - angs0.insert(AngleID(atom0, atom1, atom2), GromacsAngle(angle.function(), theta)); - } - } - } - }; + // get all of the dihedral terms (could be a lot) + auto parts = GromacsDihedral::constructImproper(improper.function(), + phi, theta); - // get all of the dihedrals in this molecule - auto extract_dihedrals = [&](bool is_lambda1) - { - bool has_funcs(false); + DihedralID impid(atom0, atom1, atom2, atom3); - FourAtomFunctions funcs; + for (const auto &part : parts) { + if (is_lambda1) + dihs1.insert(impid, part); + else + dihs0.insert(impid, part); + } + } + } + }; - const auto phi = InternalPotential::symbols().dihedral().phi(); - const auto theta = InternalPotential::symbols().improper().theta(); + // get all of the CMAP terms in this molecule + auto extract_cmaps = [&](bool is_lambda1) { + bool has_cmaps(false); + CMAPFunctions cmaps; - try - { - if (is_lambda1) - funcs = mol.property(map["dihedral1"]).asA(); - else - funcs = mol.property(map["dihedral0"]).asA(); - has_funcs = true; - } - catch (...) - { - } + try { + if (is_lambda1) + cmaps = mol.property(map["cmap1"]).asA(); + else + cmaps = mol.property(map["cmap0"]).asA(); + + has_cmaps = true; + } catch (...) { + } + + if (has_cmaps) { + QHash cmap_params; + + for (const auto &cmap : cmaps.parameters()) { + AtomIdx atom0 = molinfo.atomIdx(cmap.atom0()); + AtomIdx atom1 = molinfo.atomIdx(cmap.atom1()); + AtomIdx atom2 = molinfo.atomIdx(cmap.atom2()); + AtomIdx atom3 = molinfo.atomIdx(cmap.atom3()); + AtomIdx atom4 = molinfo.atomIdx(cmap.atom4()); + + if ((atom0 > atom4) or (atom0 == atom4 and atom1 > atom2)) { + qSwap(atom0, atom4); + qSwap(atom1, atom2); + } + + // we will store the CMAP parameter as a string - this + // will let us de-duplicate parameters later + if (cmap_params.contains(cmap.parameter())) { + if (is_lambda1) + cmaps1.insert(CMAPID(atom0, atom1, atom2, atom3, atom4), + cmap_params[cmap.parameter()]); + else + cmaps0.insert(CMAPID(atom0, atom1, atom2, atom3, atom4), + cmap_params[cmap.parameter()]); + } else { + // store the parameter as a string + QString param_str = cmap_to_string(cmap.parameter()); + cmap_params.insert(cmap.parameter(), param_str); - if (has_funcs) - { - for (const auto &dihedral : funcs.potentials()) - { - AtomIdx atom0 = molinfo.atomIdx(dihedral.atom0()); - AtomIdx atom1 = molinfo.atomIdx(dihedral.atom1()); - AtomIdx atom2 = molinfo.atomIdx(dihedral.atom2()); - AtomIdx atom3 = molinfo.atomIdx(dihedral.atom3()); - - if (atom0 > atom3) - { - qSwap(atom0, atom3); - qSwap(atom1, atom2); - } + if (is_lambda1) + cmaps1.insert(CMAPID(atom0, atom1, atom2, atom3, atom4), + param_str); + else + cmaps0.insert(CMAPID(atom0, atom1, atom2, atom3, atom4), + param_str); + } + } + } + }; - // get all of the dihedral terms (could be a lot) - auto parts = GromacsDihedral::construct(dihedral.function(), phi); + const QVector> functions = { + extract_atoms, extract_bonds, extract_angles, extract_dihedrals, + extract_cmaps}; - DihedralID dihid(atom0, atom1, atom2, atom3); + if (uses_parallel) { + tbb::parallel_for(tbb::blocked_range(0, functions.count(), 1), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + functions[i](false); + functions[i](true); + } + }); + } else { + for (int i = 0; i < functions.count(); ++i) { + functions[i](false); + functions[i](true); + } + } - for (const auto &part : parts) - { - if (is_lambda1) - dihs1.insert(dihid, part); - else - dihs0.insert(dihid, part); - } - } - } + // sanitise this object + this->_pvt_sanitise(); + this->_pvt_sanitise(true); + } - bool has_imps(false); + // Regular molecule. + else { + // get the name either from the molecule name or the name of the first + // residue + nme = mol.name(); - FourAtomFunctions imps; + if (nme.isEmpty()) { + nme = mol.residue(ResIdx(0)).name(); + } - try - { - if (is_lambda1) - imps = mol.property(map["improper1"]).asA(); - else - imps = mol.property(map["improper0"]).asA(); - has_imps = true; - } - catch (...) - { - } + // replace any strings in the name with underscores + nme = nme.simplified().replace(" ", "_"); - if (has_imps) - { - for (const auto &improper : imps.potentials()) - { - AtomIdx atom0 = molinfo.atomIdx(improper.atom0()); - AtomIdx atom1 = molinfo.atomIdx(improper.atom1()); - AtomIdx atom2 = molinfo.atomIdx(improper.atom2()); - AtomIdx atom3 = molinfo.atomIdx(improper.atom3()); + // get the forcefield for this molecule + try { + ffield0 = mol.property(map["forcefield"]).asA(); + } catch (...) { + warns.append( + QObject::tr("Cannot find a valid MM forcefield for this molecule!")); + } - // get all of the dihedral terms (could be a lot) - auto parts = GromacsDihedral::constructImproper(improper.function(), phi, theta); - - DihedralID impid(atom0, atom1, atom2, atom3); - - for (const auto &part : parts) - { - if (is_lambda1) - dihs1.insert(impid, part); - else - dihs0.insert(impid, part); - } - } - } - }; - - // get all of the CMAP terms in this molecule - auto extract_cmaps = [&](bool is_lambda1) - { - bool has_cmaps(false); - CMAPFunctions cmaps; - - try - { - if (is_lambda1) - cmaps = mol.property(map["cmap1"]).asA(); - else - cmaps = mol.property(map["cmap0"]).asA(); + const auto molinfo = mol.info(); - has_cmaps = true; - } - catch (...) - { - } + bool uses_parallel = true; + if (map["parallel"].hasValue()) { + uses_parallel = map["parallel"].value().asA().value(); + } + + // get information about all atoms in this molecule + auto extract_atoms = [&]() { + atms0 = QVector(molinfo.nAtoms()); + + AtomMasses masses; + AtomElements elements; + AtomCharges charges; + AtomIntProperty groups; + AtomStringProperty atomtypes; + AtomStringProperty bondtypes; + + bool has_mass(false), has_elem(false), has_chg(false), has_group(false), + has_type(false); + bool has_bondtype(false); + + try { + masses = mol.property(map["mass"]).asA(); + has_mass = true; + } catch (...) { + } + + if (not has_mass) { + try { + elements = mol.property(map["element"]).asA(); + has_elem = true; + } catch (...) { + } + } + + try { + charges = mol.property(map["charge"]).asA(); + has_chg = true; + } catch (...) { + } + + try { + groups = mol.property(map["charge_group"]).asA(); + has_group = true; + } catch (...) { + } + + try { + atomtypes = mol.property(map["atomtype"]).asA(); + has_type = true; + } catch (...) { + } + + try { + bondtypes = mol.property(map["bondtype"]).asA(); + has_bondtype = true; + } catch (...) { + } + + if (not(has_chg and has_type and (has_elem or has_mass))) { + warns.append( + QObject::tr( + "Cannot find valid charge, atomtype and (element or mass) " + "properties for the molecule. These are needed! " + "has_charge=%1, has_atomtype=%2, has_mass=%3, has_element=%4") + .arg(has_chg) + .arg(has_type) + .arg(has_mass) + .arg(has_elem)); + return; + } - if (has_cmaps) - { - QHash cmap_params; - - for (const auto &cmap : cmaps.parameters()) - { - AtomIdx atom0 = molinfo.atomIdx(cmap.atom0()); - AtomIdx atom1 = molinfo.atomIdx(cmap.atom1()); - AtomIdx atom2 = molinfo.atomIdx(cmap.atom2()); - AtomIdx atom3 = molinfo.atomIdx(cmap.atom3()); - AtomIdx atom4 = molinfo.atomIdx(cmap.atom4()); - - if ((atom0 > atom4) or (atom0 == atom4 and atom1 > atom2)) - { - qSwap(atom0, atom4); - qSwap(atom1, atom2); - } + // run through the atoms in AtomIdx order + auto extract_atom = [&](int iatm) { + AtomIdx i(iatm); - // we will store the CMAP parameter as a string - this - // will let us de-duplicate parameters later - if (cmap_params.contains(cmap.parameter())) - { - if (is_lambda1) - cmaps1.insert(CMAPID(atom0, atom1, atom2, atom3, atom4), - cmap_params[cmap.parameter()]); - else - cmaps0.insert(CMAPID(atom0, atom1, atom2, atom3, atom4), - cmap_params[cmap.parameter()]); - } - else - { - // store the parameter as a string - QString param_str = cmap_to_string(cmap.parameter()); - cmap_params.insert(cmap.parameter(), param_str); - - if (is_lambda1) - cmaps1.insert(CMAPID(atom0, atom1, atom2, atom3, atom4), param_str); - else - cmaps0.insert(CMAPID(atom0, atom1, atom2, atom3, atom4), param_str); - } - } - } - }; + const auto cgatomidx = molinfo.cgAtomIdx(i); + const auto residx = molinfo.parentResidue(i); - const QVector> functions = {extract_atoms, extract_bonds, extract_angles, - extract_dihedrals, extract_cmaps}; + QString chainname; - if (uses_parallel) - { - tbb::parallel_for(tbb::blocked_range(0, functions.count(), 1), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - functions[i](false); - functions[i](true); - } }); - } - else - { - for (int i = 0; i < functions.count(); ++i) - { - functions[i](false); - functions[i](true); - } + if (molinfo.isWithinChain(residx)) { + chainname = molinfo.name(molinfo.parentChain(residx)).value(); } - // sanitise this object - this->_pvt_sanitise(); - this->_pvt_sanitise(true); - } - - // Regular molecule. - else - { - // get the name either from the molecule name or the name of the first - // residue - nme = mol.name(); - - if (nme.isEmpty()) - { - nme = mol.residue(ResIdx(0)).name(); - } + // atom numbers have to count up sequentially from 1 + int atomnum = i + 1; + QString atomnam = molinfo.name(i); - // replace any strings in the name with underscores - nme = nme.simplified().replace(" ", "_"); + // assuming that residues are in the same order as the atoms + int resnum = residx + 1; + QString resnam = molinfo.name(residx); - // get the forcefield for this molecule - try - { - ffield0 = mol.property(map["forcefield"]).asA(); - } - catch (...) - { - warns.append(QObject::tr("Cannot find a valid MM forcefield for this molecule!")); + // people like to preserve the residue numbers of ligands and + // proteins. This is very challenging for the gromacs topology, + // as it would force a different topology for every solvent molecule, + // so deciding on the difference between protein/ligand and solvent + // is tough. Will preserve the residue number if the number of + // residues is greater than 1 and the number of atoms is greater + // than 32 (so octanol is a solvent) + if (molinfo.nResidues() > 1 or molinfo.nAtoms() > 32) { + resnum = molinfo.number(residx).value(); } - const auto molinfo = mol.info(); + int group = atomnum; - bool uses_parallel = true; - if (map["parallel"].hasValue()) - { - uses_parallel = map["parallel"].value().asA().value(); + if (has_group) { + group = groups[cgatomidx]; } - // get information about all atoms in this molecule - auto extract_atoms = [&]() - { - atms0 = QVector(molinfo.nAtoms()); - - AtomMasses masses; - AtomElements elements; - AtomCharges charges; - AtomIntProperty groups; - AtomStringProperty atomtypes; - AtomStringProperty bondtypes; - - bool has_mass(false), has_elem(false), has_chg(false), has_group(false), has_type(false); - bool has_bondtype(false); - - try - { - masses = mol.property(map["mass"]).asA(); - has_mass = true; - } - catch (...) - { - } - - if (not has_mass) - { - try - { - elements = mol.property(map["element"]).asA(); - has_elem = true; - } - catch (...) - { - } - } - - try - { - charges = mol.property(map["charge"]).asA(); - has_chg = true; - } - catch (...) - { - } - - try - { - groups = mol.property(map["charge_group"]).asA(); - has_group = true; - } - catch (...) - { - } - - try - { - atomtypes = mol.property(map["atomtype"]).asA(); - has_type = true; - } - catch (...) - { - } - - try - { - bondtypes = mol.property(map["bondtype"]).asA(); - has_bondtype = true; - } - catch (...) - { - } - - if (not(has_chg and has_type and (has_elem or has_mass))) - { - warns.append(QObject::tr("Cannot find valid charge, atomtype and (element or mass) " - "properties for the molecule. These are needed! " - "has_charge=%1, has_atomtype=%2, has_mass=%3, has_element=%4") - .arg(has_chg) - .arg(has_type) - .arg(has_mass) - .arg(has_elem)); - return; - } - - // run through the atoms in AtomIdx order - auto extract_atom = [&](int iatm) - { - AtomIdx i(iatm); - - const auto cgatomidx = molinfo.cgAtomIdx(i); - const auto residx = molinfo.parentResidue(i); - - QString chainname; - - if (molinfo.isWithinChain(residx)) - { - chainname = molinfo.name(molinfo.parentChain(residx)).value(); - } - - // atom numbers have to count up sequentially from 1 - int atomnum = i + 1; - QString atomnam = molinfo.name(i); - - // assuming that residues are in the same order as the atoms - int resnum = residx + 1; - QString resnam = molinfo.name(residx); - - // people like to preserve the residue numbers of ligands and - // proteins. This is very challenging for the gromacs topology, - // as it would force a different topology for every solvent molecule, - // so deciding on the difference between protein/ligand and solvent - // is tough. Will preserve the residue number if the number of - // residues is greater than 1 and the number of atoms is greater - // than 32 (so octanol is a solvent) - if (molinfo.nResidues() > 1 or molinfo.nAtoms() > 32) - { - resnum = molinfo.number(residx).value(); - } - - int group = atomnum; - - if (has_group) - { - group = groups[cgatomidx]; - } - - auto charge = charges[cgatomidx]; - - SireUnits::Dimension::MolarMass mass; - - if (has_mass) - { - mass = masses[cgatomidx]; - } - else - { - mass = elements[cgatomidx].mass(); - } - - if (mass < 0) - { - // not allowed to have a negative mass - mass = 1.0 * g_per_mol; - } - - QString atomtype = atomtypes[cgatomidx]; - - auto &atom = atms0[i]; - atom.setName(atomnam); - atom.setNumber(atomnum); - atom.setResidueName(resnam); - atom.setResidueNumber(resnum); - atom.setChainName(chainname); - atom.setChargeGroup(group); - atom.setCharge(charge); - atom.setMass(mass); - atom.setAtomType(atomtype); - - if (has_bondtype) - { - atom.setBondType(bondtypes[cgatomidx]); - } - else - { - atom.setBondType(atomtype); - } - }; - - if (uses_parallel) - { - tbb::parallel_for(tbb::blocked_range(0, molinfo.nAtoms()), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - extract_atom(i); - } }); - } - else - { - for (int i = 0; i < molinfo.nAtoms(); ++i) - { - extract_atom(i); - } - } - }; - - // get all of the bonds in this molecule - auto extract_bonds = [&]() - { - bool has_conn(false), has_funcs(false); - - TwoAtomFunctions funcs; - Connectivity conn; + auto charge = charges[cgatomidx]; - const auto R = InternalPotential::symbols().bond().r(); + SireUnits::Dimension::MolarMass mass; - try - { - funcs = mol.property(map["bond"]).asA(); - has_funcs = true; - } - catch (...) - { - } + if (has_mass) { + mass = masses[cgatomidx]; + } else { + mass = elements[cgatomidx].mass(); + } - try - { - conn = mol.property(map["connectivity"]).asA(); - has_conn = true; - } - catch (...) - { - } + if (mass < 0) { + // not allowed to have a negative mass + mass = 1.0 * g_per_mol; + } - // get the bond potentials first - if (has_funcs) - { - for (const auto &bond : funcs.potentials()) - { - AtomIdx atom0 = molinfo.atomIdx(bond.atom0()); - AtomIdx atom1 = molinfo.atomIdx(bond.atom1()); + QString atomtype = atomtypes[cgatomidx]; - if (atom0 > atom1) - qSwap(atom0, atom1); + auto &atom = atms0[i]; + atom.setName(atomnam); + atom.setNumber(atomnum); + atom.setResidueName(resnam); + atom.setResidueNumber(resnum); + atom.setChainName(chainname); + atom.setChargeGroup(group); + atom.setCharge(charge); + atom.setMass(mass); + atom.setAtomType(atomtype); - bnds0.insert(BondID(atom0, atom1), GromacsBond(bond.function(), R)); - } - } + if (has_bondtype) { + atom.setBondType(bondtypes[cgatomidx]); + } else { + atom.setBondType(atomtype); + } + }; - // now fill in any missing bonded atoms with null bonds - if (has_conn) - { - for (const auto &bond : conn.getBonds()) - { - AtomIdx atom0 = molinfo.atomIdx(bond.atom0()); - AtomIdx atom1 = molinfo.atomIdx(bond.atom1()); + if (uses_parallel) { + tbb::parallel_for(tbb::blocked_range(0, molinfo.nAtoms()), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + extract_atom(i); + } + }); + } else { + for (int i = 0; i < molinfo.nAtoms(); ++i) { + extract_atom(i); + } + } + }; - if (atom0 > atom1) - qSwap(atom0, atom1); + // get all of the bonds in this molecule + auto extract_bonds = [&]() { + bool has_conn(false), has_funcs(false); - BondID b(atom0, atom1); + TwoAtomFunctions funcs; + Connectivity conn; - if (not bnds0.contains(b)) - { - bnds0.insert(b, GromacsBond(5)); // function 5 is a simple connection - } - } - } - }; + const auto R = InternalPotential::symbols().bond().r(); - // get all of the angles in this molecule - auto extract_angles = [&]() - { - bool has_funcs(false); - bool has_ubfuncs(false); + try { + funcs = mol.property(map["bond"]).asA(); + has_funcs = true; + } catch (...) { + } - ThreeAtomFunctions funcs; - TwoAtomFunctions ubfuncs; + try { + conn = mol.property(map["connectivity"]).asA(); + has_conn = true; + } catch (...) { + } - const auto theta = InternalPotential::symbols().angle().theta(); - const auto r = InternalPotential::symbols().ureyBradley().r(); + // get the bond potentials first + if (has_funcs) { + for (const auto &bond : funcs.potentials()) { + AtomIdx atom0 = molinfo.atomIdx(bond.atom0()); + AtomIdx atom1 = molinfo.atomIdx(bond.atom1()); - try - { - funcs = mol.property(map["angle"]).asA(); - has_funcs = true; - } - catch (...) - { - } + if (atom0 > atom1) + qSwap(atom0, atom1); - try - { - ubfuncs = mol.property(map["urey-bradley"]).asA(); - has_ubfuncs = true; - } - catch (...) - { - } + bnds0.insert(BondID(atom0, atom1), GromacsBond(bond.function(), R)); + } + } - if (has_funcs) - { - for (const auto &angle : funcs.potentials()) - { - AtomIdx atom0 = molinfo.atomIdx(angle.atom0()); - AtomIdx atom1 = molinfo.atomIdx(angle.atom1()); - AtomIdx atom2 = molinfo.atomIdx(angle.atom2()); - - if (atom0 > atom2) - qSwap(atom0, atom2); - - if (has_ubfuncs) - { - angs0.insert(AngleID(atom0, atom1, atom2), - GromacsAngle(angle.function(), theta, - ubfuncs.potential(atom0, atom2), r)); - } - else - { - angs0.insert(AngleID(atom0, atom1, atom2), - GromacsAngle(angle.function(), theta)); - } - } - } - }; + // now fill in any missing bonded atoms with null bonds + if (has_conn) { + for (const auto &bond : conn.getBonds()) { + AtomIdx atom0 = molinfo.atomIdx(bond.atom0()); + AtomIdx atom1 = molinfo.atomIdx(bond.atom1()); - // get all of the dihedrals in this molecule - auto extract_dihedrals = [&]() - { - bool has_funcs(false); + if (atom0 > atom1) + qSwap(atom0, atom1); - FourAtomFunctions funcs; + BondID b(atom0, atom1); - const auto phi = InternalPotential::symbols().dihedral().phi(); - const auto theta = InternalPotential::symbols().improper().theta(); + if (not bnds0.contains(b)) { + bnds0.insert(b, + GromacsBond(5)); // function 5 is a simple connection + } + } + } + }; - try - { - funcs = mol.property(map["dihedral"]).asA(); - has_funcs = true; - } - catch (...) - { - } + // get all of the angles in this molecule + auto extract_angles = [&]() { + bool has_funcs(false); + bool has_ubfuncs(false); + + ThreeAtomFunctions funcs; + TwoAtomFunctions ubfuncs; + + const auto theta = InternalPotential::symbols().angle().theta(); + const auto r = InternalPotential::symbols().ureyBradley().r(); + + try { + funcs = mol.property(map["angle"]).asA(); + has_funcs = true; + } catch (...) { + } + + try { + ubfuncs = mol.property(map["urey-bradley"]).asA(); + has_ubfuncs = true; + } catch (...) { + } + + if (has_funcs) { + for (const auto &angle : funcs.potentials()) { + AtomIdx atom0 = molinfo.atomIdx(angle.atom0()); + AtomIdx atom1 = molinfo.atomIdx(angle.atom1()); + AtomIdx atom2 = molinfo.atomIdx(angle.atom2()); + + if (atom0 > atom2) + qSwap(atom0, atom2); + + if (has_ubfuncs) { + angs0.insert(AngleID(atom0, atom1, atom2), + GromacsAngle(angle.function(), theta, + ubfuncs.potential(atom0, atom2), r)); + } else { + angs0.insert(AngleID(atom0, atom1, atom2), + GromacsAngle(angle.function(), theta)); + } + } + } + }; - if (has_funcs) - { - for (const auto &dihedral : funcs.potentials()) - { - AtomIdx atom0 = molinfo.atomIdx(dihedral.atom0()); - AtomIdx atom1 = molinfo.atomIdx(dihedral.atom1()); - AtomIdx atom2 = molinfo.atomIdx(dihedral.atom2()); - AtomIdx atom3 = molinfo.atomIdx(dihedral.atom3()); - - if (atom0 > atom3) - { - qSwap(atom0, atom3); - qSwap(atom1, atom2); - } + // get all of the dihedrals in this molecule + auto extract_dihedrals = [&]() { + bool has_funcs(false); - // get all of the dihedral terms (could be a lot) - auto parts = GromacsDihedral::construct(dihedral.function(), phi); + FourAtomFunctions funcs; - DihedralID dihid(atom0, atom1, atom2, atom3); + const auto phi = InternalPotential::symbols().dihedral().phi(); + const auto theta = InternalPotential::symbols().improper().theta(); - for (const auto &part : parts) - { - dihs0.insert(dihid, part); - } - } - } + try { + funcs = mol.property(map["dihedral"]).asA(); + has_funcs = true; + } catch (...) { + } - bool has_imps(false); + if (has_funcs) { + for (const auto &dihedral : funcs.potentials()) { + AtomIdx atom0 = molinfo.atomIdx(dihedral.atom0()); + AtomIdx atom1 = molinfo.atomIdx(dihedral.atom1()); + AtomIdx atom2 = molinfo.atomIdx(dihedral.atom2()); + AtomIdx atom3 = molinfo.atomIdx(dihedral.atom3()); - FourAtomFunctions imps; + if (atom0 > atom3) { + qSwap(atom0, atom3); + qSwap(atom1, atom2); + } - try - { - imps = mol.property(map["improper"]).asA(); - has_imps = true; - } - catch (...) - { - } + // get all of the dihedral terms (could be a lot) + auto parts = GromacsDihedral::construct(dihedral.function(), phi); - if (has_imps) - { + DihedralID dihid(atom0, atom1, atom2, atom3); - for (const auto &improper : imps.potentials()) - { - AtomIdx atom0 = molinfo.atomIdx(improper.atom0()); - AtomIdx atom1 = molinfo.atomIdx(improper.atom1()); - AtomIdx atom2 = molinfo.atomIdx(improper.atom2()); - AtomIdx atom3 = molinfo.atomIdx(improper.atom3()); + for (const auto &part : parts) { + dihs0.insert(dihid, part); + } + } + } - // get all of the dihedral terms (could be a lot) - auto parts = GromacsDihedral::constructImproper(improper.function(), phi, theta); + bool has_imps(false); - DihedralID impid(atom0, atom1, atom2, atom3); + FourAtomFunctions imps; - for (const auto &part : parts) - { - dihs0.insert(impid, part); - } - } - } - }; + try { + imps = mol.property(map["improper"]).asA(); + has_imps = true; + } catch (...) { + } - // get all of the CMAP terms in this molecule - auto extract_cmaps = [&]() - { - bool has_cmaps(false); - CMAPFunctions cmaps; + if (has_imps) { - try - { - cmaps = mol.property(map["cmap"]).asA(); - has_cmaps = true; - } - catch (...) - { - } + for (const auto &improper : imps.potentials()) { + AtomIdx atom0 = molinfo.atomIdx(improper.atom0()); + AtomIdx atom1 = molinfo.atomIdx(improper.atom1()); + AtomIdx atom2 = molinfo.atomIdx(improper.atom2()); + AtomIdx atom3 = molinfo.atomIdx(improper.atom3()); - if (has_cmaps) - { - QHash cmap_params; - - for (const auto &cmap : cmaps.parameters()) - { - AtomIdx atom0 = molinfo.atomIdx(cmap.atom0()); - AtomIdx atom1 = molinfo.atomIdx(cmap.atom1()); - AtomIdx atom2 = molinfo.atomIdx(cmap.atom2()); - AtomIdx atom3 = molinfo.atomIdx(cmap.atom3()); - AtomIdx atom4 = molinfo.atomIdx(cmap.atom4()); - - if ((atom0 > atom4) or (atom0 == atom4 and atom1 > atom2)) - { - qSwap(atom0, atom4); - qSwap(atom1, atom2); - } + // get all of the dihedral terms (could be a lot) + auto parts = GromacsDihedral::constructImproper(improper.function(), + phi, theta); - // we will store the CMAP parameter as a string - this - // will let us de-duplicate parameters later - if (cmap_params.contains(cmap.parameter())) - { - cmaps0.insert(CMAPID(atom0, atom1, atom2, atom3, atom4), - cmap_params[cmap.parameter()]); - } - else - { - // store the parameter as a string - QString param_str = cmap_to_string(cmap.parameter()); - cmap_params.insert(cmap.parameter(), param_str); + DihedralID impid(atom0, atom1, atom2, atom3); - cmaps0.insert(CMAPID(atom0, atom1, atom2, atom3, atom4), param_str); - } - } - } - }; + for (const auto &part : parts) { + dihs0.insert(impid, part); + } + } + } + }; - const QVector> functions = {extract_atoms, extract_bonds, extract_angles, - extract_dihedrals, extract_cmaps}; + // get all of the CMAP terms in this molecule + auto extract_cmaps = [&]() { + bool has_cmaps(false); + CMAPFunctions cmaps; + + try { + cmaps = mol.property(map["cmap"]).asA(); + has_cmaps = true; + } catch (...) { + } + + if (has_cmaps) { + QHash cmap_params; + + for (const auto &cmap : cmaps.parameters()) { + AtomIdx atom0 = molinfo.atomIdx(cmap.atom0()); + AtomIdx atom1 = molinfo.atomIdx(cmap.atom1()); + AtomIdx atom2 = molinfo.atomIdx(cmap.atom2()); + AtomIdx atom3 = molinfo.atomIdx(cmap.atom3()); + AtomIdx atom4 = molinfo.atomIdx(cmap.atom4()); + + if ((atom0 > atom4) or (atom0 == atom4 and atom1 > atom2)) { + qSwap(atom0, atom4); + qSwap(atom1, atom2); + } + + // we will store the CMAP parameter as a string - this + // will let us de-duplicate parameters later + if (cmap_params.contains(cmap.parameter())) { + cmaps0.insert(CMAPID(atom0, atom1, atom2, atom3, atom4), + cmap_params[cmap.parameter()]); + } else { + // store the parameter as a string + QString param_str = cmap_to_string(cmap.parameter()); + cmap_params.insert(cmap.parameter(), param_str); + + cmaps0.insert(CMAPID(atom0, atom1, atom2, atom3, atom4), param_str); + } + } + } + }; - if (uses_parallel) - { - tbb::parallel_for(tbb::blocked_range(0, functions.count(), 1), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - functions[i](); - } }); - } - else - { - for (int i = 0; i < functions.count(); ++i) - { - functions[i](); - } - } + const QVector> functions = { + extract_atoms, extract_bonds, extract_angles, extract_dihedrals, + extract_cmaps}; - // sanitise this object - this->_pvt_sanitise(); + if (uses_parallel) { + tbb::parallel_for(tbb::blocked_range(0, functions.count(), 1), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + functions[i](); + } + }); + } else { + for (int i = 0; i < functions.count(); ++i) { + functions[i](); + } } + + // sanitise this object + this->_pvt_sanitise(); + } } /** Copy constructor */ GroMolType::GroMolType(const GroMolType &other) - : nme(other.nme), warns(other.warns), atms0(other.atms0), atms1(other.atms1), first_atoms0(other.first_atoms0), - first_atoms1(other.first_atoms1), bnds0(other.bnds0), bnds1(other.bnds1), angs0(other.angs0), angs1(other.angs1), - dihs0(other.dihs0), dihs1(other.dihs1), cmaps0(other.cmaps0), cmaps1(other.cmaps1), - explicit_pairs(other.explicit_pairs), - ffield0(other.ffield0), ffield1(other.ffield1), nexcl0(other.nexcl0), - nexcl1(other.nexcl1), is_perturbable(other.is_perturbable) -{ -} + : nme(other.nme), warns(other.warns), atms0(other.atms0), + atms1(other.atms1), first_atoms0(other.first_atoms0), + first_atoms1(other.first_atoms1), bnds0(other.bnds0), bnds1(other.bnds1), + angs0(other.angs0), angs1(other.angs1), dihs0(other.dihs0), + dihs1(other.dihs1), cmaps0(other.cmaps0), cmaps1(other.cmaps1), + explicit_pairs(other.explicit_pairs), ffield0(other.ffield0), + ffield1(other.ffield1), nexcl0(other.nexcl0), nexcl1(other.nexcl1), + is_perturbable(other.is_perturbable) {} /** Destructor */ -GroMolType::~GroMolType() -{ -} +GroMolType::~GroMolType() {} /** Copy assignment operator */ -GroMolType &GroMolType::operator=(const GroMolType &other) -{ - if (this != &other) - { - nme = other.nme; - warns = other.warns; - atms0 = other.atms0; - atms1 = other.atms1; - first_atoms0 = other.first_atoms0; - first_atoms1 = other.first_atoms1; - bnds0 = other.bnds0; - bnds1 = other.bnds1; - angs0 = other.angs0; - angs1 = other.angs1; - dihs0 = other.dihs0; - dihs1 = other.dihs1; - cmaps0 = other.cmaps0; - cmaps1 = other.cmaps1; - explicit_pairs = other.explicit_pairs; - ffield0 = other.ffield0; - ffield1 = other.ffield1; - nexcl0 = other.nexcl0; - nexcl0 = other.nexcl1; - is_perturbable = other.is_perturbable; - } - - return *this; +GroMolType &GroMolType::operator=(const GroMolType &other) { + if (this != &other) { + nme = other.nme; + warns = other.warns; + atms0 = other.atms0; + atms1 = other.atms1; + first_atoms0 = other.first_atoms0; + first_atoms1 = other.first_atoms1; + bnds0 = other.bnds0; + bnds1 = other.bnds1; + angs0 = other.angs0; + angs1 = other.angs1; + dihs0 = other.dihs0; + dihs1 = other.dihs1; + cmaps0 = other.cmaps0; + cmaps1 = other.cmaps1; + explicit_pairs = other.explicit_pairs; + ffield0 = other.ffield0; + ffield1 = other.ffield1; + nexcl0 = other.nexcl0; + nexcl0 = other.nexcl1; + is_perturbable = other.is_perturbable; + } + + return *this; } /** Comparison operator */ -bool GroMolType::operator==(const GroMolType &other) const -{ - return nme == other.nme and warns == other.warns and atms0 == other.atms0 and atms1 == other.atms1 and - first_atoms0 == other.first_atoms0 and first_atoms1 == other.first_atoms1 and bnds0 == other.bnds0 and - bnds1 == other.bnds1 and angs0 == other.angs0 and angs1 == other.angs1 and dihs0 == other.dihs0 and - dihs1 == other.dihs1 and cmaps0 == other.cmaps0 and cmaps1 == other.cmaps1 and - explicit_pairs == other.explicit_pairs and - nexcl0 == other.nexcl0 and nexcl1 == other.nexcl1 and - is_perturbable == other.is_perturbable; +bool GroMolType::operator==(const GroMolType &other) const { + return nme == other.nme and warns == other.warns and atms0 == other.atms0 and + atms1 == other.atms1 and first_atoms0 == other.first_atoms0 and + first_atoms1 == other.first_atoms1 and bnds0 == other.bnds0 and + bnds1 == other.bnds1 and angs0 == other.angs0 and + angs1 == other.angs1 and dihs0 == other.dihs0 and + dihs1 == other.dihs1 and cmaps0 == other.cmaps0 and + cmaps1 == other.cmaps1 and explicit_pairs == other.explicit_pairs and + nexcl0 == other.nexcl0 and nexcl1 == other.nexcl1 and + is_perturbable == other.is_perturbable; } /** Comparison operator */ -bool GroMolType::operator!=(const GroMolType &other) const -{ - return not operator==(other); +bool GroMolType::operator!=(const GroMolType &other) const { + return not operator==(other); } -const char *GroMolType::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); +const char *GroMolType::typeName() { + return QMetaType::typeName(qMetaTypeId()); } -const char *GroMolType::what() const -{ - return GroMolType::typeName(); -} +const char *GroMolType::what() const { return GroMolType::typeName(); } /** Return whether or not this object is null */ -bool GroMolType::isNull() const -{ - return this->operator==(GroMolType()); -} +bool GroMolType::isNull() const { return this->operator==(GroMolType()); } /** Return whether or not this molecule is perturbable. */ -bool GroMolType::isPerturbable() const -{ - return this->is_perturbable; -} +bool GroMolType::isPerturbable() const { return this->is_perturbable; } /** Return a string form for this object */ -QString GroMolType::toString() const -{ - if (this->isNull()) - return QObject::tr("GroMolType::null"); +QString GroMolType::toString() const { + if (this->isNull()) + return QObject::tr("GroMolType::null"); - return QObject::tr("GroMolType( name() = '%1', nExcludedAtoms() = %2 )").arg(name()).arg(nExcludedAtoms()); + return QObject::tr("GroMolType( name() = '%1', nExcludedAtoms() = %2 )") + .arg(name()) + .arg(nExcludedAtoms()); } /** Set the name of this moleculetype */ -void GroMolType::setName(const QString &name) -{ - nme = name; -} +void GroMolType::setName(const QString &name) { nme = name; } /** Return the name of this moleculetype */ -QString GroMolType::name() const -{ - return nme; -} +QString GroMolType::name() const { return nme; } /** Return the guessed forcefield for this molecule type */ -MMDetail GroMolType::forcefield(bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot extract forcefield. The molecule isn't perturbable!")); +MMDetail GroMolType::forcefield(bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error(QObject::tr( + "Cannot extract forcefield. The molecule isn't perturbable!")); - if (is_lambda1) - return ffield1; - else - return ffield0; + if (is_lambda1) + return ffield1; + else + return ffield0; } /** Set the number of excluded atoms */ -void GroMolType::setNExcludedAtoms(qint64 n, bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot set excluded atoms. The molecule isn't perturbable!")); +void GroMolType::setNExcludedAtoms(qint64 n, bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error(QObject::tr( + "Cannot set excluded atoms. The molecule isn't perturbable!")); - if (n >= 0) - if (is_lambda1) - nexcl1 = n; - else - nexcl0 = n; + if (n >= 0) + if (is_lambda1) + nexcl1 = n; else - { - if (is_lambda1) - nexcl1 = 0; - else - nexcl0 = 0; - } + nexcl0 = n; + else { + if (is_lambda1) + nexcl1 = 0; + else + nexcl0 = 0; + } } /** Return the number of excluded atoms */ -qint64 GroMolType::nExcludedAtoms(bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get excluded atoms. The molecule isn't perturbable!")); +qint64 GroMolType::nExcludedAtoms(bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error(QObject::tr( + "Cannot get excluded atoms. The molecule isn't perturbable!")); - if (is_lambda1) - return nexcl1; - else - return nexcl0; + if (is_lambda1) + return nexcl1; + else + return nexcl0; } /** Add an atom to this moleculetype, with specified atom type, residue number, residue name, atom name, charge group, charge and mass */ -void GroMolType::addAtom(const GroAtom &atom, bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot add atom. The molecule isn't perturbable!")); +void GroMolType::addAtom(const GroAtom &atom, bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot add atom. The molecule isn't perturbable!")); - if (not atom.isNull()) - { - if (is_lambda1) - atms1.append(atom); - else - atms0.append(atom); - } + if (not atom.isNull()) { + if (is_lambda1) + atms1.append(atom); + else + atms0.append(atom); + } } /** Return whether or not this molecule needs sanitising */ -bool GroMolType::needsSanitising(bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot check needs sanitise. The molecule isn't perturbable!")); - - if (is_lambda1) - { - if (atms1.isEmpty()) - return false; - else - return ffield1.isNull() or first_atoms1.isEmpty(); - } +bool GroMolType::needsSanitising(bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error(QObject::tr( + "Cannot check needs sanitise. The molecule isn't perturbable!")); + + if (is_lambda1) { + if (atms1.isEmpty()) + return false; else - { - if (atms0.isEmpty()) - return false; - else - return ffield0.isNull() or first_atoms0.isEmpty(); - } + return ffield1.isNull() or first_atoms1.isEmpty(); + } else { + if (atms0.isEmpty()) + return false; + else + return ffield0.isNull() or first_atoms0.isEmpty(); + } } /** Return the number of atoms in this molecule */ -int GroMolType::nAtoms(bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get number of atoms. The molecule isn't perturbable!")); - - if (needsSanitising(is_lambda1)) - { - GroMolType other(*this); - other._pvt_sanitise(is_lambda1); - return other.nAtoms(is_lambda1); - } +int GroMolType::nAtoms(bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error(QObject::tr( + "Cannot get number of atoms. The molecule isn't perturbable!")); + + if (needsSanitising(is_lambda1)) { + GroMolType other(*this); + other._pvt_sanitise(is_lambda1); + return other.nAtoms(is_lambda1); + } else { + if (is_lambda1) + return atms1.count(); else - { - if (is_lambda1) - return atms1.count(); - else - return atms0.count(); - } + return atms0.count(); + } } /** Return the number of residues in this molecule */ -int GroMolType::nResidues(bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get number of residues. The molecule isn't perturbable!")); - - if (needsSanitising(is_lambda1)) - { - GroMolType other(*this); - other._pvt_sanitise(is_lambda1); - return other.nResidues(is_lambda1); - } +int GroMolType::nResidues(bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error(QObject::tr( + "Cannot get number of residues. The molecule isn't perturbable!")); + + if (needsSanitising(is_lambda1)) { + GroMolType other(*this); + other._pvt_sanitise(is_lambda1); + return other.nResidues(is_lambda1); + } else { + if (is_lambda1) + return first_atoms1.count(); else - { - if (is_lambda1) - return first_atoms1.count(); - else - return first_atoms0.count(); - } + return first_atoms0.count(); + } } /** Return the atom at index 'atomidx' */ -GroAtom GroMolType::atom(const AtomIdx &atomidx, bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get atom. The molecule isn't perturbable!")); - - if (needsSanitising(is_lambda1)) - { - GroMolType other(*this); - other._pvt_sanitise(is_lambda1); - return other.atom(atomidx, is_lambda1); - } - else - { - if (is_lambda1) - { - int i = atomidx.map(atms1.count()); - return atms1.constData()[i]; - } - else - { - int i = atomidx.map(atms0.count()); - return atms0.constData()[i]; - } - } +GroAtom GroMolType::atom(const AtomIdx &atomidx, bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot get atom. The molecule isn't perturbable!")); + + if (needsSanitising(is_lambda1)) { + GroMolType other(*this); + other._pvt_sanitise(is_lambda1); + return other.atom(atomidx, is_lambda1); + } else { + if (is_lambda1) { + int i = atomidx.map(atms1.count()); + return atms1.constData()[i]; + } else { + int i = atomidx.map(atms0.count()); + return atms0.constData()[i]; + } + } } /** Return the atom with number 'atomnum' */ -GroAtom GroMolType::atom(const AtomNum &atomnum, bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get atom by number. The molecule isn't perturbable!")); +GroAtom GroMolType::atom(const AtomNum &atomnum, bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error(QObject::tr( + "Cannot get atom by number. The molecule isn't perturbable!")); + + if (needsSanitising(is_lambda1)) { + GroMolType other(*this); + other._pvt_sanitise(is_lambda1); + return other.atom(atomnum, is_lambda1); + } + + if (is_lambda1) { + for (int i = 0; i < atms1.count(); ++i) { + if (atms1.constData()[i].number() == atomnum) { + return atms1.constData()[i]; + } + } + } else { + for (int i = 0; i < atms0.count(); ++i) { + if (atms0.constData()[i].number() == atomnum) { + return atms0.constData()[i]; + } + } + } + + throw SireMol::missing_atom( + QObject::tr("There is no atom with number '%1' in this molecule '%2'") + .arg(atomnum.toString()) + .arg(this->toString()), + CODELOC); +} - if (needsSanitising(is_lambda1)) - { - GroMolType other(*this); - other._pvt_sanitise(is_lambda1); - return other.atom(atomnum, is_lambda1); - } +/** Return the first atom with name 'atomnam'. If you want all atoms + with this name then call 'atoms(AtomName atomname)' */ +GroAtom GroMolType::atom(const AtomName &atomnam, bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error(QObject::tr( + "Cannot get atom by name. The molecule isn't perturbable!")); + + if (needsSanitising(is_lambda1)) { + GroMolType other(*this); + other._pvt_sanitise(is_lambda1); + return other.atom(atomnam, is_lambda1); + } + + if (is_lambda1) { + for (int i = 0; i < atms1.count(); ++i) { + if (atms1.constData()[i].name() == atomnam) { + return atms1.constData()[i]; + } + } + } else { + for (int i = 0; i < atms0.count(); ++i) { + if (atms0.constData()[i].name() == atomnam) { + return atms0.constData()[i]; + } + } + } + + throw SireMol::missing_atom( + QObject::tr("There is no atom with name '%1' in this molecule '%2'") + .arg(atomnam.toString()) + .arg(this->toString()), + CODELOC); +} - if (is_lambda1) - { - for (int i = 0; i < atms1.count(); ++i) - { - if (atms1.constData()[i].number() == atomnum) - { - return atms1.constData()[i]; - } - } +/** Set the atom type of the specified atom */ +void GroMolType::setAtomType(const AtomIdx &atomidx, const QString &atomtype, + bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot set atom type. The molecule isn't perturbable!")); + + if (needsSanitising(is_lambda1)) { + GroMolType other(*this); + other._pvt_sanitise(is_lambda1); + other.setAtomType(atomidx, atomtype, is_lambda1); + return; + } + + if (is_lambda1) { + atms1[atomidx.map(atms1.count())].setAtomType(atomtype); + } else { + atms0[atomidx.map(atms0.count())].setAtomType(atomtype); + } +} + +/** Return all atoms that have the passed name. Returns an empty + list if there are no atoms with this name */ +QVector GroMolType::atoms(const AtomName &atomnam, + bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error(QObject::tr( + "Cannot get atoms by name. The molecule isn't perturbable!")); + + if (needsSanitising(is_lambda1)) { + GroMolType other(*this); + other._pvt_sanitise(is_lambda1); + return other.atoms(atomnam, is_lambda1); + } + + QVector ret; + + if (is_lambda1) { + for (int i = 0; i < atms1.count(); ++i) { + if (atms1.constData()[i].name() == atomnam) { + ret.append(atms1.constData()[i]); + } } - else - { - for (int i = 0; i < atms0.count(); ++i) - { - if (atms0.constData()[i].number() == atomnum) - { - return atms0.constData()[i]; - } - } + } else { + for (int i = 0; i < atms0.count(); ++i) { + if (atms0.constData()[i].name() == atomnam) { + ret.append(atms0.constData()[i]); + } } + } - throw SireMol::missing_atom(QObject::tr("There is no atom with number '%1' in this molecule '%2'") - .arg(atomnum.toString()) - .arg(this->toString()), - CODELOC); -} - -/** Return the first atom with name 'atomnam'. If you want all atoms - with this name then call 'atoms(AtomName atomname)' */ -GroAtom GroMolType::atom(const AtomName &atomnam, bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get atom by name. The molecule isn't perturbable!")); - - if (needsSanitising(is_lambda1)) - { - GroMolType other(*this); - other._pvt_sanitise(is_lambda1); - return other.atom(atomnam, is_lambda1); - } - - if (is_lambda1) - { - for (int i = 0; i < atms1.count(); ++i) - { - if (atms1.constData()[i].name() == atomnam) - { - return atms1.constData()[i]; - } - } - } - else - { - for (int i = 0; i < atms0.count(); ++i) - { - if (atms0.constData()[i].name() == atomnam) - { - return atms0.constData()[i]; - } - } - } - - throw SireMol::missing_atom(QObject::tr("There is no atom with name '%1' in this molecule '%2'") - .arg(atomnam.toString()) - .arg(this->toString()), - CODELOC); -} - -/** Set the atom type of the specified atom */ -void GroMolType::setAtomType(const AtomIdx &atomidx, const QString &atomtype, bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot set atom type. The molecule isn't perturbable!")); - - if (needsSanitising(is_lambda1)) - { - GroMolType other(*this); - other._pvt_sanitise(is_lambda1); - other.setAtomType(atomidx, atomtype, is_lambda1); - return; - } - - if (is_lambda1) - { - atms1[atomidx.map(atms1.count())].setAtomType(atomtype); - } - else - { - atms0[atomidx.map(atms0.count())].setAtomType(atomtype); - } -} - -/** Return all atoms that have the passed name. Returns an empty - list if there are no atoms with this name */ -QVector GroMolType::atoms(const AtomName &atomnam, bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get atoms by name. The molecule isn't perturbable!")); - - if (needsSanitising(is_lambda1)) - { - GroMolType other(*this); - other._pvt_sanitise(is_lambda1); - return other.atoms(atomnam, is_lambda1); - } - - QVector ret; - - if (is_lambda1) - { - for (int i = 0; i < atms1.count(); ++i) - { - if (atms1.constData()[i].name() == atomnam) - { - ret.append(atms1.constData()[i]); - } - } - } - else - { - for (int i = 0; i < atms0.count(); ++i) - { - if (atms0.constData()[i].name() == atomnam) - { - ret.append(atms0.constData()[i]); - } - } - } - - return ret; + return ret; } /** Return all of the atoms in this molecule */ -QVector GroMolType::atoms(bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get atoms. The molecule isn't perturbable!")); +QVector GroMolType::atoms(bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot get atoms. The molecule isn't perturbable!")); - if (needsSanitising(is_lambda1)) - { - GroMolType other(*this); - other._pvt_sanitise(is_lambda1); - return other.atoms(is_lambda1); - } + if (needsSanitising(is_lambda1)) { + GroMolType other(*this); + other._pvt_sanitise(is_lambda1); + return other.atoms(is_lambda1); + } - if (is_lambda1) - return this->atms1; - else - return this->atms0; + if (is_lambda1) + return this->atms1; + else + return this->atms0; } /** Set the atoms to the passed vector */ -void GroMolType::setAtoms(const QVector &atoms, bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot set atoms. The molecule isn't perturbable!")); +void GroMolType::setAtoms(const QVector &atoms, bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot set atoms. The molecule isn't perturbable!")); - if (is_lambda1) - this->atms1 = atoms; - else - this->atms0 = atoms; + if (is_lambda1) + this->atms1 = atoms; + else + this->atms0 = atoms; } /** Return all of the atoms in the specified residue */ -QVector GroMolType::atoms(const ResIdx &residx, bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get atoms by residue. The molecule isn't perturbable!")); - - if (needsSanitising(is_lambda1)) - { - GroMolType other(*this); - other._pvt_sanitise(is_lambda1); - return other.atoms(residx, is_lambda1); - } +QVector GroMolType::atoms(const ResIdx &residx, + bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error(QObject::tr( + "Cannot get atoms by residue. The molecule isn't perturbable!")); - if (is_lambda1) - { - int ires = residx.map(first_atoms1.count()); + if (needsSanitising(is_lambda1)) { + GroMolType other(*this); + other._pvt_sanitise(is_lambda1); + return other.atoms(residx, is_lambda1); + } - int start = first_atoms1.constData()[ires]; - int end = atms1.count(); + if (is_lambda1) { + int ires = residx.map(first_atoms1.count()); - if (ires + 1 < first_atoms1.count()) - { - end = first_atoms1.constData()[ires + 1]; - } + int start = first_atoms1.constData()[ires]; + int end = atms1.count(); - return atms1.mid(start, end); + if (ires + 1 < first_atoms1.count()) { + end = first_atoms1.constData()[ires + 1]; } - else - { - int ires = residx.map(first_atoms0.count()); - int start = first_atoms0.constData()[ires]; - int end = atms0.count(); + return atms1.mid(start, end); + } else { + int ires = residx.map(first_atoms0.count()); - if (ires + 1 < first_atoms0.count()) - { - end = first_atoms0.constData()[ires + 1]; - } + int start = first_atoms0.constData()[ires]; + int end = atms0.count(); - return atms0.mid(start, end); + if (ires + 1 < first_atoms0.count()) { + end = first_atoms0.constData()[ires + 1]; } + + return atms0.mid(start, end); + } } /** Return all of the atoms in the specified residue(s) */ -QVector GroMolType::atoms(const ResNum &resnum, bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get atoms by residue number. The molecule isn't perturbable!")); +QVector GroMolType::atoms(const ResNum &resnum, + bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error(QObject::tr( + "Cannot get atoms by residue number. The molecule isn't perturbable!")); - if (needsSanitising(is_lambda1)) - { - GroMolType other(*this); - other._pvt_sanitise(is_lambda1); - return other.atoms(resnum, is_lambda1); - } + if (needsSanitising(is_lambda1)) { + GroMolType other(*this); + other._pvt_sanitise(is_lambda1); + return other.atoms(resnum, is_lambda1); + } - // find the indicies all all matching residues - QList idxs; + // find the indicies all all matching residues + QList idxs; - if (is_lambda1) - { - for (int idx = 0; idx < first_atoms1.count(); ++idx) - { - if (atms1[first_atoms1.at(idx)].residueNumber() == resnum) - { - idxs.append(ResIdx(idx)); - } - } + if (is_lambda1) { + for (int idx = 0; idx < first_atoms1.count(); ++idx) { + if (atms1[first_atoms1.at(idx)].residueNumber() == resnum) { + idxs.append(ResIdx(idx)); + } } - else - { - for (int idx = 0; idx < first_atoms0.count(); ++idx) - { - if (atms0[first_atoms0.at(idx)].residueNumber() == resnum) - { - idxs.append(ResIdx(idx)); - } - } + } else { + for (int idx = 0; idx < first_atoms0.count(); ++idx) { + if (atms0[first_atoms0.at(idx)].residueNumber() == resnum) { + idxs.append(ResIdx(idx)); + } } + } - QVector ret; + QVector ret; - for (const auto &idx : idxs) - { - ret += this->atoms(idx, is_lambda1); - } + for (const auto &idx : idxs) { + ret += this->atoms(idx, is_lambda1); + } - return ret; + return ret; } /** Return all of the atoms in the specified residue(s) */ -QVector GroMolType::atoms(const ResName &resnam, bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get atoms by residue name. The molecule isn't perturbable!")); +QVector GroMolType::atoms(const ResName &resnam, + bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error(QObject::tr( + "Cannot get atoms by residue name. The molecule isn't perturbable!")); - if (needsSanitising(is_lambda1)) - { - GroMolType other(*this); - other._pvt_sanitise(is_lambda1); - return other.atoms(resnam, is_lambda1); - } + if (needsSanitising(is_lambda1)) { + GroMolType other(*this); + other._pvt_sanitise(is_lambda1); + return other.atoms(resnam, is_lambda1); + } - // find the indicies all all matching residues - QList idxs; + // find the indicies all all matching residues + QList idxs; - if (is_lambda1) - { - for (int idx = 0; idx < first_atoms1.count(); ++idx) - { - if (atms1[first_atoms1.at(idx)].residueName() == resnam) - { - idxs.append(ResIdx(idx)); - } - } + if (is_lambda1) { + for (int idx = 0; idx < first_atoms1.count(); ++idx) { + if (atms1[first_atoms1.at(idx)].residueName() == resnam) { + idxs.append(ResIdx(idx)); + } } - else - { - for (int idx = 0; idx < first_atoms0.count(); ++idx) - { - if (atms0[first_atoms0.at(idx)].residueName() == resnam) - { - idxs.append(ResIdx(idx)); - } - } + } else { + for (int idx = 0; idx < first_atoms0.count(); ++idx) { + if (atms0[first_atoms0.at(idx)].residueName() == resnam) { + idxs.append(ResIdx(idx)); + } } + } - QVector ret; + QVector ret; - for (const auto &idx : idxs) - { - ret += this->atoms(idx, is_lambda1); - } + for (const auto &idx : idxs) { + ret += this->atoms(idx, is_lambda1); + } - return ret; + return ret; } /** Internal function to do the non-forcefield parts of sanitising */ -void GroMolType::_pvt_sanitise(bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot sanitise. The molecule isn't perturbable!")); +void GroMolType::_pvt_sanitise(bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot sanitise. The molecule isn't perturbable!")); - // sort the atoms so that they are in residue number / atom number order, and - // we check and remove duplicate atom numbers + // sort the atoms so that they are in residue number / atom number order, and + // we check and remove duplicate atom numbers - if (is_lambda1) - first_atoms1.append(0); - else - first_atoms0.append(0); + if (is_lambda1) + first_atoms1.append(0); + else + first_atoms0.append(0); } /** Sanitise this moleculetype. This assumes that the moleculetype has @@ -2222,521 +1887,477 @@ void GroMolType::_pvt_sanitise(bool is_lambda1) 'warnings' function. It also uses the passed defaults from the top file, together with the information in the molecule to guess the forcefield for the molecule */ -void GroMolType::sanitise(QString elecstyle, QString vdwstyle, QString combrule, double elec14, double vdw14, - bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot call sanitise. The molecule isn't perturbable!")); +void GroMolType::sanitise(QString elecstyle, QString vdwstyle, QString combrule, + double elec14, double vdw14, bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot call sanitise. The molecule isn't perturbable!")); - if (not needsSanitising(is_lambda1)) - return; - - this->_pvt_sanitise(is_lambda1); + if (not needsSanitising(is_lambda1)) + return; - // also check that the bonds/angles/dihedrals all refer to actual atoms... + this->_pvt_sanitise(is_lambda1); - // work out the bond, angle and dihedral function styles. We will - // do this assuming that anything other than simple harmonic/cosine is - // an "interesting" gromacs-style forcefield - QString bondstyle = "harmonic"; + // also check that the bonds/angles/dihedrals all refer to actual atoms... - if (is_lambda1) - { - for (auto it = bnds1.constBegin(); it != bnds1.constEnd(); ++it) - { - if (not(it.value().isSimple() and it.value().isHarmonic())) - { - bondstyle = "gromacs"; - break; - } - } + // work out the bond, angle and dihedral function styles. We will + // do this assuming that anything other than simple harmonic/cosine is + // an "interesting" gromacs-style forcefield + QString bondstyle = "harmonic"; - QString anglestyle = "harmonic"; + if (is_lambda1) { + for (auto it = bnds1.constBegin(); it != bnds1.constEnd(); ++it) { + if (not(it.value().isSimple() and it.value().isHarmonic())) { + bondstyle = "gromacs"; + break; + } + } - for (auto it = angs1.constBegin(); it != angs1.constEnd(); ++it) - { - if (not(it.value().isSimple() and it.value().isHarmonic())) - { - anglestyle = "gromacs"; - break; - } - } + QString anglestyle = "harmonic"; - QString dihedralstyle = "cosine"; + for (auto it = angs1.constBegin(); it != angs1.constEnd(); ++it) { + if (not(it.value().isSimple() and it.value().isHarmonic())) { + anglestyle = "gromacs"; + break; + } + } - for (auto it = dihs1.constBegin(); it != dihs1.constEnd(); ++it) - { - if (not(it.value().isSimple() and it.value().isCosine())) - { - dihedralstyle = "gromacs"; - break; - } - } + QString dihedralstyle = "cosine"; - // finally generate a forcefield description for this molecule based on the - // passed defaults and the functional forms of the internals - ffield1 = - MMDetail::guessFrom(combrule, elecstyle, vdwstyle, elec14, vdw14, bondstyle, anglestyle, dihedralstyle); + for (auto it = dihs1.constBegin(); it != dihs1.constEnd(); ++it) { + if (not(it.value().isSimple() and it.value().isCosine())) { + dihedralstyle = "gromacs"; + break; + } } - else - { - for (auto it = bnds0.constBegin(); it != bnds0.constEnd(); ++it) - { - if (not(it.value().isSimple() and it.value().isHarmonic())) - { - bondstyle = "gromacs"; - break; - } - } - QString anglestyle = "harmonic"; + // finally generate a forcefield description for this molecule based on the + // passed defaults and the functional forms of the internals + ffield1 = MMDetail::guessFrom(combrule, elecstyle, vdwstyle, elec14, vdw14, + bondstyle, anglestyle, dihedralstyle); + } else { + for (auto it = bnds0.constBegin(); it != bnds0.constEnd(); ++it) { + if (not(it.value().isSimple() and it.value().isHarmonic())) { + bondstyle = "gromacs"; + break; + } + } - for (auto it = angs0.constBegin(); it != angs0.constEnd(); ++it) - { - if (not(it.value().isSimple() and it.value().isHarmonic())) - { - anglestyle = "gromacs"; - break; - } - } + QString anglestyle = "harmonic"; - QString dihedralstyle = "cosine"; + for (auto it = angs0.constBegin(); it != angs0.constEnd(); ++it) { + if (not(it.value().isSimple() and it.value().isHarmonic())) { + anglestyle = "gromacs"; + break; + } + } - for (auto it = dihs0.constBegin(); it != dihs0.constEnd(); ++it) - { - if (not(it.value().isSimple() and it.value().isCosine())) - { - dihedralstyle = "gromacs"; - break; - } - } + QString dihedralstyle = "cosine"; - // finally generate a forcefield description for this molecule based on the - // passed defaults and the functional forms of the internals - ffield0 = - MMDetail::guessFrom(combrule, elecstyle, vdwstyle, elec14, vdw14, bondstyle, anglestyle, dihedralstyle); + for (auto it = dihs0.constBegin(); it != dihs0.constEnd(); ++it) { + if (not(it.value().isSimple() and it.value().isCosine())) { + dihedralstyle = "gromacs"; + break; + } } -} -/** Add a warning that has been generated while parsing or creatig this object */ -void GroMolType::addWarning(const QString &warning) -{ - warns.append(warning); + // finally generate a forcefield description for this molecule based on the + // passed defaults and the functional forms of the internals + ffield0 = MMDetail::guessFrom(combrule, elecstyle, vdwstyle, elec14, vdw14, + bondstyle, anglestyle, dihedralstyle); + } } +/** Add a warning that has been generated while parsing or creatig this object + */ +void GroMolType::addWarning(const QString &warning) { warns.append(warning); } + /** Return any warnings associated with this moleculetype */ -QStringList GroMolType::warnings() const -{ - return warns; -} +QStringList GroMolType::warnings() const { return warns; } /** Add the passed bond to the molecule */ -void GroMolType::addBond(const BondID &bond, const GromacsBond ¶m, bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot add bond. The molecule isn't perturbable!")); +void GroMolType::addBond(const BondID &bond, const GromacsBond ¶m, + bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot add bond. The molecule isn't perturbable!")); - if (is_lambda1) - bnds1.insert(bond, param); - else - bnds0.insert(bond, param); + if (is_lambda1) + bnds1.insert(bond, param); + else + bnds0.insert(bond, param); } /** Add the passed angle to the molecule */ -void GroMolType::addAngle(const AngleID &angle, const GromacsAngle ¶m, bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot add angle. The molecule isn't perturbable!")); +void GroMolType::addAngle(const AngleID &angle, const GromacsAngle ¶m, + bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot add angle. The molecule isn't perturbable!")); - if (is_lambda1) - angs1.insert(angle, param); - else - angs0.insert(angle, param); + if (is_lambda1) + angs1.insert(angle, param); + else + angs0.insert(angle, param); } /** Add the passed dihedral to the molecule */ -void GroMolType::addDihedral(const DihedralID &dihedral, const GromacsDihedral ¶m, bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot add dihedral. The molecule isn't perturbable!")); +void GroMolType::addDihedral(const DihedralID &dihedral, + const GromacsDihedral ¶m, bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot add dihedral. The molecule isn't perturbable!")); - if (is_lambda1) - dihs1.insert(dihedral, param); - else - dihs0.insert(dihedral, param); + if (is_lambda1) + dihs1.insert(dihedral, param); + else + dihs0.insert(dihedral, param); } /** Add the passed bonds to the molecule */ -void GroMolType::addBonds(const QMultiHash &bonds, bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot add bonds. The molecule isn't perturbable!")); +void GroMolType::addBonds(const QMultiHash &bonds, + bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot add bonds. The molecule isn't perturbable!")); - if (is_lambda1) - bnds1 += bonds; - else - bnds0 += bonds; + if (is_lambda1) + bnds1 += bonds; + else + bnds0 += bonds; } /** Add the passed angles to the molecule */ -void GroMolType::addAngles(const QMultiHash &angles, bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot add angles. The molecule isn't perturbable!")); +void GroMolType::addAngles(const QMultiHash &angles, + bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot add angles. The molecule isn't perturbable!")); - if (is_lambda1) - angs1 += angles; - else - angs0 += angles; + if (is_lambda1) + angs1 += angles; + else + angs0 += angles; } /** Add the passed dihedrals to the molecule */ -void GroMolType::addDihedrals(const QMultiHash &dihedrals, bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot add dihedrals. The molecule isn't perturbable!")); +void GroMolType::addDihedrals( + const QMultiHash &dihedrals, bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot add dihedrals. The molecule isn't perturbable!")); - if (is_lambda1) - dihs1 += dihedrals; - else - dihs0 += dihedrals; + if (is_lambda1) + dihs1 += dihedrals; + else + dihs0 += dihedrals; } /** Add the passed CMAPs to the molecule */ -void GroMolType::addCMAPs(const QHash &cmaps, bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - { - throw SireError::incompatible_error(QObject::tr("Cannot add CMAPs. The molecule isn't perturbable!")); - } +void GroMolType::addCMAPs(const QHash &cmaps, + bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) { + throw SireError::incompatible_error( + QObject::tr("Cannot add CMAPs. The molecule isn't perturbable!")); + } - if (is_lambda1) - { - for (auto it = cmaps.constBegin(); it != cmaps.constEnd(); ++it) - { - cmaps1.insert(it.key(), it.value()); - } + if (is_lambda1) { + for (auto it = cmaps.constBegin(); it != cmaps.constEnd(); ++it) { + cmaps1.insert(it.key(), it.value()); } - else - { - for (auto it = cmaps.constBegin(); it != cmaps.constEnd(); ++it) - { - cmaps0.insert(it.key(), it.value()); - } + } else { + for (auto it = cmaps.constBegin(); it != cmaps.constEnd(); ++it) { + cmaps0.insert(it.key(), it.value()); } + } } /** Return all of the bonds */ -QMultiHash GroMolType::bonds(bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get bonds. The molecule isn't perturbable!")); +QMultiHash GroMolType::bonds(bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot get bonds. The molecule isn't perturbable!")); - if (is_lambda1) - return bnds1; - else - return bnds0; + if (is_lambda1) + return bnds1; + else + return bnds0; } /** Return all of the angles */ -QMultiHash GroMolType::angles(bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get angles. The molecule isn't perturbable!")); +QMultiHash GroMolType::angles(bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot get angles. The molecule isn't perturbable!")); - if (is_lambda1) - return angs1; - else - return angs0; + if (is_lambda1) + return angs1; + else + return angs0; } /** Return all of the dihedrals */ -QMultiHash GroMolType::dihedrals(bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get dihedrals. The molecule isn't perturbable!")); +QMultiHash +GroMolType::dihedrals(bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot get dihedrals. The molecule isn't perturbable!")); - if (is_lambda1) - return dihs1; - else - return dihs0; + if (is_lambda1) + return dihs1; + else + return dihs0; } /** Return all of the cmaps */ -QHash GroMolType::cmaps(bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot get CMAPs. The molecule isn't perturbable!")); +QHash GroMolType::cmaps(bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot get CMAPs. The molecule isn't perturbable!")); - if (is_lambda1) - return cmaps1; - else - return cmaps0; + if (is_lambda1) + return cmaps1; + else + return cmaps0; } /** Add an explicit 1-4 pair (from a [pairs] funct=2 line) with given * coulomb and LJ scale factors */ -void GroMolType::addExplicitPair(const BondID &pair, double cscl, double ljscl) -{ - explicit_pairs.insert(pair, qMakePair(cscl, ljscl)); +void GroMolType::addExplicitPair(const BondID &pair, double cscl, + double ljscl) { + explicit_pairs.insert(pair, qMakePair(cscl, ljscl)); } /** Return the explicit 1-4 pair scale factors (from [pairs] funct=2) */ -QHash> GroMolType::explicitPairs() const -{ - return explicit_pairs; +QHash> GroMolType::explicitPairs() const { + return explicit_pairs; } /** Sanitise all of the CMAP terms - this sets the string equal to "1", * as the information contained previously has already been read */ -void GroMolType::sanitiseCMAPs(bool is_lambda1) -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot sanitise CMAPs. The molecule isn't perturbable!")); +void GroMolType::sanitiseCMAPs(bool is_lambda1) { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot sanitise CMAPs. The molecule isn't perturbable!")); - if (is_lambda1) - { - for (auto it = cmaps1.begin(); it != cmaps1.end(); ++it) - { - it.value() = "1"; - } + if (is_lambda1) { + for (auto it = cmaps1.begin(); it != cmaps1.end(); ++it) { + it.value() = "1"; } - else - { - for (auto it = cmaps0.begin(); it != cmaps0.end(); ++it) - { - it.value() = "1"; - } + } else { + for (auto it = cmaps0.begin(); it != cmaps0.end(); ++it) { + it.value() = "1"; } + } } /** Return whether or not this is a topology for water. This should return true for all water models (including TIP4P and TIP5P) */ -bool GroMolType::isWater(bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot check water. The molecule isn't perturbable!")); +bool GroMolType::isWater(bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot check water. The molecule isn't perturbable!")); + + if (nResidues(is_lambda1) == 1) { + if (nAtoms(is_lambda1) >= 3 and + nAtoms(is_lambda1) <= 5) // catch SPC/TIP3P to TIP5P + { + // the total mass of the molecule should be 18 (rounded) + // and the number of oxygens should be 1 and hydrogens should be 2 + int noxy = 0; + int nhyd = 0; + int total_mass = 0; + + if (is_lambda1) { + for (const auto &atm : atms1) { + // round down the mass to the nearest integer unit, so to + // exclude isotopes + const int mass = int(std::floor(atm.mass().value())); + + total_mass += mass; + + if (total_mass > 18) + return false; - if (nResidues(is_lambda1) == 1) - { - if (nAtoms(is_lambda1) >= 3 and nAtoms(is_lambda1) <= 5) // catch SPC/TIP3P to TIP5P - { - // the total mass of the molecule should be 18 (rounded) - // and the number of oxygens should be 1 and hydrogens should be 2 - int noxy = 0; - int nhyd = 0; - int total_mass = 0; + if (mass == 16) { + noxy += 1; + if (noxy > 1) + return false; + } else if (mass == 1) { + nhyd += 1; + if (nhyd > 2) + return false; + } + } - if (is_lambda1) - { - for (const auto &atm : atms1) - { - // round down the mass to the nearest integer unit, so to - // exclude isotopes - const int mass = int(std::floor(atm.mass().value())); - - total_mass += mass; - - if (total_mass > 18) - return false; - - if (mass == 16) - { - noxy += 1; - if (noxy > 1) - return false; - } - else if (mass == 1) - { - nhyd += 1; - if (nhyd > 2) - return false; - } - } + if (noxy == 1 and nhyd == 2) + return true; + else + return false; + } else { + for (const auto &atm : atms0) { + // round down the mass to the nearest integer unit, so to + // exclude isotopes + const int mass = int(std::floor(atm.mass().value())); - if (noxy == 1 and nhyd == 2) - return true; - else - return false; - } - else - { - for (const auto &atm : atms0) - { - // round down the mass to the nearest integer unit, so to - // exclude isotopes - const int mass = int(std::floor(atm.mass().value())); - - total_mass += mass; - - if (total_mass > 18) - return false; - - if (mass == 16) - { - noxy += 1; - if (noxy > 1) - return false; - } - else if (mass == 1) - { - nhyd += 1; - if (nhyd > 2) - return false; - } - } + total_mass += mass; - if (noxy == 1 and nhyd == 2) - return true; - else - return false; - } + if (total_mass > 18) + return false; + + if (mass == 16) { + noxy += 1; + if (noxy > 1) + return false; + } else if (mass == 1) { + nhyd += 1; + if (nhyd > 2) + return false; + } } + + if (noxy == 1 and nhyd == 2) + return true; + else + return false; + } } + } - return false; + return false; } /** Return the settles lines for this molecule. This currently only returns settles lines for water molecules. These lines are used to constrain the bonds/angles of the water molecule */ -QStringList GroMolType::settlesLines(bool is_lambda1) const -{ - // The molecule is not perturbable! - if (is_lambda1 and not this->is_perturbable) - throw SireError::incompatible_error(QObject::tr("Cannot check settles. The molecule isn't perturbable!")); - - if (not this->isWater(is_lambda1)) - return QStringList(); - - QStringList lines; - - // lambda function to check whether a four point water model - // is OPC water, which is determined by the virtual site charge - // value being < -1.1 - auto is_opc = [this, is_lambda1]() -> bool { - if (is_lambda1) - { - for (const auto &atm : atms1) - { - if (atm.mass().value() < 1.0) // virtual site - { - if (atm.charge().value() < -1.1) - return true; - else - return false; - } - } +QStringList GroMolType::settlesLines(bool is_lambda1) const { + // The molecule is not perturbable! + if (is_lambda1 and not this->is_perturbable) + throw SireError::incompatible_error( + QObject::tr("Cannot check settles. The molecule isn't perturbable!")); + + if (not this->isWater(is_lambda1)) + return QStringList(); + + QStringList lines; + + // lambda function to check whether a four point water model + // is OPC water, which is determined by the virtual site charge + // value being < -1.1 + auto is_opc = [this, is_lambda1]() -> bool { + if (is_lambda1) { + for (const auto &atm : atms1) { + if (atm.mass().value() < 1.0) // virtual site + { + if (atm.charge().value() < -1.1) + return true; + else + return false; } - else + } + } else { + for (const auto &atm : atms0) { + if (atm.mass().value() < 1.0) // virtual site { - for (const auto &atm : atms0) - { - if (atm.mass().value() < 1.0) // virtual site - { - if (atm.charge().value() < -1.1) - return true; - else - return false; - } - } + if (atm.charge().value() < -1.1) + return true; + else + return false; } + } + } - return false; - }; + return false; + }; + + lines.append("[ settles ]"); + lines.append("; OW funct doh dhh"); + + // Equilibrium OH and HH bond lengths. (Default to TIP3P values). + double hh_length = 0.15136000; + double oh_length = 0.09572000; + + if (nAtoms(is_lambda1) == 4) { + // TIP4P/OPC + if (is_opc()) { + oh_length = 0.08724331; + hh_length = 0.1371205; + } else { + hh_length = 0.15139; + } + } else if (nAtoms(is_lambda1) == 5) { + // TIP5P + hh_length = 0.15139; + } + + lines.append(QString("1 1 %1 %2") + .arg(oh_length, 7, 'f', 5) + .arg(hh_length, 7, 'f', 5)); + + lines.append(""); + lines.append("[ exclusions ]"); + + if (nAtoms(is_lambda1) == 3) { + // TIP3P or SPC + lines.append("1 2 3"); + lines.append("2 1 3"); + lines.append("3 1 2"); + } else if (nAtoms(is_lambda1) == 4) { + + // TIP4P/OPC + lines.append("1 2 3 4"); + lines.append("2 1 3 4"); + lines.append("3 1 2 4"); + lines.append("4 1 2 3"); + + // Add virtual site information. + lines.append(""); + lines.append("[ virtual_sites3 ]"); + lines.append("; Vsite from funct a b"); - lines.append("[ settles ]"); - lines.append("; OW funct doh dhh"); + // Check for OPC water. + if (is_opc()) + lines.append( + "4 1 2 3 1 0.1477224 0.1477224"); + else + lines.append("4 1 2 3 1 0.128012065 " + "0.128012065"); + } else if (nAtoms(is_lambda1) == 5) { + // TIP5P + lines.append("1 2 3 4 5"); + lines.append("2 1 3 4 5"); + lines.append("3 1 2 4 5"); + lines.append("4 1 2 3 5"); + lines.append("5 1 2 3 4"); + + // Add virtual site information. + lines.append(""); + lines.append("[ virtual_sites3 ]"); + lines.append("; Vsite from funct a b " + " c"); + lines.append("4 1 2 3 4 -0.344908262 " + "-0.34490826 -6.4437903493"); + lines.append("5 1 2 3 4 -0.344908262 " + "-0.34490826 6.4437903493"); + } - // Equilibrium OH and HH bond lengths. (Default to TIP3P values). - double hh_length = 0.15136000; - double oh_length = 0.09572000; - - if (nAtoms(is_lambda1) == 4) - { - // TIP4P/OPC - if (is_opc()) - { - oh_length = 0.08724331; - hh_length = 0.1371205; - } - else - { - hh_length = 0.15139; - } - } - else if (nAtoms(is_lambda1) == 5) - { - // TIP5P - hh_length = 0.15139; - } - - lines.append(QString("1 1 %1 %2").arg(oh_length, 7, 'f', 5).arg(hh_length, 7, 'f', 5)); - - lines.append(""); - lines.append("[ exclusions ]"); - - if (nAtoms(is_lambda1) == 3) - { - // TIP3P or SPC - lines.append("1 2 3"); - lines.append("2 1 3"); - lines.append("3 1 2"); - } - else if (nAtoms(is_lambda1) == 4) - { - - // TIP4P/OPC - lines.append("1 2 3 4"); - lines.append("2 1 3 4"); - lines.append("3 1 2 4"); - lines.append("4 1 2 3"); - - // Add virtual site information. - lines.append(""); - lines.append("[ virtual_sites3 ]"); - lines.append("; Vsite from funct a b"); - - // Check for OPC water. - if (is_opc()) - lines.append("4 1 2 3 1 0.1477224 0.1477224"); - else - lines.append("4 1 2 3 1 0.128012065 0.128012065"); - } - else if (nAtoms(is_lambda1) == 5) - { - // TIP5P - lines.append("1 2 3 4 5"); - lines.append("2 1 3 4 5"); - lines.append("3 1 2 4 5"); - lines.append("4 1 2 3 5"); - lines.append("5 1 2 3 4"); - - // Add virtual site information. - lines.append(""); - lines.append("[ virtual_sites3 ]"); - lines.append("; Vsite from funct a b c"); - lines.append("4 1 2 3 4 -0.344908262 -0.34490826 -6.4437903493"); - lines.append("5 1 2 3 4 -0.344908262 -0.34490826 6.4437903493"); - } - - return lines; -} + return lines; +} //////////////// //////////////// Implementation of GroSystem @@ -2744,224 +2365,167 @@ QStringList GroMolType::settlesLines(bool is_lambda1) const static const RegisterMetaType r_grosys(NO_ROOT); -QDataStream &operator<<(QDataStream &ds, const GroSystem &grosys) -{ - writeHeader(ds, r_grosys, 1); +QDataStream &operator<<(QDataStream &ds, const GroSystem &grosys) { + writeHeader(ds, r_grosys, 1); - SharedDataStream sds(ds); + SharedDataStream sds(ds); - sds << grosys.nme << grosys.moltypes << grosys.nmols; + sds << grosys.nme << grosys.moltypes << grosys.nmols; - return ds; + return ds; } -QDataStream &operator>>(QDataStream &ds, GroSystem &grosys) -{ - VersionID v = readHeader(ds, r_grosys); +QDataStream &operator>>(QDataStream &ds, GroSystem &grosys) { + VersionID v = readHeader(ds, r_grosys); - if (v == 1) - { - SharedDataStream sds(ds); + if (v == 1) { + SharedDataStream sds(ds); - sds >> grosys.nme >> grosys.moltypes >> grosys.nmols; + sds >> grosys.nme >> grosys.moltypes >> grosys.nmols; - grosys.total_nmols = 0; + grosys.total_nmols = 0; - for (auto it = grosys.nmols.constBegin(); it != grosys.nmols.constEnd(); ++it) - { - grosys.total_nmols += *it; - } + for (auto it = grosys.nmols.constBegin(); it != grosys.nmols.constEnd(); + ++it) { + grosys.total_nmols += *it; } - else - throw version_error(v, "1", r_grosys, CODELOC); + } else + throw version_error(v, "1", r_grosys, CODELOC); - return ds; + return ds; } /** Construct a null GroSystem */ -GroSystem::GroSystem() : total_nmols(0) -{ -} +GroSystem::GroSystem() : total_nmols(0) {} /** Construct a GroSystem with the passed name */ -GroSystem::GroSystem(const QString &name) : nme(name), total_nmols(0) -{ -} +GroSystem::GroSystem(const QString &name) : nme(name), total_nmols(0) {} /** Copy constructor */ GroSystem::GroSystem(const GroSystem &other) - : nme(other.nme), moltypes(other.moltypes), nmols(other.nmols), total_nmols(other.total_nmols) -{ -} + : nme(other.nme), moltypes(other.moltypes), nmols(other.nmols), + total_nmols(other.total_nmols) {} /** Destructor */ -GroSystem::~GroSystem() -{ -} +GroSystem::~GroSystem() {} /** Copy assignment operator */ -GroSystem &GroSystem::operator=(const GroSystem &other) -{ - nme = other.nme; - moltypes = other.moltypes; - nmols = other.nmols; - total_nmols = other.total_nmols; - return *this; +GroSystem &GroSystem::operator=(const GroSystem &other) { + nme = other.nme; + moltypes = other.moltypes; + nmols = other.nmols; + total_nmols = other.total_nmols; + return *this; } /** Comparison operator */ -bool GroSystem::operator==(const GroSystem &other) const -{ - return nme == other.nme and total_nmols == other.total_nmols and moltypes == other.moltypes and - nmols == other.nmols; +bool GroSystem::operator==(const GroSystem &other) const { + return nme == other.nme and total_nmols == other.total_nmols and + moltypes == other.moltypes and nmols == other.nmols; } /** Comparison operator */ -bool GroSystem::operator!=(const GroSystem &other) const -{ - return not operator==(other); +bool GroSystem::operator!=(const GroSystem &other) const { + return not operator==(other); } /** Return the molecule type of the ith molecule */ -QString GroSystem::operator[](int i) const -{ - i = Index(i).map(total_nmols); +QString GroSystem::operator[](int i) const { + i = Index(i).map(total_nmols); - auto it2 = moltypes.constBegin(); - for (auto it = nmols.constBegin(); it != nmols.constEnd(); ++it) - { - if (i < *it) - { - return *it2; - } - else - { - i -= *it; - ++it2; - } + auto it2 = moltypes.constBegin(); + for (auto it = nmols.constBegin(); it != nmols.constEnd(); ++it) { + if (i < *it) { + return *it2; + } else { + i -= *it; + ++it2; } + } - // we should never get here... - throw SireError::program_bug(QObject::tr("How did we get here? %1 : %2 : %3") - .arg(i) - .arg(Sire::toString(moltypes)) - .arg(Sire::toString(nmols)), - CODELOC); + // we should never get here... + throw SireError::program_bug(QObject::tr("How did we get here? %1 : %2 : %3") + .arg(i) + .arg(Sire::toString(moltypes)) + .arg(Sire::toString(nmols)), + CODELOC); - return QString(); + return QString(); } /** Return the molecule type of the ith molecule */ -QString GroSystem::at(int i) const -{ - return operator[](i); -} +QString GroSystem::at(int i) const { return operator[](i); } /** Return the number of molecules in the system */ -int GroSystem::size() const -{ - return total_nmols; -} +int GroSystem::size() const { return total_nmols; } /** Return the number of molecules in the system */ -int GroSystem::count() const -{ - return size(); -} +int GroSystem::count() const { return size(); } /** Return the number of molecules in the system */ -int GroSystem::nMolecules() const -{ - return size(); -} +int GroSystem::nMolecules() const { return size(); } -const char *GroSystem::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); +const char *GroSystem::typeName() { + return QMetaType::typeName(qMetaTypeId()); } -const char *GroSystem::what() const -{ - return GroSystem::typeName(); -} +const char *GroSystem::what() const { return GroSystem::typeName(); } /** Return the name of the system */ -QString GroSystem::name() const -{ - return nme; -} +QString GroSystem::name() const { return nme; } /** Set the name of the system */ -void GroSystem::setName(QString name) -{ - nme = name; -} +void GroSystem::setName(QString name) { nme = name; } /** Return a string representation of this system */ -QString GroSystem::toString() const -{ - if (this->isNull()) - { - return QObject::tr("GroSystem::null"); - } - else if (this->isEmpty()) - { - return QObject::tr("GroSystem( %1 : empty )").arg(this->name()); - } - else - { - return QObject::tr("GroSystem( %1 : nMolecules()=%2 )").arg(this->name()).arg(this->nMolecules()); - } +QString GroSystem::toString() const { + if (this->isNull()) { + return QObject::tr("GroSystem::null"); + } else if (this->isEmpty()) { + return QObject::tr("GroSystem( %1 : empty )").arg(this->name()); + } else { + return QObject::tr("GroSystem( %1 : nMolecules()=%2 )") + .arg(this->name()) + .arg(this->nMolecules()); + } } /** Return whether or not this is a null GroSystem */ -bool GroSystem::isNull() const -{ - return nme.isNull() and total_nmols == 0; -} +bool GroSystem::isNull() const { return nme.isNull() and total_nmols == 0; } /** Return whether or not this is an empty system (no molecules) */ -bool GroSystem::isEmpty() const -{ - return total_nmols == 0; -} +bool GroSystem::isEmpty() const { return total_nmols == 0; } /** Return the list of unique molecule types held in the system */ -QStringList GroSystem::uniqueTypes() const -{ - QStringList typs; +QStringList GroSystem::uniqueTypes() const { + QStringList typs; - for (const auto &moltype : moltypes) - { - if (not typs.contains(moltype)) - { - typs.append(moltype); - } + for (const auto &moltype : moltypes) { + if (not typs.contains(moltype)) { + typs.append(moltype); } + } - return typs; + return typs; } /** Add (optionally ncopies) copies of the molecule with type 'moltype' to the system */ -void GroSystem::add(QString moltype, int ncopies) -{ - if (ncopies <= 0) - return; +void GroSystem::add(QString moltype, int ncopies) { + if (ncopies <= 0) + return; - if (total_nmols > 0) - { - if (moltypes.back() == moltype) - { - nmols.back() += ncopies; - total_nmols += ncopies; - return; - } + if (total_nmols > 0) { + if (moltypes.back() == moltype) { + nmols.back() += ncopies; + total_nmols += ncopies; + return; } + } - moltypes.append(moltype); - nmols.append(ncopies); - total_nmols += ncopies; + moltypes.append(moltype); + nmols.append(ncopies); + total_nmols += ncopies; } //////////////// @@ -2971,6487 +2535,5979 @@ void GroSystem::add(QString moltype, int ncopies) const RegisterParser register_grotop; static const RegisterMetaType r_grotop; -QDataStream &operator<<(QDataStream &ds, const GroTop &grotop) -{ - writeHeader(ds, r_grotop, 2); +QDataStream &operator<<(QDataStream &ds, const GroTop &grotop) { + writeHeader(ds, r_grotop, 2); - SharedDataStream sds(ds); + SharedDataStream sds(ds); - sds << grotop.include_path << grotop.included_files << grotop.expanded_lines << grotop.atom_types - << grotop.bond_potentials << grotop.ang_potentials << grotop.dih_potentials - << grotop.cmap_potentials << grotop.moltypes << grotop.grosys - << grotop.nb_func_type << grotop.combining_rule << grotop.fudge_lj << grotop.fudge_qq << grotop.parse_warnings - << grotop.generate_pairs << static_cast(grotop); + sds << grotop.include_path << grotop.included_files << grotop.expanded_lines + << grotop.atom_types << grotop.bond_potentials << grotop.ang_potentials + << grotop.dih_potentials << grotop.cmap_potentials << grotop.moltypes + << grotop.grosys << grotop.nb_func_type << grotop.combining_rule + << grotop.fudge_lj << grotop.fudge_qq << grotop.parse_warnings + << grotop.generate_pairs << static_cast(grotop); - return ds; + return ds; } -QDataStream &operator>>(QDataStream &ds, GroTop &grotop) -{ - VersionID v = readHeader(ds, r_grotop); - - if (v == 1 or v == 2) - { - SharedDataStream sds(ds); +QDataStream &operator>>(QDataStream &ds, GroTop &grotop) { + VersionID v = readHeader(ds, r_grotop); - sds >> grotop.include_path >> grotop.included_files >> grotop.expanded_lines >> grotop.atom_types >> - grotop.bond_potentials >> grotop.ang_potentials >> grotop.dih_potentials; + if (v == 1 or v == 2) { + SharedDataStream sds(ds); - if (v == 2) - sds >> grotop.cmap_potentials; - else - grotop.cmap_potentials.clear(); + sds >> grotop.include_path >> grotop.included_files >> + grotop.expanded_lines >> grotop.atom_types >> grotop.bond_potentials >> + grotop.ang_potentials >> grotop.dih_potentials; - sds >> grotop.moltypes >> grotop.grosys >> grotop.nb_func_type >> - grotop.combining_rule >> grotop.fudge_lj >> grotop.fudge_qq >> - grotop.parse_warnings >> grotop.generate_pairs >> static_cast(grotop); - } + if (v == 2) + sds >> grotop.cmap_potentials; else - throw version_error(v, "1", r_grotop, CODELOC); + grotop.cmap_potentials.clear(); - return ds; + sds >> grotop.moltypes >> grotop.grosys >> grotop.nb_func_type >> + grotop.combining_rule >> grotop.fudge_lj >> grotop.fudge_qq >> + grotop.parse_warnings >> grotop.generate_pairs >> + static_cast(grotop); + } else + throw version_error(v, "1", r_grotop, CODELOC); + + return ds; } -// first thing is to parse in the gromacs files. These use #include, #define, #if etc. -// so we need to pull all of them together into a single set of lines +// first thing is to parse in the gromacs files. These use #include, #define, +// #if etc. so we need to pull all of them together into a single set of lines /** Internal function to return a LJParameter from the passed W and V values for the passed Gromacs combining rule */ -static LJParameter toLJParameter(double v, double w, int rule) -{ - if (rule == 2 or rule == 3) - { - // v = sigma in nm, and w = epsilon in kJ mol-1 - return LJParameter::fromSigmaAndEpsilon(v * nanometer, w * kJ_per_mol); - } - else - { - // v = 4 epsilon sigma^6 in kJ mol-1 nm^6, w = 4 epsilon sigma^12 in kJ mol-1 nm^12 - // so sigma = (w/v)^1/6 and epsilon = v^2 / 4w - return LJParameter::fromSigmaAndEpsilon(std::pow(w / v, 1.0 / 6.0) * nanometer, - (v * v / (4.0 * w)) * kJ_per_mol); - } -} - -/** Internal function to convert a LJParameter to V and W based on the passed gromacs - combining rule */ -static std::tuple fromLJParameter(const LJParameter &lj, int rule) -{ - const double sigma = lj.sigma().to(nanometer); - const double epsilon = lj.epsilon().to(kJ_per_mol); - - if (rule == 2 or rule == 3) - { - return std::make_tuple(sigma, epsilon); - } - else - { - double sig6 = SireMaths::pow(sigma, 6); - double v = 4.0 * epsilon * sig6; - double w = v * sig6; - - return std::make_tuple(v, w); - } +static LJParameter toLJParameter(double v, double w, int rule) { + if (rule == 2 or rule == 3) { + // v = sigma in nm, and w = epsilon in kJ mol-1 + return LJParameter::fromSigmaAndEpsilon(v * nanometer, w * kJ_per_mol); + } else { + // v = 4 epsilon sigma^6 in kJ mol-1 nm^6, w = 4 epsilon sigma^12 in kJ + // mol-1 nm^12 so sigma = (w/v)^1/6 and epsilon = v^2 / 4w + return LJParameter::fromSigmaAndEpsilon(std::pow(w / v, 1.0 / 6.0) * + nanometer, + (v * v / (4.0 * w)) * kJ_per_mol); + } +} + +/** Internal function to convert a LJParameter to V and W based on the passed + gromacs combining rule */ +static std::tuple fromLJParameter(const LJParameter &lj, + int rule) { + const double sigma = lj.sigma().to(nanometer); + const double epsilon = lj.epsilon().to(kJ_per_mol); + + if (rule == 2 or rule == 3) { + return std::make_tuple(sigma, epsilon); + } else { + double sig6 = SireMaths::pow(sigma, 6); + double v = 4.0 * epsilon * sig6; + double w = v * sig6; + + return std::make_tuple(v, w); + } } /** Internal function to create a string version of the LJ function type */ -static QString _getVDWStyle(int type) -{ - if (type == 1) - return "lj"; - else if (type == 2) - return "buckingham"; - else - throw SireError::invalid_arg( - QObject::tr("Cannot find the VDW function type from value '%1'. Should be 1 or 2.").arg(type), CODELOC); - - return QString(); -} +static QString _getVDWStyle(int type) { + if (type == 1) + return "lj"; + else if (type == 2) + return "buckingham"; + else + throw SireError::invalid_arg( + QObject::tr("Cannot find the VDW function type from value '%1'. Should " + "be 1 or 2.") + .arg(type), + CODELOC); -/** Internal function to convert a MMDetail description of the LJ function type back - to the gromacs integer */ -static int _getVDWStyleFromFF(const MMDetail &ffield) -{ - if (ffield.usesLJTerm()) - return 1; - else if (ffield.usesBuckinghamTerm()) - return 2; - else - throw SireError::invalid_arg(QObject::tr("Cannot find the VDW function type for forcefield\n%1\n. " - "This writer only support LJ or Buckingham VDW terms.") - .arg(ffield.toString()), - CODELOC); + return QString(); +} + +/** Internal function to convert a MMDetail description of the LJ function type + back to the gromacs integer */ +static int _getVDWStyleFromFF(const MMDetail &ffield) { + if (ffield.usesLJTerm()) + return 1; + else if (ffield.usesBuckinghamTerm()) + return 2; + else + throw SireError::invalid_arg( + QObject::tr("Cannot find the VDW function type for forcefield\n%1\n. " + "This writer only support LJ or Buckingham VDW terms.") + .arg(ffield.toString()), + CODELOC); - return 0; + return 0; } /** Internal function to create the string version of the combining rules */ -static QString _getCombiningRules(int type) -{ - if (type == 1 or type == 3) - return "geometric"; - else if (type == 2) - return "arithmetic"; - else - throw SireError::invalid_arg( - QObject::tr("Cannot find the combining rules type from value '%1'. Should be 1, 2 or 3.").arg(type), - CODELOC); +static QString _getCombiningRules(int type) { + if (type == 1 or type == 3) + return "geometric"; + else if (type == 2) + return "arithmetic"; + else + throw SireError::invalid_arg( + QObject::tr("Cannot find the combining rules type from value '%1'. " + "Should be 1, 2 or 3.") + .arg(type), + CODELOC); - return QString(); + return QString(); } -/** Internal function to create the combining rules from the passed forcefield */ -int _getCombiningRulesFromFF(const MMDetail &ffield) -{ - if (ffield.usesGeometricCombiningRules()) - return 1; // I don't know what 3 is... - else if (ffield.usesArithmeticCombiningRules()) - return 2; - else - throw SireError::invalid_arg(QObject::tr("Cannot find the combining rules to match the forcefield\n%1\n" - "Valid options are arithmetic or geometric.") - .arg(ffield.toString()), - CODELOC); +/** Internal function to create the combining rules from the passed forcefield + */ +int _getCombiningRulesFromFF(const MMDetail &ffield) { + if (ffield.usesGeometricCombiningRules()) + return 1; // I don't know what 3 is... + else if (ffield.usesArithmeticCombiningRules()) + return 2; + else + throw SireError::invalid_arg( + QObject::tr( + "Cannot find the combining rules to match the forcefield\n%1\n" + "Valid options are arithmetic or geometric.") + .arg(ffield.toString()), + CODELOC); - return 0; + return 0; } /** Constructor */ GroTop::GroTop() - : ConcreteProperty(), nb_func_type(0), combining_rule(0), fudge_lj(0), fudge_qq(0), - generate_pairs(false) -{ -} + : ConcreteProperty(), nb_func_type(0), + combining_rule(0), fudge_lj(0), fudge_qq(0), generate_pairs(false) {} /** This function gets the gromacs include path from the passed property map, as well as the current system environment */ -void GroTop::getIncludePath(const PropertyMap &map) -{ - QStringList path; +void GroTop::getIncludePath(const PropertyMap &map) { + QStringList path; - // now, see if the path is given in "GROMACS_PATH" in map - try - { - const auto p = map["GROMACS_PATH"]; + // now, see if the path is given in "GROMACS_PATH" in map + try { + const auto p = map["GROMACS_PATH"]; - if (p.hasValue()) - { - path += p.value().asA().toString().split(":", Qt::SkipEmptyParts); - } - else if (p.source() != "GROMACS_PATH") - { - path += p.source().split(":", Qt::SkipEmptyParts); - } - } - catch (...) - { + if (p.hasValue()) { + path += p.value().asA().toString().split( + ":", Qt::SkipEmptyParts); + } else if (p.source() != "GROMACS_PATH") { + path += p.source().split(":", Qt::SkipEmptyParts); } + } catch (...) { + } - // now, see if the path is given in the "GROMACS_PATH" environment variable - QString val = QString::fromLocal8Bit(qgetenv("GROMACS_PATH")); + // now, see if the path is given in the "GROMACS_PATH" environment variable + QString val = QString::fromLocal8Bit(qgetenv("GROMACS_PATH")); - if (not val.isEmpty()) - { - path += val.split(":", Qt::SkipEmptyParts); - } + if (not val.isEmpty()) { + path += val.split(":", Qt::SkipEmptyParts); + } - // now go through each path and convert it into an absolute path based on the - // current directory - for (const auto &p : path) - { - include_path.append(QFileInfo(p).canonicalFilePath()); - } + // now go through each path and convert it into an absolute path based on the + // current directory + for (const auto &p : path) { + include_path.append(QFileInfo(p).canonicalFilePath()); + } } /** Construct to read in the data from the file called 'filename'. The passed property map can be used to pass extra parameters to control the parsing */ GroTop::GroTop(const QString &filename, const PropertyMap &map) - : ConcreteProperty(filename, map), nb_func_type(0), combining_rule(0), fudge_lj(0), - fudge_qq(0), generate_pairs(false) -{ - this->getIncludePath(map); + : ConcreteProperty(filename, map), nb_func_type(0), + combining_rule(0), fudge_lj(0), fudge_qq(0), generate_pairs(false) { + this->getIncludePath(map); - // parse the data in the parse function, passing in the absolute path - // to the directory that contains this file - this->parseLines(QFileInfo(filename).absolutePath(), map); + // parse the data in the parse function, passing in the absolute path + // to the directory that contains this file + this->parseLines(QFileInfo(filename).absolutePath(), map); - // now make sure that everything is correct with this object - this->assertSane(); + // now make sure that everything is correct with this object + this->assertSane(); } /** Construct to read in the data from the passed text lines. The passed property map can be used to pass extra parameters to control the parsing */ GroTop::GroTop(const QStringList &lines, const PropertyMap &map) - : ConcreteProperty(lines, map), nb_func_type(0), combining_rule(0), fudge_lj(0), - fudge_qq(0), generate_pairs(false) -{ - this->getIncludePath(map); + : ConcreteProperty(lines, map), nb_func_type(0), + combining_rule(0), fudge_lj(0), fudge_qq(0), generate_pairs(false) { + this->getIncludePath(map); - // parse the data in the parse function, assuming the file has - // come from the current directory - this->parseLines(QDir::current().absolutePath(), map); + // parse the data in the parse function, assuming the file has + // come from the current directory + this->parseLines(QDir::current().absolutePath(), map); - // now make sure that everything is correct with this object - this->assertSane(); + // now make sure that everything is correct with this object + this->assertSane(); } /** Internal function used to generate the lines for the defaults section */ -static QStringList writeDefaults(const MMDetail &ffield) -{ - QStringList lines; - lines.append("; Gromacs Topology File written by Sire"); - lines.append(QString("; File written %1").arg(QDateTime::currentDateTime().toString("MM/dd/yy hh:mm:ss"))); +static QStringList writeDefaults(const MMDetail &ffield) { + QStringList lines; + lines.append("; Gromacs Topology File written by Sire"); + lines.append( + QString("; File written %1") + .arg(QDateTime::currentDateTime().toString("MM/dd/yy hh:mm:ss"))); - lines.append("[ defaults ]"); - lines.append("; nbfunc comb-rule gen-pairs fudgeLJ fudgeQQ"); + lines.append("[ defaults ]"); + lines.append( + "; nbfunc comb-rule gen-pairs fudgeLJ fudgeQQ"); - // all forcefields we support have gen-pairs = true (gromacs only understands 'yes' and 'no') - const QString gen_pairs = "yes"; + // all forcefields we support have gen-pairs = true (gromacs only understands + // 'yes' and 'no') + const QString gen_pairs = "yes"; - lines.append(QString(" %1 %2 %3 %4 %5") - .arg(_getVDWStyleFromFF(ffield)) - .arg(_getCombiningRulesFromFF(ffield)) - .arg(gen_pairs) - .arg(ffield.vdw14ScaleFactor()) - .arg(ffield.electrostatic14ScaleFactor())); + lines.append( + QString(" %1 %2 %3 %4 %5") + .arg(_getVDWStyleFromFF(ffield)) + .arg(_getCombiningRulesFromFF(ffield)) + .arg(gen_pairs) + .arg(ffield.vdw14ScaleFactor()) + .arg(ffield.electrostatic14ScaleFactor())); - lines.append(""); + lines.append(""); - return lines; + return lines; } -/** Internal function used to write all of the atom types. This function requires - that the atom types for all molecules are all consistent, i.e. atom type - X has the same mass, vdw parameters, element type etc. for all molecules */ -static QStringList writeAtomTypes(QMap, GroMolType> &moltyps, - QHash &cmap_potentials, - const QMap, Molecule> &molecules, const MMDetail &ffield, - const PropertyMap &map) -{ - // first, get a list of all atom types involved in CMAP parameters - // (I've passed this in as a reference, in case in the future we can fix this - // problem and update the CMAP parameters being written out. For now, we - // just raise an exception if this is detected as a problem) - QSet cmap_atom_types; - - for (auto it = cmap_potentials.constBegin(); it != cmap_potentials.constEnd(); ++it) - { - for (const auto &atom_type : cmap_id_to_atomtypes(it.key())) - { - cmap_atom_types.insert(atom_type); - } - } +/** Internal function used to write all of the atom types. This function + requires that the atom types for all molecules are all consistent, i.e. atom + type X has the same mass, vdw parameters, element type etc. for all molecules + */ +static QStringList +writeAtomTypes(QMap, GroMolType> &moltyps, + QHash &cmap_potentials, + const QMap, Molecule> &molecules, + const MMDetail &ffield, const PropertyMap &map) { + // first, get a list of all atom types involved in CMAP parameters + // (I've passed this in as a reference, in case in the future we can fix this + // problem and update the CMAP parameters being written out. For now, we + // just raise an exception if this is detected as a problem) + QSet cmap_atom_types; + + for (auto it = cmap_potentials.constBegin(); it != cmap_potentials.constEnd(); + ++it) { + for (const auto &atom_type : cmap_id_to_atomtypes(it.key())) { + cmap_atom_types.insert(atom_type); + } + } + + // next, build up a dictionary of all of the unique atom types + QHash atomtypes; + QHash param_hash; + + auto elemprop = map["element"]; + auto massprop = map["mass"]; + auto ljprop = map["LJ"]; + + // get the combining rules - these determine the format of the LJ parameter in + // the file + const int combining_rules = _getCombiningRulesFromFF(ffield); + + for (auto it = moltyps.begin(); it != moltyps.end(); ++it) { + auto moltyp = it.value(); - // next, build up a dictionary of all of the unique atom types - QHash atomtypes; - QHash param_hash; + // Store whether the molecule is perturbable. + const auto is_perturbable = moltyp.isPerturbable(); - auto elemprop = map["element"]; - auto massprop = map["mass"]; - auto ljprop = map["LJ"]; + // Rename property keys. + if (is_perturbable) { + elemprop = "element0"; + massprop = "mass0"; + ljprop = "LJ0"; + } else { + elemprop = map["element"]; + massprop = map["mass"]; + ljprop = map["LJ"]; + } - // get the combining rules - these determine the format of the LJ parameter in the file - const int combining_rules = _getCombiningRulesFromFF(ffield); + // Whether we need to update the atoms. + bool update_atoms0 = false; + bool update_atoms1 = false; - for (auto it = moltyps.begin(); it != moltyps.end(); ++it) - { - auto moltyp = it.value(); + auto atoms = moltyp.atoms(); - // Store whether the molecule is perturbable. - const auto is_perturbable = moltyp.isPerturbable(); + for (int i = 0; i < atoms.count(); ++i) { + auto atom = atoms[i]; + auto atomtype = atom.atomType(); - // Rename property keys. - if (is_perturbable) - { - elemprop = "element0"; - massprop = "mass0"; - ljprop = "LJ0"; - } - else - { - elemprop = map["element"]; - massprop = map["mass"]; - ljprop = map["LJ"]; - } + // Get the corresponding atom in the molecule. + const auto mol = molecules[it.key()]; + const auto cgatomidx = mol.info().cgAtomIdx(AtomIdx(i)); - // Whether we need to update the atoms. - bool update_atoms0 = false; - bool update_atoms1 = false; + // Was this formerly a perturbable molecule. + const bool was_perturbable = mol.hasProperty("was_perturbable"); - auto atoms = moltyp.atoms(); + // now get the corresponding Element and LJ properties for this atom + Element elem; - for (int i = 0; i < atoms.count(); ++i) - { - auto atom = atoms[i]; - auto atomtype = atom.atomType(); + try { + elem = mol.property(elemprop).asA()[cgatomidx]; + } catch (...) { + elem = Element::elementWithMass( + mol.property(massprop).asA()[cgatomidx]); + } - // Get the corresponding atom in the molecule. - const auto mol = molecules[it.key()]; - const auto cgatomidx = mol.info().cgAtomIdx(AtomIdx(i)); + double chg = + 0; // always use a zero charge as this will be supplied with the atom - // Was this formerly a perturbable molecule. - const bool was_perturbable = mol.hasProperty("was_perturbable"); + auto lj = mol.property(ljprop).asA()[cgatomidx]; + auto ljparams = ::fromLJParameter(lj, combining_rules); - // now get the corresponding Element and LJ properties for this atom - Element elem; + QString particle_type = "A"; // A is for Atom - try - { - elem = mol.property(elemprop).asA()[cgatomidx]; - } - catch (...) - { - elem = Element::elementWithMass(mol.property(massprop).asA()[cgatomidx]); + // This is a dummy atom. + if (elem.nProtons() == 0 and lj.isDummy()) { + if (is_perturbable) + atomtype += "_du"; + + // Only label dummies for regular simulations. + else if (not was_perturbable) + particle_type = "D"; + + if (cmap_atom_types.contains(atomtype)) { + throw SireError::incompatible_error( + QObject::tr( + "Cannot write a dummy atom type '%1' for a CMAP parameter.") + .arg(atomtype), + CODELOC); + } + + // Flag that we need to update the atoms. + update_atoms0 = true; + } + + // This is a new atom type. + if (not atomtypes.contains(atomtype)) { + atomtypes.insert(atomtype, + QString(" %1 %2 %3 %4 %5 %6 %7") + .arg(atomtype, 5) + .arg(elem.nProtons(), 4) + .arg(elem.mass().to(g_per_mol), 10, 'f', 6) + .arg(chg, 10, 'f', 6) + .arg(particle_type, 6) + .arg(std::get<0>(ljparams), 10, 'f', 6) + .arg(std::get<1>(ljparams), 10, 'f', 6)); + + // Hash the atom type against its parameter string, minus the type. + param_hash.insert(atomtypes[atomtype].mid(6), atomtype); + + if (update_atoms0) { + // Set the type. + atom.setAtomType(atomtype); + + // Update the atoms in the vector. + atoms[i] = atom; + } + } + // This type has been seen before. + else { + // Create the type string. + auto type_string = QString(" %1 %2 %3 %4 %5 %6 %7") + .arg(atomtype, 5) + .arg(elem.nProtons(), 4) + .arg(elem.mass().to(g_per_mol), 10, 'f', 6) + .arg(chg, 10, 'f', 6) + .arg(particle_type, 6) + .arg(std::get<0>(ljparams), 10, 'f', 6) + .arg(std::get<1>(ljparams), 10, 'f', 6); + + // The parameters for this type differ. + if (atomtypes[atomtype] != type_string) { + if (cmap_atom_types.contains(atomtype)) { + throw SireError::incompatible_error( + QObject::tr("Cannot write a CMAP parameter for atom type '%1' " + "with different " + "parameters.") + .arg(atomtype), + CODELOC); + } + + // First check the values to see if there's an existing type + // with these parameters. + const auto params = param_hash.keys(); + const auto param_string = type_string.mid(6); + + // A type already exists with these parameters. + if (params.contains(type_string.mid(6))) { + // Use the existing type. + atomtype = param_hash[param_string]; + + // Set the type. + atom.setAtomType(atomtype); + + // Update the atoms in the vector. + atoms[i] = atom; + + // Flag that the atoms need to be updated. + update_atoms0 = true; + } + + // Create a new type. + else { + // Whether this type has already been added. + bool is_added = false; + + // Append "x" until we have a new type. + while (atomtypes.contains(atomtype)) { + atomtype += "x"; + + // Recreate the type string. + type_string = QString(" %1 %2 %3 %4 %5 %6 %7") + .arg(atomtype, 5) + .arg(elem.nProtons(), 4) + .arg(elem.mass().to(g_per_mol), 10, 'f', 6) + .arg(chg, 10, 'f', 6) + .arg(particle_type, 6) + .arg(std::get<0>(ljparams), 10, 'f', 6) + .arg(std::get<1>(ljparams), 10, 'f', 6); + + // Make sure we haven't already added this type. + if (atomtypes.contains(atomtype) and + atomtypes[atomtype] == type_string) { + is_added = true; + break; + } } - double chg = 0; // always use a zero charge as this will be supplied with the atom - - auto lj = mol.property(ljprop).asA()[cgatomidx]; - auto ljparams = ::fromLJParameter(lj, combining_rules); - - QString particle_type = "A"; // A is for Atom - - // This is a dummy atom. - if (elem.nProtons() == 0 and lj.isDummy()) - { - if (is_perturbable) - atomtype += "_du"; - - // Only label dummies for regular simulations. - else if (not was_perturbable) - particle_type = "D"; + // Set the type. + atom.setAtomType(atomtype); - if (cmap_atom_types.contains(atomtype)) - { - throw SireError::incompatible_error( - QObject::tr("Cannot write a dummy atom type '%1' for a CMAP parameter.").arg(atomtype), - CODELOC); - } - - // Flag that we need to update the atoms. - update_atoms0 = true; - } - - // This is a new atom type. - if (not atomtypes.contains(atomtype)) - { - atomtypes.insert(atomtype, QString(" %1 %2 %3 %4 %5 %6 %7") - .arg(atomtype, 5) - .arg(elem.nProtons(), 4) - .arg(elem.mass().to(g_per_mol), 10, 'f', 6) - .arg(chg, 10, 'f', 6) - .arg(particle_type, 6) - .arg(std::get<0>(ljparams), 10, 'f', 6) - .arg(std::get<1>(ljparams), 10, 'f', 6)); - - // Hash the atom type against its parameter string, minus the type. - param_hash.insert(atomtypes[atomtype].mid(6), atomtype); - - if (update_atoms0) - { - // Set the type. - atom.setAtomType(atomtype); - - // Update the atoms in the vector. - atoms[i] = atom; - } + // Add the new type. + if (not is_added) { + atomtypes.insert(atomtype, type_string); + param_hash.insert(type_string.mid(6), atomtype); } - // This type has been seen before. - else - { - // Create the type string. - auto type_string = QString(" %1 %2 %3 %4 %5 %6 %7") - .arg(atomtype, 5) - .arg(elem.nProtons(), 4) - .arg(elem.mass().to(g_per_mol), 10, 'f', 6) - .arg(chg, 10, 'f', 6) - .arg(particle_type, 6) - .arg(std::get<0>(ljparams), 10, 'f', 6) - .arg(std::get<1>(ljparams), 10, 'f', 6); - - // The parameters for this type differ. - if (atomtypes[atomtype] != type_string) - { - if (cmap_atom_types.contains(atomtype)) - { - throw SireError::incompatible_error( - QObject::tr("Cannot write a CMAP parameter for atom type '%1' with different " - "parameters.") - .arg(atomtype), - CODELOC); - } - - // First check the values to see if there's an existing type - // with these parameters. - const auto params = param_hash.keys(); - const auto param_string = type_string.mid(6); - // A type already exists with these parameters. - if (params.contains(type_string.mid(6))) - { - // Use the existing type. - atomtype = param_hash[param_string]; + // Update the atoms in the vector. + atoms[i] = atom; - // Set the type. - atom.setAtomType(atomtype); + // Flag that the atoms need to be updated. + update_atoms0 = true; + } + } else { + if (update_atoms0) { + // Set the type. + atom.setAtomType(atomtype); - // Update the atoms in the vector. - atoms[i] = atom; + // Update the atoms in the vector. + atoms[i] = atom; + } + } + } + } - // Flag that the atoms need to be updated. - update_atoms0 = true; - } + // Update the atoms. + if (update_atoms0) { + moltyp.setAtoms(atoms); + } - // Create a new type. - else - { - // Whether this type has already been added. - bool is_added = false; - - // Append "x" until we have a new type. - while (atomtypes.contains(atomtype)) - { - atomtype += "x"; - - // Recreate the type string. - type_string = QString(" %1 %2 %3 %4 %5 %6 %7") - .arg(atomtype, 5) - .arg(elem.nProtons(), 4) - .arg(elem.mass().to(g_per_mol), 10, 'f', 6) - .arg(chg, 10, 'f', 6) - .arg(particle_type, 6) - .arg(std::get<0>(ljparams), 10, 'f', 6) - .arg(std::get<1>(ljparams), 10, 'f', 6); - - // Make sure we haven't already added this type. - if (atomtypes.contains(atomtype) and atomtypes[atomtype] == type_string) - { - is_added = true; - break; - } - } + // Add additional atom types from lambda = 1. + if (is_perturbable) { + auto atoms = moltyp.atoms(true); - // Set the type. - atom.setAtomType(atomtype); + for (int i = 0; i < atoms.count(); ++i) { + auto atom = atoms[i]; + auto atomtype = atom.atomType(); - // Add the new type. - if (not is_added) - { - atomtypes.insert(atomtype, type_string); - param_hash.insert(type_string.mid(6), atomtype); - } + // Get the corresponding atom in the molecule. + const auto mol = molecules[it.key()]; + const auto cgatomidx = mol.info().cgAtomIdx(AtomIdx(i)); - // Update the atoms in the vector. - atoms[i] = atom; + // now get the corresponding Element and LJ properties for this atom + Element elem; - // Flag that the atoms need to be updated. - update_atoms0 = true; - } - } - else - { - if (update_atoms0) - { - // Set the type. - atom.setAtomType(atomtype); - - // Update the atoms in the vector. - atoms[i] = atom; - } - } - } + try { + elem = mol.property("element1").asA()[cgatomidx]; + } catch (...) { + elem = Element::elementWithMass( + mol.property("mass1").asA()[cgatomidx]); } - // Update the atoms. - if (update_atoms0) - { - moltyp.setAtoms(atoms); - } + double chg = 0; // always use a zero charge as this will be supplied + // with the atom - // Add additional atom types from lambda = 1. - if (is_perturbable) - { - auto atoms = moltyp.atoms(true); + auto lj = mol.property("LJ1").asA()[cgatomidx]; + auto ljparams = ::fromLJParameter(lj, combining_rules); - for (int i = 0; i < atoms.count(); ++i) - { - auto atom = atoms[i]; - auto atomtype = atom.atomType(); + QString particle_type = "A"; // A is for Atom - // Get the corresponding atom in the molecule. - const auto mol = molecules[it.key()]; - const auto cgatomidx = mol.info().cgAtomIdx(AtomIdx(i)); + // This is a dummy atom. + if (elem.nProtons() == 0 and lj.isDummy()) { + if (cmap_atom_types.contains(atomtype)) { + throw SireError::incompatible_error( + QObject::tr( + "Cannot write a dummy atom type '%1' for a CMAP parameter.") + .arg(atomtype), + CODELOC); + } + + atomtype += "_du"; + + // Flag that we need to update the atoms. + update_atoms1 = true; + } + + // This is a new atom type. + if (not atomtypes.contains(atomtype)) { + atomtypes.insert(atomtype, + QString(" %1 %2 %3 %4 %5 %6 %7") + .arg(atomtype, 5) + .arg(elem.nProtons(), 4) + .arg(elem.mass().to(g_per_mol), 10, 'f', 6) + .arg(chg, 10, 'f', 6) + .arg(particle_type, 6) + .arg(std::get<0>(ljparams), 10, 'f', 6) + .arg(std::get<1>(ljparams), 10, 'f', 6)); + + // Hash the atom type against its parameter string, minus the type. + param_hash.insert(atomtypes[atomtype].mid(6), atomtype); + + if (update_atoms1) { + // Set the type. + atom.setAtomType(atomtype); + + // Update the atoms in the vector. + atoms[i] = atom; + } + } + + // This type has been seen before. + else { + // Create the type string. + auto type_string = QString(" %1 %2 %3 %4 %5 %6 %7") + .arg(atomtype, 5) + .arg(elem.nProtons(), 4) + .arg(elem.mass().to(g_per_mol), 10, 'f', 6) + .arg(chg, 10, 'f', 6) + .arg(particle_type, 6) + .arg(std::get<0>(ljparams), 10, 'f', 6) + .arg(std::get<1>(ljparams), 10, 'f', 6); + + // The parameters for this type differ. + if (atomtypes[atomtype] != type_string) { + if (cmap_atom_types.contains(atomtype)) { + throw SireError::incompatible_error( + QObject::tr("Cannot write a CMAP parameter for atom type " + "'%1' with different " + "parameters.") + .arg(atomtype), + CODELOC); + } - // now get the corresponding Element and LJ properties for this atom - Element elem; + // First check the values to see if there's an existing type + // with these parameters. + const auto params = param_hash.keys(); + const auto param_string = type_string.mid(6); - try - { - elem = mol.property("element1").asA()[cgatomidx]; - } - catch (...) - { - elem = Element::elementWithMass(mol.property("mass1").asA()[cgatomidx]); - } + // A type already exists with these parameters. + if (params.contains(type_string.mid(6))) { + // Use the existing type. + atomtype = param_hash[param_string]; - double chg = 0; // always use a zero charge as this will be supplied with the atom + // Set the type. + atom.setAtomType(atomtype); - auto lj = mol.property("LJ1").asA()[cgatomidx]; - auto ljparams = ::fromLJParameter(lj, combining_rules); + // Update the atoms in the vector. + atoms[i] = atom; - QString particle_type = "A"; // A is for Atom + // Flag that the atoms need to be updated. + update_atoms1 = true; + } - // This is a dummy atom. - if (elem.nProtons() == 0 and lj.isDummy()) - { - if (cmap_atom_types.contains(atomtype)) - { - throw SireError::incompatible_error( - QObject::tr("Cannot write a dummy atom type '%1' for a CMAP parameter.").arg(atomtype), - CODELOC); - } + // Create a new type. + else { + // Whether this type has already been added. + bool is_added = false; - atomtype += "_du"; + // Append "x" until we have a new type. + while (atomtypes.contains(atomtype)) { + atomtype += "x"; - // Flag that we need to update the atoms. - update_atoms1 = true; - } + // Recreate the type string. + type_string = QString(" %1 %2 %3 %4 %5 %6 %7") + .arg(atomtype, 5) + .arg(elem.nProtons(), 4) + .arg(elem.mass().to(g_per_mol), 10, 'f', 6) + .arg(chg, 10, 'f', 6) + .arg(particle_type, 6) + .arg(std::get<0>(ljparams), 10, 'f', 6) + .arg(std::get<1>(ljparams), 10, 'f', 6); - // This is a new atom type. - if (not atomtypes.contains(atomtype)) - { - atomtypes.insert(atomtype, QString(" %1 %2 %3 %4 %5 %6 %7") - .arg(atomtype, 5) - .arg(elem.nProtons(), 4) - .arg(elem.mass().to(g_per_mol), 10, 'f', 6) - .arg(chg, 10, 'f', 6) - .arg(particle_type, 6) - .arg(std::get<0>(ljparams), 10, 'f', 6) - .arg(std::get<1>(ljparams), 10, 'f', 6)); - - // Hash the atom type against its parameter string, minus the type. - param_hash.insert(atomtypes[atomtype].mid(6), atomtype); - - if (update_atoms1) - { - // Set the type. - atom.setAtomType(atomtype); - - // Update the atoms in the vector. - atoms[i] = atom; - } + // Make sure we haven't already added this type. + if (atomtypes.contains(atomtype) and + atomtypes[atomtype] == type_string) { + is_added = true; + break; } + } - // This type has been seen before. - else - { - // Create the type string. - auto type_string = QString(" %1 %2 %3 %4 %5 %6 %7") - .arg(atomtype, 5) - .arg(elem.nProtons(), 4) - .arg(elem.mass().to(g_per_mol), 10, 'f', 6) - .arg(chg, 10, 'f', 6) - .arg(particle_type, 6) - .arg(std::get<0>(ljparams), 10, 'f', 6) - .arg(std::get<1>(ljparams), 10, 'f', 6); - - // The parameters for this type differ. - if (atomtypes[atomtype] != type_string) - { - if (cmap_atom_types.contains(atomtype)) - { - throw SireError::incompatible_error( - QObject::tr("Cannot write a CMAP parameter for atom type '%1' with different " - "parameters.") - .arg(atomtype), - CODELOC); - } - - // First check the values to see if there's an existing type - // with these parameters. - const auto params = param_hash.keys(); - const auto param_string = type_string.mid(6); - - // A type already exists with these parameters. - if (params.contains(type_string.mid(6))) - { - // Use the existing type. - atomtype = param_hash[param_string]; - - // Set the type. - atom.setAtomType(atomtype); - - // Update the atoms in the vector. - atoms[i] = atom; - - // Flag that the atoms need to be updated. - update_atoms1 = true; - } + // Set the type. + atom.setAtomType(atomtype); - // Create a new type. - else - { - // Whether this type has already been added. - bool is_added = false; - - // Append "x" until we have a new type. - while (atomtypes.contains(atomtype)) - { - atomtype += "x"; - - // Recreate the type string. - type_string = QString(" %1 %2 %3 %4 %5 %6 %7") - .arg(atomtype, 5) - .arg(elem.nProtons(), 4) - .arg(elem.mass().to(g_per_mol), 10, 'f', 6) - .arg(chg, 10, 'f', 6) - .arg(particle_type, 6) - .arg(std::get<0>(ljparams), 10, 'f', 6) - .arg(std::get<1>(ljparams), 10, 'f', 6); - - // Make sure we haven't already added this type. - if (atomtypes.contains(atomtype) and atomtypes[atomtype] == type_string) - { - is_added = true; - break; - } - } - - // Set the type. - atom.setAtomType(atomtype); - - // Add the new type. - if (not is_added) - { - atomtypes.insert(atomtype, type_string); - param_hash.insert(type_string.mid(6), atomtype); - } + // Add the new type. + if (not is_added) { + atomtypes.insert(atomtype, type_string); + param_hash.insert(type_string.mid(6), atomtype); + } - // Update the atoms in the vector. - atoms[i] = atom; + // Update the atoms in the vector. + atoms[i] = atom; - // Flag that the atoms need to be updated. - update_atoms1 = true; - } - } - else - { - if (update_atoms1) - { - // Set the type. - atom.setAtomType(atomtype); - - // Update the atoms in the vector. - atoms[i] = atom; - } - } - } + // Flag that the atoms need to be updated. + update_atoms1 = true; } + } else { + if (update_atoms1) { + // Set the type. + atom.setAtomType(atomtype); - // Update the atoms. - if (update_atoms1) - { - moltyp.setAtoms(atoms, true); + // Update the atoms in the vector. + atoms[i] = atom; } + } } + } - // Update the map. - if (update_atoms0 or update_atoms1) - { - moltyps[it.key()] = moltyp; - } + // Update the atoms. + if (update_atoms1) { + moltyp.setAtoms(atoms, true); + } } - // now sort and write all of the atomtypes - QStringList lines; - auto keys = atomtypes.keys(); - std::sort(keys.begin(), keys.end()); + // Update the map. + if (update_atoms0 or update_atoms1) { + moltyps[it.key()] = moltyp; + } + } - lines.append("[ atomtypes ]"); - lines.append("; name at.num mass charge ptype sigma epsilon"); + // now sort and write all of the atomtypes + QStringList lines; + auto keys = atomtypes.keys(); + std::sort(keys.begin(), keys.end()); - for (const auto &key : keys) - { - lines.append(atomtypes[key]); - } + lines.append("[ atomtypes ]"); + lines.append("; name at.num mass charge ptype sigma " + " epsilon"); - lines.append(""); + for (const auto &key : keys) { + lines.append(atomtypes[key]); + } - return lines; + lines.append(""); + + return lines; } /** Write all of the CMAP types */ -static QStringList writeCMAPTypes(const QHash &cmap_params) -{ - QStringList lines; +static QStringList +writeCMAPTypes(const QHash &cmap_params) { + QStringList lines; - if (cmap_params.isEmpty()) - { - return lines; // no cmap parameters - } + if (cmap_params.isEmpty()) { + return lines; // no cmap parameters + } - lines.append("[ cmaptypes ]"); + lines.append("[ cmaptypes ]"); - auto keys = cmap_params.keys(); - std::sort(keys.begin(), keys.end()); + auto keys = cmap_params.keys(); + std::sort(keys.begin(), keys.end()); - for (auto key : keys) - { - const auto &cmap = cmap_params[key]; - key = key.replace(";", " "); + for (auto key : keys) { + const auto &cmap = cmap_params[key]; + key = key.replace(";", " "); - // Create the line with the parameters. - lines.append(QString("%1 %2") - .arg(key) - .arg(cmap_to_string(cmap))); - } + // Create the line with the parameters. + lines.append(QString("%1 %2").arg(key).arg(cmap_to_string(cmap))); + } - lines.append(""); + lines.append(""); - return lines; + return lines; } /** Internal function used to convert a Gromacs Moltyp to a set of lines */ -static QStringList writeMolType(const QString &name, const GroMolType &moltype, const Molecule &mol, - bool uses_parallel, int combining_rules = 2) -{ - QStringList lines; +static QStringList writeMolType(const QString &name, const GroMolType &moltype, + const Molecule &mol, bool uses_parallel, + int combining_rules = 2) { + QStringList lines; + + lines.append("[ moleculetype ]"); + lines.append("; name nrexcl"); + lines.append(QString("%1 %2").arg(name).arg(moltype.nExcludedAtoms())); + lines.append(""); + + QStringList atomlines, bondlines, anglines, dihlines, cmaplines, scllines; + + // Store whether the molecule is perturbable. + const auto is_perturbable = moltype.isPerturbable(); + + // write all of the atoms + auto write_atoms = [&]() { + if (is_perturbable) { + // Get the atoms from the molecule. + const auto &atoms0 = moltype.atoms(); + const auto &atoms1 = moltype.atoms(true); + + // Loop over all of the atoms. + for (int i = 0; i < atoms0.count(); ++i) { + const auto &atom0 = atoms0[i]; + const auto &atom1 = atoms1[i]; + + // Extract the atom types. + auto atomtype0 = atom0.atomType(); + auto atomtype1 = atom1.atomType(); + + // Get the corresponding atom in the molecule. + const auto cgatomidx = mol.info().cgAtomIdx(AtomIdx(i)); + + // Get the element property at each end state. + + Element elem0; + Element elem1; + + try { + elem0 = mol.property("element0").asA()[cgatomidx]; + } catch (...) { + elem0 = Element::elementWithMass( + mol.property("mass0").asA()[cgatomidx]); + } + + try { + elem1 = mol.property("element1").asA()[cgatomidx]; + } catch (...) { + elem1 = Element::elementWithMass( + mol.property("mass1").asA()[cgatomidx]); + } + + QString resnum = QString::number(atom0.residueNumber().value()); + + if (not atom0.chainName().isNull()) { + resnum += atom0.chainName().value(); + } + + atomlines.append( + QString("%1 %2 %3 %4 %5 %6 %7 %8 %9 %10 %11") + .arg(atom0.number().value(), 6) + .arg(atomtype0, 5) + .arg(resnum, 6) + .arg(atom0.residueName().value(), 4) + .arg(atom0.name().value(), 4) + .arg(atom0.chargeGroup(), 4) + .arg(atom0.charge().to(mod_electron), 10, 'f', 6) + .arg(atom0.mass().to(g_per_mol), 10, 'f', 6) + .arg(atomtype1, 5) + .arg(atom1.charge().to(mod_electron), 10, 'f', 6) + .arg(atom1.mass().to(g_per_mol), 10, 'f', 6)); + } + } else { + // Get the atoms from the molecule. + const auto &atoms = moltype.atoms(); + + // Loop over all of the atoms. + for (int i = 0; i < atoms.count(); ++i) { + const auto &atom = atoms[i]; + + QString resnum = QString::number(atom.residueNumber().value()); + + if (not atom.chainName().isNull()) { + resnum += atom.chainName().value(); + } + + atomlines.append(QString("%1 %2 %3 %4 %5 %6 %7 %8") + .arg(atom.number().value(), 6) + .arg(atom.atomType(), 4) + .arg(resnum, 6) + .arg(atom.residueName().value(), 4) + .arg(atom.name().value(), 4) + .arg(atom.chargeGroup(), 4) + .arg(atom.charge().to(mod_electron), 10, 'f', 6) + .arg(atom.mass().to(g_per_mol), 10, 'f', 6)); + } + } + + atomlines.append(""); + }; + + // write all of the bonds + auto write_bonds = [&]() { + if (is_perturbable) { + // Get the bonds from the molecule. + const auto &bonds0 = moltype.bonds(); + const auto &bonds1 = moltype.bonds(true); + + // Sets to contain the BondIDs at lambda = 0 and lambda = 1. + QSet bonds0_idx; + QSet bonds1_idx; + + // Loop over all bonds at lambda = 0. + for (const auto &idx : bonds0.uniqueKeys()) + bonds0_idx.insert(idx); + + // Loop over all bonds at lambda = 1. + for (const auto &idx : bonds1.uniqueKeys()) { + if (bonds0_idx.contains(idx.mirror())) + bonds1_idx.insert(idx.mirror()); + else + bonds1_idx.insert(idx); + } + + // Now work out the BondIDs that are unique at lambda = 0 and lambda = 1, + // as well as those that are shared. + QSet bonds0_uniq_idx; + QSet bonds1_uniq_idx; + QSet bonds_shared_idx; + + // lambda = 0 + for (const auto &idx : bonds0_idx) { + if (not bonds1_idx.contains(idx)) + bonds0_uniq_idx.insert(idx); + else + bonds_shared_idx.insert(idx); + } - lines.append("[ moleculetype ]"); - lines.append("; name nrexcl"); - lines.append(QString("%1 %2").arg(name).arg(moltype.nExcludedAtoms())); - lines.append(""); + // lambda = 1 + for (const auto &idx : bonds1_idx) { + if (not bonds0_idx.contains(idx)) + bonds1_uniq_idx.insert(idx); + else + bonds_shared_idx.insert(idx); + } + + // First create parameter records for the bonds unique to lambda = 0/1. + + // lambda = 0 + for (const auto &idx : bonds0_uniq_idx) { + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = idx.atom0().asA().value() + 1; + int atom1 = idx.atom1().asA().value() + 1; + + // Get all of the parameters for this BondID. + const auto ¶ms = bonds0.values(idx); + + // Loop over all of the parameters. + for (const auto ¶m : params) { + QStringList param_string; + for (const auto &p : param.parameters()) + param_string.append(QString::number(p)); + + bondlines.append(QString("%1 %2 %3 %4 0.0000 0.0000") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(param.functionType(), 6) + .arg(param_string.join(" "))); + } + } + + // lambda = 1 + for (const auto &idx : bonds1_uniq_idx) { + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = idx.atom0().asA().value() + 1; + int atom1 = idx.atom1().asA().value() + 1; + + // Get all of the parameters for this BondID. + const auto ¶ms = bonds1.values(idx); + + // Loop over all of the parameters. + for (const auto ¶m : params) { + QStringList param_string; + for (const auto &p : param.parameters()) + param_string.append(QString::number(p)); + + bondlines.append(QString("%1 %2 %3 0.0000 0.0000 %4") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(param.functionType(), 6) + .arg(param_string.join(" "))); + } + } + + // Next add the shared bond parameters. + + for (auto idx : bonds_shared_idx) { + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = idx.atom0().asA().value() + 1; + int atom1 = idx.atom1().asA().value() + 1; + + // Get a list of the parameters at lambda = 0. + const auto ¶ms0 = bonds0.values(idx); + + // Invert the index. + if (not bonds1.contains(idx)) + idx = idx.mirror(); + + // Get a list of the parameters at lambda = 1. + const auto ¶ms1 = bonds1.values(idx); + + // More or same number of records at lambda = 0. + if (params0.count() >= params1.count()) { + for (int i = 0; i < params1.count(); ++i) { + QStringList param_string0; + for (const auto &p : params0[i].parameters()) + param_string0.append(QString::number(p)); + + QStringList param_string1; + for (const auto &p : params1[i].parameters()) + param_string1.append(QString::number(p)); + + bondlines.append(QString("%1 %2 %3 %4 %5") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(params0[i].functionType(), 6) + .arg(param_string0.join(" ")) + .arg(param_string1.join(" "))); + } + + // Now add parameters for which there is no matching record + // at lambda = 1. + for (int i = params1.count(); i < params0.count(); ++i) { + QStringList param_string; + for (const auto &p : params0[i].parameters()) + param_string.append(QString::number(p)); + + bondlines.append(QString("%1 %2 %3 %4 0.0000 0.0000") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(params0[i].functionType(), 6) + .arg(param_string.join(" "))); + } + } - QStringList atomlines, bondlines, anglines, dihlines, cmaplines, scllines; + // More records at lambda = 1. + else { + for (int i = 0; i < params0.count(); ++i) { + QStringList param_string0; + for (const auto &p : params0[i].parameters()) + param_string0.append(QString::number(p)); - // Store whether the molecule is perturbable. - const auto is_perturbable = moltype.isPerturbable(); + QStringList param_string1; + for (const auto &p : params1[i].parameters()) + param_string1.append(QString::number(p)); - // write all of the atoms - auto write_atoms = [&]() - { - if (is_perturbable) - { - // Get the atoms from the molecule. - const auto &atoms0 = moltype.atoms(); - const auto &atoms1 = moltype.atoms(true); - - // Loop over all of the atoms. - for (int i = 0; i < atoms0.count(); ++i) - { - const auto &atom0 = atoms0[i]; - const auto &atom1 = atoms1[i]; - - // Extract the atom types. - auto atomtype0 = atom0.atomType(); - auto atomtype1 = atom1.atomType(); - - // Get the corresponding atom in the molecule. - const auto cgatomidx = mol.info().cgAtomIdx(AtomIdx(i)); - - // Get the element property at each end state. - - Element elem0; - Element elem1; - - try - { - elem0 = mol.property("element0").asA()[cgatomidx]; - } - catch (...) - { - elem0 = Element::elementWithMass(mol.property("mass0").asA()[cgatomidx]); - } - - try - { - elem1 = mol.property("element1").asA()[cgatomidx]; - } - catch (...) - { - elem1 = Element::elementWithMass(mol.property("mass1").asA()[cgatomidx]); - } - - QString resnum = QString::number(atom0.residueNumber().value()); - - if (not atom0.chainName().isNull()) - { - resnum += atom0.chainName().value(); - } - - atomlines.append(QString("%1 %2 %3 %4 %5 %6 %7 %8 %9 %10 %11") - .arg(atom0.number().value(), 6) - .arg(atomtype0, 5) - .arg(resnum, 6) - .arg(atom0.residueName().value(), 4) - .arg(atom0.name().value(), 4) - .arg(atom0.chargeGroup(), 4) - .arg(atom0.charge().to(mod_electron), 10, 'f', 6) - .arg(atom0.mass().to(g_per_mol), 10, 'f', 6) - .arg(atomtype1, 5) - .arg(atom1.charge().to(mod_electron), 10, 'f', 6) - .arg(atom1.mass().to(g_per_mol), 10, 'f', 6)); - } - } - else - { - // Get the atoms from the molecule. - const auto &atoms = moltype.atoms(); - - // Loop over all of the atoms. - for (int i = 0; i < atoms.count(); ++i) - { - const auto &atom = atoms[i]; - - QString resnum = QString::number(atom.residueNumber().value()); - - if (not atom.chainName().isNull()) - { - resnum += atom.chainName().value(); - } - - atomlines.append(QString("%1 %2 %3 %4 %5 %6 %7 %8") - .arg(atom.number().value(), 6) - .arg(atom.atomType(), 4) - .arg(resnum, 6) - .arg(atom.residueName().value(), 4) - .arg(atom.name().value(), 4) - .arg(atom.chargeGroup(), 4) - .arg(atom.charge().to(mod_electron), 10, 'f', 6) - .arg(atom.mass().to(g_per_mol), 10, 'f', 6)); - } - } - - atomlines.append(""); - }; - - // write all of the bonds - auto write_bonds = [&]() - { - if (is_perturbable) - { - // Get the bonds from the molecule. - const auto &bonds0 = moltype.bonds(); - const auto &bonds1 = moltype.bonds(true); - - // Sets to contain the BondIDs at lambda = 0 and lambda = 1. - QSet bonds0_idx; - QSet bonds1_idx; - - // Loop over all bonds at lambda = 0. - for (const auto &idx : bonds0.uniqueKeys()) - bonds0_idx.insert(idx); - - // Loop over all bonds at lambda = 1. - for (const auto &idx : bonds1.uniqueKeys()) - { - if (bonds0_idx.contains(idx.mirror())) - bonds1_idx.insert(idx.mirror()); - else - bonds1_idx.insert(idx); - } - - // Now work out the BondIDs that are unique at lambda = 0 and lambda = 1, - // as well as those that are shared. - QSet bonds0_uniq_idx; - QSet bonds1_uniq_idx; - QSet bonds_shared_idx; - - // lambda = 0 - for (const auto &idx : bonds0_idx) - { - if (not bonds1_idx.contains(idx)) - bonds0_uniq_idx.insert(idx); - else - bonds_shared_idx.insert(idx); - } - - // lambda = 1 - for (const auto &idx : bonds1_idx) - { - if (not bonds0_idx.contains(idx)) - bonds1_uniq_idx.insert(idx); - else - bonds_shared_idx.insert(idx); - } - - // First create parameter records for the bonds unique to lambda = 0/1. - - // lambda = 0 - for (const auto &idx : bonds0_uniq_idx) - { - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = idx.atom0().asA().value() + 1; - int atom1 = idx.atom1().asA().value() + 1; - - // Get all of the parameters for this BondID. - const auto ¶ms = bonds0.values(idx); - - // Loop over all of the parameters. - for (const auto ¶m : params) - { - QStringList param_string; - for (const auto &p : param.parameters()) - param_string.append(QString::number(p)); - - bondlines.append(QString("%1 %2 %3 %4 0.0000 0.0000") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(param.functionType(), 6) - .arg(param_string.join(" "))); - } - } - - // lambda = 1 - for (const auto &idx : bonds1_uniq_idx) - { - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = idx.atom0().asA().value() + 1; - int atom1 = idx.atom1().asA().value() + 1; - - // Get all of the parameters for this BondID. - const auto ¶ms = bonds1.values(idx); - - // Loop over all of the parameters. - for (const auto ¶m : params) - { - QStringList param_string; - for (const auto &p : param.parameters()) - param_string.append(QString::number(p)); - - bondlines.append(QString("%1 %2 %3 0.0000 0.0000 %4") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(param.functionType(), 6) - .arg(param_string.join(" "))); - } - } - - // Next add the shared bond parameters. - - for (auto idx : bonds_shared_idx) - { - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = idx.atom0().asA().value() + 1; - int atom1 = idx.atom1().asA().value() + 1; - - // Get a list of the parameters at lambda = 0. - const auto ¶ms0 = bonds0.values(idx); - - // Invert the index. - if (not bonds1.contains(idx)) - idx = idx.mirror(); - - // Get a list of the parameters at lambda = 1. - const auto ¶ms1 = bonds1.values(idx); - - // More or same number of records at lambda = 0. - if (params0.count() >= params1.count()) - { - for (int i = 0; i < params1.count(); ++i) - { - QStringList param_string0; - for (const auto &p : params0[i].parameters()) - param_string0.append(QString::number(p)); - - QStringList param_string1; - for (const auto &p : params1[i].parameters()) - param_string1.append(QString::number(p)); - - bondlines.append(QString("%1 %2 %3 %4 %5") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(params0[i].functionType(), 6) - .arg(param_string0.join(" ")) - .arg(param_string1.join(" "))); - } - - // Now add parameters for which there is no matching record - // at lambda = 1. - for (int i = params1.count(); i < params0.count(); ++i) - { - QStringList param_string; - for (const auto &p : params0[i].parameters()) - param_string.append(QString::number(p)); - - bondlines.append(QString("%1 %2 %3 %4 0.0000 0.0000") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(params0[i].functionType(), 6) - .arg(param_string.join(" "))); - } - } - - // More records at lambda = 1. - else - { - for (int i = 0; i < params0.count(); ++i) - { - QStringList param_string0; - for (const auto &p : params0[i].parameters()) - param_string0.append(QString::number(p)); - - QStringList param_string1; - for (const auto &p : params1[i].parameters()) - param_string1.append(QString::number(p)); - - bondlines.append(QString("%1 %2 %3 %4 %5") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(params1[i].functionType(), 6) - .arg(param_string0.join(" ")) - .arg(param_string1.join(" "))); - } - - // Now add parameters for which there is no matching record - // at lambda = 0. - for (int i = params0.count(); i < params1.count(); ++i) - { - QStringList param_string; - for (const auto &p : params1[i].parameters()) - param_string.append(QString::number(p)); - - bondlines.append(QString("%1 %2 %3 0.0000 0.0000 %4") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(params1[i].functionType(), 6) - .arg(param_string.join(" "))); - } - } - } - } - else - { - // Get the bonds from the molecule. - const auto &bonds = moltype.bonds(); - - for (auto it = bonds.constBegin(); it != bonds.constEnd(); ++it) - { - const auto &bond = it.key(); - const auto ¶m = it.value(); - - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = bond.atom0().asA().value() + 1; - int atom1 = bond.atom1().asA().value() + 1; - - QStringList params; - for (const auto &p : param.parameters()) - params.append(QString::number(p)); - - bondlines.append(QString("%1 %2 %3 %4") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(param.functionType(), 6) - .arg(params.join(" "))); - } - } - - std::sort(bondlines.begin(), bondlines.end()); - }; - - // write all of the angles - auto write_angs = [&]() - { - if (is_perturbable) - { - // Get the angles from the molecule. - const auto &angles0 = moltype.angles(); - const auto &angles1 = moltype.angles(true); - - // Sets to contain the AngleIDs at lambda = 0 and lambda = 1. - QSet angles0_idx; - QSet angles1_idx; - - // Loop over all angles at lambda = 0. - for (const auto &idx : angles0.uniqueKeys()) - angles0_idx.insert(idx); - - // Loop over all angles at lambda = 1. - for (const auto &idx : angles1.uniqueKeys()) - { - if (angles0_idx.contains(idx.mirror())) - angles1_idx.insert(idx.mirror()); - else - angles1_idx.insert(idx); - } - - // Now work out the AngleIDs that are unique at lambda = 0 and lambda = 1, - // as well as those that are shared. - QSet angles0_uniq_idx; - QSet angles1_uniq_idx; - QSet angles_shared_idx; - - // lambda = 0 - for (const auto &idx : angles0_idx) - { - if (not angles1_idx.contains(idx)) - angles0_uniq_idx.insert(idx); - else - angles_shared_idx.insert(idx); - } - - // lambda = 1 - for (const auto &idx : angles1_idx) - { - if (not angles0_idx.contains(idx)) - angles1_uniq_idx.insert(idx); - else - angles_shared_idx.insert(idx); - } - - // First create parameter records for the angles unique to lambda = 0/1. - - // lambda = 0 - for (const auto &idx : angles0_uniq_idx) - { - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = idx.atom0().asA().value() + 1; - int atom1 = idx.atom1().asA().value() + 1; - int atom2 = idx.atom2().asA().value() + 1; - - // Get all of the parameters for this AngleID. - const auto ¶ms = angles0.values(idx); - - // Loop over all of the parameters. - for (const auto ¶m : params) - { - QStringList param_string; - for (const auto &p : param.parameters()) - param_string.append(QString::number(p)); - - anglines.append(QString("%1 %2 %3 %4 %5 0.0000 0.0000") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(param.functionType(), 7) - .arg(param_string.join(" "))); - } - } - - // lambda = 1 - for (const auto &idx : angles1_uniq_idx) - { - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = idx.atom0().asA().value() + 1; - int atom1 = idx.atom1().asA().value() + 1; - int atom2 = idx.atom2().asA().value() + 1; - - // Get all of the parameters for this AngleID. - const auto ¶ms = angles1.values(idx); - - // Loop over all of the parameters. - for (const auto ¶m : params) - { - QStringList param_string; - for (const auto &p : param.parameters()) - param_string.append(QString::number(p)); - - anglines.append(QString("%1 %2 %3 %4 0.0000 0.0000 %5") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(param.functionType(), 7) - .arg(param_string.join(" "))); - } - } - - // Next add the shared angle parameters. - - for (auto idx : angles_shared_idx) - { - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = idx.atom0().asA().value() + 1; - int atom1 = idx.atom1().asA().value() + 1; - int atom2 = idx.atom2().asA().value() + 1; - - // Get a list of the parameters at lambda = 0. - const auto ¶ms0 = angles0.values(idx); - - // Invert the index. - if (not angles1.contains(idx)) - idx = idx.mirror(); - - // Get a list of the parameters at lambda = 1. - const auto ¶ms1 = angles1.values(idx); - - // More or same number of records at lambda = 0. - if (params0.count() >= params1.count()) - { - for (int i = 0; i < params1.count(); ++i) - { - QStringList param_string0; - for (const auto &p : params0[i].parameters()) - param_string0.append(QString::number(p)); - - QStringList param_string1; - for (const auto &p : params1[i].parameters()) - param_string1.append(QString::number(p)); - - anglines.append(QString("%1 %2 %3 %4 %5 %6") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(params0[i].functionType(), 7) - .arg(param_string0.join(" ")) - .arg(param_string1.join(" "))); - } - - // Now add parameters for which there is no matching record - // at lambda = 1. - for (int i = params1.count(); i < params0.count(); ++i) - { - QStringList param_string; - for (const auto &p : params0[i].parameters()) - param_string.append(QString::number(p)); - - anglines.append(QString("%1 %2 %3 %4 %5 0.0000 0.0000") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(params0[i].functionType(), 7) - .arg(param_string.join(" "))); - } - } - - // More records at lambda = 1. - else - { - for (int i = 0; i < params0.count(); ++i) - { - QStringList param_string0; - for (const auto &p : params0[i].parameters()) - param_string0.append(QString::number(p)); - - QStringList param_string1; - for (const auto &p : params1[i].parameters()) - param_string1.append(QString::number(p)); - - anglines.append(QString("%1 %2 %3 %4 %5 %6") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(params1[i].functionType(), 7) - .arg(param_string0.join(" ")) - .arg(param_string1.join(" "))); - } - - // Now add parameters for which there is no matching record - // at lambda = 0. - for (int i = params0.count(); i < params1.count(); ++i) - { - QStringList param_string; - for (const auto &p : params1[i].parameters()) - param_string.append(QString::number(p)); - - anglines.append(QString("%1 %2 %3 %4 0.0000 0.0000 %5") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(params1[i].functionType(), 7) - .arg(param_string.join(" "))); - } - } - } - } - else - { - // Get the angles from the molecule. - const auto &angles = moltype.angles(); - - for (auto it = angles.constBegin(); it != angles.constEnd(); ++it) - { - const auto &angle = it.key(); - const auto ¶m = it.value(); - - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = angle.atom0().asA().value() + 1; - int atom1 = angle.atom1().asA().value() + 1; - int atom2 = angle.atom2().asA().value() + 1; - - QStringList params; - for (const auto &p : param.parameters()) - params.append(QString::number(p)); - - anglines.append(QString("%1 %2 %3 %4 %5") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(param.functionType(), 7) - .arg(params.join(" "))); - } - } - - std::sort(anglines.begin(), anglines.end()); - }; - - // write all of the dihedrals/impropers (they are merged) - auto write_dihs = [&]() - { - if (is_perturbable) - { - // Get the dihedrals from the molecule. - const auto &dihedrals0 = moltype.dihedrals(); - const auto &dihedrals1 = moltype.dihedrals(true); - - // Sets to contain the DihedralID at lambda = 0 and lambda = 1. - QSet dihedrals0_idx; - QSet dihedrals1_idx; - - // Loop over all dihedrals at lambda = 0. - for (const auto &idx : dihedrals0.uniqueKeys()) - dihedrals0_idx.insert(idx); - - // Loop over all dihedrals at lambda = 1. - for (const auto &idx : dihedrals1.uniqueKeys()) - { - if (dihedrals0_idx.contains(idx.mirror())) - dihedrals1_idx.insert(idx.mirror()); - else - dihedrals1_idx.insert(idx); - } - - // Now work out the DihedralIDs that are unique at lambda = 0 and lambda = 1, - // as well as those that are shared. - QSet dihedrals0_uniq_idx; - QSet dihedrals1_uniq_idx; - QSet dihedrals_shared_idx; - - // lambda = 0 - for (const auto &idx : dihedrals0_idx) - { - if (not dihedrals1_idx.contains(idx)) - dihedrals0_uniq_idx.insert(idx); - else - dihedrals_shared_idx.insert(idx); - } - - // lambda = 1 - for (const auto &idx : dihedrals1_idx) - { - if (not dihedrals0_idx.contains(idx)) - dihedrals1_uniq_idx.insert(idx); - else - dihedrals_shared_idx.insert(idx); - } - - // First create parameter records for the dihedrals unique to lambda = 0/1. - - // lambda = 0 - for (const auto &idx : dihedrals0_uniq_idx) - { - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = idx.atom0().asA().value() + 1; - int atom1 = idx.atom1().asA().value() + 1; - int atom2 = idx.atom2().asA().value() + 1; - int atom3 = idx.atom3().asA().value() + 1; - - // Get all of the parameters for this DihedralID. - const auto ¶ms = dihedrals0.values(idx); - - // Loop over all of the parameters. - for (const auto ¶m : params) - { - QStringList param_string; - for (const auto &p : param.parameters()) - param_string.append(QString::number(p)); - - // Get the periodicity of the dihedral term. This is the last - // parameter entry. - auto periodicity = param.parameters().last(); - - dihlines.append(QString("%1 %2 %3 %4 %5 %6 0 0.0000 %7") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(atom3, 6) - .arg(param.functionType(), 6) - .arg(param_string.join(" ")) - .arg(periodicity)); - } - } - - // lambda = 1 - for (const auto &idx : dihedrals1_uniq_idx) - { - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = idx.atom0().asA().value() + 1; - int atom1 = idx.atom1().asA().value() + 1; - int atom2 = idx.atom2().asA().value() + 1; - int atom3 = idx.atom3().asA().value() + 1; - - // Get all of the parameters for this AngleID. - const auto ¶ms = dihedrals1.values(idx); - - // Loop over all of the parameters. - for (const auto ¶m : params) - { - QStringList param_string; - for (const auto &p : param.parameters()) - param_string.append(QString::number(p)); - - // Get the periodicity of the dihedral term. This is the last - // parameter entry. - auto periodicity = param.parameters().last(); - - dihlines.append(QString("%1 %2 %3 %4 %5 0 0.0000 %6 %7") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(atom3, 6) - .arg(param.functionType(), 6) - .arg(periodicity) - .arg(param_string.join(" "))); - } - } - - // Next add the shared dihedral parameters. - - for (auto idx : dihedrals_shared_idx) - { - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = idx.atom0().asA().value() + 1; - int atom1 = idx.atom1().asA().value() + 1; - int atom2 = idx.atom2().asA().value() + 1; - int atom3 = idx.atom3().asA().value() + 1; - - // Get a list of the parameters at lambda = 0. - const auto ¶ms0 = dihedrals0.values(idx); - - // Invert the index. - if (not dihedrals1.contains(idx)) - idx = idx.mirror(); - - // Get a list of the parameters at lambda = 1. - const auto ¶ms1 = dihedrals1.values(idx); - - // Create two hashes between the periodicity of each dihedral - // term and its corresponding parameters. - - // The maximum periodicity recorded. - int max_per = 0; - - // lambda = 0 - QHash params0_hash; - for (const auto ¶m : params0) - { - // Extract the periodicity and update the hash. - int periodicity = int(param.parameters().last()); - params0_hash.insert(periodicity, param); - - // If necessary, update the maximum periodicity. - if (periodicity > max_per) - max_per = periodicity; - } - - // lambda = 1 - QHash params1_hash; - for (const auto ¶m : params1) - { - // Extract the periodicity and update the hash. - int periodicity = int(param.parameters().last()); - params1_hash.insert(periodicity, param); - - // If necessary, update the maximum periodicity. - if (periodicity > max_per) - max_per = periodicity; - } - - // Loop over the range of dihedral periodicities observed. - for (int i = 0; i <= max_per; ++i) - { - // There is a term at lambda = 0 with this periodicity. - if (params0_hash.contains(i)) - { - QStringList param_string0; - QStringList param_string1; - for (const auto &p : params0_hash[i].parameters()) - param_string0.append(QString::number(p)); - - // There is a term at lambda = 1 with this periodicity. - if (params1_hash.contains(i)) - { - for (const auto &p : params1_hash[i].parameters()) - param_string1.append(QString::number(p)); - } - // No term, create a zero term with the same periodicity. - else - { - param_string1.append(QString::number(0)); - param_string1.append(QString::number(0.0000)); - param_string1.append(QString::number(i)); - } - - // Append the dihedral term. - dihlines.append(QString("%1 %2 %3 %4 %5 %6 %7") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(atom3, 6) - .arg(params0_hash[i].functionType(), 6) - .arg(param_string0.join(" ")) - .arg(param_string1.join(" "))); - } - else - { - // There is a term at lambda = 1 with this periodicity. - if (params1_hash.contains(i)) - { - QStringList param_string0; - QStringList param_string1; - - // No lambda = 0 term, create a zero term with the same periodicity. - param_string0.append(QString::number(0)); - param_string0.append(QString::number(0.0000)); - param_string0.append(QString::number(i)); - - for (const auto &p : params1_hash[i].parameters()) - param_string1.append(QString::number(p)); - - // Append the dihedral term. - dihlines.append(QString("%1 %2 %3 %4 %5 %6 %7") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(atom3, 6) - .arg(params1_hash[i].functionType(), 6) - .arg(param_string0.join(" ")) - .arg(param_string1.join(" "))); - } - } - } - } - } - else - { - // Get the dihedrals from the molecule. - const auto &dihedrals = moltype.dihedrals(); - - for (auto it = dihedrals.constBegin(); it != dihedrals.constEnd(); ++it) - { - const auto &dihedral = it.key(); - const auto ¶m = it.value(); - - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = dihedral.atom0().asA().value() + 1; - int atom1 = dihedral.atom1().asA().value() + 1; - int atom2 = dihedral.atom2().asA().value() + 1; - int atom3 = dihedral.atom3().asA().value() + 1; - - QStringList params; - for (const auto &p : param.parameters()) - params.append(QString::number(p)); - - dihlines.append(QString("%1 %2 %3 %4 %5 %6") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(atom3, 6) - .arg(param.functionType(), 6) - .arg(params.join(" "))); - } - } - - std::sort(dihlines.begin(), dihlines.end()); - }; - - // write all of the cmaps - auto write_cmaps = [&]() - { - if (is_perturbable) - { - const auto cmaps0 = moltype.cmaps(); - const auto cmaps1 = moltype.cmaps(true); - - if (cmaps0 != cmaps1) - { - throw SireError::unsupported( - QObject::tr("The molecule '%1' has different CMAP parameters at lambda = 0 and lambda = 1. " - "This is not supported yet by the Sire parser!") - .arg(moltype.name()), - CODELOC); - } - } - - const auto cmaps = moltype.cmaps(); - - for (auto it = cmaps.constBegin(); it != cmaps.constEnd(); ++it) - { - const auto &cmap = it.key(); - const auto ¶m = it.value(); - - // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed - int atom0 = cmap.atom0().asA().value() + 1; - int atom1 = cmap.atom1().asA().value() + 1; - int atom2 = cmap.atom2().asA().value() + 1; - int atom3 = cmap.atom3().asA().value() + 1; - int atom4 = cmap.atom4().asA().value() + 1; - - bool ok; - int function_type = param.toInt(&ok); - - if (not ok) - { - throw SireError::program_bug(QObject::tr( - "The CMAP parameter '%2' for %1 is not a valid integer. " - "This is a bug in Sire, please report it.") - .arg(cmap.toString()) - .arg(param), - CODELOC); - } - - // format is the index of each atom, plus the function type, - cmaplines.append(QString("%1 %2 %3 %4 %5 %6") - .arg(atom0, 6) - .arg(atom1, 6) - .arg(atom2, 6) - .arg(atom3, 6) - .arg(atom4, 6) - .arg(function_type, 6)); - } - - std::sort(cmaplines.begin(), cmaplines.end()); - }; - - // write all of the pairs (1-4 scaling factors). This is needed even though - // we have set autogenerate pairs to "yes" - auto write_pairs = [&]() - { - // Store the molinfo object; - const auto molinfo = mol.info(); - - if (is_perturbable) - { - CLJNBPairs scl0; - CLJNBPairs scl1; - - try - { - scl0 = mol.property("intrascale0").asA(); - } - catch (...) - { - return; - } - - try - { - scl1 = mol.property("intrascale1").asA(); - } - catch (...) - { - return; - } - - AtomLJs ljs0; - AtomLJs ljs1; - - try - { - ljs0 = mol.property("LJ0").asA(); - } - catch (...) - { - } - - try - { - ljs1 = mol.property("LJ1").asA(); - } - catch (...) - { - } - - // A set of recorded 1-4 pairs. - QSet> recorded_pairs; - - bool fix_null_perturbable_14s = false; - - if (mol.hasProperty("fix_null_perturbable_14s")) - fix_null_perturbable_14s = mol.property("fix_null_perturbable_14s").asA().value(); - - // Must record every pair that has a non-default scaling factor. - // Loop over intrascale matrix by cut-groups to avoid N^2 loop. - for (int i = 0; i < scl0.nGroups(); ++i) - { - for (int j = 0; j < scl0.nGroups(); ++j) - { - const auto s0 = scl0.get(CGIdx(i), CGIdx(j)); - const auto s1 = scl1.get(CGIdx(i), CGIdx(j)); - - if (not s0.isEmpty() and not s1.isEmpty()) - { - const auto idxs0 = molinfo.getAtomsIn(CGIdx(i)); - const auto idxs1 = molinfo.getAtomsIn(CGIdx(j)); - - for (const auto &idx0 : idxs0) - { - for (const auto &idx1 : idxs1) - { - QPair pair = QPair(idx0, idx1); - - // Make sure this is a new atom pair. - if (not recorded_pairs.contains(pair)) - { - // Insert the pair and its inverse. - recorded_pairs.insert(pair); - pair = QPair(idx1, idx0); - recorded_pairs.insert(pair); - - const auto s0 = scl0.get(idx0, idx1); - const auto s1 = scl1.get(idx0, idx1); - - if (not((s0.coulomb() == 0 and s0.lj() == 0 and s1.coulomb() == 0 and - s1.lj() == 0) or - (s0.coulomb() == 1 and s0.lj() == 1 and s1.coulomb() == 1 and - s1.lj() == 1))) - { - // This is a non-default pair. - if (fix_null_perturbable_14s) - { - // get the initial and perturbed charge and LJ parameters - const auto &lj0_0 = ljs0.get(idx0); - const auto &lj0_1 = ljs1.get(idx0); - const auto &lj1_0 = ljs0.get(idx1); - const auto &lj1_1 = ljs1.get(idx1); - - if (lj0_0.epsilon().value() == 0 or lj0_1.epsilon().value() == 0 or - lj1_0.epsilon().value() == 0 or lj1_1.epsilon().value() == 0) - { - // we need to avoid having a null 1-4 LJ parameter, so use the non-dummy state - LJParameter lj0, lj1; - - if (lj0_0.epsilon().value() == 0) - lj0 = lj0_1; - else - lj0 = lj0_0; - - if (lj1_0.epsilon().value() == 0) - lj1 = lj1_1; - else - lj1 = lj1_0; - - auto lj = lj0.combineArithmetic(lj1); - - double scl = s0.lj(); - - if (scl == 0) - scl = s1.lj(); - - scllines.append(QString("%1 %2 1 %3 %4 %3 %4") - .arg(idx0 + 1, 6) - .arg(idx1 + 1, 6) - .arg(lj.sigma().to(nanometer), 11, 'f', 5) - .arg(scl * lj.epsilon().to(kJ_per_mol), 11, 'f', 5)); - - continue; - } - } - - scllines.append(QString("%1 %2 1").arg(idx0 + 1, 6).arg(idx1 + 1, 6)); - } - } - } - } - } - } - } - } - else - { - CLJNBPairs scl; - - try - { - scl = mol.property("intrascale").asA(); - } - catch (...) - { - return; - } - - // Get LJ and charge properties for writing funct=2 explicit pairs. - AtomLJs ljs; - AtomCharges charges; - bool has_ljs = false; - bool has_charges = false; - - // Determine the combining rules from the forcefield (default to arithmetic = 2). - const int local_combining_rules = combining_rules; - - try - { - ljs = mol.property("LJ").asA(); - has_ljs = true; - } - catch (...) - { - } - - try - { - charges = mol.property("charge").asA(); - has_charges = true; - } - catch (...) - { - } - - // A set of recorded 1-4 pairs. - QSet> recorded_pairs; - - // Must record every pair that has a non-default scaling factor. - // Loop over intrascale matrix by cut-groups to avoid N^2 loop. - for (int i = 0; i < scl.nGroups(); ++i) - { - for (int j = 0; j < scl.nGroups(); ++j) - { - const auto s = scl.get(CGIdx(i), CGIdx(j)); - - if (not s.isEmpty()) - { - const auto idxs0 = molinfo.getAtomsIn(CGIdx(i)); - const auto idxs1 = molinfo.getAtomsIn(CGIdx(j)); - - for (const auto &idx0 : idxs0) - { - for (const auto &idx1 : idxs1) - { - QPair pair = QPair(idx0, idx1); - - // Make sure this is a new atom pair. - if (not recorded_pairs.contains(pair)) - { - // Insert the pair and its inverse. - recorded_pairs.insert(pair); - pair = QPair(idx1, idx0); - recorded_pairs.insert(pair); - - const auto s = scl.get(idx0, idx1); - - if (s.coulomb() == 0 and s.lj() == 0) - { - // Fully excluded: don't write. - } - else if (s.coulomb() == 1 and s.lj() == 1) - { - // Full 1-4 interaction (e.g. GLYCAM with SCNB=1.0, SCEE=1.0). - // Must write as funct=2 with explicit LJ parameters because - // funct=1 with gen-pairs would apply fudgeLJ and reduce the - // interaction, and not listing the pair would give zero interaction. - if (has_ljs and has_charges) - { - const auto cgidx0 = molinfo.cgAtomIdx(idx0); - const auto cgidx1 = molinfo.cgAtomIdx(idx1); - - const auto &lj0 = ljs.at(cgidx0); - const auto &lj1 = ljs.at(cgidx1); - - LJParameter lj_ij; - if (local_combining_rules == 2) - lj_ij = lj0.combineArithmetic(lj1); - else - lj_ij = lj0.combineGeometric(lj1); - - const double qi = - charges.at(cgidx0).to(mod_electron); - const double qj = - charges.at(cgidx1).to(mod_electron); - - scllines.append( - QString("%1 %2 2 1.0 %3 %4 %5 %6") - .arg(idx0 + 1, 6) - .arg(idx1 + 1, 6) - .arg(qi, 11, 'f', 6) - .arg(qj, 11, 'f', 6) - .arg(lj_ij.sigma().to(nanometer), 18, 'e', 11) - .arg(lj_ij.epsilon().to(kJ_per_mol), 18, 'e', 11)); - } - else - { - // Fall back to funct=1; the energy will be wrong if - // fudgeLJ != 1.0, but we have no LJ parameters to use. - scllines.append( - QString("%1 %2 1").arg(idx0 + 1, 6).arg(idx1 + 1, 6)); - } - } - else - { - // Standard partial 1-4 (e.g. fudgeQQ/fudgeLJ): write as funct=1. - scllines.append( - QString("%1 %2 1").arg(idx0 + 1, 6).arg(idx1 + 1, 6)); - } - } - } - } - } - } - } - } - }; - - const QVector> funcs = {write_atoms, write_bonds, write_angs, - write_dihs, write_cmaps, write_pairs}; - - if (uses_parallel) - { - tbb::parallel_for(tbb::blocked_range(0, funcs.count(), 1), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - funcs[i](); - } }); - } - else - { - for (int i = 0; i < funcs.count(); ++i) - { - funcs[i](); - } - } - - lines.append("[ atoms ]"); - if (is_perturbable) - lines.append( - "; nr type0 resnr residue atom cgnr charge0 mass0 type1 charge1 mass1"); - else - lines.append("; nr type resnr residue atom cgnr charge mass"); - lines.append(atomlines); - - // we need to detect whether this is a water molecule. If so, then we - // need to add in "settles" lines to constrain bonds / angles of the - // water molecule - const bool is_water = moltype.isWater(); - - if (is_water) - { - lines.append("#ifdef FLEXIBLE"); - } - - if (not bondlines.isEmpty()) - { - lines.append("[ bonds ]"); - lines.append("; ai aj funct parameters"); - lines += bondlines; - lines.append(""); - } - - if (not scllines.isEmpty()) - { - lines.append("[ pairs ]"); - lines.append("; ai aj funct "); - lines += scllines; - lines.append(""); - } - - if (not anglines.isEmpty()) - { - lines.append("[ angles ]"); - lines.append("; ai aj ak funct parameters"); - lines += anglines; - lines.append(""); - } - - if (not dihlines.isEmpty()) - { - lines.append("[ dihedrals ]"); - lines.append("; ai aj ak al funct parameters"); - lines += dihlines; - lines.append(""); - } - - if (not cmaplines.isEmpty()) - { - lines.append("[ cmap ]"); - lines.append("; ai aj ak al am funct"); - lines += cmaplines; - lines.append(""); - } - - if (is_water) - { - lines.append("#else"); - lines.append(""); - lines += moltype.settlesLines(); - lines.append(""); - lines.append("#endif"); - } - - return lines; -} - -/** Internal function used to convert an array of Gromacs Moltyps into - lines of a Gromacs topology file */ -static QStringList writeMolTypes(const QMap, GroMolType> &moltyps, - const QMap, Molecule> &examples, bool uses_parallel, - bool isSorted = false) -{ - QHash typs; - - if (uses_parallel) - { - const QVector> keys = moltyps.keys().toVector(); - QMutex mutex; - - tbb::parallel_for(tbb::blocked_range(0, keys.count(), 1), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - QStringList typlines = - ::writeMolType(keys[i].second, moltyps[keys[i]], examples[keys[i]], - uses_parallel); - - QMutexLocker lkr(&mutex); - typs.insert(keys[i].second, typlines); - } }); - } - else - { - for (auto it = moltyps.constBegin(); it != moltyps.constEnd(); ++it) - { - typs.insert(it.key().second, - ::writeMolType(it.key().second, it.value(), examples[it.key()], - uses_parallel)); - } - } - - QStringList keys; - for (const auto &key : moltyps.keys()) - keys.append(key.second); - - if (isSorted) - keys.sort(); - - QStringList lines; - - for (const auto &key : keys) - { - lines += typs[key]; - lines += ""; - } - - return lines; -} - -/** Internal function used to write the system part of the gromacs file */ -static QStringList writeSystem(QString name, const QVector &mol_to_moltype) -{ - QStringList lines; - lines.append("[ system ]"); - lines.append(name); - lines.append(""); - lines.append("[ molecules ]"); - lines.append(";molecule name nr."); - - QString lastmol; - - int count = 0; - - for (auto it = mol_to_moltype.constBegin(); it != mol_to_moltype.constEnd(); ++it) - { - if (*it != lastmol) - { - if (lastmol.isNull()) - { - lastmol = *it; - count = 1; - } - else - { - lines.append(QString("%1 %2").arg(lastmol, 14).arg(count, 6)); - lastmol = *it; - count = 1; - } - } - else - count += 1; - } - - lines.append(QString("%1 %2").arg(lastmol, 14).arg(count, 6)); - - lines.append(""); - - return lines; -} - -/** Function called by the below constructor to sanitise all of the CMAP terms - * that have been loaded into an intermediate state. The aim is to create a - * database of unique CMAP terms, and then make sure that each set of - * atoms that reference those CMAP terms use a consistent set of atom - * types. - */ -static QHash sanitiseCMAPs(QHash &name_to_mtyp, - QMap, GroMolType> &idx_name_to_mtyp) -{ - QHash cmap_potentials; - - // first, go through all of the molecules and extract out all of the - // unique CMAP terms - they are already written in a Gromacs string - // format - QHash unique_cmaps; - - // get a list of pointers to all of the molecule types - QVector moltypes; - - moltypes.reserve(name_to_mtyp.count() + idx_name_to_mtyp.count()); - - for (auto it = name_to_mtyp.begin(); it != name_to_mtyp.end(); ++it) - { - moltypes.append(&it.value()); - } - - for (auto it = idx_name_to_mtyp.begin(); it != idx_name_to_mtyp.end(); ++it) - { - moltypes.append(&it.value()); - } - - // first, create a set of all existing atom types - QSet existing_atom_types; - - for (auto mol : moltypes) - { - if (mol->isPerturbable()) - { - for (const auto &atom : mol->atoms(false)) - { - existing_atom_types.insert(atom.atomType()); - } - - for (const auto &atom : mol->atoms(true)) - { - existing_atom_types.insert(atom.atomType()); - } - } - else - { - for (const auto &atom : mol->atoms()) - { - existing_atom_types.insert(atom.atomType()); - } - } - } - - auto get_atomtype_count = [&](const QString &atm_type, int count) -> QString - { - // convert the count to a letter - // e.g. 0 -> A, 1 -> B, ..., 25 -> Z, 26 -> AA, 27 -> AB, ... - QString suffix = ""; - - while (count >= 0) - { - suffix = QChar('A' + (count % 26)) + suffix; - count = count / 26 - 1; - } - - return atm_type + suffix; - }; - - auto get_new_atomtype = [&](const QString &atm_type, int count) -> QString - { - // make sure there is no "old" atom type that is the same as the new one - while (existing_atom_types.contains(get_atomtype_count(atm_type, count))) - { - count += 1; - } - - return get_atomtype_count(atm_type, count); - }; - - for (auto mol : moltypes) - { - if (mol->isPerturbable()) - { - // do this both for lambda = 0 and lambda = 1 - const auto cmaps0 = mol->cmaps(); - const auto cmaps1 = mol->cmaps(true); - - for (auto it = cmaps0.constBegin(); it != cmaps0.constEnd(); ++it) - { - const auto &atoms = it.key(); - const auto ¶m = it.value(); - CMAPParameter cmap; - - if (not unique_cmaps.contains(param)) - { - cmap = string_to_cmap(param); - unique_cmaps.insert(param, cmap); - } - else - { - cmap = unique_cmaps[param]; - } - - // get the atom types for the atoms in this CMAP - AtomID is AtomIdx - const auto atm0 = mol->atom(atoms.atom0().asA()).atomType(); - const auto atm1 = mol->atom(atoms.atom1().asA()).atomType(); - const auto atm2 = mol->atom(atoms.atom2().asA()).atomType(); - const auto atm3 = mol->atom(atoms.atom3().asA()).atomType(); - const auto atm4 = mol->atom(atoms.atom4().asA()).atomType(); - - // create the key for the combination of these atom types - // and a "1" function type - const auto key = get_cmap_id(atm0, atm1, atm2, atm3, atm4, 1); - - // have we seen this key before? - if (cmap_potentials.contains(key)) - { - // check that we are consistent - if (cmap_potentials[key] != cmap) - { - int count = 0; - auto new_atm_type = get_new_atomtype(atm2, count); - auto new_key = get_cmap_id(atm0, atm1, new_atm_type, atm3, atm4, 1); - - while (cmap_potentials.contains(new_key) and cmap_potentials[new_key] != cmap) - { - count += 1; - new_atm_type = get_new_atomtype(atm2, count); - new_key = get_cmap_id(atm0, atm1, new_atm_type, atm3, atm4, 1); - } - - // we have found a new atom type that can be used for this new CMAP - mol->setAtomType(atoms.atom2().asA(), new_atm_type); - - if (not cmap_potentials.contains(new_key)) - { - cmap_potentials.insert(new_key, cmap); - } - } - } - else - { - // we have not seen this key before, so add it - cmap_potentials.insert(key, cmap); - } - } - - for (auto it = cmaps1.constBegin(); it != cmaps1.constEnd(); ++it) - { - const auto &atoms = it.key(); - const auto ¶m = it.value(); - CMAPParameter cmap; - - if (not unique_cmaps.contains(param)) - { - cmap = string_to_cmap(param); - unique_cmaps.insert(param, cmap); - } - else - { - cmap = unique_cmaps[param]; - } - - // get the atom types for the atoms in this CMAP - AtomID is AtomIdx - const auto atm0 = mol->atom(atoms.atom0().asA(), true).atomType(); - const auto atm1 = mol->atom(atoms.atom1().asA(), true).atomType(); - const auto atm2 = mol->atom(atoms.atom2().asA(), true).atomType(); - const auto atm3 = mol->atom(atoms.atom3().asA(), true).atomType(); - const auto atm4 = mol->atom(atoms.atom4().asA(), true).atomType(); - - // create the key for the combination of these atom types - // and a "1" function type - const auto key = get_cmap_id(atm0, atm1, atm2, atm3, atm4, 1); - - // have we seen this key before? - if (cmap_potentials.contains(key)) - { - // check that we are consistent - if (cmap_potentials[key] != cmap) - { - int count = 0; - auto new_atm_type = get_new_atomtype(atm2, count); - auto new_key = get_cmap_id(atm0, atm1, new_atm_type, atm3, atm4, 1); - - while (cmap_potentials.contains(new_key) and cmap_potentials[new_key] != cmap) - { - count += 1; - new_atm_type = get_new_atomtype(atm2, count); - new_key = get_cmap_id(atm0, atm1, new_atm_type, atm3, atm4, 1); - } - - // we have found a new atom type that can be used for this new CMAP - mol->setAtomType(atoms.atom2().asA(), new_atm_type, true); - - if (not cmap_potentials.contains(new_key)) - { - cmap_potentials.insert(new_key, cmap); - } - } - } - else - { - // we have not seen this key before, so add it - cmap_potentials.insert(key, cmap); - } - } - - mol->sanitiseCMAPs(); - mol->sanitiseCMAPs(true); - } - else - { - const auto cmaps = mol->cmaps(); - - for (auto it = cmaps.constBegin(); it != cmaps.constEnd(); ++it) - { - const auto &atoms = it.key(); - const auto ¶m = it.value(); - CMAPParameter cmap; - - if (not unique_cmaps.contains(param)) - { - cmap = string_to_cmap(param); - unique_cmaps.insert(param, cmap); - } - else - { - cmap = unique_cmaps[param]; - } - - // get the atom types for the atoms in this CMAP - AtomID is AtomIdx - const auto atm0 = mol->atom(atoms.atom0().asA()).atomType(); - const auto atm1 = mol->atom(atoms.atom1().asA()).atomType(); - const auto atm2 = mol->atom(atoms.atom2().asA()).atomType(); - const auto atm3 = mol->atom(atoms.atom3().asA()).atomType(); - const auto atm4 = mol->atom(atoms.atom4().asA()).atomType(); - - // create the key for the combination of these atom types - // and a "1" function type - const auto key = get_cmap_id(atm0, atm1, atm2, atm3, atm4, 1); - - // have we seen this key before? - if (cmap_potentials.contains(key)) - { - // check that we are consistent - if (cmap_potentials[key] != cmap) - { - int count = 0; - auto new_atm_type = get_new_atomtype(atm2, count); - auto new_key = get_cmap_id(atm0, atm1, new_atm_type, atm3, atm4, 1); - - while (cmap_potentials.contains(new_key) and cmap_potentials[new_key] != cmap) - { - count += 1; - new_atm_type = get_new_atomtype(atm2, count); - new_key = get_cmap_id(atm0, atm1, new_atm_type, atm3, atm4, 1); - } - - // we have found a new atom type that can be used for this new CMAP - mol->setAtomType(atoms.atom2().asA(), new_atm_type); - - if (not cmap_potentials.contains(new_key)) - { - cmap_potentials.insert(new_key, cmap); - } - } - } - else - { - // we have not seen this key before, so add it - cmap_potentials.insert(key, cmap); - } - } - - mol->sanitiseCMAPs(); - } - } - - return cmap_potentials; -} - -/** Construct this parser by extracting all necessary information from the - passed SireSystem::System, looking for the properties that are specified - in the passed property map */ -GroTop::GroTop(const SireSystem::System &system, const PropertyMap &map) - : ConcreteProperty(map), nb_func_type(0), combining_rule(0), fudge_lj(0), fudge_qq(0), - generate_pairs(false) -{ - // get the MolNums of each molecule in the System - this returns the - // numbers in MolIdx order - const QVector molnums = system.getMoleculeNumbers().toVector(); - - if (molnums.isEmpty()) - { - // no molecules in the system - this->operator=(GroTop()); - return; - } - - bool isSorted = true; - if (map["sort"].hasValue()) - { - isSorted = map["sort"].value().asA().value(); - } - - // Search for waters and crystal waters. The user can speficy the residue name - // for crystal waters using the "crystal_water" property in the map. If the user - // wishes to preserve a custom water topology naming, then they can use "skip_water". - SelectResult waters; - SelectResult xtal_waters; - if (map.specified("crystal_water")) - { - auto xtal_water_resname = map["crystal_water"].source(); - xtal_waters = system.search(QString("resname %1").arg(xtal_water_resname)); - - if (not map.specified("skip_water")) - { - waters = - system.search( - QString("(not mols with property is_non_searchable_water) and (water and not resname %1") - .arg(xtal_water_resname)); - } - } - else - { - waters = system.search("(not mols with property is_non_searchable_water) and water"); - } - - // Extract the molecule numbers of the water molecules. - auto water_nums = waters.molNums(); - - // Extract the molecule numbers of the crystal water molecules. - auto xtal_water_nums = xtal_waters.molNums(); - - // Loop over the molecules to find the non-water molecules. - QList non_water_nums; - for (const auto &num : molnums) - { - if (not water_nums.contains(num) and not xtal_water_nums.contains(num)) - non_water_nums.append(num); - } - - // Create a hash between MolNum and index in the system. - QHash molnum_to_idx; - - for (int i = 0; i < molnums.count(); ++i) - { - molnum_to_idx.insert(molnums[i], i); - } - - // Initialise data structures to map molecules to their respective - // GroMolTypes. - QVector mol_to_moltype(molnums.count()); - QMap, GroMolType> idx_name_to_mtyp; - QMap, Molecule> idx_name_to_example; - QHash name_to_mtyp; - - // First add the non-water molecules. - for (int i = 0; i < non_water_nums.count(); ++i) - { - // Extract the molecule number of the molecule and work out - // the index in the system. - auto molnum = non_water_nums[i]; - auto idx = molnum_to_idx[molnum]; - - // Generate a GroMolType type for this molecule and get its name. - auto moltype = GroMolType(system[molnum].molecule(), map); - auto name = moltype.name(); - - // We have already recorded this name. - if (name_to_mtyp.contains(name)) - { - if (moltype != name_to_mtyp[name]) - { - // This has the same name but different details. Give this a new name. - int j = 0; - - while (true) - { - j++; - name = QString("%1_%2").arg(moltype.name()).arg(j); - - if (name_to_mtyp.contains(name)) - { - if (moltype == name_to_mtyp[name]) - // Match :-) - break; - } - else - { - // New moltype. - idx_name_to_mtyp.insert(QPair(idx, name), moltype); - name_to_mtyp.insert(name, moltype); - - // save an example of this molecule so that we can - // extract any other details necessary - idx_name_to_example.insert(QPair(idx, name), system[molnum].molecule()); - - break; - } - - // We have got here, meaning that we need to try a different name. - } - } - } - // Name not previously recorded. - else - { - name_to_mtyp.insert(name, moltype); - idx_name_to_mtyp.insert(QPair(idx, moltype.name()), moltype); - idx_name_to_example.insert(QPair(idx, name), system[molnum].molecule()); - } - - // Store the name of the molecule type. - mol_to_moltype[idx] = name; - } - - // Now deal with the water molecules. - if (waters.count() > 0) - { - // Extract the GroMolType of the first water molecule. - auto water_type = GroMolType(system[water_nums[0]].molecule(), map); - auto name = water_type.name(); - auto molnum = water_nums[0]; - auto idx = molnum_to_idx[molnum]; - - // Populate the mappings. - name_to_mtyp.insert(name, water_type); - idx_name_to_mtyp.insert(QPair(idx, water_type.name()), water_type); - idx_name_to_example.insert(QPair(idx, name), system[molnum].molecule()); - - for (int i = 0; i < water_nums.count(); ++i) - { - // Extract the molecule number of the molecule and work out - // the index in the system. - auto molnum = water_nums[i]; - auto idx = molnum_to_idx[molnum]; - - // Store the name of the molecule type. - mol_to_moltype[idx] = name; - } - } - - // Now add the crystal waters. - if (xtal_waters.count() > 0) - { - // Extract the GroMolType of the first water molecule. - auto water_type = GroMolType(system[xtal_water_nums[0]].molecule(), map); - auto name = water_type.name(); - auto molnum = xtal_water_nums[0]; - auto idx = molnum_to_idx[molnum]; - - // Populate the mappings. - name_to_mtyp.insert(name, water_type); - idx_name_to_mtyp.insert(QPair(idx, water_type.name()), water_type); - idx_name_to_example.insert(QPair(idx, name), system[molnum].molecule()); - - for (int i = 0; i < xtal_water_nums.count(); ++i) - { - // Extract the molecule number of the molecule and work out - // the index in the system. - auto molnum = xtal_water_nums[i]; - auto idx = molnum_to_idx[molnum]; - - // Store the name of the molecule type. - mol_to_moltype[idx] = name; - } - } - - QStringList errors; - - // first, we need to extract the common forcefield from the molecules - MMDetail ffield = idx_name_to_mtyp.constBegin()->forcefield(); - - for (auto it = idx_name_to_mtyp.constBegin(); it != idx_name_to_mtyp.constEnd(); ++it) - { - if (not ffield.isCompatibleWith(it.value().forcefield())) - { - errors.append(QObject::tr("The forcefield for molecule '%1' is not " - "compatible with that for other molecules.\n%1 versus\n%2") - .arg(it.key().second) - .arg(it.value().forcefield().toString()) - .arg(ffield.toString())); - } - } - - if (not errors.isEmpty()) - { - throw SireError::incompatible_error( - QObject::tr("Cannot write this system to a Gromacs Top file as the forcefields of the " - "molecules are incompatible with one another.\n%1") - .arg(errors.join("\n\n")), - CODELOC); - } - - // first, we need to de-deduplicate and sanitise all of the CMAP terms - cmap_potentials = sanitiseCMAPs(name_to_mtyp, idx_name_to_mtyp); - - // next, we need to write the defaults section of the file - QStringList lines = ::writeDefaults(ffield); - - // next, we need to extract and write all of the atom types from all of - // the molecules - lines += ::writeAtomTypes(idx_name_to_mtyp, cmap_potentials, idx_name_to_example, ffield, map); - - lines += ::writeCMAPTypes(cmap_potentials); - - lines += ::writeMolTypes(idx_name_to_mtyp, idx_name_to_example, usesParallel(), - isSorted); - - // now write the system part - lines += ::writeSystem(system.name(), mol_to_moltype); - - if (not errors.isEmpty()) - { - throw SireIO::parse_error( - QObject::tr("Errors converting the system to a Gromacs Top format...\n%1").arg(lines.join("\n")), CODELOC); - } - - // we don't need params any more, so free the memory - idx_name_to_mtyp.clear(); - idx_name_to_example.clear(); - mol_to_moltype.clear(); - - // now we have the lines, reparse them to make sure that they are correct - // and we have a fully-constructed and sane GroTop object - GroTop parsed(lines, map); - - this->operator=(parsed); -} - -/** Copy constructor */ -GroTop::GroTop(const GroTop &other) - : ConcreteProperty(other), include_path(other.include_path), - included_files(other.included_files), expanded_lines(other.expanded_lines), atom_types(other.atom_types), - bond_potentials(other.bond_potentials), ang_potentials(other.ang_potentials), - dih_potentials(other.dih_potentials), cmap_potentials(other.cmap_potentials), - moltypes(other.moltypes), grosys(other.grosys), - nb_func_type(other.nb_func_type), combining_rule(other.combining_rule), fudge_lj(other.fudge_lj), - fudge_qq(other.fudge_qq), parse_warnings(other.parse_warnings), generate_pairs(other.generate_pairs) -{ -} - -/** Destructor */ -GroTop::~GroTop() -{ -} - -/** Copy assignment operator */ -GroTop &GroTop::operator=(const GroTop &other) -{ - if (this != &other) - { - include_path = other.include_path; - included_files = other.included_files; - expanded_lines = other.expanded_lines; - atom_types = other.atom_types; - bond_potentials = other.bond_potentials; - ang_potentials = other.ang_potentials; - dih_potentials = other.dih_potentials; - cmap_potentials = other.cmap_potentials; - moltypes = other.moltypes; - grosys = other.grosys; - nb_func_type = other.nb_func_type; - combining_rule = other.combining_rule; - fudge_lj = other.fudge_lj; - fudge_qq = other.fudge_qq; - parse_warnings = other.parse_warnings; - generate_pairs = other.generate_pairs; - MoleculeParser::operator=(other); - } - - return *this; -} - -/** Comparison operator */ -bool GroTop::operator==(const GroTop &other) const -{ - return include_path == other.include_path and included_files == other.included_files and - expanded_lines == other.expanded_lines and MoleculeParser::operator==(other); -} - -/** Comparison operator */ -bool GroTop::operator!=(const GroTop &other) const -{ - return not operator==(other); -} - -/** Return the C++ name for this class */ -const char *GroTop::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); -} - -/** Return the C++ name for this class */ -const char *GroTop::what() const -{ - return GroTop::typeName(); -} - -bool GroTop::isTopology() const -{ - return true; -} - -/** Return the list of names of directories in which to search for - include files. The directories are either absolute, or relative - to the current directory. If "absolute_paths" is true then - the full absolute paths for directories that exist on this - machine will be returned */ -QStringList GroTop::includePath(bool absolute_paths) const -{ - if (absolute_paths) - { - QStringList abspaths; - - for (const auto &path : include_path) - { - QFileInfo file(path); - - if (file.exists()) - abspaths.append(file.absoluteFilePath()); - } - - return abspaths; - } - else - return include_path; -} - -/** Return the list of names of files that were included when reading or - writing this file. The files are relative. If "absolute_paths" - is true then the full absolute paths for the files will be - used */ -QStringList GroTop::includedFiles(bool absolute_paths) const -{ - // first, go through the list of included files - QStringList files; - - for (auto it = included_files.constBegin(); it != included_files.constEnd(); ++it) - { - files += it.value(); - } - - if (absolute_paths) - { - // these are already absolute filenames - return files; - } - else - { - // subtract any paths that relate to the current directory or GROMACS_PATH - QString curpath = QDir::current().absolutePath(); - - for (auto it = files.begin(); it != files.end(); ++it) - { - if (it->startsWith(curpath)) - { - *it = it->mid(curpath.length() + 1); - } - else - { - for (const auto &path : include_path) - { - if (it->startsWith(path)) - { - *it = it->mid(path.length() + 1); - } - } - } - } - - return files; - } -} - -/** Return the parser that has been constructed by reading in the passed - file using the passed properties */ -MoleculeParserPtr GroTop::construct(const QString &filename, const PropertyMap &map) const -{ - return GroTop(filename, map); -} - -/** Return the parser that has been constructed by reading in the passed - text lines using the passed properties */ -MoleculeParserPtr GroTop::construct(const QStringList &lines, const PropertyMap &map) const -{ - return GroTop(lines, map); -} - -/** Return the parser that has been constructed by extract all necessary - data from the passed SireSystem::System using the specified properties */ -MoleculeParserPtr GroTop::construct(const SireSystem::System &system, const PropertyMap &map) const -{ - return GroTop(system, map); -} - -/** Return a string representation of this parser */ -QString GroTop::toString() const -{ - return QObject::tr("GroTop( includePath() = [%1], includedFiles() = [%2] )") - .arg(includePath().join(", ")) - .arg(includedFiles().join(", ")); -} - -/** Return the format name that is used to identify this file format within Sire */ -QString GroTop::formatName() const -{ - return "GROTOP"; -} - -/** Return a description of the file format */ -QString GroTop::formatDescription() const -{ - return QObject::tr("Gromacs Topology format files."); -} - -/** Return the suffixes that these files are normally associated with */ -QStringList GroTop::formatSuffix() const -{ - static const QStringList suffixes = {"top", "grotop", "gtop"}; - return suffixes; -} - -/** Function that is called to assert that this object is sane. This - should raise an exception if the parser is in an invalid state */ -void GroTop::assertSane() const -{ - // check state, raise SireError::program_bug if we are in an invalid state -} - -/** Return the atom type data for the passed atom type. This returns - null data if it is not present */ -GromacsAtomType GroTop::atomType(const QString &atm) const -{ - return atom_types.value(atm, GromacsAtomType()); -} - -/** Return the ID string for the bond atom types 'atm0' 'atm1'. This - creates the string 'atm0;atm1' or 'atm1;atm0' depending on which - of the atoms is lower. The ';' character is used as a separator - as it cannot be in the atom names, as it is used as a comment - character in the Gromacs Top file */ -static QString get_bond_id(const QString &atm0, const QString &atm1, int func_type) -{ - if (func_type == 0) // default type - func_type = 1; - - if (atm0 < atm1) - { - return QString("%1;%2;%3").arg(atm0, atm1).arg(func_type); - } - else - { - return QString("%1;%2;%3").arg(atm1, atm0).arg(func_type); - } -} - -/** Return the ID string for the angle atom types 'atm0' 'atm1' 'atm2'. This - creates the string 'atm0;atm1;atm2' or 'atm2;atm1;atm0' depending on which - of the atoms is lower. The ';' character is used as a separator - as it cannot be in the atom names, as it is used as a comment - character in the Gromacs Top file */ -static QString get_angle_id(const QString &atm0, const QString &atm1, const QString &atm2, int func_type) -{ - if (func_type == 0) - func_type = 1; // default type - - if (atm0 < atm2) - { - return QString("%1;%2;%3;%4").arg(atm0, atm1, atm2).arg(func_type); - } - else - { - return QString("%1;%2;%3;%4").arg(atm2, atm1, atm0).arg(func_type); - } -} - -/** Return the ID string for the dihedral atom types 'atm0' 'atm1' 'atm2' 'atm3'. This - creates the string 'atm0;atm1;atm2;atm3' or 'atm3;atm2;atm1;atm0' depending on which - of the atoms is lower. The ';' character is used as a separator - as it cannot be in the atom names, as it is used as a comment - character in the Gromacs Top file */ -static QString get_dihedral_id(const QString &atm0, const QString &atm1, const QString &atm2, const QString &atm3, - int func_type) -{ - if ((atm0 < atm3) or (atm0 == atm3 and atm1 <= atm2)) - { - return QString("%1;%2;%3;%4;%5").arg(atm0, atm1, atm2, atm3).arg(func_type); - } - else - { - return QString("%1;%2;%3;%4;%5").arg(atm3, atm2, atm1, atm0).arg(func_type); - } -} - -/** Return the Gromacs System that describes the list of molecules that should - be contained */ -GroSystem GroTop::groSystem() const -{ - return grosys; -} - -/** Return the bond potential data for the passed pair of atoms. This only returns - the most recently inserted parameter for this pair. Use 'bonds' if you want - to allow for multiple return values */ -GromacsBond GroTop::bond(const QString &atm0, const QString &atm1, int func_type) const -{ - return bond_potentials.value(get_bond_id(atm0, atm1, func_type), GromacsBond()); -} - -/** Return the bond potential data for the passed pair of atoms. This returns - a list of all associated parameters */ -QList GroTop::bonds(const QString &atm0, const QString &atm1, int func_type) const -{ - return bond_potentials.values(get_bond_id(atm0, atm1, func_type)); -} - -/** Return the angle potential data for the passed triple of atoms. This only returns - the most recently inserted parameter for these atoms. Use 'angles' if you want - to allow for multiple return values */ -GromacsAngle GroTop::angle(const QString &atm0, const QString &atm1, const QString &atm2, int func_type) const -{ - return ang_potentials.value(get_angle_id(atm0, atm1, atm2, func_type), GromacsAngle()); -} - -/** Return the angle potential data for the passed triple of atoms. This returns - a list of all associated parameters */ -QList GroTop::angles(const QString &atm0, const QString &atm1, const QString &atm2, int func_type) const -{ - return ang_potentials.values(get_angle_id(atm0, atm1, atm2, func_type)); -} - -/** Search for a dihedral type parameter that matches the atom types - atom0-atom1-atom2-atom3. This will try to find an exact match. If that fails, - it will then use one of the wildcard matches. Returns a null string if there - is no match. This will return the key into the dih_potentials dictionary */ -QString GroTop::searchForDihType(const QString &atm0, const QString &atm1, const QString &atm2, const QString &atm3, - int func_type) const -{ - QString key = get_dihedral_id(atm0, atm1, atm2, atm3, func_type); - - // qDebug() << "SEARCHING FOR" << key; - - if (dih_potentials.contains(key)) - { - // qDebug() << "FOUND" << key; - return key; - } - - static const QString wild = "X"; - - // look for *-atm1-atm2-atm3 - key = get_dihedral_id(wild, atm1, atm2, atm3, func_type); - - if (dih_potentials.contains(key)) - { - // qDebug() << "FOUND" << key; - return key; - } - - // look for *-atm2-atm1-atm0 - key = get_dihedral_id(wild, atm2, atm1, atm0, func_type); - - if (dih_potentials.contains(key)) - { - // qDebug() << "FOUND" << key; - return key; - } - - // this failed. Look for *-atm1-atm2-* or *-atm2-atm1-* - key = get_dihedral_id(wild, atm1, atm2, wild, func_type); - - if (dih_potentials.contains(key)) - { - // qDebug() << "FOUND" << key; - return key; - } - - key = get_dihedral_id(wild, atm2, atm1, wild, func_type); - - if (dih_potentials.contains(key)) - { - // qDebug() << "FOUND" << key; - return key; - } - - // look for *-*-atm2-atm3 - key = get_dihedral_id(wild, wild, atm2, atm3, func_type); - - if (dih_potentials.contains(key)) - { - // qDebug() << "FOUND" << key; - return key; - } - - // look for *-*-atm1-atm0 - key = get_dihedral_id(wild, wild, atm1, atm0, func_type); - - if (dih_potentials.contains(key)) - { - // qDebug() << "FOUND" << key; - return key; - } - - // look for atm0-*-*-atm3 or atm3-*-*-atm0 - key = get_dihedral_id(atm0, wild, wild, atm3, func_type); - - if (dih_potentials.contains(key)) - { - return key; - } + bondlines.append(QString("%1 %2 %3 %4 %5") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(params1[i].functionType(), 6) + .arg(param_string0.join(" ")) + .arg(param_string1.join(" "))); + } + + // Now add parameters for which there is no matching record + // at lambda = 0. + for (int i = params0.count(); i < params1.count(); ++i) { + QStringList param_string; + for (const auto &p : params1[i].parameters()) + param_string.append(QString::number(p)); + + bondlines.append(QString("%1 %2 %3 0.0000 0.0000 %4") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(params1[i].functionType(), 6) + .arg(param_string.join(" "))); + } + } + } + } else { + // Get the bonds from the molecule. + const auto &bonds = moltype.bonds(); + + for (auto it = bonds.constBegin(); it != bonds.constEnd(); ++it) { + const auto &bond = it.key(); + const auto ¶m = it.value(); + + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = bond.atom0().asA().value() + 1; + int atom1 = bond.atom1().asA().value() + 1; + + QStringList params; + for (const auto &p : param.parameters()) + params.append(QString::number(p)); + + bondlines.append(QString("%1 %2 %3 %4") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(param.functionType(), 6) + .arg(params.join(" "))); + } + } + + std::sort(bondlines.begin(), bondlines.end()); + }; + + // write all of the angles + auto write_angs = [&]() { + if (is_perturbable) { + // Get the angles from the molecule. + const auto &angles0 = moltype.angles(); + const auto &angles1 = moltype.angles(true); + + // Sets to contain the AngleIDs at lambda = 0 and lambda = 1. + QSet angles0_idx; + QSet angles1_idx; + + // Loop over all angles at lambda = 0. + for (const auto &idx : angles0.uniqueKeys()) + angles0_idx.insert(idx); + + // Loop over all angles at lambda = 1. + for (const auto &idx : angles1.uniqueKeys()) { + if (angles0_idx.contains(idx.mirror())) + angles1_idx.insert(idx.mirror()); + else + angles1_idx.insert(idx); + } + + // Now work out the AngleIDs that are unique at lambda = 0 and lambda = 1, + // as well as those that are shared. + QSet angles0_uniq_idx; + QSet angles1_uniq_idx; + QSet angles_shared_idx; + + // lambda = 0 + for (const auto &idx : angles0_idx) { + if (not angles1_idx.contains(idx)) + angles0_uniq_idx.insert(idx); + else + angles_shared_idx.insert(idx); + } - // finally look for *-*-*-* - key = get_dihedral_id(wild, wild, wild, wild, func_type); + // lambda = 1 + for (const auto &idx : angles1_idx) { + if (not angles0_idx.contains(idx)) + angles1_uniq_idx.insert(idx); + else + angles_shared_idx.insert(idx); + } + + // First create parameter records for the angles unique to lambda = 0/1. + + // lambda = 0 + for (const auto &idx : angles0_uniq_idx) { + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = idx.atom0().asA().value() + 1; + int atom1 = idx.atom1().asA().value() + 1; + int atom2 = idx.atom2().asA().value() + 1; + + // Get all of the parameters for this AngleID. + const auto ¶ms = angles0.values(idx); + + // Loop over all of the parameters. + for (const auto ¶m : params) { + QStringList param_string; + for (const auto &p : param.parameters()) + param_string.append(QString::number(p)); + + anglines.append(QString("%1 %2 %3 %4 %5 0.0000 0.0000") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(param.functionType(), 7) + .arg(param_string.join(" "))); + } + } + + // lambda = 1 + for (const auto &idx : angles1_uniq_idx) { + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = idx.atom0().asA().value() + 1; + int atom1 = idx.atom1().asA().value() + 1; + int atom2 = idx.atom2().asA().value() + 1; + + // Get all of the parameters for this AngleID. + const auto ¶ms = angles1.values(idx); + + // Loop over all of the parameters. + for (const auto ¶m : params) { + QStringList param_string; + for (const auto &p : param.parameters()) + param_string.append(QString::number(p)); + + anglines.append(QString("%1 %2 %3 %4 0.0000 0.0000 %5") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(param.functionType(), 7) + .arg(param_string.join(" "))); + } + } + + // Next add the shared angle parameters. + + for (auto idx : angles_shared_idx) { + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = idx.atom0().asA().value() + 1; + int atom1 = idx.atom1().asA().value() + 1; + int atom2 = idx.atom2().asA().value() + 1; + + // Get a list of the parameters at lambda = 0. + const auto ¶ms0 = angles0.values(idx); + + // Invert the index. + if (not angles1.contains(idx)) + idx = idx.mirror(); + + // Get a list of the parameters at lambda = 1. + const auto ¶ms1 = angles1.values(idx); + + // More or same number of records at lambda = 0. + if (params0.count() >= params1.count()) { + for (int i = 0; i < params1.count(); ++i) { + QStringList param_string0; + for (const auto &p : params0[i].parameters()) + param_string0.append(QString::number(p)); + + QStringList param_string1; + for (const auto &p : params1[i].parameters()) + param_string1.append(QString::number(p)); + + anglines.append(QString("%1 %2 %3 %4 %5 %6") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(params0[i].functionType(), 7) + .arg(param_string0.join(" ")) + .arg(param_string1.join(" "))); + } + + // Now add parameters for which there is no matching record + // at lambda = 1. + for (int i = params1.count(); i < params0.count(); ++i) { + QStringList param_string; + for (const auto &p : params0[i].parameters()) + param_string.append(QString::number(p)); + + anglines.append(QString("%1 %2 %3 %4 %5 0.0000 0.0000") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(params0[i].functionType(), 7) + .arg(param_string.join(" "))); + } + } + + // More records at lambda = 1. + else { + for (int i = 0; i < params0.count(); ++i) { + QStringList param_string0; + for (const auto &p : params0[i].parameters()) + param_string0.append(QString::number(p)); + + QStringList param_string1; + for (const auto &p : params1[i].parameters()) + param_string1.append(QString::number(p)); + + anglines.append(QString("%1 %2 %3 %4 %5 %6") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(params1[i].functionType(), 7) + .arg(param_string0.join(" ")) + .arg(param_string1.join(" "))); + } + + // Now add parameters for which there is no matching record + // at lambda = 0. + for (int i = params0.count(); i < params1.count(); ++i) { + QStringList param_string; + for (const auto &p : params1[i].parameters()) + param_string.append(QString::number(p)); + + anglines.append(QString("%1 %2 %3 %4 0.0000 0.0000 %5") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(params1[i].functionType(), 7) + .arg(param_string.join(" "))); + } + } + } + } else { + // Get the angles from the molecule. + const auto &angles = moltype.angles(); + + for (auto it = angles.constBegin(); it != angles.constEnd(); ++it) { + const auto &angle = it.key(); + const auto ¶m = it.value(); + + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = angle.atom0().asA().value() + 1; + int atom1 = angle.atom1().asA().value() + 1; + int atom2 = angle.atom2().asA().value() + 1; + + QStringList params; + for (const auto &p : param.parameters()) + params.append(QString::number(p)); + + anglines.append(QString("%1 %2 %3 %4 %5") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(param.functionType(), 7) + .arg(params.join(" "))); + } + } + + std::sort(anglines.begin(), anglines.end()); + }; + + // write all of the dihedrals/impropers (they are merged) + auto write_dihs = [&]() { + if (is_perturbable) { + // Get the dihedrals from the molecule. + const auto &dihedrals0 = moltype.dihedrals(); + const auto &dihedrals1 = moltype.dihedrals(true); + + // Sets to contain the DihedralID at lambda = 0 and lambda = 1. + QSet dihedrals0_idx; + QSet dihedrals1_idx; + + // Loop over all dihedrals at lambda = 0. + for (const auto &idx : dihedrals0.uniqueKeys()) + dihedrals0_idx.insert(idx); + + // Loop over all dihedrals at lambda = 1. + for (const auto &idx : dihedrals1.uniqueKeys()) { + if (dihedrals0_idx.contains(idx.mirror())) + dihedrals1_idx.insert(idx.mirror()); + else + dihedrals1_idx.insert(idx); + } + + // Now work out the DihedralIDs that are unique at lambda = 0 and lambda = + // 1, as well as those that are shared. + QSet dihedrals0_uniq_idx; + QSet dihedrals1_uniq_idx; + QSet dihedrals_shared_idx; + + // lambda = 0 + for (const auto &idx : dihedrals0_idx) { + if (not dihedrals1_idx.contains(idx)) + dihedrals0_uniq_idx.insert(idx); + else + dihedrals_shared_idx.insert(idx); + } - if (dih_potentials.contains(key)) - { - // qDebug() << "FOUND" << key; - return key; + // lambda = 1 + for (const auto &idx : dihedrals1_idx) { + if (not dihedrals0_idx.contains(idx)) + dihedrals1_uniq_idx.insert(idx); + else + dihedrals_shared_idx.insert(idx); + } + + // First create parameter records for the dihedrals unique to lambda = + // 0/1. + + // lambda = 0 + for (const auto &idx : dihedrals0_uniq_idx) { + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = idx.atom0().asA().value() + 1; + int atom1 = idx.atom1().asA().value() + 1; + int atom2 = idx.atom2().asA().value() + 1; + int atom3 = idx.atom3().asA().value() + 1; + + // Get all of the parameters for this DihedralID. + const auto ¶ms = dihedrals0.values(idx); + + // Loop over all of the parameters. + for (const auto ¶m : params) { + QStringList param_string; + for (const auto &p : param.parameters()) + param_string.append(QString::number(p)); + + // Get the periodicity of the dihedral term. This is the last + // parameter entry. + auto periodicity = param.parameters().last(); + + dihlines.append(QString("%1 %2 %3 %4 %5 %6 0 0.0000 %7") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(atom3, 6) + .arg(param.functionType(), 6) + .arg(param_string.join(" ")) + .arg(periodicity)); + } + } + + // lambda = 1 + for (const auto &idx : dihedrals1_uniq_idx) { + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = idx.atom0().asA().value() + 1; + int atom1 = idx.atom1().asA().value() + 1; + int atom2 = idx.atom2().asA().value() + 1; + int atom3 = idx.atom3().asA().value() + 1; + + // Get all of the parameters for this AngleID. + const auto ¶ms = dihedrals1.values(idx); + + // Loop over all of the parameters. + for (const auto ¶m : params) { + QStringList param_string; + for (const auto &p : param.parameters()) + param_string.append(QString::number(p)); + + // Get the periodicity of the dihedral term. This is the last + // parameter entry. + auto periodicity = param.parameters().last(); + + dihlines.append(QString("%1 %2 %3 %4 %5 0 0.0000 %6 %7") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(atom3, 6) + .arg(param.functionType(), 6) + .arg(periodicity) + .arg(param_string.join(" "))); + } + } + + // Next add the shared dihedral parameters. + + for (auto idx : dihedrals_shared_idx) { + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = idx.atom0().asA().value() + 1; + int atom1 = idx.atom1().asA().value() + 1; + int atom2 = idx.atom2().asA().value() + 1; + int atom3 = idx.atom3().asA().value() + 1; + + // Get a list of the parameters at lambda = 0. + const auto ¶ms0 = dihedrals0.values(idx); + + // Invert the index. + if (not dihedrals1.contains(idx)) + idx = idx.mirror(); + + // Get a list of the parameters at lambda = 1. + const auto ¶ms1 = dihedrals1.values(idx); + + // Create two hashes between the periodicity of each dihedral + // term and its corresponding parameters. + + // The maximum periodicity recorded. + int max_per = 0; + + // lambda = 0 + QHash params0_hash; + for (const auto ¶m : params0) { + // Extract the periodicity and update the hash. + int periodicity = int(param.parameters().last()); + params0_hash.insert(periodicity, param); + + // If necessary, update the maximum periodicity. + if (periodicity > max_per) + max_per = periodicity; + } + + // lambda = 1 + QHash params1_hash; + for (const auto ¶m : params1) { + // Extract the periodicity and update the hash. + int periodicity = int(param.parameters().last()); + params1_hash.insert(periodicity, param); + + // If necessary, update the maximum periodicity. + if (periodicity > max_per) + max_per = periodicity; + } + + // Loop over the range of dihedral periodicities observed. + for (int i = 0; i <= max_per; ++i) { + // There is a term at lambda = 0 with this periodicity. + if (params0_hash.contains(i)) { + QStringList param_string0; + QStringList param_string1; + for (const auto &p : params0_hash[i].parameters()) + param_string0.append(QString::number(p)); + + // There is a term at lambda = 1 with this periodicity. + if (params1_hash.contains(i)) { + for (const auto &p : params1_hash[i].parameters()) + param_string1.append(QString::number(p)); + } + // No term, create a zero term with the same periodicity. + else { + param_string1.append(QString::number(0)); + param_string1.append(QString::number(0.0000)); + param_string1.append(QString::number(i)); + } + + // Append the dihedral term. + dihlines.append(QString("%1 %2 %3 %4 %5 %6 %7") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(atom3, 6) + .arg(params0_hash[i].functionType(), 6) + .arg(param_string0.join(" ")) + .arg(param_string1.join(" "))); + } else { + // There is a term at lambda = 1 with this periodicity. + if (params1_hash.contains(i)) { + QStringList param_string0; + QStringList param_string1; + + // No lambda = 0 term, create a zero term with the same + // periodicity. + param_string0.append(QString::number(0)); + param_string0.append(QString::number(0.0000)); + param_string0.append(QString::number(i)); + + for (const auto &p : params1_hash[i].parameters()) + param_string1.append(QString::number(p)); + + // Append the dihedral term. + dihlines.append(QString("%1 %2 %3 %4 %5 %6 %7") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(atom3, 6) + .arg(params1_hash[i].functionType(), 6) + .arg(param_string0.join(" ")) + .arg(param_string1.join(" "))); + } + } + } + } + } else { + // Get the dihedrals from the molecule. + const auto &dihedrals = moltype.dihedrals(); + + for (auto it = dihedrals.constBegin(); it != dihedrals.constEnd(); ++it) { + const auto &dihedral = it.key(); + const auto ¶m = it.value(); + + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = dihedral.atom0().asA().value() + 1; + int atom1 = dihedral.atom1().asA().value() + 1; + int atom2 = dihedral.atom2().asA().value() + 1; + int atom3 = dihedral.atom3().asA().value() + 1; + + QStringList params; + for (const auto &p : param.parameters()) + params.append(QString::number(p)); + + dihlines.append(QString("%1 %2 %3 %4 %5 %6") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(atom3, 6) + .arg(param.functionType(), 6) + .arg(params.join(" "))); + } + } + + std::sort(dihlines.begin(), dihlines.end()); + }; + + // write all of the cmaps + auto write_cmaps = [&]() { + if (is_perturbable) { + const auto cmaps0 = moltype.cmaps(); + const auto cmaps1 = moltype.cmaps(true); + + if (cmaps0 != cmaps1) { + throw SireError::unsupported( + QObject::tr("The molecule '%1' has different CMAP parameters at " + "lambda = 0 and lambda = 1. " + "This is not supported yet by the Sire parser!") + .arg(moltype.name()), + CODELOC); + } } - return QString(); -} + const auto cmaps = moltype.cmaps(); -/** Return the dihedral potential data for the passed quad of atoms. This only returns - the most recently inserted parameter for these atoms. Use 'dihedrals' if you want - to allow for multiple return values */ -GromacsDihedral GroTop::dihedral(const QString &atm0, const QString &atm1, const QString &atm2, const QString &atm3, - int func_type) const -{ - return dih_potentials.value(searchForDihType(atm0, atm1, atm2, atm3, func_type), GromacsDihedral()); -} + for (auto it = cmaps.constBegin(); it != cmaps.constEnd(); ++it) { + const auto &cmap = it.key(); + const auto ¶m = it.value(); -/** Return the dihedral potential data for the passed quad of atoms. This returns - a list of all associated parameters */ -QList GroTop::dihedrals(const QString &atm0, const QString &atm1, const QString &atm2, - const QString &atm3, int func_type) const -{ - return dih_potentials.values(searchForDihType(atm0, atm1, atm2, atm3, func_type)); -} + // AtomID is AtomIdx. Add 1, as gromacs is 1-indexed + int atom0 = cmap.atom0().asA().value() + 1; + int atom1 = cmap.atom1().asA().value() + 1; + int atom2 = cmap.atom2().asA().value() + 1; + int atom3 = cmap.atom3().asA().value() + 1; + int atom4 = cmap.atom4().asA().value() + 1; -/** Return all of the CMAP potentials for the passed quint of atom types, for the - * passed function type. This returns a list of all associated parameters - * (or an empty list if none exist) */ -QList GroTop::cmaps(const QString &atm0, const QString &atm1, const QString &atm2, - const QString &atm3, const QString &atm4, int func_type) const -{ - // get the key for this cmap - QString key = get_cmap_id(atm0, atm1, atm2, atm3, atm4, func_type); + bool ok; + int function_type = param.toInt(&ok); - auto it = cmap_potentials.find(key); + if (not ok) { + throw SireError::program_bug( + QObject::tr( + "The CMAP parameter '%2' for %1 is not a valid integer. " + "This is a bug in Sire, please report it.") + .arg(cmap.toString()) + .arg(param), + CODELOC); + } - if (it == cmap_potentials.end()) - { - // no cmap found - return QList(); - } - else - { - // return the cmap - QList cmaps; - cmaps.append(it.value()); - return cmaps; + // format is the index of each atom, plus the function type, + cmaplines.append(QString("%1 %2 %3 %4 %5 %6") + .arg(atom0, 6) + .arg(atom1, 6) + .arg(atom2, 6) + .arg(atom3, 6) + .arg(atom4, 6) + .arg(function_type, 6)); } -} -/** Return the atom types loaded from this file */ -QHash GroTop::atomTypes() const -{ - return atom_types; -} + std::sort(cmaplines.begin(), cmaplines.end()); + }; -/** Return the bond potentials loaded from this file */ -QMultiHash GroTop::bondPotentials() const -{ - return bond_potentials; -} + // write all of the pairs (1-4 scaling factors). This is needed even though + // we have set autogenerate pairs to "yes" + auto write_pairs = [&]() { + // Store the molinfo object; + const auto molinfo = mol.info(); -/** Return the angle potentials loaded from this file */ -QMultiHash GroTop::anglePotentials() const -{ - return ang_potentials; -} + if (is_perturbable) { + CLJNBPairs scl0; + CLJNBPairs scl1; -/** Return the dihedral potentials loaded from this file */ -QMultiHash GroTop::dihedralPotentials() const -{ - return dih_potentials; -} + try { + scl0 = mol.property("intrascale0").asA(); + } catch (...) { + return; + } -/** Return the moleculetype with name 'name'. This returns an invalid (empty) - GroMolType if one with this name does not exist */ -GroMolType GroTop::moleculeType(const QString &name) const -{ - for (const auto &moltype : moltypes) - { - if (moltype.name() == name) - return moltype; - } + try { + scl1 = mol.property("intrascale1").asA(); + } catch (...) { + return; + } + + AtomLJs ljs0; + AtomLJs ljs1; + AtomCharges charges0; + AtomCharges charges1; + bool has_ljs = false; + bool has_charges = false; + + try { + ljs0 = mol.property("LJ0").asA(); + ljs1 = mol.property("LJ1").asA(); + has_ljs = true; + } catch (...) { + } + + try { + charges0 = mol.property("charge0").asA(); + charges1 = mol.property("charge1").asA(); + has_charges = true; + } catch (...) { + } + + // A set of recorded 1-4 pairs. + QSet> recorded_pairs; + + bool fix_null_perturbable_14s = false; + + if (mol.hasProperty("fix_null_perturbable_14s")) + fix_null_perturbable_14s = mol.property("fix_null_perturbable_14s") + .asA() + .value(); + + // Must record every pair that has a non-default scaling factor. + // Loop over intrascale matrix by cut-groups to avoid N^2 loop. + for (int i = 0; i < scl0.nGroups(); ++i) { + for (int j = 0; j < scl0.nGroups(); ++j) { + const auto s0 = scl0.get(CGIdx(i), CGIdx(j)); + const auto s1 = scl1.get(CGIdx(i), CGIdx(j)); + + if (not s0.isEmpty() and not s1.isEmpty()) { + const auto idxs0 = molinfo.getAtomsIn(CGIdx(i)); + const auto idxs1 = molinfo.getAtomsIn(CGIdx(j)); + + for (const auto &idx0 : idxs0) { + for (const auto &idx1 : idxs1) { + QPair pair = + QPair(idx0, idx1); + + // Make sure this is a new atom pair. + if (not recorded_pairs.contains(pair)) { + // Insert the pair and its inverse. + recorded_pairs.insert(pair); + pair = QPair(idx1, idx0); + recorded_pairs.insert(pair); + + const auto s0 = scl0.get(idx0, idx1); + const auto s1 = scl1.get(idx0, idx1); + + if (s0.coulomb() == 1 and s0.lj() == 1 and + s1.coulomb() == 1 and s1.lj() == 1) { + // Both endstates have full 1-4 interaction (e.g. GLYCAM + // SCNB=1.0/SCEE=1.0). Write as funct=2 with explicit LJ + // parameters from state 0 (identical in both states). + if (has_ljs and has_charges) { + const auto cgidx0 = molinfo.cgAtomIdx(idx0); + const auto cgidx1 = molinfo.cgAtomIdx(idx1); + + const auto &lj0 = ljs0.at(cgidx0); + const auto &lj1 = ljs0.at(cgidx1); + + LJParameter lj_ij; + if (combining_rules == 2) + lj_ij = lj0.combineArithmetic(lj1); + else + lj_ij = lj0.combineGeometric(lj1); + + const double qi = charges0.at(cgidx0).to(mod_electron); + const double qj = charges0.at(cgidx1).to(mod_electron); + + scllines.append( + QString("%1 %2 2 1.0 %3 %4 %5 %6") + .arg(idx0 + 1, 6) + .arg(idx1 + 1, 6) + .arg(qi, 11, 'f', 6) + .arg(qj, 11, 'f', 6) + .arg(lj_ij.sigma().to(nanometer), 18, 'e', 11) + .arg(lj_ij.epsilon().to(kJ_per_mol), 18, 'e', + 11)); + } else { + scllines.append(QString("%1 %2 1") + .arg(idx0 + 1, 6) + .arg(idx1 + 1, 6)); + } + } else if (not(s0.coulomb() == 0 and s0.lj() == 0 and + s1.coulomb() == 0 and s1.lj() == 0)) { + // This is a non-default, non-full pair (e.g. standard AMBER + // partial scaling, or a mixed perturbation). + if (fix_null_perturbable_14s) { + // get the initial and perturbed charge and LJ parameters + const auto &lj0_0 = ljs0.get(idx0); + const auto &lj0_1 = ljs1.get(idx0); + const auto &lj1_0 = ljs0.get(idx1); + const auto &lj1_1 = ljs1.get(idx1); + + if (lj0_0.epsilon().value() == 0 or + lj0_1.epsilon().value() == 0 or + lj1_0.epsilon().value() == 0 or + lj1_1.epsilon().value() == 0) { + // we need to avoid having a null 1-4 LJ parameter, so + // use the non-dummy state + LJParameter lj0, lj1; + + if (lj0_0.epsilon().value() == 0) + lj0 = lj0_1; + else + lj0 = lj0_0; - return GroMolType(); -} + if (lj1_0.epsilon().value() == 0) + lj1 = lj1_1; + else + lj1 = lj1_0; -/** Return all of the moleculetypes that have been loaded from this file */ -QVector GroTop::moleculeTypes() const -{ - return moltypes; -} + auto lj = (combining_rules == 2) ? lj0.combineArithmetic(lj1) + : lj0.combineGeometric(lj1); -/** Return whether or not the gromacs preprocessor would change these lines */ -static bool gromacs_preprocess_would_change(const QVector &lines, bool use_parallel, - const QHash &defines) -{ - // create the regexps that are needed to find all of the - // data that may be #define'd - QVector regexps; + double scl = s0.lj(); - if (not defines.isEmpty()) - { - regexps.reserve(defines.count()); + if (scl == 0) + scl = s1.lj(); - for (const auto &key : defines.keys()) - { - regexps.append(QRegularExpression(QString("\\s+%1\\s*").arg(key))); - } - } + scllines.append( + QString("%1 %2 1 %3 %4 %3 %4") + .arg(idx0 + 1, 6) + .arg(idx1 + 1, 6) + .arg(lj.sigma().to(nanometer), 11, 'f', 5) + .arg(scl * lj.epsilon().to(kJ_per_mol), 11, 'f', + 5)); - // function that says whether or not an individual line would change - auto lineWillChange = [&](const QString &line) - { - if (line.indexOf(QLatin1String(";")) != -1 or line.indexOf(QLatin1String("#include")) != -1 or - line.indexOf(QLatin1String("#ifdef")) != -1 or line.indexOf(QLatin1String("#ifndef")) != -1 or - line.indexOf(QLatin1String("#else")) != -1 or line.indexOf(QLatin1String("#endif")) != -1 or - line.indexOf(QLatin1String("#define")) != -1 or line.indexOf(QLatin1String("#error")) != -1) - { - return true; - } - else - { - for (int i = 0; i < regexps.count(); ++i) - { - if (line.contains(regexps.constData()[i])) - return true; - } + continue; + } + } - if (line.trimmed().endsWith("\\")) - { - // this is a continuation line - return true; + scllines.append(QString("%1 %2 1") + .arg(idx0 + 1, 6) + .arg(idx1 + 1, 6)); + } + } + } } - - return false; + } } - }; + } + } else { + CLJNBPairs scl; - const auto lines_data = lines.constData(); - - if (use_parallel) - { - QMutex mutex; - - bool must_change = false; - - tbb::parallel_for(tbb::blocked_range(0, lines.count()), [&](const tbb::blocked_range &r) - { - if (not must_change) - { - for (int i = r.begin(); i < r.end(); ++i) - { - if (lineWillChange(lines_data[i])) - { - QMutexLocker lkr(&mutex); - must_change = true; - break; + try { + scl = mol.property("intrascale").asA(); + } catch (...) { + return; + } + + // Get LJ and charge properties for writing funct=2 explicit pairs. + AtomLJs ljs; + AtomCharges charges; + bool has_ljs = false; + bool has_charges = false; + + + try { + ljs = mol.property("LJ").asA(); + has_ljs = true; + } catch (...) { + } + + try { + charges = mol.property("charge").asA(); + has_charges = true; + } catch (...) { + } + + // A set of recorded 1-4 pairs. + QSet> recorded_pairs; + + // Must record every pair that has a non-default scaling factor. + // Loop over intrascale matrix by cut-groups to avoid N^2 loop. + for (int i = 0; i < scl.nGroups(); ++i) { + for (int j = 0; j < scl.nGroups(); ++j) { + const auto s = scl.get(CGIdx(i), CGIdx(j)); + + if (not s.isEmpty()) { + const auto idxs0 = molinfo.getAtomsIn(CGIdx(i)); + const auto idxs1 = molinfo.getAtomsIn(CGIdx(j)); + + for (const auto &idx0 : idxs0) { + for (const auto &idx1 : idxs1) { + QPair pair = + QPair(idx0, idx1); + + // Make sure this is a new atom pair. + if (not recorded_pairs.contains(pair)) { + // Insert the pair and its inverse. + recorded_pairs.insert(pair); + pair = QPair(idx1, idx0); + recorded_pairs.insert(pair); + + const auto s = scl.get(idx0, idx1); + + if (s.coulomb() == 0 and s.lj() == 0) { + // Fully excluded: don't write. + } else if (s.coulomb() == 1 and s.lj() == 1) { + // Full 1-4 interaction (e.g. GLYCAM with SCNB=1.0, + // SCEE=1.0). Must write as funct=2 with explicit LJ + // parameters because funct=1 with gen-pairs would apply + // fudgeLJ and reduce the interaction, and not listing the + // pair would give zero interaction. + if (has_ljs and has_charges) { + const auto cgidx0 = molinfo.cgAtomIdx(idx0); + const auto cgidx1 = molinfo.cgAtomIdx(idx1); + + const auto &lj0 = ljs.at(cgidx0); + const auto &lj1 = ljs.at(cgidx1); + + LJParameter lj_ij; + if (combining_rules == 2) + lj_ij = lj0.combineArithmetic(lj1); + else + lj_ij = lj0.combineGeometric(lj1); + + const double qi = charges.at(cgidx0).to(mod_electron); + const double qj = charges.at(cgidx1).to(mod_electron); + + scllines.append( + QString("%1 %2 2 1.0 %3 %4 %5 %6") + .arg(idx0 + 1, 6) + .arg(idx1 + 1, 6) + .arg(qi, 11, 'f', 6) + .arg(qj, 11, 'f', 6) + .arg(lj_ij.sigma().to(nanometer), 18, 'e', 11) + .arg(lj_ij.epsilon().to(kJ_per_mol), 18, 'e', + 11)); + } else { + // Fall back to funct=1; the energy will be wrong if + // fudgeLJ != 1.0, but we have no LJ parameters to use. + scllines.append(QString("%1 %2 1") + .arg(idx0 + 1, 6) + .arg(idx1 + 1, 6)); } + } else { + // Standard partial 1-4 (e.g. fudgeQQ/fudgeLJ): write as + // funct=1. + scllines.append(QString("%1 %2 1") + .arg(idx0 + 1, 6) + .arg(idx1 + 1, 6)); + } } - } }); - - return must_change; - } - else - { - for (int i = 0; i < lines.count(); ++i) - { - if (lineWillChange(lines_data[i])) - return true; - } - } - - return false; -} - -/** Return the full path to the file 'filename' searching through the - Gromacs file path. This throws an exception if the file is not found */ -QString GroTop::findIncludeFile(QString filename, QString current_dir) -{ - // new file, so first see if this filename is absolute - QFileInfo file(filename); - - // is the filename absolute? - if (file.isAbsolute()) - { - if (not(file.exists() and file.isReadable())) - { - throw SireError::io_error(QObject::tr("Cannot find the file '%1'. Please make sure that this file exists " - "and is readable") - .arg(filename), - CODELOC); + } + } + } } - - return filename; + } } + }; - // does this exist from the current directory? - file = QFileInfo(QString("%1/%2").arg(current_dir).arg(filename)); + const QVector> funcs = {write_atoms, write_bonds, + write_angs, write_dihs, + write_cmaps, write_pairs}; - if (file.exists() and file.isReadable()) - return file.absoluteFilePath(); + if (uses_parallel) { + tbb::parallel_for(tbb::blocked_range(0, funcs.count(), 1), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + funcs[i](); + } + }); + } else { + for (int i = 0; i < funcs.count(); ++i) { + funcs[i](); + } + } + + lines.append("[ atoms ]"); + if (is_perturbable) + lines.append("; nr type0 resnr residue atom cgnr charge0 " + "mass0 type1 charge1 mass1"); + else + lines.append( + "; nr type resnr residue atom cgnr charge mass"); + lines.append(atomlines); + + // we need to detect whether this is a water molecule. If so, then we + // need to add in "settles" lines to constrain bonds / angles of the + // water molecule + const bool is_water = moltype.isWater(); + + if (is_water) { + lines.append("#ifdef FLEXIBLE"); + } + + if (not bondlines.isEmpty()) { + lines.append("[ bonds ]"); + lines.append("; ai aj funct parameters"); + lines += bondlines; + lines.append(""); + } - // otherwise search the GROMACS_PATH - for (const auto &path : include_path) - { - file = QFileInfo(QString("%1/%2").arg(path).arg(filename)); + if (not scllines.isEmpty()) { + lines.append("[ pairs ]"); + lines.append("; ai aj funct "); + lines += scllines; + lines.append(""); + } - if (file.exists() and file.isReadable()) - { - return file.absoluteFilePath(); - } - } + if (not anglines.isEmpty()) { + lines.append("[ angles ]"); + lines.append("; ai aj ak funct parameters"); + lines += anglines; + lines.append(""); + } - // nothing was found! - throw SireError::io_error( - QObject::tr("Cannot find the file '%1' using GROMACS_PATH = [ %2 ], current directory '%3'. " - "Please make " - "sure the file exists and is readable within your GROMACS_PATH from the " - "current directory '%3' (e.g. " - "set the GROMACS_PATH environment variable to include the directory " - "that contains '%1', or copy this file into one of the existing " - "directories [ %2 ])") - .arg(filename) - .arg(include_path.join(", ")) - .arg(current_dir), - CODELOC); + if (not dihlines.isEmpty()) { + lines.append("[ dihedrals ]"); + lines.append("; ai aj ak al funct parameters"); + lines += dihlines; + lines.append(""); + } - return QString(); -} + if (not cmaplines.isEmpty()) { + lines.append("[ cmap ]"); + lines.append("; ai aj ak al am funct"); + lines += cmaplines; + lines.append(""); + } -/** This function will use the Gromacs search path to find and load the - passed include file. This will load the file and return the - un-preprocessed text. The file, together with its QFileInfo, will - be saved in the 'included_files' hash */ -QVector GroTop::loadInclude(QString filename, QString current_dir) -{ - // try to find the file - QString absfile = findIncludeFile(filename, current_dir); + if (is_water) { + lines.append("#else"); + lines.append(""); + lines += moltype.settlesLines(); + lines.append(""); + lines.append("#endif"); + } - // now load the file - return MoleculeParser::readTextFile(absfile); + return lines; } -/** This function scans through a set of gromacs file lines and expands all - macros, removes all comments and includes all #included files */ -QVector GroTop::preprocess(const QVector &lines, QHash &defines, - const QString ¤t_directory, const QString &parent_file) -{ - // first, scan through to see if anything needs changing - if (not gromacs_preprocess_would_change(lines, usesParallel(), defines)) - { - // nothing to do - return lines; +/** Internal function used to convert an array of Gromacs Moltyps into + lines of a Gromacs topology file */ +static QStringList +writeMolTypes(const QMap, GroMolType> &moltyps, + const QMap, Molecule> &examples, + bool uses_parallel, bool isSorted = false) { + QHash typs; + + if (uses_parallel) { + const QVector> keys = moltyps.keys().toVector(); + QMutex mutex; + + tbb::parallel_for(tbb::blocked_range(0, keys.count(), 1), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + QStringList typlines = + ::writeMolType(keys[i].second, moltyps[keys[i]], + examples[keys[i]], uses_parallel); + + QMutexLocker lkr(&mutex); + typs.insert(keys[i].second, typlines); + } + }); + } else { + for (auto it = moltyps.constBegin(); it != moltyps.constEnd(); ++it) { + typs.insert(it.key().second, + ::writeMolType(it.key().second, it.value(), + examples[it.key()], uses_parallel)); } + } - // Ok, we have to change the lines... - QVector new_lines; - new_lines.reserve(lines.count()); + QStringList keys; + for (const auto &key : moltyps.keys()) + keys.append(key.second); - // regexps used to parse the files... - QRegularExpression include_regexp("\\#include\\s*(<([^\"<>|\\b]+)>|\"([^\"<>|\\b]+)\")"); + if (isSorted) + keys.sort(); - // loop through all of the lines... - QVectorIterator lines_it(lines); + QStringList lines; - QList ifparse; + for (const auto &key : keys) { + lines += typs[key]; + lines += ""; + } - while (lines_it.hasNext()) - { - QString line = lines_it.next(); - - // remove any comments - if (line.indexOf(QLatin1String(";")) != -1) - { - line = line.mid(0, line.indexOf(QLatin1String(";"))).simplified(); + return lines; +} - // this is just an empty line, so ignore it - if (line.isEmpty()) - { - continue; - } - } - else if (line.startsWith("*")) - { - // the whole line is a comment - continue; - } - else - { - // simplify the line to remove weirdness - line = line.simplified(); - } +/** Internal function used to write the system part of the gromacs file */ +static QStringList writeSystem(QString name, + const QVector &mol_to_moltype) { + QStringList lines; + lines.append("[ system ]"); + lines.append(name); + lines.append(""); + lines.append("[ molecules ]"); + lines.append(";molecule name nr."); - // now look to see if the line should be joined to the next line - while (line.endsWith("\\")) - { - if (not lines_it.hasNext()) - { - throw SireIO::parse_error( - QObject::tr("Continuation line on the last line of the Gromacs file! '%1'").arg(line), CODELOC); - } + QString lastmol; - // replace this last slash with a space - line = line.left(line.length() - 1) + " "; + int count = 0; - line += lines_it.next(); - line = line.simplified(); - } + for (auto it = mol_to_moltype.constBegin(); it != mol_to_moltype.constEnd(); + ++it) { + if (*it != lastmol) { + if (lastmol.isNull()) { + lastmol = *it; + count = 1; + } else { + lines.append(QString("%1 %2").arg(lastmol, 14).arg(count, 6)); + lastmol = *it; + count = 1; + } + } else + count += 1; + } - // first, look to see if the line starts with #error, as this should - // terminate processing - if (line.startsWith("#error")) - { - // stop processing, and pass the error to the user - line = line.mid(6).simplified(); - throw SireIO::parse_error(QObject::tr("Error in Gromacs file! '%1'").arg(line), CODELOC); - } + lines.append(QString("%1 %2").arg(lastmol, 14).arg(count, 6)); - // now look to see if there is an #ifdef - if (line.startsWith("#ifdef")) - { - // we have an ifdef - has it been defined? - auto symbol = line.split(" ", Qt::SkipEmptyParts).last(); + lines.append(""); - // push the current parse state (whether we parse if or else) - ifparse.append(defines.value(symbol, "0") != "0"); - continue; - } + return lines; +} - // now look to see if there is an #ifndef - if (line.startsWith("#ifndef")) - { - // we have an ifndef - has it been defined? - auto symbol = line.split(" ", Qt::SkipEmptyParts).last(); +/** Function called by the below constructor to sanitise all of the CMAP terms + * that have been loaded into an intermediate state. The aim is to create a + * database of unique CMAP terms, and then make sure that each set of + * atoms that reference those CMAP terms use a consistent set of atom + * types. + */ +static QHash +sanitiseCMAPs(QHash &name_to_mtyp, + QMap, GroMolType> &idx_name_to_mtyp) { + QHash cmap_potentials; + + // first, go through all of the molecules and extract out all of the + // unique CMAP terms - they are already written in a Gromacs string + // format + QHash unique_cmaps; + + // get a list of pointers to all of the molecule types + QVector moltypes; + + moltypes.reserve(name_to_mtyp.count() + idx_name_to_mtyp.count()); - // push the current parse state (whether we parse if or else) - ifparse.append(defines.value(symbol, "0") == "0"); - continue; - } + for (auto it = name_to_mtyp.begin(); it != name_to_mtyp.end(); ++it) { + moltypes.append(&it.value()); + } - if (line == "#else") - { - // switch the last ifdef state - if (ifparse.isEmpty()) - throw SireIO::parse_error(QObject::tr("Unmatched '#else' in the GROMACS file!"), CODELOC); + for (auto it = idx_name_to_mtyp.begin(); it != idx_name_to_mtyp.end(); ++it) { + moltypes.append(&it.value()); + } - ifparse.last() = not ifparse.last(); - continue; - } + // first, create a set of all existing atom types + QSet existing_atom_types; - if (line == "#endif") - { - // pop off the last 'ifdef' state - if (ifparse.isEmpty()) - throw SireIO::parse_error(QObject::tr("Unmatched '#endif' in the GROMACS file!"), CODELOC); + for (auto mol : moltypes) { + if (mol->isPerturbable()) { + for (const auto &atom : mol->atoms(false)) { + existing_atom_types.insert(atom.atomType()); + } + + for (const auto &atom : mol->atoms(true)) { + existing_atom_types.insert(atom.atomType()); + } + } else { + for (const auto &atom : mol->atoms()) { + existing_atom_types.insert(atom.atomType()); + } + } + } + + auto get_atomtype_count = [&](const QString &atm_type, int count) -> QString { + // convert the count to a letter + // e.g. 0 -> A, 1 -> B, ..., 25 -> Z, 26 -> AA, 27 -> AB, ... + QString suffix = ""; + + while (count >= 0) { + suffix = QChar('A' + (count % 26)) + suffix; + count = count / 26 - 1; + } + + return atm_type + suffix; + }; + + auto get_new_atomtype = [&](const QString &atm_type, int count) -> QString { + // make sure there is no "old" atom type that is the same as the new one + while (existing_atom_types.contains(get_atomtype_count(atm_type, count))) { + count += 1; + } + + return get_atomtype_count(atm_type, count); + }; + + for (auto mol : moltypes) { + if (mol->isPerturbable()) { + // do this both for lambda = 0 and lambda = 1 + const auto cmaps0 = mol->cmaps(); + const auto cmaps1 = mol->cmaps(true); + + for (auto it = cmaps0.constBegin(); it != cmaps0.constEnd(); ++it) { + const auto &atoms = it.key(); + const auto ¶m = it.value(); + CMAPParameter cmap; + + if (not unique_cmaps.contains(param)) { + cmap = string_to_cmap(param); + unique_cmaps.insert(param, cmap); + } else { + cmap = unique_cmaps[param]; + } + + // get the atom types for the atoms in this CMAP - AtomID is AtomIdx + const auto atm0 = mol->atom(atoms.atom0().asA()).atomType(); + const auto atm1 = mol->atom(atoms.atom1().asA()).atomType(); + const auto atm2 = mol->atom(atoms.atom2().asA()).atomType(); + const auto atm3 = mol->atom(atoms.atom3().asA()).atomType(); + const auto atm4 = mol->atom(atoms.atom4().asA()).atomType(); + + // create the key for the combination of these atom types + // and a "1" function type + const auto key = get_cmap_id(atm0, atm1, atm2, atm3, atm4, 1); + + // have we seen this key before? + if (cmap_potentials.contains(key)) { + // check that we are consistent + if (cmap_potentials[key] != cmap) { + int count = 0; + auto new_atm_type = get_new_atomtype(atm2, count); + auto new_key = get_cmap_id(atm0, atm1, new_atm_type, atm3, atm4, 1); + + while (cmap_potentials.contains(new_key) and + cmap_potentials[new_key] != cmap) { + count += 1; + new_atm_type = get_new_atomtype(atm2, count); + new_key = get_cmap_id(atm0, atm1, new_atm_type, atm3, atm4, 1); + } + + // we have found a new atom type that can be used for this new CMAP + mol->setAtomType(atoms.atom2().asA(), new_atm_type); + + if (not cmap_potentials.contains(new_key)) { + cmap_potentials.insert(new_key, cmap); + } + } + } else { + // we have not seen this key before, so add it + cmap_potentials.insert(key, cmap); + } + } + + for (auto it = cmaps1.constBegin(); it != cmaps1.constEnd(); ++it) { + const auto &atoms = it.key(); + const auto ¶m = it.value(); + CMAPParameter cmap; + + if (not unique_cmaps.contains(param)) { + cmap = string_to_cmap(param); + unique_cmaps.insert(param, cmap); + } else { + cmap = unique_cmaps[param]; + } + + // get the atom types for the atoms in this CMAP - AtomID is AtomIdx + const auto atm0 = + mol->atom(atoms.atom0().asA(), true).atomType(); + const auto atm1 = + mol->atom(atoms.atom1().asA(), true).atomType(); + const auto atm2 = + mol->atom(atoms.atom2().asA(), true).atomType(); + const auto atm3 = + mol->atom(atoms.atom3().asA(), true).atomType(); + const auto atm4 = + mol->atom(atoms.atom4().asA(), true).atomType(); - ifparse.removeLast(); - continue; - } + // create the key for the combination of these atom types + // and a "1" function type + const auto key = get_cmap_id(atm0, atm1, atm2, atm3, atm4, 1); - if (not ifparse.isEmpty()) - { - // are we allowed to read this? - if (not ifparse.last()) - { - // no, this is blocked out - continue; + // have we seen this key before? + if (cmap_potentials.contains(key)) { + // check that we are consistent + if (cmap_potentials[key] != cmap) { + int count = 0; + auto new_atm_type = get_new_atomtype(atm2, count); + auto new_key = get_cmap_id(atm0, atm1, new_atm_type, atm3, atm4, 1); + + while (cmap_potentials.contains(new_key) and + cmap_potentials[new_key] != cmap) { + count += 1; + new_atm_type = get_new_atomtype(atm2, count); + new_key = get_cmap_id(atm0, atm1, new_atm_type, atm3, atm4, 1); + } + + // we have found a new atom type that can be used for this new CMAP + mol->setAtomType(atoms.atom2().asA(), new_atm_type, true); + + if (not cmap_potentials.contains(new_key)) { + cmap_potentials.insert(new_key, cmap); } + } + } else { + // we have not seen this key before, so add it + cmap_potentials.insert(key, cmap); } + } + + mol->sanitiseCMAPs(); + mol->sanitiseCMAPs(true); + } else { + const auto cmaps = mol->cmaps(); - // now look for any #define lines - if (line.startsWith("#define")) - { - auto words = line.split(" ", Qt::SkipEmptyParts); - - if (words.count() == 1) - throw SireIO::parse_error(QObject::tr("Malformed #define line in Gromacs file? %1").arg(line), CODELOC); - - if (words.count() == 2) - { - defines.insert(words[1], "1"); - } - else - { - auto key = words[1]; - words.takeFirst(); - words.takeFirst(); - defines.insert(key, words.join(" ")); - } + for (auto it = cmaps.constBegin(); it != cmaps.constEnd(); ++it) { + const auto &atoms = it.key(); + const auto ¶m = it.value(); + CMAPParameter cmap; - continue; + if (not unique_cmaps.contains(param)) { + cmap = string_to_cmap(param); + unique_cmaps.insert(param, cmap); + } else { + cmap = unique_cmaps[param]; } - // now try to substitute any 'defines' in the line with their defined values - for (auto it = defines.constBegin(); it != defines.constEnd(); ++it) - { - if (line.indexOf(it.key()) != -1) - { - auto words = line.split(" ", Qt::SkipEmptyParts); - - for (int i = 0; i < words.count(); ++i) - { - if (words[i] == it.key()) - { - words[i] = it.value(); - } - } - - line = words.join(" "); - } - } + // get the atom types for the atoms in this CMAP - AtomID is AtomIdx + const auto atm0 = mol->atom(atoms.atom0().asA()).atomType(); + const auto atm1 = mol->atom(atoms.atom1().asA()).atomType(); + const auto atm2 = mol->atom(atoms.atom2().asA()).atomType(); + const auto atm3 = mol->atom(atoms.atom3().asA()).atomType(); + const auto atm4 = mol->atom(atoms.atom4().asA()).atomType(); - // skip BioSimSpace position restraint includes - if (line.contains("#include \"posre")) - { - continue; - } + // create the key for the combination of these atom types + // and a "1" function type + const auto key = get_cmap_id(atm0, atm1, atm2, atm3, atm4, 1); - // now look for #include lines - if (line.startsWith("#include")) - { - // now insert the contents of any included files - auto m = include_regexp.match(line); + // have we seen this key before? + if (cmap_potentials.contains(key)) { + // check that we are consistent + if (cmap_potentials[key] != cmap) { + int count = 0; + auto new_atm_type = get_new_atomtype(atm2, count); + auto new_key = get_cmap_id(atm0, atm1, new_atm_type, atm3, atm4, 1); - if (not m.hasMatch()) - { - throw SireIO::parse_error(QObject::tr("Malformed #include line in Gromacs file? %1").arg(line), - CODELOC); + while (cmap_potentials.contains(new_key) and + cmap_potentials[new_key] != cmap) { + count += 1; + new_atm_type = get_new_atomtype(atm2, count); + new_key = get_cmap_id(atm0, atm1, new_atm_type, atm3, atm4, 1); } - // we have to include a file - auto filename = m.captured(m.lastCapturedIndex()); - - // now find the absolute path to the file... - auto absfile = findIncludeFile(filename, current_directory); + // we have found a new atom type that can be used for this new CMAP + mol->setAtomType(atoms.atom2().asA(), new_atm_type); - // now load the file - auto included_lines = MoleculeParser::readTextFile(absfile); + if (not cmap_potentials.contains(new_key)) { + cmap_potentials.insert(new_key, cmap); + } + } + } else { + // we have not seen this key before, so add it + cmap_potentials.insert(key, cmap); + } + } - // now get the absolute path to the included file - auto parts = absfile.split("/"); - parts.removeLast(); + mol->sanitiseCMAPs(); + } + } - // fully preprocess these lines using the current set of defines - included_lines = preprocess(included_lines, defines, parts.join("/"), absfile); + return cmap_potentials; +} - // add these included lines to the set - new_lines.reserve(new_lines.count() + included_lines.count()); - new_lines += included_lines; +/** Construct this parser by extracting all necessary information from the + passed SireSystem::System, looking for the properties that are specified + in the passed property map */ +GroTop::GroTop(const SireSystem::System &system, const PropertyMap &map) + : ConcreteProperty(map), nb_func_type(0), + combining_rule(0), fudge_lj(0), fudge_qq(0), generate_pairs(false) { + // get the MolNums of each molecule in the System - this returns the + // numbers in MolIdx order + const QVector molnums = system.getMoleculeNumbers().toVector(); + + if (molnums.isEmpty()) { + // no molecules in the system + this->operator=(GroTop()); + return; + } + + bool isSorted = true; + if (map["sort"].hasValue()) { + isSorted = map["sort"].value().asA().value(); + } + + // Search for waters and crystal waters. The user can speficy the residue name + // for crystal waters using the "crystal_water" property in the map. If the + // user wishes to preserve a custom water topology naming, then they can use + // "skip_water". + SelectResult waters; + SelectResult xtal_waters; + if (map.specified("crystal_water")) { + auto xtal_water_resname = map["crystal_water"].source(); + xtal_waters = system.search(QString("resname %1").arg(xtal_water_resname)); + + if (not map.specified("skip_water")) { + waters = system.search( + QString("(not mols with property is_non_searchable_water) and (water " + "and not resname %1") + .arg(xtal_water_resname)); + } + } else { + waters = system.search( + "(not mols with property is_non_searchable_water) and water"); + } + + // Extract the molecule numbers of the water molecules. + auto water_nums = waters.molNums(); + + // Extract the molecule numbers of the crystal water molecules. + auto xtal_water_nums = xtal_waters.molNums(); + + // Loop over the molecules to find the non-water molecules. + QList non_water_nums; + for (const auto &num : molnums) { + if (not water_nums.contains(num) and not xtal_water_nums.contains(num)) + non_water_nums.append(num); + } + + // Create a hash between MolNum and index in the system. + QHash molnum_to_idx; + + for (int i = 0; i < molnums.count(); ++i) { + molnum_to_idx.insert(molnums[i], i); + } + + // Initialise data structures to map molecules to their respective + // GroMolTypes. + QVector mol_to_moltype(molnums.count()); + QMap, GroMolType> idx_name_to_mtyp; + QMap, Molecule> idx_name_to_example; + QHash name_to_mtyp; + + // First add the non-water molecules. + for (int i = 0; i < non_water_nums.count(); ++i) { + // Extract the molecule number of the molecule and work out + // the index in the system. + auto molnum = non_water_nums[i]; + auto idx = molnum_to_idx[molnum]; + + // Generate a GroMolType type for this molecule and get its name. + auto moltype = GroMolType(system[molnum].molecule(), map); + auto name = moltype.name(); + + // We have already recorded this name. + if (name_to_mtyp.contains(name)) { + if (moltype != name_to_mtyp[name]) { + // This has the same name but different details. Give this a new name. + int j = 0; + + while (true) { + j++; + name = QString("%1_%2").arg(moltype.name()).arg(j); + + if (name_to_mtyp.contains(name)) { + if (moltype == name_to_mtyp[name]) + // Match :-) + break; + } else { + // New moltype. + idx_name_to_mtyp.insert(QPair(idx, name), moltype); + name_to_mtyp.insert(name, moltype); - // finally, record that this file depends on the included file - included_files[parent_file].append(absfile); + // save an example of this molecule so that we can + // extract any other details necessary + idx_name_to_example.insert(QPair(idx, name), + system[molnum].molecule()); - continue; - } + break; + } + + // We have got here, meaning that we need to try a different name. + } + } + } + // Name not previously recorded. + else { + name_to_mtyp.insert(name, moltype); + idx_name_to_mtyp.insert(QPair(idx, moltype.name()), + moltype); + idx_name_to_example.insert(QPair(idx, name), + system[molnum].molecule()); + } + + // Store the name of the molecule type. + mol_to_moltype[idx] = name; + } + + // Now deal with the water molecules. + if (waters.count() > 0) { + // Extract the GroMolType of the first water molecule. + auto water_type = GroMolType(system[water_nums[0]].molecule(), map); + auto name = water_type.name(); + auto molnum = water_nums[0]; + auto idx = molnum_to_idx[molnum]; + + // Populate the mappings. + name_to_mtyp.insert(name, water_type); + idx_name_to_mtyp.insert(QPair(idx, water_type.name()), + water_type); + idx_name_to_example.insert(QPair(idx, name), + system[molnum].molecule()); + + for (int i = 0; i < water_nums.count(); ++i) { + // Extract the molecule number of the molecule and work out + // the index in the system. + auto molnum = water_nums[i]; + auto idx = molnum_to_idx[molnum]; + + // Store the name of the molecule type. + mol_to_moltype[idx] = name; + } + } + + // Now add the crystal waters. + if (xtal_waters.count() > 0) { + // Extract the GroMolType of the first water molecule. + auto water_type = GroMolType(system[xtal_water_nums[0]].molecule(), map); + auto name = water_type.name(); + auto molnum = xtal_water_nums[0]; + auto idx = molnum_to_idx[molnum]; + + // Populate the mappings. + name_to_mtyp.insert(name, water_type); + idx_name_to_mtyp.insert(QPair(idx, water_type.name()), + water_type); + idx_name_to_example.insert(QPair(idx, name), + system[molnum].molecule()); + + for (int i = 0; i < xtal_water_nums.count(); ++i) { + // Extract the molecule number of the molecule and work out + // the index in the system. + auto molnum = xtal_water_nums[i]; + auto idx = molnum_to_idx[molnum]; + + // Store the name of the molecule type. + mol_to_moltype[idx] = name; + } + } + + QStringList errors; + + // first, we need to extract the common forcefield from the molecules + MMDetail ffield = idx_name_to_mtyp.constBegin()->forcefield(); + + for (auto it = idx_name_to_mtyp.constBegin(); + it != idx_name_to_mtyp.constEnd(); ++it) { + if (not ffield.isCompatibleWith(it.value().forcefield())) { + errors.append( + QObject::tr( + "The forcefield for molecule '%1' is not " + "compatible with that for other molecules.\n%1 versus\n%2") + .arg(it.key().second) + .arg(it.value().forcefield().toString()) + .arg(ffield.toString())); + } + } + + if (not errors.isEmpty()) { + throw SireError::incompatible_error( + QObject::tr("Cannot write this system to a Gromacs Top file as the " + "forcefields of the " + "molecules are incompatible with one another.\n%1") + .arg(errors.join("\n\n")), + CODELOC); + } - // finally, make sure that we have not missed any '#' directives... - if (line.startsWith("#")) - { - throw SireIO::parse_error(QObject::tr("Unrecognised directive on Gromacs file line '%1'").arg(line), - CODELOC); - } + // first, we need to de-deduplicate and sanitise all of the CMAP terms + cmap_potentials = sanitiseCMAPs(name_to_mtyp, idx_name_to_mtyp); - // skip empty lines - if (not line.isEmpty()) - { - // otherwise this is a normal line, so append this to the set of new_lines - new_lines.append(line); - } - } + // next, we need to write the defaults section of the file + QStringList lines = ::writeDefaults(ffield); - if (not ifparse.isEmpty()) - { - throw SireIO::parse_error(QObject::tr("Unmatched #ifdef or #ifndef in Gromacs file!"), CODELOC); - } + // next, we need to extract and write all of the atom types from all of + // the molecules + lines += ::writeAtomTypes(idx_name_to_mtyp, cmap_potentials, + idx_name_to_example, ffield, map); - return new_lines; -} + lines += ::writeCMAPTypes(cmap_potentials); -/** Return the non-bonded function type for the molecules in this file */ -int GroTop::nonBondedFunctionType() const -{ - return nb_func_type; -} + lines += ::writeMolTypes(idx_name_to_mtyp, idx_name_to_example, + usesParallel(), isSorted); -/** Return the combining rules to use for the molecules in this file */ -int GroTop::combiningRules() const -{ - return combining_rule; -} + // now write the system part + lines += ::writeSystem(system.name(), mol_to_moltype); -/** Return the Lennard Jones fudge factor for the molecules in this file */ -double GroTop::fudgeLJ() const -{ - return fudge_lj; -} + if (not errors.isEmpty()) { + throw SireIO::parse_error( + QObject::tr( + "Errors converting the system to a Gromacs Top format...\n%1") + .arg(lines.join("\n")), + CODELOC); + } -/** Return the electrostatic fudge factor for the molecules in this file */ -double GroTop::fudgeQQ() const -{ - return fudge_qq; -} + // we don't need params any more, so free the memory + idx_name_to_mtyp.clear(); + idx_name_to_example.clear(); + mol_to_moltype.clear(); -/** Return whether or not the non-bonded pairs should be automatically generated - for the molecules in this file */ -bool GroTop::generateNonBondedPairs() const -{ - return generate_pairs; -} + // now we have the lines, reparse them to make sure that they are correct + // and we have a fully-constructed and sane GroTop object + GroTop parsed(lines, map); -/** Return the expanded set of lines (after preprocessing) */ -const QVector &GroTop::expandedLines() const -{ - return expanded_lines; + this->operator=(parsed); } -/** Public function used to return the list of post-processed lines */ -QStringList GroTop::postprocessedLines() const -{ - return expanded_lines.toList(); -} +/** Copy constructor */ +GroTop::GroTop(const GroTop &other) + : ConcreteProperty(other), + include_path(other.include_path), included_files(other.included_files), + expanded_lines(other.expanded_lines), atom_types(other.atom_types), + bond_potentials(other.bond_potentials), + ang_potentials(other.ang_potentials), + dih_potentials(other.dih_potentials), + cmap_potentials(other.cmap_potentials), moltypes(other.moltypes), + grosys(other.grosys), nb_func_type(other.nb_func_type), + combining_rule(other.combining_rule), fudge_lj(other.fudge_lj), + fudge_qq(other.fudge_qq), parse_warnings(other.parse_warnings), + generate_pairs(other.generate_pairs) {} -/** Internal function, called by ::interpret() that processes all of the data - from all of the directives, returning a set of warnings */ -QStringList GroTop::processDirectives(const QMap &taglocs, const QHash &ntags) -{ - // internal function that returns the lines associated with the - // specified directive - auto getLines = [&](const QString &directive, int n) -> QStringList - { - if (n >= ntags.value(directive, 0)) - { - return QStringList(); - } +/** Destructor */ +GroTop::~GroTop() {} - bool found = false; - int start = 0; - int end = expandedLines().count(); +/** Copy assignment operator */ +GroTop &GroTop::operator=(const GroTop &other) { + if (this != &other) { + include_path = other.include_path; + included_files = other.included_files; + expanded_lines = other.expanded_lines; + atom_types = other.atom_types; + bond_potentials = other.bond_potentials; + ang_potentials = other.ang_potentials; + dih_potentials = other.dih_potentials; + cmap_potentials = other.cmap_potentials; + moltypes = other.moltypes; + grosys = other.grosys; + nb_func_type = other.nb_func_type; + combining_rule = other.combining_rule; + fudge_lj = other.fudge_lj; + fudge_qq = other.fudge_qq; + parse_warnings = other.parse_warnings; + generate_pairs = other.generate_pairs; + MoleculeParser::operator=(other); + } - // find the tag - for (auto it = taglocs.constBegin(); it != taglocs.constEnd(); ++it) - { - if (it.value() == directive) - { - if (n == 0) - { - found = true; - start = it.key() + 1; - - ++it; - - if (it != taglocs.constEnd()) - { - end = it.key(); - } + return *this; +} - break; - } - else - n -= 1; - } - } +/** Comparison operator */ +bool GroTop::operator==(const GroTop &other) const { + return include_path == other.include_path and + included_files == other.included_files and + expanded_lines == other.expanded_lines and + MoleculeParser::operator==(other); +} - if (not found) - throw SireError::program_bug( - QObject::tr("Cannot find tag '%1' at index '%2'. This should not happen!").arg(directive).arg(n), - CODELOC); +/** Comparison operator */ +bool GroTop::operator!=(const GroTop &other) const { + return not operator==(other); +} - QStringList lines; +/** Return the C++ name for this class */ +const char *GroTop::typeName() { + return QMetaType::typeName(qMetaTypeId()); +} - for (int i = start; i < end; ++i) - { - lines.append(expandedLines().constData()[i]); - } +/** Return the C++ name for this class */ +const char *GroTop::what() const { return GroTop::typeName(); } - return lines; - }; +bool GroTop::isTopology() const { return true; } - // return the lines associated with the directive at line 'linenum' - auto getDirectiveLines = [&](int linenum) -> QStringList - { - auto it = taglocs.constFind(linenum); +/** Return the list of names of directories in which to search for + include files. The directories are either absolute, or relative + to the current directory. If "absolute_paths" is true then + the full absolute paths for directories that exist on this + machine will be returned */ +QStringList GroTop::includePath(bool absolute_paths) const { + if (absolute_paths) { + QStringList abspaths; - if (it == taglocs.constEnd()) - throw SireError::program_bug( - QObject::tr("Cannot find a tag associated with line '%1'. This should not happen!").arg(linenum), - CODELOC); + for (const auto &path : include_path) { + QFileInfo file(path); - int start = it.key() + 1; - int end = expandedLines().count(); + if (file.exists()) + abspaths.append(file.absoluteFilePath()); + } - ++it; + return abspaths; + } else + return include_path; +} - if (it != taglocs.constEnd()) - end = it.key(); +/** Return the list of names of files that were included when reading or + writing this file. The files are relative. If "absolute_paths" + is true then the full absolute paths for the files will be + used */ +QStringList GroTop::includedFiles(bool absolute_paths) const { + // first, go through the list of included files + QStringList files; - QStringList lines; + for (auto it = included_files.constBegin(); it != included_files.constEnd(); + ++it) { + files += it.value(); + } - for (int i = start; i < end; ++i) - { - lines.append(expandedLines().constData()[i]); + if (absolute_paths) { + // these are already absolute filenames + return files; + } else { + // subtract any paths that relate to the current directory or GROMACS_PATH + QString curpath = QDir::current().absolutePath(); + + for (auto it = files.begin(); it != files.end(); ++it) { + if (it->startsWith(curpath)) { + *it = it->mid(curpath.length() + 1); + } else { + for (const auto &path : include_path) { + if (it->startsWith(path)) { + *it = it->mid(path.length() + 1); + } } + } + } - return lines; - }; + return files; + } +} - // return all of the lines associated with all copies of the passed directive - auto getAllLines = [&](const QString &directive) -> QStringList - { - QStringList lines; +/** Return the parser that has been constructed by reading in the passed + file using the passed properties */ +MoleculeParserPtr GroTop::construct(const QString &filename, + const PropertyMap &map) const { + return GroTop(filename, map); +} - for (int i = 0; i < ntags.value(directive, 0); ++i) - { - lines += getLines(directive, i); - } +/** Return the parser that has been constructed by reading in the passed + text lines using the passed properties */ +MoleculeParserPtr GroTop::construct(const QStringList &lines, + const PropertyMap &map) const { + return GroTop(lines, map); +} - return lines; - }; +/** Return the parser that has been constructed by extract all necessary + data from the passed SireSystem::System using the specified properties */ +MoleculeParserPtr GroTop::construct(const SireSystem::System &system, + const PropertyMap &map) const { + return GroTop(system, map); +} - // interpret a bool from the passed string - auto gromacs_toBool = [&](const QString &word, bool *ok) - { - QString w = word.toLower(); +/** Return a string representation of this parser */ +QString GroTop::toString() const { + return QObject::tr("GroTop( includePath() = [%1], includedFiles() = [%2] )") + .arg(includePath().join(", ")) + .arg(includedFiles().join(", ")); +} - if (ok) - *ok = true; +/** Return the format name that is used to identify this file format within Sire + */ +QString GroTop::formatName() const { return "GROTOP"; } - if (w == "yes" or w == "y" or w == "true" or w == "1") - { - return true; - } - else if (w == "no" or w == "n" or w == "false" or w == "0") - { - return false; - } - else - { - if (ok) - *ok = false; - return false; - } - }; +/** Return a description of the file format */ +QString GroTop::formatDescription() const { + return QObject::tr("Gromacs Topology format files."); +} - // internal function to process the defaults lines - auto processDefaults = [&]() - { - QStringList warnings; +/** Return the suffixes that these files are normally associated with */ +QStringList GroTop::formatSuffix() const { + static const QStringList suffixes = {"top", "grotop", "gtop"}; + return suffixes; +} - // there should only be one defaults line - const auto lines = getLines("defaults", 0); +/** Function that is called to assert that this object is sane. This + should raise an exception if the parser is in an invalid state */ +void GroTop::assertSane() const { + // check state, raise SireError::program_bug if we are in an invalid state +} - if (lines.isEmpty()) - throw SireIO::parse_error(QObject::tr("The required data for the '[defaults]' directive in Gromacs is " - "not supplied. This is not a valid Gromacs topology file!"), - CODELOC); +/** Return the atom type data for the passed atom type. This returns + null data if it is not present */ +GromacsAtomType GroTop::atomType(const QString &atm) const { + return atom_types.value(atm, GromacsAtomType()); +} - auto words = lines[0].split(" ", Qt::SkipEmptyParts); +/** Return the ID string for the bond atom types 'atm0' 'atm1'. This + creates the string 'atm0;atm1' or 'atm1;atm0' depending on which + of the atoms is lower. The ';' character is used as a separator + as it cannot be in the atom names, as it is used as a comment + character in the Gromacs Top file */ +static QString get_bond_id(const QString &atm0, const QString &atm1, + int func_type) { + if (func_type == 0) // default type + func_type = 1; - // there should be five words; non-bonded function type, combinination rule, - // generate pairs, fudge LJ and fudge QQ - if (words.count() < 5) - { - throw SireIO::parse_error(QObject::tr("There is insufficient data for the '[defaults]' line '%1'. This is " - "not a valid Gromacs topology file!") - .arg(lines[0]), - CODELOC); - } + if (atm0 < atm1) { + return QString("%1;%2;%3").arg(atm0, atm1).arg(func_type); + } else { + return QString("%1;%2;%3").arg(atm1, atm0).arg(func_type); + } +} - bool ok; - int nbtyp = words[0].toInt(&ok); +/** Return the ID string for the angle atom types 'atm0' 'atm1' 'atm2'. This + creates the string 'atm0;atm1;atm2' or 'atm2;atm1;atm0' depending on which + of the atoms is lower. The ';' character is used as a separator + as it cannot be in the atom names, as it is used as a comment + character in the Gromacs Top file */ +static QString get_angle_id(const QString &atm0, const QString &atm1, + const QString &atm2, int func_type) { + if (func_type == 0) + func_type = 1; // default type + + if (atm0 < atm2) { + return QString("%1;%2;%3;%4").arg(atm0, atm1, atm2).arg(func_type); + } else { + return QString("%1;%2;%3;%4").arg(atm2, atm1, atm0).arg(func_type); + } +} + +/** Return the ID string for the dihedral atom types 'atm0' 'atm1' 'atm2' + 'atm3'. This creates the string 'atm0;atm1;atm2;atm3' or + 'atm3;atm2;atm1;atm0' depending on which of the atoms is lower. The ';' + character is used as a separator as it cannot be in the atom names, as it is + used as a comment character in the Gromacs Top file */ +static QString get_dihedral_id(const QString &atm0, const QString &atm1, + const QString &atm2, const QString &atm3, + int func_type) { + if ((atm0 < atm3) or (atm0 == atm3 and atm1 <= atm2)) { + return QString("%1;%2;%3;%4;%5").arg(atm0, atm1, atm2, atm3).arg(func_type); + } else { + return QString("%1;%2;%3;%4;%5").arg(atm3, atm2, atm1, atm0).arg(func_type); + } +} - if (not ok) - throw SireIO::parse_error(QObject::tr("The first value for the '[defaults]' line '%1' is not an integer. " - "This is not a valid Gromacs topology file!") - .arg(lines[0]), - CODELOC); +/** Return the Gromacs System that describes the list of molecules that should + be contained */ +GroSystem GroTop::groSystem() const { return grosys; } - int combrule = words[1].toInt(&ok); +/** Return the bond potential data for the passed pair of atoms. This only + returns the most recently inserted parameter for this pair. Use 'bonds' if + you want to allow for multiple return values */ +GromacsBond GroTop::bond(const QString &atm0, const QString &atm1, + int func_type) const { + return bond_potentials.value(get_bond_id(atm0, atm1, func_type), + GromacsBond()); +} - if (not ok) - throw SireIO::parse_error(QObject::tr("The second value for the '[defaults]' line '%1' is not an integer. " - "This is not a valid Gromacs topology file!") - .arg(lines[0]), - CODELOC); +/** Return the bond potential data for the passed pair of atoms. This returns + a list of all associated parameters */ +QList GroTop::bonds(const QString &atm0, const QString &atm1, + int func_type) const { + return bond_potentials.values(get_bond_id(atm0, atm1, func_type)); +} - bool gen_pairs = gromacs_toBool(words[2], &ok); +/** Return the angle potential data for the passed triple of atoms. This only + returns the most recently inserted parameter for these atoms. Use 'angles' if + you want to allow for multiple return values */ +GromacsAngle GroTop::angle(const QString &atm0, const QString &atm1, + const QString &atm2, int func_type) const { + return ang_potentials.value(get_angle_id(atm0, atm1, atm2, func_type), + GromacsAngle()); +} - if (not ok) - throw SireIO::parse_error(QObject::tr("The third value for the '[defaults]' line '%1' is not a yes/no. " - "This is not a valid Gromacs topology file!") - .arg(lines[0]), - CODELOC); +/** Return the angle potential data for the passed triple of atoms. This returns + a list of all associated parameters */ +QList GroTop::angles(const QString &atm0, const QString &atm1, + const QString &atm2, int func_type) const { + return ang_potentials.values(get_angle_id(atm0, atm1, atm2, func_type)); +} - double lj = words[3].toDouble(&ok); +/** Search for a dihedral type parameter that matches the atom types + atom0-atom1-atom2-atom3. This will try to find an exact match. If that + fails, it will then use one of the wildcard matches. Returns a null string if + there is no match. This will return the key into the dih_potentials + dictionary */ +QString GroTop::searchForDihType(const QString &atm0, const QString &atm1, + const QString &atm2, const QString &atm3, + int func_type) const { + QString key = get_dihedral_id(atm0, atm1, atm2, atm3, func_type); - if (not ok) - throw SireIO::parse_error(QObject::tr("The fourth value for the '[defaults]' line '%1' is not a double. " - "This is not a valid Gromacs topology file!") - .arg(lines[0]), - CODELOC); + // qDebug() << "SEARCHING FOR" << key; - double qq = words[4].toDouble(&ok); + if (dih_potentials.contains(key)) { + // qDebug() << "FOUND" << key; + return key; + } - if (not ok) - throw SireIO::parse_error(QObject::tr("The fifth value for the '[defaults]' line '%1' is not a double. " - "This is not a valid Gromacs topology file!") - .arg(lines[0]), - CODELOC); + static const QString wild = "X"; - // validate and then save these values - if (nbtyp <= 0 or nbtyp > 2) - { - warnings.append(QObject::tr("A non-supported non-bonded function type (%1) " - "is requested.") - .arg(nbtyp)); - } + // look for *-atm1-atm2-atm3 + key = get_dihedral_id(wild, atm1, atm2, atm3, func_type); - if (combrule <= 0 or combrule > 3) - { - warnings.append(QObject::tr("A non-supported combinig rule (%1) is requested!").arg(combrule)); - } + if (dih_potentials.contains(key)) { + // qDebug() << "FOUND" << key; + return key; + } - if (lj < 0 or lj > 1) - { - warnings.append(QObject::tr("An invalid value of fudge_lj (%1) is requested!").arg(lj)); + // look for *-atm2-atm1-atm0 + key = get_dihedral_id(wild, atm2, atm1, atm0, func_type); - if (lj < 0) - lj = 0; - else if (lj > 1) - lj = 1; - } + if (dih_potentials.contains(key)) { + // qDebug() << "FOUND" << key; + return key; + } - if (qq < 0 or qq > 1) - { - warnings.append(QObject::tr("An invalid value of fudge_qq (%1) is requested!").arg(qq)); + // this failed. Look for *-atm1-atm2-* or *-atm2-atm1-* + key = get_dihedral_id(wild, atm1, atm2, wild, func_type); - if (qq < 0) - qq = 0; - else if (qq > 1) - qq = 1; - } + if (dih_potentials.contains(key)) { + // qDebug() << "FOUND" << key; + return key; + } - // Gromacs uses a non-exact value of the Amber fudge_qq (1.0/1.2). Correct this - // in case we convert to another file format - if (std::abs(qq - (1.0 / 1.2)) < 0.01) - { - qq = 1.0 / 1.2; - } + key = get_dihedral_id(wild, atm2, atm1, wild, func_type); - nb_func_type = nbtyp; - combining_rule = combrule; - fudge_lj = lj; - fudge_qq = qq; - generate_pairs = gen_pairs; + if (dih_potentials.contains(key)) { + // qDebug() << "FOUND" << key; + return key; + } - return warnings; - }; + // look for *-*-atm2-atm3 + key = get_dihedral_id(wild, wild, atm2, atm3, func_type); - // wildcard atomtype (this is 'X' in gromacs files) - static const QString wildcard_atomtype = "X"; + if (dih_potentials.contains(key)) { + // qDebug() << "FOUND" << key; + return key; + } - // Whether the file uses a "bond_type" atom type. - bool is_bond_type = false; + // look for *-*-atm1-atm0 + key = get_dihedral_id(wild, wild, atm1, atm0, func_type); - // internal function to process the atomtypes lines - auto processAtomTypes = [&]() - { - QStringList warnings; + if (dih_potentials.contains(key)) { + // qDebug() << "FOUND" << key; + return key; + } - // get all 'atomtypes' lines - const auto lines = getAllLines("atomtypes"); + // look for atm0-*-*-atm3 or atm3-*-*-atm0 + key = get_dihedral_id(atm0, wild, wild, atm3, func_type); - // the database of all atom types - QHash typs; + if (dih_potentials.contains(key)) { + return key; + } - // now parse each atom - for (const auto &line : lines) - { - const auto words = line.split(" ", Qt::SkipEmptyParts); - - // should either have 2 words (atom type, mass) or - // have 6 words; atom type, mass, charge, type, V, W or - // have 7 words; atom type, atom number, mass, charge, type, V, W - // have 8 words; atom type, bond type, atom number, mass, charge, type, V, W - if (words.count() < 2) - { - warnings.append(QObject::tr("There is not enough data for the " - "atomtype data '%1'. Skipping this line.") - .arg(line)); - continue; - } + // finally look for *-*-*-* + key = get_dihedral_id(wild, wild, wild, wild, func_type); - GromacsAtomType typ; + if (dih_potentials.contains(key)) { + // qDebug() << "FOUND" << key; + return key; + } - if (words.count() < 6) - { - // only getting the atom type and mass - bool ok_mass; - double mass = words[1].toDouble(&ok_mass); + return QString(); +} - if (not ok_mass) - { - warnings.append(QObject::tr("Could not interpret the atom type data " - "from line '%1'. Skipping this line.") - .arg(line)); - continue; - } +/** Return the dihedral potential data for the passed quad of atoms. This only + returns the most recently inserted parameter for these atoms. Use 'dihedrals' + if you want to allow for multiple return values */ +GromacsDihedral GroTop::dihedral(const QString &atm0, const QString &atm1, + const QString &atm2, const QString &atm3, + int func_type) const { + return dih_potentials.value( + searchForDihType(atm0, atm1, atm2, atm3, func_type), GromacsDihedral()); +} - typ = GromacsAtomType(words[0], mass * g_per_mol); - } - else if (words.count() < 7) - { - bool ok_mass, ok_charge, ok_ptyp, ok_v, ok_w; - double mass = words[1].toDouble(&ok_mass); - double chg = words[2].toDouble(&ok_charge); - auto ptyp = GromacsAtomType::toParticleType(words[3], &ok_ptyp); - double v = words[4].toDouble(&ok_v); - double w = words[5].toDouble(&ok_w); - - if (not(ok_mass and ok_charge and ok_ptyp and ok_v and ok_w)) - { - warnings.append(QObject::tr("Could not interpret the atom type data " - "from line '%1'. Skipping this line.") - .arg(line)); - continue; - } +/** Return the dihedral potential data for the passed quad of atoms. This + returns a list of all associated parameters */ +QList +GroTop::dihedrals(const QString &atm0, const QString &atm1, const QString &atm2, + const QString &atm3, int func_type) const { + return dih_potentials.values( + searchForDihType(atm0, atm1, atm2, atm3, func_type)); +} - typ = GromacsAtomType(words[0], mass * g_per_mol, chg * mod_electron, ptyp, - ::toLJParameter(v, w, combining_rule)); - } - else if (words.count() < 8) - { - bool ok_mass, ok_elem, ok_charge, ok_ptyp, ok_v, ok_w; - int nprotons = words[1].toInt(&ok_elem); - double mass = words[2].toDouble(&ok_mass); - double chg = words[3].toDouble(&ok_charge); - auto ptyp = GromacsAtomType::toParticleType(words[4], &ok_ptyp); - double v = words[5].toDouble(&ok_v); - double w = words[6].toDouble(&ok_w); - - if (not(ok_mass and ok_charge and ok_ptyp and ok_v and ok_w)) - { - warnings.append(QObject::tr("Could not interpret the atom type data " - "from line '%1'. Skipping this line.") - .arg(line)); - continue; - } +/** Return all of the CMAP potentials for the passed quint of atom types, for + * the passed function type. This returns a list of all associated parameters + * (or an empty list if none exist) */ +QList GroTop::cmaps(const QString &atm0, const QString &atm1, + const QString &atm2, const QString &atm3, + const QString &atm4, int func_type) const { + // get the key for this cmap + QString key = get_cmap_id(atm0, atm1, atm2, atm3, atm4, func_type); - if (ok_elem) - { - typ = GromacsAtomType(words[0], mass * g_per_mol, chg * mod_electron, ptyp, - ::toLJParameter(v, w, combining_rule), Element(nprotons)); - } - else - { - // some gromacs files don't use 'nprotons', but instead give - // a "bond_type" ambertype - typ = GromacsAtomType(words[0], words[1], mass * g_per_mol, chg * mod_electron, ptyp, - ::toLJParameter(v, w, combining_rule), - Element::elementWithMass(mass * g_per_mol)); - - is_bond_type = true; - } - } - else if (words.count() < 9) - { - bool ok_mass, ok_elem, ok_charge, ok_ptyp, ok_v, ok_w; - int nprotons = words[2].toInt(&ok_elem); - double mass = words[3].toDouble(&ok_mass); - double chg = words[4].toDouble(&ok_charge); - auto ptyp = GromacsAtomType::toParticleType(words[5], &ok_ptyp); - double v = words[6].toDouble(&ok_v); - double w = words[7].toDouble(&ok_w); - - if (not(ok_mass and ok_charge and ok_ptyp and ok_v and ok_w)) - { - warnings.append(QObject::tr("Could not interpret the atom type data " - "from line '%1'. Skipping this line.") - .arg(line)); - continue; - } + auto it = cmap_potentials.find(key); - typ = GromacsAtomType(words[0], words[1], mass * g_per_mol, chg * mod_electron, ptyp, - ::toLJParameter(v, w, combining_rule), Element(nprotons)); - } - else - { - warnings.append(QObject::tr("The atomtype line '%1' contains more data " - "than is expected!") - .arg(line)); - } + if (it == cmap_potentials.end()) { + // no cmap found + return QList(); + } else { + // return the cmap + QList cmaps; + cmaps.append(it.value()); + return cmaps; + } +} - if (typs.contains(typ.atomType())) - { - // only replace if the new type is fully specified - if (typ.hasMassOnly()) - continue; +/** Return the atom types loaded from this file */ +QHash GroTop::atomTypes() const { return atom_types; } - warnings.append(QObject::tr("The data for atom type '%1' exists already! " - "This will now be replaced with new data from line '%2'") - .arg(typ.atomType()) - .arg(line)); - } +/** Return the bond potentials loaded from this file */ +QMultiHash GroTop::bondPotentials() const { + return bond_potentials; +} - typs.insert(typ.atomType(), typ); - } +/** Return the angle potentials loaded from this file */ +QMultiHash GroTop::anglePotentials() const { + return ang_potentials; +} - // save the database of types - atom_types = typs; +/** Return the dihedral potentials loaded from this file */ +QMultiHash +GroTop::dihedralPotentials() const { + return dih_potentials; +} - return warnings; - }; +/** Return the moleculetype with name 'name'. This returns an invalid (empty) + GroMolType if one with this name does not exist */ +GroMolType GroTop::moleculeType(const QString &name) const { + for (const auto &moltype : moltypes) { + if (moltype.name() == name) + return moltype; + } - // internal function to process the bondtypes lines - auto processBondTypes = [&]() - { - QStringList warnings; + return GroMolType(); +} - // get all 'bondtypes' lines - const auto lines = getAllLines("bondtypes"); +/** Return all of the moleculetypes that have been loaded from this file */ +QVector GroTop::moleculeTypes() const { return moltypes; } - // save into a database of bonds - QMultiHash bnds; +/** Return whether or not the gromacs preprocessor would change these lines */ +static bool +gromacs_preprocess_would_change(const QVector &lines, + bool use_parallel, + const QHash &defines) { + // create the regexps that are needed to find all of the + // data that may be #define'd + QVector regexps; + + if (not defines.isEmpty()) { + regexps.reserve(defines.count()); + + for (const auto &key : defines.keys()) { + regexps.append(QRegularExpression(QString("\\s+%1\\s*").arg(key))); + } + } + + // function that says whether or not an individual line would change + auto lineWillChange = [&](const QString &line) { + if (line.indexOf(QLatin1String(";")) != -1 or + line.indexOf(QLatin1String("#include")) != -1 or + line.indexOf(QLatin1String("#ifdef")) != -1 or + line.indexOf(QLatin1String("#ifndef")) != -1 or + line.indexOf(QLatin1String("#else")) != -1 or + line.indexOf(QLatin1String("#endif")) != -1 or + line.indexOf(QLatin1String("#define")) != -1 or + line.indexOf(QLatin1String("#error")) != -1) { + return true; + } else { + for (int i = 0; i < regexps.count(); ++i) { + if (line.contains(regexps.constData()[i])) + return true; + } + + if (line.trimmed().endsWith("\\")) { + // this is a continuation line + return true; + } + + return false; + } + }; + + const auto lines_data = lines.constData(); + + if (use_parallel) { + QMutex mutex; + + bool must_change = false; + + tbb::parallel_for(tbb::blocked_range(0, lines.count()), + [&](const tbb::blocked_range &r) { + if (not must_change) { + for (int i = r.begin(); i < r.end(); ++i) { + if (lineWillChange(lines_data[i])) { + QMutexLocker lkr(&mutex); + must_change = true; + break; + } + } + } + }); - for (const auto &line : lines) - { - // each line should contain the atom types of the two atoms, then - // the function type, then the parameters for the function - const auto words = line.split(" ", Qt::SkipEmptyParts); - - if (words.count() < 3) - { - warnings.append(QObject::tr("There is not enough data on the " - "line '%1' to extract a Gromacs bond parameter. Skipping line.") - .arg(line)); - continue; - } + return must_change; + } else { + for (int i = 0; i < lines.count(); ++i) { + if (lineWillChange(lines_data[i])) + return true; + } + } - const auto atm0 = words[0]; - const auto atm1 = words[1]; + return false; +} - bool ok; - int func_type = words[2].toInt(&ok); - - if (not ok) - { - warnings.append(QObject::tr("Unable to determine the function type " - "for the bond on line '%1'. Skipping line.") - .arg(line)); - continue; - } +/** Return the full path to the file 'filename' searching through the + Gromacs file path. This throws an exception if the file is not found */ +QString GroTop::findIncludeFile(QString filename, QString current_dir) { + // new file, so first see if this filename is absolute + QFileInfo file(filename); + + // is the filename absolute? + if (file.isAbsolute()) { + if (not(file.exists() and file.isReadable())) { + throw SireError::io_error(QObject::tr("Cannot find the file '%1'. Please " + "make sure that this file exists " + "and is readable") + .arg(filename), + CODELOC); + } - // now read in all of the remaining values as numbers... - QList params; + return filename; + } - for (int i = 3; i < words.count(); ++i) - { - double param = words[i].toDouble(&ok); + // does this exist from the current directory? + file = QFileInfo(QString("%1/%2").arg(current_dir).arg(filename)); - if (ok) - params.append(param); - } + if (file.exists() and file.isReadable()) + return file.absoluteFilePath(); - GromacsBond bond; + // otherwise search the GROMACS_PATH + for (const auto &path : include_path) { + file = QFileInfo(QString("%1/%2").arg(path).arg(filename)); - try - { - bond = GromacsBond(func_type, params); - } - catch (const SireError::exception &e) - { - warnings.append(QObject::tr("Unable to extract the correct information " - "to form a bond from line '%1'. Error is '%2'") - .arg(line) - .arg(e.error())); - continue; - } + if (file.exists() and file.isReadable()) { + return file.absoluteFilePath(); + } + } - QString key = get_bond_id(atm0, atm1, bond.functionType()); - bnds.insert(key, bond); - } + // nothing was found! + throw SireError::io_error( + QObject::tr( + "Cannot find the file '%1' using GROMACS_PATH = [ %2 ], current " + "directory '%3'. " + "Please make " + "sure the file exists and is readable within your GROMACS_PATH from " + "the " + "current directory '%3' (e.g. " + "set the GROMACS_PATH environment variable to include the directory " + "that contains '%1', or copy this file into one of the existing " + "directories [ %2 ])") + .arg(filename) + .arg(include_path.join(", ")) + .arg(current_dir), + CODELOC); - bond_potentials = bnds; + return QString(); +} - return warnings; - }; +/** This function will use the Gromacs search path to find and load the + passed include file. This will load the file and return the + un-preprocessed text. The file, together with its QFileInfo, will + be saved in the 'included_files' hash */ +QVector GroTop::loadInclude(QString filename, QString current_dir) { + // try to find the file + QString absfile = findIncludeFile(filename, current_dir); - // internal function to process the pairtypes lines - auto processPairTypes = [&]() - { - QStringList warnings; + // now load the file + return MoleculeParser::readTextFile(absfile); +} - // get all 'bondtypes' lines - const auto lines = getAllLines("pairtypes"); +/** This function scans through a set of gromacs file lines and expands all + macros, removes all comments and includes all #included files */ +QVector GroTop::preprocess(const QVector &lines, + QHash &defines, + const QString ¤t_directory, + const QString &parent_file) { + // first, scan through to see if anything needs changing + if (not gromacs_preprocess_would_change(lines, usesParallel(), defines)) { + // nothing to do + return lines; + } - if (not lines.isEmpty()) - { - warnings.append(QString("Ignoring %1 pairtypes lines.").arg(lines.count())); - warnings.append(QString("e.g. the first ignored pairtypes line is")); - warnings.append(lines[0]); - } + // Ok, we have to change the lines... + QVector new_lines; + new_lines.reserve(lines.count()); - return warnings; - }; + // regexps used to parse the files... + QRegularExpression include_regexp( + "\\#include\\s*(<([^\"<>|\\b]+)>|\"([^\"<>|\\b]+)\")"); - // internal function to process the angletypes lines - auto processAngleTypes = [&]() - { - QStringList warnings; + // loop through all of the lines... + QVectorIterator lines_it(lines); - // get all 'bondtypes' lines - const auto lines = getAllLines("angletypes"); + QList ifparse; - // save into a database of angles - QMultiHash angs; + while (lines_it.hasNext()) { + QString line = lines_it.next(); - for (const auto &line : lines) - { - // each line should contain the atom types of the three atoms, then - // the function type, then the parameters for the function - const auto words = line.split(" ", Qt::SkipEmptyParts); - - if (words.count() < 4) - { - warnings.append(QObject::tr("There is not enough data on the " - "line '%1' to extract a Gromacs angle parameter. Skipping line.") - .arg(line)); - continue; - } + // remove any comments + if (line.indexOf(QLatin1String(";")) != -1) { + line = line.mid(0, line.indexOf(QLatin1String(";"))).simplified(); - const auto atm0 = words[0]; - const auto atm1 = words[1]; - const auto atm2 = words[2]; + // this is just an empty line, so ignore it + if (line.isEmpty()) { + continue; + } + } else if (line.startsWith("*")) { + // the whole line is a comment + continue; + } else { + // simplify the line to remove weirdness + line = line.simplified(); + } - bool ok; - int func_type = words[3].toInt(&ok); - - if (not ok) - { - warnings.append(QObject::tr("Unable to determine the function type " - "for the angle on line '%1'. Skipping line.") - .arg(line)); - continue; - } + // now look to see if the line should be joined to the next line + while (line.endsWith("\\")) { + if (not lines_it.hasNext()) { + throw SireIO::parse_error( + QObject::tr( + "Continuation line on the last line of the Gromacs file! '%1'") + .arg(line), + CODELOC); + } - // now read in all of the remaining values as numbers... - QList params; + // replace this last slash with a space + line = line.left(line.length() - 1) + " "; - for (int i = 4; i < words.count(); ++i) - { - double param = words[i].toDouble(&ok); + line += lines_it.next(); + line = line.simplified(); + } - if (ok) - params.append(param); - } + // first, look to see if the line starts with #error, as this should + // terminate processing + if (line.startsWith("#error")) { + // stop processing, and pass the error to the user + line = line.mid(6).simplified(); + throw SireIO::parse_error( + QObject::tr("Error in Gromacs file! '%1'").arg(line), CODELOC); + } - GromacsAngle angle; + // now look to see if there is an #ifdef + if (line.startsWith("#ifdef")) { + // we have an ifdef - has it been defined? + auto symbol = line.split(" ", Qt::SkipEmptyParts).last(); - try - { - angle = GromacsAngle(func_type, params); - } - catch (const SireError::exception &e) - { - warnings.append(QObject::tr("Unable to extract the correct information " - "to form an angle from line '%1'. Error is '%2'") - .arg(line) - .arg(e.error())); - continue; - } + // push the current parse state (whether we parse if or else) + ifparse.append(defines.value(symbol, "0") != "0"); + continue; + } - QString key = get_angle_id(atm0, atm1, atm2, angle.functionType()); - angs.insert(key, angle); - } + // now look to see if there is an #ifndef + if (line.startsWith("#ifndef")) { + // we have an ifndef - has it been defined? + auto symbol = line.split(" ", Qt::SkipEmptyParts).last(); - ang_potentials = angs; + // push the current parse state (whether we parse if or else) + ifparse.append(defines.value(symbol, "0") == "0"); + continue; + } - return warnings; - }; + if (line == "#else") { + // switch the last ifdef state + if (ifparse.isEmpty()) + throw SireIO::parse_error( + QObject::tr("Unmatched '#else' in the GROMACS file!"), CODELOC); - // internal function to process the dihedraltypes lines - auto processDihedralTypes = [&]() - { - QStringList warnings; + ifparse.last() = not ifparse.last(); + continue; + } - // get all 'bondtypes' lines - const auto lines = getAllLines("dihedraltypes"); + if (line == "#endif") { + // pop off the last 'ifdef' state + if (ifparse.isEmpty()) + throw SireIO::parse_error( + QObject::tr("Unmatched '#endif' in the GROMACS file!"), CODELOC); - // save into a database of dihedrals - QMultiHash dihs; + ifparse.removeLast(); + continue; + } - for (const auto &line : lines) - { - // each line should contain the atom types of the four atoms, then - // the function type, then the parameters for the function. - //(however, some files have the atom types of just two atoms, which - // I assume are the two middle atoms of the dihedral...) - const auto words = line.split(" ", Qt::SkipEmptyParts); - - if (words.count() < 3) - { - warnings.append(QObject::tr("There is not enough data on the " - "line '%1' to extract a Gromacs dihedral parameter. Skipping line.") - .arg(line)); - continue; - } + if (not ifparse.isEmpty()) { + // are we allowed to read this? + if (not ifparse.last()) { + // no, this is blocked out + continue; + } + } - // first, let's try to parse this assuming that it is a 2-atom dihedral line... - //(most cases this should fail) - auto atm0 = wildcard_atomtype; - auto atm1 = words[0]; - auto atm2 = words[1]; - auto atm3 = wildcard_atomtype; + // now look for any #define lines + if (line.startsWith("#define")) { + auto words = line.split(" ", Qt::SkipEmptyParts); - GromacsDihedral dihedral; + if (words.count() == 1) + throw SireIO::parse_error( + QObject::tr("Malformed #define line in Gromacs file? %1").arg(line), + CODELOC); - bool ok; - int func_type = words[2].toInt(&ok); + if (words.count() == 2) { + defines.insert(words[1], "1"); + } else { + auto key = words[1]; + words.takeFirst(); + words.takeFirst(); + defines.insert(key, words.join(" ")); + } - if (ok) - { - // this may be a two-atom dihedral - read in the rest of the parameters - QList params; - - for (int i = 3; i < words.count(); ++i) - { - double param = words[i].toDouble(&ok); - if (ok) - params.append(param); - } + continue; + } - try - { - dihedral = GromacsDihedral(func_type, params); - ok = true; - } - catch (...) - { - ok = false; - } - } + // now try to substitute any 'defines' in the line with their defined values + for (auto it = defines.constBegin(); it != defines.constEnd(); ++it) { + if (line.indexOf(it.key()) != -1) { + auto words = line.split(" ", Qt::SkipEmptyParts); - if (not ok) - { - // we couldn't parse as a two-atom dihedral, so parse as a four-atom dihedral + for (int i = 0; i < words.count(); ++i) { + if (words[i] == it.key()) { + words[i] = it.value(); + } + } - if (words.count() < 5) - { - warnings.append(QObject::tr("There is not enough data on the " - "line '%1' to extract a Gromacs dihedral parameter. Skipping line.") - .arg(line)); - continue; - } + line = words.join(" "); + } + } - atm0 = words[0]; - atm1 = words[1]; - atm2 = words[2]; - atm3 = words[3]; + // skip BioSimSpace position restraint includes + if (line.contains("#include \"posre")) { + continue; + } - bool ok; - int func_type = words[4].toInt(&ok); + // now look for #include lines + if (line.startsWith("#include")) { + // now insert the contents of any included files + auto m = include_regexp.match(line); - if (not ok) - { - warnings.append(QObject::tr("Unable to determine the function type " - "for the dihedral on line '%1'. Skipping line.") - .arg(line)); - continue; - } + if (not m.hasMatch()) { + throw SireIO::parse_error( + QObject::tr("Malformed #include line in Gromacs file? %1") + .arg(line), + CODELOC); + } - // now read in all of the remaining values as numbers... - QList params; + // we have to include a file + auto filename = m.captured(m.lastCapturedIndex()); - for (int i = 5; i < words.count(); ++i) - { - double param = words[i].toDouble(&ok); + // now find the absolute path to the file... + auto absfile = findIncludeFile(filename, current_directory); - if (ok) - params.append(param); - } + // now load the file + auto included_lines = MoleculeParser::readTextFile(absfile); - try - { - dihedral = GromacsDihedral(func_type, params); - } - catch (const SireError::exception &e) - { - warnings.append(QObject::tr("Unable to extract the correct information " - "to form a dihedral from line '%1'. Error is '%2'") - .arg(line) - .arg(e.error())); - continue; - } - } + // now get the absolute path to the included file + auto parts = absfile.split("/"); + parts.removeLast(); - QString key = get_dihedral_id(atm0, atm1, atm2, atm3, dihedral.functionType()); - dihs.insert(key, dihedral); - } + // fully preprocess these lines using the current set of defines + included_lines = + preprocess(included_lines, defines, parts.join("/"), absfile); - dih_potentials = dihs; + // add these included lines to the set + new_lines.reserve(new_lines.count() + included_lines.count()); + new_lines += included_lines; - return warnings; - }; + // finally, record that this file depends on the included file + included_files[parent_file].append(absfile); - // internal function to process the constrainttypes lines - auto processConstraintTypes = [&]() - { - QStringList warnings; + continue; + } - // get all 'bondtypes' lines - const auto lines = getAllLines("constrainttypes"); + // finally, make sure that we have not missed any '#' directives... + if (line.startsWith("#")) { + throw SireIO::parse_error( + QObject::tr("Unrecognised directive on Gromacs file line '%1'") + .arg(line), + CODELOC); + } - if (not lines.isEmpty()) - { - warnings.append(QString("Ignoring %1 'constrainttypes' lines").arg(lines.count())); - warnings.append(QString("e.g. the first ignored constrainttypes line is")); - warnings.append(lines[0]); - } + // skip empty lines + if (not line.isEmpty()) { + // otherwise this is a normal line, so append this to the set of new_lines + new_lines.append(line); + } + } - return warnings; - }; + if (not ifparse.isEmpty()) { + throw SireIO::parse_error( + QObject::tr("Unmatched #ifdef or #ifndef in Gromacs file!"), CODELOC); + } - // internal function to process the nonbond_params lines - auto processNonBondParams = [&]() - { - QStringList warnings; + return new_lines; +} - // get all 'bondtypes' lines - const auto lines = getAllLines("nonbond_params"); +/** Return the non-bonded function type for the molecules in this file */ +int GroTop::nonBondedFunctionType() const { return nb_func_type; } - if (not lines.isEmpty()) - { - warnings.append(QString("Ignoring %1 'nonbond_params' lines").arg(lines.count())); - warnings.append(QString("e.g. the first ignored nonbond_params line is")); - warnings.append(lines[0]); - } +/** Return the combining rules to use for the molecules in this file */ +int GroTop::combiningRules() const { return combining_rule; } - return warnings; - }; +/** Return the Lennard Jones fudge factor for the molecules in this file */ +double GroTop::fudgeLJ() const { return fudge_lj; } - // internal function to process the cmaptypes lines - auto processCMAPTypes = [&]() - { - QStringList warnings; +/** Return the electrostatic fudge factor for the molecules in this file */ +double GroTop::fudgeQQ() const { return fudge_qq; } + +/** Return whether or not the non-bonded pairs should be automatically generated + for the molecules in this file */ +bool GroTop::generateNonBondedPairs() const { return generate_pairs; } + +/** Return the expanded set of lines (after preprocessing) */ +const QVector &GroTop::expandedLines() const { return expanded_lines; } - // get all 'cmaptypes' lines - const auto lines = getAllLines("cmaptypes"); +/** Public function used to return the list of post-processed lines */ +QStringList GroTop::postprocessedLines() const { + return expanded_lines.toList(); +} - // save into a database of cmap parameters - the index is the - // combination of the five atom types for the matching atoms - QHash cmaps; +/** Internal function, called by ::interpret() that processes all of the data + from all of the directives, returning a set of warnings */ +QStringList GroTop::processDirectives(const QMap &taglocs, + const QHash &ntags) { + // internal function that returns the lines associated with the + // specified directive + auto getLines = [&](const QString &directive, int n) -> QStringList { + if (n >= ntags.value(directive, 0)) { + return QStringList(); + } - for (const auto &line : lines) - { - // each line should contain the atom types of the five atoms, - // followed by the function type number (1), followed by the - // number of rows and columns, followed by num_rows*num_cols - // values for the cmap function - const auto words = line.split(" ", Qt::SkipEmptyParts); - - if (words.count() < 8) - { - warnings.append(QObject::tr("There is not enough data on the " - "line '%1' to extract a Gromacs CMAP parameter. Skipping line.") - .arg(line)); - continue; - } + bool found = false; + int start = 0; + int end = expandedLines().count(); - // first, get the five atom types - const auto &atm0 = words[0]; - const auto &atm1 = words[1]; - const auto &atm2 = words[2]; - const auto &atm3 = words[3]; - const auto &atm4 = words[4]; + // find the tag + for (auto it = taglocs.constBegin(); it != taglocs.constEnd(); ++it) { + if (it.value() == directive) { + if (n == 0) { + found = true; + start = it.key() + 1; - // now, get the function type - bool ok; + ++it; - int func_type = words[5].toInt(&ok); + if (it != taglocs.constEnd()) { + end = it.key(); + } - if (not ok) - { - warnings.append(QObject::tr("Unable to determine the function type " - "for the cmap on line '%1'. Skipping line.") - .arg(line)); - continue; - } + break; + } else + n -= 1; + } + } - // gromacs currently only supports function type 1 - so do we! - if (func_type != 1) - { - warnings.append(QObject::tr("The function type for the cmap on line '%1' is not supported. " - "Only function type 1 is supported. Skipping line.") - .arg(line)); - continue; - } + if (not found) + throw SireError::program_bug( + QObject::tr( + "Cannot find tag '%1' at index '%2'. This should not happen!") + .arg(directive) + .arg(n), + CODELOC); - // now get the number of rows and columns - int nrows = words[6].toInt(&ok); + QStringList lines; - if (not ok) - { - warnings.append(QObject::tr("Unable to determine the number of rows " - "for the cmap on line '%1'. Skipping line.") - .arg(line)); - continue; - } + for (int i = start; i < end; ++i) { + lines.append(expandedLines().constData()[i]); + } - int ncols = words[7].toInt(&ok); + return lines; + }; - if (not ok) - { - warnings.append(QObject::tr("Unable to determine the number of columns " - "for the cmap on line '%1'. Skipping line.") - .arg(line)); - continue; - } + // return the lines associated with the directive at line 'linenum' + auto getDirectiveLines = [&](int linenum) -> QStringList { + auto it = taglocs.constFind(linenum); - // there should be nrows*ncols values after this - if (words.count() != 8 + nrows * ncols) - { - warnings.append(QObject::tr("The number of values for the cmap on line '%1' is not correct. " - "There should be %2 values, but there are %3. Skipping line.") - .arg(line) - .arg(nrows * ncols) - .arg(words.count() - 8)); - continue; - } + if (it == taglocs.constEnd()) + throw SireError::program_bug( + QObject::tr("Cannot find a tag associated with line '%1'. This " + "should not happen!") + .arg(linenum), + CODELOC); - // just do some DOS protection, should now have more than 512 rows or columns - if (nrows > 512 or ncols > 512) - { - warnings.append(QObject::tr("The number of rows (%1) or columns (%2) for the cmap on line '%3' is too large. " - "Skipping line.") - .arg(nrows) - .arg(ncols) - .arg(line)); - continue; - } + int start = it.key() + 1; + int end = expandedLines().count(); - // check that the number of rows and columns is not negative or zero - if (nrows <= 0 or ncols <= 0) - { - warnings.append(QObject::tr("The number of rows (%1) or columns (%2) for the cmap on line '%3' is not positive. " - "Skipping line.") - .arg(nrows) - .arg(ncols) - .arg(line)); - continue; - } + ++it; - // we can now read in the cmap values - QVector cmap_values(nrows * ncols); - auto *cmap_values_data = cmap_values.data(); + if (it != taglocs.constEnd()) + end = it.key(); - ok = true; + QStringList lines; - for (int i = 0; i < nrows * ncols; ++i) - { - bool val_ok = true; - double value = words[8 + i].toDouble(&val_ok); + for (int i = start; i < end; ++i) { + lines.append(expandedLines().constData()[i]); + } - if (not val_ok) - { - warnings.append(QObject::tr("Unable to read the value %1 for the cmap on line '%2'. Skipping line.") - .arg(i) - .arg(line)); - ok = false; - break; - } + return lines; + }; - cmap_values_data[i] = value; - } + // return all of the lines associated with all copies of the passed directive + auto getAllLines = [&](const QString &directive) -> QStringList { + QStringList lines; - if (not ok) - { - continue; - } + for (int i = 0; i < ntags.value(directive, 0); ++i) { + lines += getLines(directive, i); + } - CMAPParameter cmap(Array2D::fromColumnMajorVector(cmap_values, nrows, ncols)); + return lines; + }; + + // interpret a bool from the passed string + auto gromacs_toBool = [&](const QString &word, bool *ok) { + QString w = word.toLower(); + + if (ok) + *ok = true; + + if (w == "yes" or w == "y" or w == "true" or w == "1") { + return true; + } else if (w == "no" or w == "n" or w == "false" or w == "0") { + return false; + } else { + if (ok) + *ok = false; + return false; + } + }; + + // internal function to process the defaults lines + auto processDefaults = [&]() { + QStringList warnings; + + // there should only be one defaults line + const auto lines = getLines("defaults", 0); + + if (lines.isEmpty()) + throw SireIO::parse_error( + QObject::tr( + "The required data for the '[defaults]' directive in Gromacs is " + "not supplied. This is not a valid Gromacs topology file!"), + CODELOC); + + auto words = lines[0].split(" ", Qt::SkipEmptyParts); + + // there should be five words; non-bonded function type, combinination rule, + // generate pairs, fudge LJ and fudge QQ + if (words.count() < 5) { + throw SireIO::parse_error( + QObject::tr("There is insufficient data for the '[defaults]' line " + "'%1'. This is " + "not a valid Gromacs topology file!") + .arg(lines[0]), + CODELOC); + } + + bool ok; + int nbtyp = words[0].toInt(&ok); + + if (not ok) + throw SireIO::parse_error( + QObject::tr("The first value for the '[defaults]' line '%1' is not " + "an integer. " + "This is not a valid Gromacs topology file!") + .arg(lines[0]), + CODELOC); + + int combrule = words[1].toInt(&ok); + + if (not ok) + throw SireIO::parse_error( + QObject::tr("The second value for the '[defaults]' line '%1' is not " + "an integer. " + "This is not a valid Gromacs topology file!") + .arg(lines[0]), + CODELOC); + + bool gen_pairs = gromacs_toBool(words[2], &ok); + + if (not ok) + throw SireIO::parse_error( + QObject::tr( + "The third value for the '[defaults]' line '%1' is not a yes/no. " + "This is not a valid Gromacs topology file!") + .arg(lines[0]), + CODELOC); + + double lj = words[3].toDouble(&ok); + + if (not ok) + throw SireIO::parse_error( + QObject::tr("The fourth value for the '[defaults]' line '%1' is not " + "a double. " + "This is not a valid Gromacs topology file!") + .arg(lines[0]), + CODELOC); + + double qq = words[4].toDouble(&ok); + + if (not ok) + throw SireIO::parse_error( + QObject::tr( + "The fifth value for the '[defaults]' line '%1' is not a double. " + "This is not a valid Gromacs topology file!") + .arg(lines[0]), + CODELOC); + + // validate and then save these values + if (nbtyp <= 0 or nbtyp > 2) { + warnings.append( + QObject::tr("A non-supported non-bonded function type (%1) " + "is requested.") + .arg(nbtyp)); + } + + if (combrule <= 0 or combrule > 3) { + warnings.append( + QObject::tr("A non-supported combinig rule (%1) is requested!") + .arg(combrule)); + } + + if (lj < 0 or lj > 1) { + warnings.append( + QObject::tr("An invalid value of fudge_lj (%1) is requested!") + .arg(lj)); + + if (lj < 0) + lj = 0; + else if (lj > 1) + lj = 1; + } + + if (qq < 0 or qq > 1) { + warnings.append( + QObject::tr("An invalid value of fudge_qq (%1) is requested!") + .arg(qq)); + + if (qq < 0) + qq = 0; + else if (qq > 1) + qq = 1; + } + + // Gromacs uses a non-exact value of the Amber fudge_qq (1.0/1.2). Correct + // this in case we convert to another file format + if (std::abs(qq - (1.0 / 1.2)) < 0.01) { + qq = 1.0 / 1.2; + } + + nb_func_type = nbtyp; + combining_rule = combrule; + fudge_lj = lj; + fudge_qq = qq; + generate_pairs = gen_pairs; - QString key = get_cmap_id(atm0, atm1, atm2, atm3, atm4, func_type); + return warnings; + }; + + // wildcard atomtype (this is 'X' in gromacs files) + static const QString wildcard_atomtype = "X"; + + // Whether the file uses a "bond_type" atom type. + bool is_bond_type = false; + + // internal function to process the atomtypes lines + auto processAtomTypes = [&]() { + QStringList warnings; + + // get all 'atomtypes' lines + const auto lines = getAllLines("atomtypes"); + + // the database of all atom types + QHash typs; + + // now parse each atom + for (const auto &line : lines) { + const auto words = line.split(" ", Qt::SkipEmptyParts); + + // should either have 2 words (atom type, mass) or + // have 6 words; atom type, mass, charge, type, V, W or + // have 7 words; atom type, atom number, mass, charge, type, V, W + // have 8 words; atom type, bond type, atom number, mass, charge, type, V, + // W + if (words.count() < 2) { + warnings.append(QObject::tr("There is not enough data for the " + "atomtype data '%1'. Skipping this line.") + .arg(line)); + continue; + } + + GromacsAtomType typ; + + if (words.count() < 6) { + // only getting the atom type and mass + bool ok_mass; + double mass = words[1].toDouble(&ok_mass); + + if (not ok_mass) { + warnings.append(QObject::tr("Could not interpret the atom type data " + "from line '%1'. Skipping this line.") + .arg(line)); + continue; + } + + typ = GromacsAtomType(words[0], mass * g_per_mol); + } else if (words.count() < 7) { + bool ok_mass, ok_charge, ok_ptyp, ok_v, ok_w; + double mass = words[1].toDouble(&ok_mass); + double chg = words[2].toDouble(&ok_charge); + auto ptyp = GromacsAtomType::toParticleType(words[3], &ok_ptyp); + double v = words[4].toDouble(&ok_v); + double w = words[5].toDouble(&ok_w); + + if (not(ok_mass and ok_charge and ok_ptyp and ok_v and ok_w)) { + warnings.append(QObject::tr("Could not interpret the atom type data " + "from line '%1'. Skipping this line.") + .arg(line)); + continue; + } + + typ = GromacsAtomType(words[0], mass * g_per_mol, chg * mod_electron, + ptyp, ::toLJParameter(v, w, combining_rule)); + } else if (words.count() < 8) { + bool ok_mass, ok_elem, ok_charge, ok_ptyp, ok_v, ok_w; + int nprotons = words[1].toInt(&ok_elem); + double mass = words[2].toDouble(&ok_mass); + double chg = words[3].toDouble(&ok_charge); + auto ptyp = GromacsAtomType::toParticleType(words[4], &ok_ptyp); + double v = words[5].toDouble(&ok_v); + double w = words[6].toDouble(&ok_w); + + if (not(ok_mass and ok_charge and ok_ptyp and ok_v and ok_w)) { + warnings.append(QObject::tr("Could not interpret the atom type data " + "from line '%1'. Skipping this line.") + .arg(line)); + continue; + } + + if (ok_elem) { + typ = GromacsAtomType(words[0], mass * g_per_mol, chg * mod_electron, + ptyp, ::toLJParameter(v, w, combining_rule), + Element(nprotons)); + } else { + // some gromacs files don't use 'nprotons', but instead give + // a "bond_type" ambertype + typ = GromacsAtomType(words[0], words[1], mass * g_per_mol, + chg * mod_electron, ptyp, + ::toLJParameter(v, w, combining_rule), + Element::elementWithMass(mass * g_per_mol)); + + is_bond_type = true; + } + } else if (words.count() < 9) { + bool ok_mass, ok_elem, ok_charge, ok_ptyp, ok_v, ok_w; + int nprotons = words[2].toInt(&ok_elem); + double mass = words[3].toDouble(&ok_mass); + double chg = words[4].toDouble(&ok_charge); + auto ptyp = GromacsAtomType::toParticleType(words[5], &ok_ptyp); + double v = words[6].toDouble(&ok_v); + double w = words[7].toDouble(&ok_w); + + if (not(ok_mass and ok_charge and ok_ptyp and ok_v and ok_w)) { + warnings.append(QObject::tr("Could not interpret the atom type data " + "from line '%1'. Skipping this line.") + .arg(line)); + continue; + } + + typ = GromacsAtomType( + words[0], words[1], mass * g_per_mol, chg * mod_electron, ptyp, + ::toLJParameter(v, w, combining_rule), Element(nprotons)); + } else { + warnings.append(QObject::tr("The atomtype line '%1' contains more data " + "than is expected!") + .arg(line)); + } + + if (typs.contains(typ.atomType())) { + // only replace if the new type is fully specified + if (typ.hasMassOnly()) + continue; + + warnings.append( + QObject::tr( + "The data for atom type '%1' exists already! " + "This will now be replaced with new data from line '%2'") + .arg(typ.atomType()) + .arg(line)); + } + + typs.insert(typ.atomType(), typ); + } + + // save the database of types + atom_types = typs; - cmaps.insert(key, cmap); - } + return warnings; + }; - cmap_potentials = cmaps; + // internal function to process the bondtypes lines + auto processBondTypes = [&]() { + QStringList warnings; - return warnings; - }; + // get all 'bondtypes' lines + const auto lines = getAllLines("bondtypes"); - // internal function to process moleculetype lines - auto processMoleculeTypes = [&]() - { - QStringList warnings; + // save into a database of bonds + QMultiHash bnds; - // how many moleculetypes are there? Divide them up and get - // the child tags for each moleculetype - QList> moltags; - { - // list of tags that are valid within a moleculetype - - // it is REALLY IMPORTANT that this list is kept up to date, - // as otherwise a new tag will cause the parser to move on to - // parsing a new moleculetype! - const QStringList valid_tags = {"atoms", - "bonds", - "pairs", - "pairs_nb", - "angles", - "dihedrals", - "cmap", - "exclusions", - "contraints", - "settles", - "virtual_sites2", - "virtual_sitesn", - "position_restraints", - "distance_restraints", - "orientation_restraints", - "angle_restraints", - "angle_restraints_z"}; - - auto it = taglocs.constBegin(); - - while (it != taglocs.constEnd()) - { - if (it.value() == "moleculetype") - { - // we have found another molecule - save the location - // of all of its child tags - QMultiHash tags; - tags.insert(it.value(), it.key()); - ++it; - - while (it != taglocs.constEnd()) - { - // save all child tags until we reach the end - // of definition of this moleculetype - if (valid_tags.contains(it.value())) - { - // this is a valid child tag - save its location - //(note that a tag can exist multiple times!) - tags.insert(it.value(), it.key()); - ++it; - } - else if (it.value() == "moleculetype") - { - // this is the next molecule - break; - } - else - { - // this is the end of the 'moleculetype' - ++it; - break; - } - } + for (const auto &line : lines) { + // each line should contain the atom types of the two atoms, then + // the function type, then the parameters for the function + const auto words = line.split(" ", Qt::SkipEmptyParts); - moltags.append(tags); - } - else - { - ++it; - } - } - } + if (words.count() < 3) { + warnings.append( + QObject::tr( + "There is not enough data on the " + "line '%1' to extract a Gromacs bond parameter. Skipping line.") + .arg(line)); + continue; + } - // now we define a set of functions that are needed to parse the - // various child tags + const auto atm0 = words[0]; + const auto atm1 = words[1]; - // function that extract the metadata about the moleculetype - // and returns it as a 'GroMolType' object - auto getMolType = [&](int linenum) - { - GroMolType moltype; - - // get the directives for this molecule - there should be - // one line that contains the name and number of excluded atoms - const auto lines = getDirectiveLines(linenum); - - if (lines.count() != 1) - { - moltype.addWarning(QObject::tr("Expecting only one line that " - "provides the name and number of excluded atoms for this moleculetype. " - "Instead, the number of lines is %1 : [\n%2\n]") - .arg(lines.count()) - .arg(lines.join("\n"))); - } + bool ok; + int func_type = words[2].toInt(&ok); - if (lines.count() > 0) - { - // try to read the infromation from the first line only - const auto words = lines[0].split(" "); - - if (words.count() != 2) - { - moltype.addWarning(QObject::tr("Expecting two words for the " - "moleculetype line, containing the name and number of excluded " - "atoms. Instead we get '%1'") - .arg(lines[0])); - } + if (not ok) { + warnings.append(QObject::tr("Unable to determine the function type " + "for the bond on line '%1'. Skipping line.") + .arg(line)); + continue; + } - if (words.count() > 0) - { - moltype.setName(words[0]); - } + // now read in all of the remaining values as numbers... + QList params; - if (words.count() > 1) - { - bool ok; - qint64 nexcl = words[1].toInt(&ok); - - if (not ok) - { - moltype.addWarning(QObject::tr("Expecting the second word in " - "the moleculetype line '%1' to be the number of excluded " - "atoms. It isn't!") - .arg(lines[0])); - } - else - { - moltype.setNExcludedAtoms(nexcl); - } - } - } + for (int i = 3; i < words.count(); ++i) { + double param = words[i].toDouble(&ok); - return moltype; - }; + if (ok) + params.append(param); + } - // function that extracts all of the information from the 'atoms' lines - // and adds it to the passed GroMolType - auto addAtomsTo = [&](GroMolType &moltype, int linenum) - { - QStringList lines = getDirectiveLines(linenum); - - for (const auto &line : lines) - { - // each line should contain index number, atom type, - // residue number, residue name, atom name, charge group number, - // charge (mod_electron) and mass (atomic mass) - const auto words = line.split(" "); - - if (words.count() < 6) - { - moltype.addWarning(QObject::tr("Cannot extract atom information " - "from the line '%1' as it should contain at least six words " - "(pieces of information)") - .arg(line)); - - continue; - } - else if (words.count() > 8) - { - moltype.addWarning(QObject::tr("The line containing atom information " - "'%1' contains more information than can be parsed. It should only " - "contain six-eight words (pieces of information)") - .arg(line)); - } + GromacsBond bond; - bool ok_idx, ok_resnum, ok_chggrp, ok_chg, ok_mass; + try { + bond = GromacsBond(func_type, params); + } catch (const SireError::exception &e) { + warnings.append( + QObject::tr("Unable to extract the correct information " + "to form a bond from line '%1'. Error is '%2'") + .arg(line) + .arg(e.error())); + continue; + } - const qint64 atomnum = words[0].toInt(&ok_idx); - const auto atomtyp = words[1]; - auto resnum = words[2].toInt(&ok_resnum); - QString chainname; + QString key = get_bond_id(atm0, atm1, bond.functionType()); + bnds.insert(key, bond); + } - if (not ok_resnum) - { + bond_potentials = bnds; - // could be residue_numberChain_name - const QRegularExpression re("(\\-?\\d+)([\\w\\d]*)"); + return warnings; + }; - auto m = re.match(words[2]); + // internal function to process the pairtypes lines + auto processPairTypes = [&]() { + QStringList warnings; - if (m.hasMatch()) - { - resnum = m.captured(1).toInt(&ok_resnum); - chainname = m.captured(2); - } - } + // get all 'bondtypes' lines + const auto lines = getAllLines("pairtypes"); - const auto resnam = words[3]; - const auto atmnam = words[4]; - const qint64 chggrp = words[5].toInt(&ok_chggrp); - - double chg = 0; - ok_chg = true; - if (words.count() > 6) - chg = words[6].toDouble(&ok_chg); - - double mass = 0; - ok_mass = true; - bool found_mass = false; - if (words.count() > 7) - { - mass = words[7].toDouble(&ok_mass); - found_mass = true; - } + if (not lines.isEmpty()) { + warnings.append( + QString("Ignoring %1 pairtypes lines.").arg(lines.count())); + warnings.append(QString("e.g. the first ignored pairtypes line is")); + warnings.append(lines[0]); + } - if (not(ok_idx and ok_resnum and ok_chggrp and ok_chg and ok_mass)) - { - moltype.addWarning(QObject::tr("Could not interpret the necessary " - "atom information from the line '%1' | %2 %3 %4 %5 %6") - .arg(line) - .arg(ok_idx) - .arg(ok_resnum) - .arg(ok_chggrp) - .arg(ok_chg) - .arg(ok_mass)); - continue; - } + return warnings; + }; - GroAtom atom; - atom.setNumber(atomnum); - atom.setAtomType(atomtyp); - atom.setResidueNumber(resnum); - atom.setResidueName(resnam); - atom.setChainName(chainname); - atom.setName(atmnam); - atom.setChargeGroup(chggrp); - atom.setCharge(chg * mod_electron); - atom.setMass(mass * g_per_mol); - - // we now need to look up the atom type of this atom to see if there - // is a separate bond_type - auto atom_type = atom_types.value(atomtyp); - - if ((not atom_type.isNull()) and atom_type.bondType() != atomtyp) - { - atom.setBondType(atom_type.bondType()); - } + // internal function to process the angletypes lines + auto processAngleTypes = [&]() { + QStringList warnings; - // now do the same to assign the mass if it has not been given explicitly - if ((not found_mass) and (not atom_type.isNull())) - { - atom.setMass(atom_type.mass()); - } + // get all 'bondtypes' lines + const auto lines = getAllLines("angletypes"); - if (found_mass and is_bond_type) - { - if (mass > 0 and atom_type.element() == Element("Xx")) - { - // Set the element of the atom type using the mass and - // update the record in the dictionary. - atom_type.setElement(Element::elementWithMass(mass * g_per_mol)); - this->atom_types[atomtyp] = atom_type; - } - } + // save into a database of angles + QMultiHash angs; - moltype.addAtom(atom); - } - }; + for (const auto &line : lines) { + // each line should contain the atom types of the three atoms, then + // the function type, then the parameters for the function + const auto words = line.split(" ", Qt::SkipEmptyParts); - // function that extracts all of the information from the 'bonds' lines - auto addBondsTo = [&](GroMolType &moltype, int linenum) - { - QStringList lines = getDirectiveLines(linenum); - - QMultiHash bonds; - bonds.reserve(lines.count()); - - for (const auto &line : lines) - { - const auto words = line.split(" "); - - if (words.count() < 2) - { - moltype.addWarning(QObject::tr("Cannot extract bond information " - "from the line '%1' as it should contain at least two words " - "(pieces of information)") - .arg(line)); - continue; - } + if (words.count() < 4) { + warnings.append(QObject::tr("There is not enough data on the " + "line '%1' to extract a Gromacs angle " + "parameter. Skipping line.") + .arg(line)); + continue; + } - bool ok0, ok1; + const auto atm0 = words[0]; + const auto atm1 = words[1]; + const auto atm2 = words[2]; - int atm0 = words[0].toInt(&ok0); - int atm1 = words[1].toInt(&ok1); + bool ok; + int func_type = words[3].toInt(&ok); - if (not(ok0 and ok1)) - { - moltype.addWarning(QObject::tr("Cannot extract bond information " - "from the line '%1' as the first two words need to be integers. ") - .arg(line)); - continue; - } + if (not ok) { + warnings.append( + QObject::tr("Unable to determine the function type " + "for the angle on line '%1'. Skipping line.") + .arg(line)); + continue; + } - // now see if any information about the bond is provided... - GromacsBond bond; + // now read in all of the remaining values as numbers... + QList params; - if (words.count() > 2) - { - bool ok; - int func_type = words[2].toInt(&ok); + for (int i = 4; i < words.count(); ++i) { + double param = words[i].toDouble(&ok); - if (not ok) - { - moltype.addWarning(QObject::tr("Unable to extract the correct " - "information to form a bond from line '%1' as the third word " - "is not an integer.") - .arg(line)); - continue; - } + if (ok) + params.append(param); + } - if (words.count() > 3) - { - // now read in all of the remaining values as numbers... - QList params; + GromacsAngle angle; - for (int i = 3; i < words.count(); ++i) - { - double param = words[i].toDouble(&ok); + try { + angle = GromacsAngle(func_type, params); + } catch (const SireError::exception &e) { + warnings.append( + QObject::tr("Unable to extract the correct information " + "to form an angle from line '%1'. Error is '%2'") + .arg(line) + .arg(e.error())); + continue; + } - if (ok) - params.append(param); - } + QString key = get_angle_id(atm0, atm1, atm2, angle.functionType()); + angs.insert(key, angle); + } - try - { - bond = GromacsBond(func_type, params); - } - catch (const SireError::exception &e) - { - moltype.addWarning(QObject::tr("Unable to extract the correct " - "information to form a bond from line '%1'. Error is '%2'") - .arg(line) - .arg(e.error())); - continue; - } - } - else - { - bond = GromacsBond(func_type); - } - } + ang_potentials = angs; - bonds.insert(BondID(AtomNum(atm0), AtomNum(atm1)), bond); - } + return warnings; + }; - // save the bonds in the molecule - moltype.addBonds(bonds); - }; + // internal function to process the dihedraltypes lines + auto processDihedralTypes = [&]() { + QStringList warnings; - // function that extracts all of the information from the 'angles' lines - auto addAnglesTo = [&](GroMolType &moltype, int linenum) - { - QStringList lines = getDirectiveLines(linenum); - - QMultiHash angs; - angs.reserve(lines.count()); - - for (const auto &line : lines) - { - const auto words = line.split(" "); - - if (words.count() < 3) - { - moltype.addWarning(QObject::tr("Cannot extract angle information " - "from the line '%1' as it should contain at least three words " - "(pieces of information)") - .arg(line)); - continue; - } + // get all 'bondtypes' lines + const auto lines = getAllLines("dihedraltypes"); - bool ok0, ok1, ok2; + // save into a database of dihedrals + QMultiHash dihs; - int atm0 = words[0].toInt(&ok0); - int atm1 = words[1].toInt(&ok1); - int atm2 = words[2].toInt(&ok2); + for (const auto &line : lines) { + // each line should contain the atom types of the four atoms, then + // the function type, then the parameters for the function. + //(however, some files have the atom types of just two atoms, which + // I assume are the two middle atoms of the dihedral...) + const auto words = line.split(" ", Qt::SkipEmptyParts); - if (not(ok0 and ok1 and ok2)) - { - moltype.addWarning(QObject::tr("Cannot extract angle information " - "from the line '%1' as the first three words need to be integers. ") - .arg(line)); - continue; - } + if (words.count() < 3) { + warnings.append(QObject::tr("There is not enough data on the " + "line '%1' to extract a Gromacs dihedral " + "parameter. Skipping line.") + .arg(line)); + continue; + } - // now see if any information about the angle is provided... - GromacsAngle angle; + // first, let's try to parse this assuming that it is a 2-atom dihedral + // line... + //(most cases this should fail) + auto atm0 = wildcard_atomtype; + auto atm1 = words[0]; + auto atm2 = words[1]; + auto atm3 = wildcard_atomtype; - if (words.count() > 3) - { - bool ok; - int func_type = words[3].toInt(&ok); + GromacsDihedral dihedral; - if (not ok) - { - moltype.addWarning(QObject::tr("Unable to extract the correct " - "information to form an angle from line '%1' as the fourth word " - "is not an integer.") - .arg(line)); - continue; - } + bool ok; + int func_type = words[2].toInt(&ok); - if (words.count() > 4) - { - // now read in all of the remaining values as numbers... - QList params; + if (ok) { + // this may be a two-atom dihedral - read in the rest of the parameters + QList params; - for (int i = 4; i < words.count(); ++i) - { - double param = words[i].toDouble(&ok); + for (int i = 3; i < words.count(); ++i) { + double param = words[i].toDouble(&ok); + if (ok) + params.append(param); + } - if (ok) - params.append(param); - } + try { + dihedral = GromacsDihedral(func_type, params); + ok = true; + } catch (...) { + ok = false; + } + } - try - { - angle = GromacsAngle(func_type, params); - } - catch (const SireError::exception &e) - { - moltype.addWarning(QObject::tr("Unable to extract the correct " - "information to form an angle from line '%1'. Error is '%2'") - .arg(line) - .arg(e.error())); - continue; - } - } - else - { - angle = GromacsAngle(func_type); - } - } + if (not ok) { + // we couldn't parse as a two-atom dihedral, so parse as a four-atom + // dihedral - angs.insert(AngleID(AtomNum(atm0), AtomNum(atm1), AtomNum(atm2)), angle); - } + if (words.count() < 5) { + warnings.append(QObject::tr("There is not enough data on the " + "line '%1' to extract a Gromacs dihedral " + "parameter. Skipping line.") + .arg(line)); + continue; + } - // save the angles in the molecule - moltype.addAngles(angs); - }; + atm0 = words[0]; + atm1 = words[1]; + atm2 = words[2]; + atm3 = words[3]; - // function that extracts all of the information from the 'dihedrals' lines - auto addDihedralsTo = [&](GroMolType &moltype, int linenum) - { - QStringList lines = getDirectiveLines(linenum); - - QMultiHash dihs; - dihs.reserve(lines.count()); - - for (const auto &line : lines) - { - const auto words = line.split(" "); - - if (words.count() < 4) - { - moltype.addWarning(QObject::tr("Cannot extract dihedral information " - "from the line '%1' as it should contain at least four words " - "(pieces of information)") - .arg(line)); - continue; - } + bool ok; + int func_type = words[4].toInt(&ok); - bool ok0, ok1, ok2, ok3; + if (not ok) { + warnings.append( + QObject::tr("Unable to determine the function type " + "for the dihedral on line '%1'. Skipping line.") + .arg(line)); + continue; + } - int atm0 = words[0].toInt(&ok0); - int atm1 = words[1].toInt(&ok1); - int atm2 = words[2].toInt(&ok2); - int atm3 = words[3].toInt(&ok3); + // now read in all of the remaining values as numbers... + QList params; - if (not(ok0 and ok1 and ok2 and ok3)) - { - moltype.addWarning(QObject::tr("Cannot extract dihedral information " - "from the line '%1' as the first four words need to be integers. ") - .arg(line)); - continue; - } + for (int i = 5; i < words.count(); ++i) { + double param = words[i].toDouble(&ok); - // now see if any information about the dihedral is provided... - GromacsDihedral dihedral; - - if (words.count() > 4) - { - bool ok; - int func_type = words[4].toInt(&ok); - - if (not ok) - { - moltype.addWarning( - QObject::tr("Unable to extract the correct " - "information to form a dihedral from line '%1' as the fifth word " - "is not an integer.") - .arg(line)); - continue; - } + if (ok) + params.append(param); + } - if (words.count() > 5) - { - // now read in all of the remaining values as numbers... - QList params; + try { + dihedral = GromacsDihedral(func_type, params); + } catch (const SireError::exception &e) { + warnings.append( + QObject::tr("Unable to extract the correct information " + "to form a dihedral from line '%1'. Error is '%2'") + .arg(line) + .arg(e.error())); + continue; + } + } - for (int i = 5; i < words.count(); ++i) - { - double param = words[i].toDouble(&ok); + QString key = + get_dihedral_id(atm0, atm1, atm2, atm3, dihedral.functionType()); + dihs.insert(key, dihedral); + } - if (ok) - params.append(param); - } + dih_potentials = dihs; - try - { - dihedral = GromacsDihedral(func_type, params); - } - catch (const SireError::exception &e) - { - moltype.addWarning( - QObject::tr("Unable to extract the correct " - "information to form a dihedral from line '%1'. Error is '%2'") - .arg(line) - .arg(e.error())); - continue; - } - } - else - { - dihedral = GromacsDihedral(func_type); - } - } + return warnings; + }; - dihs.insert(DihedralID(AtomNum(atm0), AtomNum(atm1), AtomNum(atm2), AtomNum(atm3)), dihedral); - } + // internal function to process the constrainttypes lines + auto processConstraintTypes = [&]() { + QStringList warnings; - // save the dihedrals in the molecule - moltype.addDihedrals(dihs); - }; - - // function that extracts all of the information from the 'cmap' lines - // function that extracts explicit 1-4 pair scale factors from the 'pairs' lines. - // funct=1 pairs are standard (use global fudge_qq/fudge_lj) and are handled - // automatically by gen-pairs, so we only need to store funct=2 explicit pairs. - // funct=2 format: ai aj 2 fudgeQQ qi qj sigma epsilon - // The LJ scale is 1.0 for funct=2 because sigma/epsilon are the full combined values. - auto addPairsTo = [&](GroMolType &moltype, int linenum) - { - QStringList lines = getDirectiveLines(linenum); - - for (const auto &line : lines) - { - const auto words = line.split(" "); - - if (words.count() < 3) - { - moltype.addWarning(QObject::tr("Cannot extract pair information " - "from the line '%1' as it should contain at least three words.") - .arg(line)); - continue; - } + // get all 'bondtypes' lines + const auto lines = getAllLines("constrainttypes"); - bool ok0, ok1, ok2; + if (not lines.isEmpty()) { + warnings.append( + QString("Ignoring %1 'constrainttypes' lines").arg(lines.count())); + warnings.append( + QString("e.g. the first ignored constrainttypes line is")); + warnings.append(lines[0]); + } - int atm0 = words[0].toInt(&ok0); - int atm1 = words[1].toInt(&ok1); - int funct = words[2].toInt(&ok2); + return warnings; + }; - if (not(ok0 and ok1 and ok2)) - { - moltype.addWarning(QObject::tr("Cannot extract pair information " - "from the line '%1' as the first three words need to be integers.") - .arg(line)); - continue; - } + // internal function to process the nonbond_params lines + auto processNonBondParams = [&]() { + QStringList warnings; - if (funct == 1) - { - // Standard pair: uses global fudge_qq/fudge_lj. - // The gen-pairs mechanism already handles these, so no explicit storage needed. - continue; - } - else if (funct == 2) - { - // Explicit pair: ai aj 2 fudgeQQ qi qj sigma epsilon - // The fudgeQQ is the coulomb scale factor; LJ params are used directly (lj_scl = 1.0). - double cscl = fudge_qq; // default to global fudge_qq if not specified - if (words.count() > 3) - { - bool ok; - double val = words[3].toDouble(&ok); - if (ok) - cscl = val; - } + // get all 'bondtypes' lines + const auto lines = getAllLines("nonbond_params"); - moltype.addExplicitPair(BondID(AtomNum(atm0), AtomNum(atm1)), cscl, 1.0); - } - else - { - moltype.addWarning(QObject::tr("Unsupported pair function type %1 in line '%2'. " - "Only funct=1 and funct=2 are supported.") - .arg(funct) - .arg(line)); - } - } - }; + if (not lines.isEmpty()) { + warnings.append( + QString("Ignoring %1 'nonbond_params' lines").arg(lines.count())); + warnings.append(QString("e.g. the first ignored nonbond_params line is")); + warnings.append(lines[0]); + } - auto addCMAPsTo = [&](GroMolType &moltype, int linenum) - { - QStringList lines = getDirectiveLines(linenum); - - QHash cmaps; - cmaps.reserve(lines.count()); - - for (const auto &line : lines) - { - const auto words = line.split(" "); - - if (words.count() < 6) - { - moltype.addWarning(QObject::tr("Cannot extract CMAP information " - "from the line '%1' as it should contain at least six words " - "(pieces of information)") - .arg(line)); - continue; - } + return warnings; + }; + + // internal function to process the cmaptypes lines + auto processCMAPTypes = [&]() { + QStringList warnings; + + // get all 'cmaptypes' lines + const auto lines = getAllLines("cmaptypes"); + + // save into a database of cmap parameters - the index is the + // combination of the five atom types for the matching atoms + QHash cmaps; + + for (const auto &line : lines) { + // each line should contain the atom types of the five atoms, + // followed by the function type number (1), followed by the + // number of rows and columns, followed by num_rows*num_cols + // values for the cmap function + const auto words = line.split(" ", Qt::SkipEmptyParts); + + if (words.count() < 8) { + warnings.append( + QObject::tr( + "There is not enough data on the " + "line '%1' to extract a Gromacs CMAP parameter. Skipping line.") + .arg(line)); + continue; + } + + // first, get the five atom types + const auto &atm0 = words[0]; + const auto &atm1 = words[1]; + const auto &atm2 = words[2]; + const auto &atm3 = words[3]; + const auto &atm4 = words[4]; + + // now, get the function type + bool ok; + + int func_type = words[5].toInt(&ok); + + if (not ok) { + warnings.append(QObject::tr("Unable to determine the function type " + "for the cmap on line '%1'. Skipping line.") + .arg(line)); + continue; + } + + // gromacs currently only supports function type 1 - so do we! + if (func_type != 1) { + warnings.append( + QObject::tr( + "The function type for the cmap on line '%1' is not supported. " + "Only function type 1 is supported. Skipping line.") + .arg(line)); + continue; + } + + // now get the number of rows and columns + int nrows = words[6].toInt(&ok); + + if (not ok) { + warnings.append(QObject::tr("Unable to determine the number of rows " + "for the cmap on line '%1'. Skipping line.") + .arg(line)); + continue; + } + + int ncols = words[7].toInt(&ok); + + if (not ok) { + warnings.append(QObject::tr("Unable to determine the number of columns " + "for the cmap on line '%1'. Skipping line.") + .arg(line)); + continue; + } + + // there should be nrows*ncols values after this + if (words.count() != 8 + nrows * ncols) { + warnings.append( + QObject::tr( + "The number of values for the cmap on line '%1' is not " + "correct. " + "There should be %2 values, but there are %3. Skipping line.") + .arg(line) + .arg(nrows * ncols) + .arg(words.count() - 8)); + continue; + } + + // just do some DOS protection, should now have more than 512 rows or + // columns + if (nrows > 512 or ncols > 512) { + warnings.append(QObject::tr("The number of rows (%1) or columns (%2) " + "for the cmap on line '%3' is too large. " + "Skipping line.") + .arg(nrows) + .arg(ncols) + .arg(line)); + continue; + } + + // check that the number of rows and columns is not negative or zero + if (nrows <= 0 or ncols <= 0) { + warnings.append( + QObject::tr("The number of rows (%1) or columns (%2) for the cmap " + "on line '%3' is not positive. " + "Skipping line.") + .arg(nrows) + .arg(ncols) + .arg(line)); + continue; + } + + // we can now read in the cmap values + QVector cmap_values(nrows * ncols); + auto *cmap_values_data = cmap_values.data(); + + ok = true; + + for (int i = 0; i < nrows * ncols; ++i) { + bool val_ok = true; + double value = words[8 + i].toDouble(&val_ok); + + if (not val_ok) { + warnings.append(QObject::tr("Unable to read the value %1 for the " + "cmap on line '%2'. Skipping line.") + .arg(i) + .arg(line)); + ok = false; + break; + } + + cmap_values_data[i] = value; + } + + if (not ok) { + continue; + } + + CMAPParameter cmap( + Array2D::fromColumnMajorVector(cmap_values, nrows, ncols)); + + QString key = get_cmap_id(atm0, atm1, atm2, atm3, atm4, func_type); + + cmaps.insert(key, cmap); + } + + cmap_potentials = cmaps; - bool ok0, ok1, ok2, ok3, ok4, ok5; - - int atm0 = words[0].toInt(&ok0); - int atm1 = words[1].toInt(&ok1); - int atm2 = words[2].toInt(&ok2); - int atm3 = words[3].toInt(&ok3); - int atm4 = words[4].toInt(&ok4); - int func = words[5].toInt(&ok5); - - if (not(ok0 and ok1 and ok2 and ok3 and ok4 and ok5)) - { - moltype.addWarning(QObject::tr("Cannot extract CMAP information " - "from the line '%1' as the first six words need to be integers. ") - .arg(line)); - continue; - } + return warnings; + }; + + // internal function to process moleculetype lines + auto processMoleculeTypes = [&]() { + QStringList warnings; + + // how many moleculetypes are there? Divide them up and get + // the child tags for each moleculetype + QList> moltags; + { + // list of tags that are valid within a moleculetype - + // it is REALLY IMPORTANT that this list is kept up to date, + // as otherwise a new tag will cause the parser to move on to + // parsing a new moleculetype! + const QStringList valid_tags = {"atoms", + "bonds", + "pairs", + "pairs_nb", + "angles", + "dihedrals", + "cmap", + "exclusions", + "contraints", + "settles", + "virtual_sites2", + "virtual_sitesn", + "position_restraints", + "distance_restraints", + "orientation_restraints", + "angle_restraints", + "angle_restraints_z"}; + + auto it = taglocs.constBegin(); + + while (it != taglocs.constEnd()) { + if (it.value() == "moleculetype") { + // we have found another molecule - save the location + // of all of its child tags + QMultiHash tags; + tags.insert(it.value(), it.key()); + ++it; + + while (it != taglocs.constEnd()) { + // save all child tags until we reach the end + // of definition of this moleculetype + if (valid_tags.contains(it.value())) { + // this is a valid child tag - save its location + //(note that a tag can exist multiple times!) + tags.insert(it.value(), it.key()); + ++it; + } else if (it.value() == "moleculetype") { + // this is the next molecule + break; + } else { + // this is the end of the 'moleculetype' + ++it; + break; + } + } + + moltags.append(tags); + } else { + ++it; + } + } + } + + // now we define a set of functions that are needed to parse the + // various child tags + + // function that extract the metadata about the moleculetype + // and returns it as a 'GroMolType' object + auto getMolType = [&](int linenum) { + GroMolType moltype; + + // get the directives for this molecule - there should be + // one line that contains the name and number of excluded atoms + const auto lines = getDirectiveLines(linenum); + + if (lines.count() != 1) { + moltype.addWarning( + QObject::tr("Expecting only one line that " + "provides the name and number of excluded atoms for " + "this moleculetype. " + "Instead, the number of lines is %1 : [\n%2\n]") + .arg(lines.count()) + .arg(lines.join("\n"))); + } + + if (lines.count() > 0) { + // try to read the infromation from the first line only + const auto words = lines[0].split(" "); + + if (words.count() != 2) { + moltype.addWarning(QObject::tr("Expecting two words for the " + "moleculetype line, containing the " + "name and number of excluded " + "atoms. Instead we get '%1'") + .arg(lines[0])); + } + + if (words.count() > 0) { + moltype.setName(words[0]); + } + + if (words.count() > 1) { + bool ok; + qint64 nexcl = words[1].toInt(&ok); + + if (not ok) { + moltype.addWarning( + QObject::tr( + "Expecting the second word in " + "the moleculetype line '%1' to be the number of excluded " + "atoms. It isn't!") + .arg(lines[0])); + } else { + moltype.setNExcludedAtoms(nexcl); + } + } + } + + return moltype; + }; - cmaps.insert(CMAPID(AtomNum(atm0), AtomNum(atm1), AtomNum(atm2), AtomNum(atm3), AtomNum(atm4)), - QString::number(func)); - } + // function that extracts all of the information from the 'atoms' lines + // and adds it to the passed GroMolType + auto addAtomsTo = [&](GroMolType &moltype, int linenum) { + QStringList lines = getDirectiveLines(linenum); + + for (const auto &line : lines) { + // each line should contain index number, atom type, + // residue number, residue name, atom name, charge group number, + // charge (mod_electron) and mass (atomic mass) + const auto words = line.split(" "); + + if (words.count() < 6) { + moltype.addWarning( + QObject::tr( + "Cannot extract atom information " + "from the line '%1' as it should contain at least six words " + "(pieces of information)") + .arg(line)); + + continue; + } else if (words.count() > 8) { + moltype.addWarning( + QObject::tr("The line containing atom information " + "'%1' contains more information than can be parsed. " + "It should only " + "contain six-eight words (pieces of information)") + .arg(line)); + } + + bool ok_idx, ok_resnum, ok_chggrp, ok_chg, ok_mass; + + const qint64 atomnum = words[0].toInt(&ok_idx); + const auto atomtyp = words[1]; + auto resnum = words[2].toInt(&ok_resnum); + QString chainname; + + if (not ok_resnum) { + + // could be residue_numberChain_name + const QRegularExpression re("(\\-?\\d+)([\\w\\d]*)"); + + auto m = re.match(words[2]); + + if (m.hasMatch()) { + resnum = m.captured(1).toInt(&ok_resnum); + chainname = m.captured(2); + } + } + + const auto resnam = words[3]; + const auto atmnam = words[4]; + const qint64 chggrp = words[5].toInt(&ok_chggrp); + + double chg = 0; + ok_chg = true; + if (words.count() > 6) + chg = words[6].toDouble(&ok_chg); + + double mass = 0; + ok_mass = true; + bool found_mass = false; + if (words.count() > 7) { + mass = words[7].toDouble(&ok_mass); + found_mass = true; + } + + if (not(ok_idx and ok_resnum and ok_chggrp and ok_chg and ok_mass)) { + moltype.addWarning( + QObject::tr( + "Could not interpret the necessary " + "atom information from the line '%1' | %2 %3 %4 %5 %6") + .arg(line) + .arg(ok_idx) + .arg(ok_resnum) + .arg(ok_chggrp) + .arg(ok_chg) + .arg(ok_mass)); + continue; + } + + GroAtom atom; + atom.setNumber(atomnum); + atom.setAtomType(atomtyp); + atom.setResidueNumber(resnum); + atom.setResidueName(resnam); + atom.setChainName(chainname); + atom.setName(atmnam); + atom.setChargeGroup(chggrp); + atom.setCharge(chg * mod_electron); + atom.setMass(mass * g_per_mol); + + // we now need to look up the atom type of this atom to see if there + // is a separate bond_type + auto atom_type = atom_types.value(atomtyp); + + if ((not atom_type.isNull()) and atom_type.bondType() != atomtyp) { + atom.setBondType(atom_type.bondType()); + } + + // now do the same to assign the mass if it has not been given + // explicitly + if ((not found_mass) and (not atom_type.isNull())) { + atom.setMass(atom_type.mass()); + } + + if (found_mass and is_bond_type) { + if (mass > 0 and atom_type.element() == Element("Xx")) { + // Set the element of the atom type using the mass and + // update the record in the dictionary. + atom_type.setElement(Element::elementWithMass(mass * g_per_mol)); + this->atom_types[atomtyp] = atom_type; + } + } + + moltype.addAtom(atom); + } + }; - // save the CMAPs in the molecule - moltype.addCMAPs(cmaps); - }; + // function that extracts all of the information from the 'bonds' lines + auto addBondsTo = [&](GroMolType &moltype, int linenum) { + QStringList lines = getDirectiveLines(linenum); - // interpret the defaults so that the forcefield for each moltype can - // be determined - const QString elecstyle = "coulomb"; - const QString vdwstyle = _getVDWStyle(nb_func_type); - const QString combrules = _getCombiningRules(combining_rule); + QMultiHash bonds; + bonds.reserve(lines.count()); - // ok, now we know the location of all child tags of each moleculetype - auto processMolType = [&](const QMultiHash &moltag) - { - auto moltype = getMolType(moltag.value("moleculetype", -1)); + for (const auto &line : lines) { + const auto words = line.split(" "); - for (auto linenum : moltag.values("atoms")) - { - addAtomsTo(moltype, linenum); - } + if (words.count() < 2) { + moltype.addWarning( + QObject::tr( + "Cannot extract bond information " + "from the line '%1' as it should contain at least two words " + "(pieces of information)") + .arg(line)); + continue; + } - for (auto linenum : moltag.values("bonds")) - { - addBondsTo(moltype, linenum); - } + bool ok0, ok1; - for (auto linenum : moltag.values("angles")) - { - addAnglesTo(moltype, linenum); - } + int atm0 = words[0].toInt(&ok0); + int atm1 = words[1].toInt(&ok1); - for (auto linenum : moltag.values("dihedrals")) - { - addDihedralsTo(moltype, linenum); - } + if (not(ok0 and ok1)) { + moltype.addWarning(QObject::tr("Cannot extract bond information " + "from the line '%1' as the first two " + "words need to be integers. ") + .arg(line)); + continue; + } - for (auto linenum : moltag.values("cmap")) - { - addCMAPsTo(moltype, linenum); - } + // now see if any information about the bond is provided... + GromacsBond bond; - for (auto linenum : moltag.values("pairs")) - { - addPairsTo(moltype, linenum); - } + if (words.count() > 2) { + bool ok; + int func_type = words[2].toInt(&ok); - // now print out warnings for any lines that are missed... - const QStringList missed_tags = {"pairs_nb", - "exclusions", - "contraints", - "settles", - "virtual_sites2", - "virtual_sitesn", - "position_restraints", - "distance_restraints", - "orientation_restraints", - "angle_restraints", - "angle_restraints_z"}; - - for (const auto &tag : missed_tags) - { - // not parsed this tag type - for (auto linenum : moltag.values(tag)) - { - const auto missed_lines = getDirectiveLines(linenum); - - if (not missed_lines.isEmpty()) - { - moltype.addWarning(QObject::tr("Ignoring %1 '%2' lines").arg(missed_lines.count()).arg(tag)); - moltype.addWarning(QString("e.g. the first ignored %1 line is").arg(tag)); - moltype.addWarning(missed_lines[0]); - } - } - } + if (not ok) { + moltype.addWarning(QObject::tr("Unable to extract the correct " + "information to form a bond from " + "line '%1' as the third word " + "is not an integer.") + .arg(line)); + continue; + } - // should be finished, run some checks that this looks sane - moltype.sanitise(elecstyle, vdwstyle, combrules, fudge_qq, fudge_lj); + if (words.count() > 3) { + // now read in all of the remaining values as numbers... + QList params; - return moltype; - }; + for (int i = 3; i < words.count(); ++i) { + double param = words[i].toDouble(&ok); - // set the size of the array of moltypes - moltypes = QVector(moltags.count()); - auto moltypes_array = moltypes.data(); + if (ok) + params.append(param); + } - // load all of the molecule types (in parallel if possible) - if (usesParallel()) - { - tbb::parallel_for(tbb::blocked_range(0, moltags.count()), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - auto moltype = processMolType(moltags.at(i)); - moltypes_array[i] = moltype; - } }); - } - else - { - for (int i = 0; i < moltags.count(); ++i) - { - auto moltype = processMolType(moltags.at(i)); - moltypes_array[i] = moltype; + try { + bond = GromacsBond(func_type, params); + } catch (const SireError::exception &e) { + moltype.addWarning(QObject::tr("Unable to extract the correct " + "information to form a bond from " + "line '%1'. Error is '%2'") + .arg(line) + .arg(e.error())); + continue; } + } else { + bond = GromacsBond(func_type); + } } - return warnings; + bonds.insert(BondID(AtomNum(atm0), AtomNum(atm1)), bond); + } + + // save the bonds in the molecule + moltype.addBonds(bonds); }; - // function used to parse the [system] part of the file - auto processSystem = [&] - { - QStringList warnings; + // function that extracts all of the information from the 'angles' lines + auto addAnglesTo = [&](GroMolType &moltype, int linenum) { + QStringList lines = getDirectiveLines(linenum); - // look for the locations of the child tags of [system] - QList> systags; - { - // list of tags that are valid within a [system] - const QStringList valid_tags = {"molecules"}; - - auto it = taglocs.constBegin(); - - while (it != taglocs.constEnd()) - { - if (it.value() == "system") - { - // we have found another 'system' - save the location - // of all of its child tags - QMultiHash tags; - tags.insert(it.value(), it.key()); - ++it; - - while (it != taglocs.constEnd()) - { - // save all child tags until we reach the end - // of definition of this system - if (valid_tags.contains(it.value())) - { - // this is a valid child tag - save its location - //(note that a tag can exist multiple times!) - tags.insert(it.value(), it.key()); - ++it; - } - else - { - // this is the end of the 'system' - ++it; - break; - } - } + QMultiHash angs; + angs.reserve(lines.count()); - systags.append(tags); - } - else - { - ++it; - } - } - } + for (const auto &line : lines) { + const auto words = line.split(" "); - // in theory, there should be one, and only one [system] - if (systags.count() != 1) - { - warnings.append(QObject::tr("There should be one, and only one " - "[system] section in a Gromacs topology file. The number of " - "[system] sections equals %1.") - .arg(systags.count())); - return warnings; + if (words.count() < 3) { + moltype.addWarning(QObject::tr("Cannot extract angle information " + "from the line '%1' as it should " + "contain at least three words " + "(pieces of information)") + .arg(line)); + continue; } - // now parse the two parts of [system] - const auto tags = systags.at(0); + bool ok0, ok1, ok2; - if (not(tags.contains("system") and tags.contains("molecules"))) - { - warnings.append(QObject::tr("The [system] section should contain " - "both [system] and [molecules]. It contains '%1'") - .arg(Sire::toString(tags))); - return warnings; + int atm0 = words[0].toInt(&ok0); + int atm1 = words[1].toInt(&ok1); + int atm2 = words[2].toInt(&ok2); + + if (not(ok0 and ok1 and ok2)) { + moltype.addWarning(QObject::tr("Cannot extract angle information " + "from the line '%1' as the first " + "three words need to be integers. ") + .arg(line)); + continue; } - // process [system] first.. - // each of these lines is part of the title of the system - GroSystem mysys(getDirectiveLines(tags.value("system")).join(" ")); + // now see if any information about the angle is provided... + GromacsAngle angle; - // now process the [molecules] - for (auto linenum : tags.values("molecules")) - { - const auto lines = getDirectiveLines(linenum); - - for (const auto &line : lines) - { - // each line should be the molecule type name, followed by the number - const auto words = line.split(" "); - - if (words.count() < 2) - { - warnings.append(QObject::tr("Cannot understand the [molecules] line " - "'%1' as it should have two words!") - .arg(line)); - continue; - } + if (words.count() > 3) { + bool ok; + int func_type = words[3].toInt(&ok); - if (words.count() > 2) - { - warnings.append(QObject::tr("Ignoring the extraneous information at " - "the end of the [molecules] line '%1'") - .arg(line)); - } + if (not ok) { + moltype.addWarning(QObject::tr("Unable to extract the correct " + "information to form an angle from " + "line '%1' as the fourth word " + "is not an integer.") + .arg(line)); + continue; + } + + if (words.count() > 4) { + // now read in all of the remaining values as numbers... + QList params; - bool ok; - int nmols = words[1].toInt(&ok); + for (int i = 4; i < words.count(); ++i) { + double param = words[i].toDouble(&ok); - if (not ok) - { - warnings.append(QObject::tr("Cannot interpret the number of molecules " - "from the [molecules] line '%1'. The second word should be an integer " - "that gives the number of molecules...") - .arg(line)); - continue; - } + if (ok) + params.append(param); + } - mysys.add(words[0], nmols); + try { + angle = GromacsAngle(func_type, params); + } catch (const SireError::exception &e) { + moltype.addWarning(QObject::tr("Unable to extract the correct " + "information to form an angle " + "from line '%1'. Error is '%2'") + .arg(line) + .arg(e.error())); + continue; } + } else { + angle = GromacsAngle(func_type); + } } - // save the system object to this GroTop - grosys = mysys; + angs.insert(AngleID(AtomNum(atm0), AtomNum(atm1), AtomNum(atm2)), + angle); + } - return warnings; + // save the angles in the molecule + moltype.addAngles(angs); }; - // process the defaults data first, as this affects the rest of the parsing - auto warnings = processDefaults(); + // function that extracts all of the information from the 'dihedrals' lines + auto addDihedralsTo = [&](GroMolType &moltype, int linenum) { + QStringList lines = getDirectiveLines(linenum); - // next, read in the atom types as these have to be present before - // reading anything else... - warnings += processAtomTypes(); + QMultiHash dihs; + dihs.reserve(lines.count()); - // now we can process the other tags - const QVector> funcs = { - processBondTypes, processPairTypes, processAngleTypes, processDihedralTypes, - processConstraintTypes, processNonBondParams, processCMAPTypes, - processMoleculeTypes, processSystem}; + for (const auto &line : lines) { + const auto words = line.split(" "); - if (usesParallel()) - { - QMutex mutex; + if (words.count() < 4) { + moltype.addWarning( + QObject::tr( + "Cannot extract dihedral information " + "from the line '%1' as it should contain at least four words " + "(pieces of information)") + .arg(line)); + continue; + } - tbb::parallel_for(tbb::blocked_range(0, funcs.count()), [&](const tbb::blocked_range &r) - { - QStringList local_warnings; + bool ok0, ok1, ok2, ok3; - for (int i = r.begin(); i < r.end(); ++i) - { - local_warnings += funcs[i](); - } + int atm0 = words[0].toInt(&ok0); + int atm1 = words[1].toInt(&ok1); + int atm2 = words[2].toInt(&ok2); + int atm3 = words[3].toInt(&ok3); - if (not local_warnings.isEmpty()) - { - QMutexLocker lkr(&mutex); - warnings += local_warnings; - } }); - } - else - { - for (int i = 0; i < funcs.count(); ++i) - { - warnings += funcs[i](); + if (not(ok0 and ok1 and ok2 and ok3)) { + moltype.addWarning(QObject::tr("Cannot extract dihedral information " + "from the line '%1' as the first four " + "words need to be integers. ") + .arg(line)); + continue; } - } - - return warnings; -} - -/** Interpret the fully expanded set of lines to extract all of the necessary data */ -void GroTop::interpret() -{ - // first, go through and find the line numbers of all tags - const QRegularExpression re("\\[\\s*([\\w\\d]+)\\s*\\]"); - // map giving the type and line number of each directive tag - QMap taglocs; + // now see if any information about the dihedral is provided... + GromacsDihedral dihedral; - const int nlines = expandedLines().count(); - const auto lines = expandedLines().constData(); + if (words.count() > 4) { + bool ok; + int func_type = words[4].toInt(&ok); - // run through this file to find all of the directives - if (usesParallel()) - { - QMutex mutex; + if (not ok) { + moltype.addWarning(QObject::tr("Unable to extract the correct " + "information to form a dihedral " + "from line '%1' as the fifth word " + "is not an integer.") + .arg(line)); + continue; + } - tbb::parallel_for(tbb::blocked_range(0, nlines), [&](const tbb::blocked_range &r) - { - QMap mylocs; + if (words.count() > 5) { + // now read in all of the remaining values as numbers... + QList params; - for (int i = r.begin(); i < r.end(); ++i) - { - auto m = re.match(lines[i]); + for (int i = 5; i < words.count(); ++i) { + double param = words[i].toDouble(&ok); - if (m.hasMatch()) - { - auto tag = m.captured(1); - mylocs.insert(i, tag); - } + if (ok) + params.append(param); } - if (not mylocs.isEmpty()) - { - QMutexLocker lkr(&mutex); - - for (auto it = mylocs.constBegin(); it != mylocs.constEnd(); ++it) - { - taglocs.insert(it.key(), it.value()); - } - } }); - } - else - { - for (int i = 0; i < nlines; ++i) - { - auto m = re.match(lines[i]); - - if (m.hasMatch()) - { - auto tag = m.captured(1); - taglocs.insert(i, tag); + try { + dihedral = GromacsDihedral(func_type, params); + } catch (const SireError::exception &e) { + moltype.addWarning(QObject::tr("Unable to extract the correct " + "information to form a dihedral " + "from line '%1'. Error is '%2'") + .arg(line) + .arg(e.error())); + continue; } + } else { + dihedral = GromacsDihedral(func_type); + } } - } - - // now, validate that this looks like a gromacs top file. Rules are taken - // from page 138 of the Gromacs 5.1 PDF reference manual - - // first, count up the number of each tag - QHash ntags; - - for (auto it = taglocs.constBegin(); it != taglocs.constEnd(); ++it) - { - if (not ntags.contains(it.value())) - { - ntags.insert(it.value(), 1); - } - else - { - ntags[it.value()] += 1; - } - } - // there should be only one 'defaults' tag - if (ntags.value("defaults", 0) != 1) - { - throw SireIO::parse_error( - QObject::tr("This is not a valid GROMACS topology file. Such files contain one, and one " - "only 'defaults' directive. The number of such directives in this file is %1.") - .arg(ntags.value("defaults", 0)), - CODELOC); - } + dihs.insert(DihedralID(AtomNum(atm0), AtomNum(atm1), AtomNum(atm2), + AtomNum(atm3)), + dihedral); + } - // now process all of the directives - auto warnings = this->processDirectives(taglocs, ntags); + // save the dihedrals in the molecule + moltype.addDihedrals(dihs); + }; - if (not warnings.isEmpty()) - { - parse_warnings = warnings; - } + // function that extracts all of the information from the 'cmap' lines + // function that extracts explicit 1-4 pair scale factors from the 'pairs' + // lines. funct=1 pairs are standard (use global fudge_qq/fudge_lj) and are + // handled automatically by gen-pairs, so we only need to store funct=2 + // explicit pairs. funct=2 format: ai aj 2 fudgeQQ qi qj sigma epsilon The + // LJ scale is 1.0 for funct=2 because sigma/epsilon are the full combined + // values. + auto addPairsTo = [&](GroMolType &moltype, int linenum) { + QStringList lines = getDirectiveLines(linenum); + + for (const auto &line : lines) { + const auto words = line.split(" "); + + if (words.count() < 3) { + moltype.addWarning(QObject::tr("Cannot extract pair information " + "from the line '%1' as it should " + "contain at least three words.") + .arg(line)); + continue; + } + + bool ok0, ok1, ok2; + + int atm0 = words[0].toInt(&ok0); + int atm1 = words[1].toInt(&ok1); + int funct = words[2].toInt(&ok2); + + if (not(ok0 and ok1 and ok2)) { + moltype.addWarning(QObject::tr("Cannot extract pair information " + "from the line '%1' as the first " + "three words need to be integers.") + .arg(line)); + continue; + } + + if (funct == 1) { + // Standard pair: uses global fudge_qq/fudge_lj. + // The gen-pairs mechanism already handles these, so no explicit + // storage needed. + continue; + } else if (funct == 2) { + // Explicit pair: ai aj 2 fudgeQQ qi qj sigma epsilon + // The fudgeQQ is the coulomb scale factor; LJ params are used + // directly (lj_scl = 1.0). + double cscl = fudge_qq; // default to global fudge_qq if not specified + if (words.count() > 3) { + bool ok; + double val = words[3].toDouble(&ok); + if (ok) + cscl = val; + } + + moltype.addExplicitPair(BondID(AtomNum(atm0), AtomNum(atm1)), cscl, + 1.0); + } else { + moltype.addWarning( + QObject::tr("Unsupported pair function type %1 in line '%2'. " + "Only funct=1 and funct=2 are supported.") + .arg(funct) + .arg(line)); + } + } + }; - this->setScore(100); -} + auto addCMAPsTo = [&](GroMolType &moltype, int linenum) { + QStringList lines = getDirectiveLines(linenum); -/** Return all of the warnings that were raised when parsing the file */ -QStringList GroTop::warnings() const -{ - QStringList w = parse_warnings; + QHash cmaps; + cmaps.reserve(lines.count()); - for (const auto &moltype : moltypes) - { - auto molwarns = moltype.warnings(); + for (const auto &line : lines) { + const auto words = line.split(" "); - if (not molwarns.isEmpty()) - { - w.append(QObject::tr("\n** Warnings for molecule type %1 **\n").arg(moltype.toString())); - w += molwarns; + if (words.count() < 6) { + moltype.addWarning( + QObject::tr( + "Cannot extract CMAP information " + "from the line '%1' as it should contain at least six words " + "(pieces of information)") + .arg(line)); + continue; } - } - - return w; -} -/** Internal function that is used to actually parse the data contained - in the lines of the file */ -void GroTop::parseLines(const QString &path, const PropertyMap &map) -{ - // first, see if there are any GROMACS defines in the passed map - // and then preprocess the lines to create the fully expanded file to parse - { - QHash defines; + bool ok0, ok1, ok2, ok3, ok4, ok5; - try - { - const auto p = map["GROMACS_DEFINE"]; + int atm0 = words[0].toInt(&ok0); + int atm1 = words[1].toInt(&ok1); + int atm2 = words[2].toInt(&ok2); + int atm3 = words[3].toInt(&ok3); + int atm4 = words[4].toInt(&ok4); + int func = words[5].toInt(&ok5); - QStringList d; + if (not(ok0 and ok1 and ok2 and ok3 and ok4 and ok5)) { + moltype.addWarning(QObject::tr("Cannot extract CMAP information " + "from the line '%1' as the first six " + "words need to be integers. ") + .arg(line)); + continue; + } - if (p.hasValue()) - { - d = p.value().asA().toString().split(":", Qt::SkipEmptyParts); - } - else if (p.source() != "GROMACS_DEFINE") - { - d = p.source().split(":", Qt::SkipEmptyParts); - } + cmaps.insert(CMAPID(AtomNum(atm0), AtomNum(atm1), AtomNum(atm2), + AtomNum(atm3), AtomNum(atm4)), + QString::number(func)); + } - for (const auto &define : d) - { - auto words = define.split("="); + // save the CMAPs in the molecule + moltype.addCMAPs(cmaps); + }; - if (words.count() == 1) - { - defines.insert(words[0].simplified(), "1"); - } - else - { - defines.insert(words[0].simplified(), words[1].simplified()); - } - } - } - catch (...) - { - } + // interpret the defaults so that the forcefield for each moltype can + // be determined + const QString elecstyle = "coulomb"; + const QString vdwstyle = _getVDWStyle(nb_func_type); + const QString combrules = _getCombiningRules(combining_rule); + + // ok, now we know the location of all child tags of each moleculetype + auto processMolType = [&](const QMultiHash &moltag) { + auto moltype = getMolType(moltag.value("moleculetype", -1)); + + for (auto linenum : moltag.values("atoms")) { + addAtomsTo(moltype, linenum); + } + + for (auto linenum : moltag.values("bonds")) { + addBondsTo(moltype, linenum); + } + + for (auto linenum : moltag.values("angles")) { + addAnglesTo(moltype, linenum); + } + + for (auto linenum : moltag.values("dihedrals")) { + addDihedralsTo(moltype, linenum); + } + + for (auto linenum : moltag.values("cmap")) { + addCMAPsTo(moltype, linenum); + } + + for (auto linenum : moltag.values("pairs")) { + addPairsTo(moltype, linenum); + } + + // now print out warnings for any lines that are missed... + const QStringList missed_tags = {"pairs_nb", + "exclusions", + "contraints", + "settles", + "virtual_sites2", + "virtual_sitesn", + "position_restraints", + "distance_restraints", + "orientation_restraints", + "angle_restraints", + "angle_restraints_z"}; + + for (const auto &tag : missed_tags) { + // not parsed this tag type + for (auto linenum : moltag.values(tag)) { + const auto missed_lines = getDirectiveLines(linenum); + + if (not missed_lines.isEmpty()) { + moltype.addWarning(QObject::tr("Ignoring %1 '%2' lines") + .arg(missed_lines.count()) + .arg(tag)); + moltype.addWarning( + QString("e.g. the first ignored %1 line is").arg(tag)); + moltype.addWarning(missed_lines[0]); + } + } + } + + // should be finished, run some checks that this looks sane + moltype.sanitise(elecstyle, vdwstyle, combrules, fudge_qq, fudge_lj); + + return moltype; + }; - // now go through an expand any macros and include the contents of any - // included files - expanded_lines = preprocess(lines(), defines, path, "."); + // set the size of the array of moltypes + moltypes = QVector(moltags.count()); + auto moltypes_array = moltypes.data(); + + // load all of the molecule types (in parallel if possible) + if (usesParallel()) { + tbb::parallel_for(tbb::blocked_range(0, moltags.count()), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + auto moltype = processMolType(moltags.at(i)); + moltypes_array[i] = moltype; + } + }); + } else { + for (int i = 0; i < moltags.count(); ++i) { + auto moltype = processMolType(moltags.at(i)); + moltypes_array[i] = moltype; + } } - // now we know that there are no macros to expand, no other files to - // include, and everything should be ok... ;-) - this->interpret(); -} - -/** This function is used to create a molecule. Any errors should be written - to the 'errors' QStringList passed as an argument */ -Molecule GroTop::createMolecule(const GroMolType &moltype, QStringList &errors, const PropertyMap &) const -{ - try - { - MolStructureEditor mol; - - // first go through and create the Molecule layout - //(the atoms are already sorted into Residues) - int cgidx = 1; - ResStructureEditor res; - ChainStructureEditor chain; - CGStructureEditor cgroup; + return warnings; + }; - auto different_chain = [&](const ChainName &name) - { - if (name.isNull() and chain.isEmpty()) - return false; - else if (name.isNull() or chain.isEmpty()) - return true; - else - return name != chain.name(); - }; + // function used to parse the [system] part of the file + auto processSystem = [&] { + QStringList warnings; - for (const auto &atom : moltype.atoms()) - { - if (cgroup.nAtoms() == 0) - { - // this is the first atom in the molecule - cgroup = mol.add(CGName(QString::number(cgidx))); - cgidx += 1; - - if (not atom.chainName().isNull()) - { - chain = mol.add(ChainName(atom.chainName())); - res = chain.add(ResNum(atom.residueNumber())); - } - else - { - res = mol.add(ResNum(atom.residueNumber())); - } + // look for the locations of the child tags of [system] + QList> systags; + { + // list of tags that are valid within a [system] + const QStringList valid_tags = {"molecules"}; - res = res.rename(atom.residueName()); - } - else if (different_chain(atom.chainName())) - { - // this atom is in a different residue in a different chain - cgroup = mol.add(CGName(QString::number(cgidx))); - cgidx += 1; - - if (atom.chainName().isNull()) - { - // residue is not in a chain - chain = ChainStructureEditor(); - res = mol.add(ResNum(atom.residueNumber())); - } - else - { - // residue is in a chain - chain = mol.add(ChainName(atom.chainName())); - res = chain.add(ResNum(atom.residueNumber())); - } + auto it = taglocs.constBegin(); - res = res.rename(atom.residueName()); - } - else if (atom.residueNumber() != res.number() or atom.residueName() != res.name()) - { - // this atom is in a different residue - cgroup = mol.add(CGName(QString::number(cgidx))); - cgidx += 1; - - res = mol.add(ResNum(atom.residueNumber())); - res = res.rename(atom.residueName()); + while (it != taglocs.constEnd()) { + if (it.value() == "system") { + // we have found another 'system' - save the location + // of all of its child tags + QMultiHash tags; + tags.insert(it.value(), it.key()); + ++it; + + while (it != taglocs.constEnd()) { + // save all child tags until we reach the end + // of definition of this system + if (valid_tags.contains(it.value())) { + // this is a valid child tag - save its location + //(note that a tag can exist multiple times!) + tags.insert(it.value(), it.key()); + ++it; + } else { + // this is the end of the 'system' + ++it; + break; } + } - // add the atom to the residue - auto a = res.add(AtomName(atom.name())); - a = a.renumber(atom.number()); - a = a.reparent(cgroup.name()); + systags.append(tags); + } else { + ++it; } + } + } - return mol.commit(); + // in theory, there should be one, and only one [system] + if (systags.count() != 1) { + warnings.append( + QObject::tr( + "There should be one, and only one " + "[system] section in a Gromacs topology file. The number of " + "[system] sections equals %1.") + .arg(systags.count())); + return warnings; } - catch (const SireError::exception &e) - { - errors.append(QObject::tr("Could not create the molecule %1. The error was %2: %3.") - .arg(moltype.name()) - .arg(e.what()) - .arg(e.why())); - if (not moltype.warnings().isEmpty()) - { - errors.append(QObject::tr("This molecule type had the following parse warnings on read:")); - errors += moltype.warnings(); - } + // now parse the two parts of [system] + const auto tags = systags.at(0); - return Molecule(); + if (not(tags.contains("system") and tags.contains("molecules"))) { + warnings.append( + QObject::tr("The [system] section should contain " + "both [system] and [molecules]. It contains '%1'") + .arg(Sire::toString(tags))); + return warnings; } -} - -/** This function is used to return atom properties for the passed molecule */ -GroTop::PropsAndErrors GroTop::getAtomProperties(const MoleculeInfo &molinfo, const GroMolType &moltype) const -{ - try - { - // create space for all of the properties - AtomStringProperty atom_type(molinfo); - AtomStringProperty bond_type(molinfo); - AtomIntProperty charge_group(molinfo); - AtomCharges atom_chgs(molinfo); - AtomMasses atom_masses(molinfo); - AtomLJs atom_ljs(molinfo); - AtomElements atom_elements(molinfo); - AtomStringProperty particle_type(molinfo); + // process [system] first.. + // each of these lines is part of the title of the system + GroSystem mysys(getDirectiveLines(tags.value("system")).join(" ")); - const auto atoms = moltype.atoms(); + // now process the [molecules] + for (auto linenum : tags.values("molecules")) { + const auto lines = getDirectiveLines(linenum); - QStringList errors; + for (const auto &line : lines) { + // each line should be the molecule type name, followed by the number + const auto words = line.split(" "); - bool uses_bondtypes = false; + if (words.count() < 2) { + warnings.append(QObject::tr("Cannot understand the [molecules] line " + "'%1' as it should have two words!") + .arg(line)); + continue; + } - // loop over each atom and look up parameters - for (int i = 0; i < atoms.count(); ++i) - { - auto cgatomidx = molinfo.cgAtomIdx(AtomIdx(i)); + if (words.count() > 2) { + warnings.append(QObject::tr("Ignoring the extraneous information at " + "the end of the [molecules] line '%1'") + .arg(line)); + } - // get the template for this atom (templates in same order as atoms) - const auto atom = atoms.constData()[i]; + bool ok; + int nmols = words[1].toInt(&ok); - // information from the template - atom_type.set(cgatomidx, atom.atomType()); - bond_type.set(cgatomidx, atom.bondType()); + if (not ok) { + warnings.append( + QObject::tr("Cannot interpret the number of molecules " + "from the [molecules] line '%1'. The second word " + "should be an integer " + "that gives the number of molecules...") + .arg(line)); + continue; + } - if (atom.atomType() != atom.bondType()) - uses_bondtypes = true; + mysys.add(words[0], nmols); + } + } - charge_group.set(cgatomidx, atom.chargeGroup()); - atom_chgs.set(cgatomidx, atom.charge()); - atom_masses.set(cgatomidx, atom.mass()); + // save the system object to this GroTop + grosys = mysys; - // information from the atom type - const auto atmtyp = atom_types.value(atom.atomType()); + return warnings; + }; - if (atmtyp.isNull()) - { - errors.append(QObject::tr("There are no parameters for the atom " - "type '%1', needed by atom '%2'") - .arg(atom.atomType()) - .arg(atom.toString())); - continue; - } - else if (atmtyp.hasMassOnly()) - { - errors.append(QObject::tr("The parameters for the atom type '%1' needed " - "by atom '%2' has mass parameters only! '%3'") - .arg(atom.atomType()) - .arg(atom.toString()) - .arg(atmtyp.toString())); - continue; - } + // process the defaults data first, as this affects the rest of the parsing + auto warnings = processDefaults(); - atom_ljs.set(cgatomidx, atmtyp.ljParameter()); - atom_elements.set(cgatomidx, atmtyp.element()); - particle_type.set(cgatomidx, atmtyp.particleTypeString()); - } + // next, read in the atom types as these have to be present before + // reading anything else... + warnings += processAtomTypes(); - Properties props; + // now we can process the other tags + const QVector> funcs = { + processBondTypes, processPairTypes, processAngleTypes, + processDihedralTypes, processConstraintTypes, processNonBondParams, + processCMAPTypes, processMoleculeTypes, processSystem}; - props.setProperty("atomtype", atom_type); + if (usesParallel()) { + QMutex mutex; - if (uses_bondtypes) - { - // this forcefield uses a different ato - props.setProperty("bondtype", bond_type); - } + tbb::parallel_for(tbb::blocked_range(0, funcs.count()), + [&](const tbb::blocked_range &r) { + QStringList local_warnings; - props.setProperty("charge_group", charge_group); - props.setProperty("charge", atom_chgs); - props.setProperty("mass", atom_masses); - props.setProperty("LJ", atom_ljs); - props.setProperty("element", atom_elements); - props.setProperty("particle_type", particle_type); + for (int i = r.begin(); i < r.end(); ++i) { + local_warnings += funcs[i](); + } - return std::make_tuple(props, errors); + if (not local_warnings.isEmpty()) { + QMutexLocker lkr(&mutex); + warnings += local_warnings; + } + }); + } else { + for (int i = 0; i < funcs.count(); ++i) { + warnings += funcs[i](); } - catch (const SireError::exception &e) - { - QStringList errors; - errors.append( - QObject::tr("Error getting atom properties for %1. %2: %3").arg(moltype.name()).arg(e.what()).arg(e.why())); - - if (not moltype.warnings().isEmpty()) - { - errors.append("There were warnings parsing this molecule template:"); - errors += moltype.warnings(); - } + } - return std::make_tuple(Properties(), errors); - } + return warnings; } -/** This internal function is used to return all of the bond properties - for the passed molecule */ -GroTop::PropsAndErrors GroTop::getBondProperties(const MoleculeInfo &molinfo, const GroMolType &moltype) const -{ - try - { - const auto R = InternalPotential::symbols().bond().r(); +/** Interpret the fully expanded set of lines to extract all of the necessary + * data */ +void GroTop::interpret() { + // first, go through and find the line numbers of all tags + const QRegularExpression re("\\[\\s*([\\w\\d]+)\\s*\\]"); - QStringList errors; + // map giving the type and line number of each directive tag + QMap taglocs; - // add in all of the bond functions, together with the connectivity of the - // molecule - auto connectivity = Connectivity(molinfo).edit(); - connectivity = connectivity.disconnectAll(); + const int nlines = expandedLines().count(); + const auto lines = expandedLines().constData(); - TwoAtomFunctions bondfuncs(molinfo); + // run through this file to find all of the directives + if (usesParallel()) { + QMutex mutex; - const auto bonds = moltype.bonds(); + tbb::parallel_for(tbb::blocked_range(0, nlines), + [&](const tbb::blocked_range &r) { + QMap mylocs; - for (auto it = bonds.constBegin(); it != bonds.constEnd(); ++it) - { - const auto &bond = it.key(); - auto potential = it.value(); + for (int i = r.begin(); i < r.end(); ++i) { + auto m = re.match(lines[i]); - AtomIdx idx0 = molinfo.atomIdx(bond.atom0()); - AtomIdx idx1 = molinfo.atomIdx(bond.atom1()); + if (m.hasMatch()) { + auto tag = m.captured(1); + mylocs.insert(i, tag); + } + } - if (idx1 < idx0) - { - qSwap(idx0, idx1); - } + if (not mylocs.isEmpty()) { + QMutexLocker lkr(&mutex); - // do we need to resolve this bond parameter (look up the parameters)? - if (not potential.isResolved()) - { - // look up the atoms in the molecule template - const auto atom0 = moltype.atom(idx0); - const auto atom1 = moltype.atom(idx1); - - // get the bond parameter for these bond types - auto new_potential = this->bond(atom0.bondType(), atom1.bondType(), potential.functionType()); - - if (not new_potential.isResolved()) - { - errors.append(QObject::tr("Cannot find the bond parameters for " - "the bond between atoms %1-%2 (atom types %3-%4, function type %5).") - .arg(atom0.toString()) - .arg(atom1.toString()) - .arg(atom0.bondType()) - .arg(atom1.bondType()) - .arg(potential.functionType())); - continue; - } + for (auto it = mylocs.constBegin(); + it != mylocs.constEnd(); ++it) { + taglocs.insert(it.key(), it.value()); + } + } + }); + } else { + for (int i = 0; i < nlines; ++i) { + auto m = re.match(lines[i]); + + if (m.hasMatch()) { + auto tag = m.captured(1); + taglocs.insert(i, tag); + } + } + } + + // now, validate that this looks like a gromacs top file. Rules are taken + // from page 138 of the Gromacs 5.1 PDF reference manual + + // first, count up the number of each tag + QHash ntags; + + for (auto it = taglocs.constBegin(); it != taglocs.constEnd(); ++it) { + if (not ntags.contains(it.value())) { + ntags.insert(it.value(), 1); + } else { + ntags[it.value()] += 1; + } + } + + // there should be only one 'defaults' tag + if (ntags.value("defaults", 0) != 1) { + throw SireIO::parse_error( + QObject::tr("This is not a valid GROMACS topology file. Such files " + "contain one, and one " + "only 'defaults' directive. The number of such directives " + "in this file is %1.") + .arg(ntags.value("defaults", 0)), + CODELOC); + } - potential = new_potential; - } + // now process all of the directives + auto warnings = this->processDirectives(taglocs, ntags); - // add the connection - if (potential.atomsAreBonded()) - { - connectivity.connect(idx0, idx1); - } + if (not warnings.isEmpty()) { + parse_warnings = warnings; + } - // now create the bond expression - auto exp = potential.toExpression(R); + this->setScore(100); +} - if (not exp.isZero()) - { - // add this expression onto any existing expression - auto oldfunc = bondfuncs.potential(idx0, idx1); +/** Return all of the warnings that were raised when parsing the file */ +QStringList GroTop::warnings() const { + QStringList w = parse_warnings; - if (not oldfunc.isZero()) - { - bondfuncs.set(idx0, idx1, exp + oldfunc); - } - else - { - bondfuncs.set(idx0, idx1, exp); - } - } - } + for (const auto &moltype : moltypes) { + auto molwarns = moltype.warnings(); - auto conn = connectivity.commit(); + if (not molwarns.isEmpty()) { + w.append(QObject::tr("\n** Warnings for molecule type %1 **\n") + .arg(moltype.toString())); + w += molwarns; + } + } - Properties props; - props.setProperty("connectivity", conn); - props.setProperty("bond", bondfuncs); + return w; +} - // if 'generate_pairs' is true, then we need to automatically generate - // the excluded atom pairs, using fudge_qq and fudge_lj for the 1-4 interactions - if (generate_pairs) - { - if (bonds.isEmpty()) - { - // there are no bonds, so there cannot be any intramolecular nonbonded - // energy (don't know how atoms are connected). This likely means - // that this is a solvent molecule, so set the intrascales to 0 - CLJNBPairs nbpairs(molinfo, CLJScaleFactor(0)); - props.setProperty("intrascale", nbpairs); - } - else - { - CLJNBPairs nbpairs(conn, CLJScaleFactor(fudge_qq, fudge_lj)); - - // Override with any explicitly specified [pairs] funct=2 entries. - // These carry their own fudgeQQ (coulomb scale) and use lj_scl=1.0 - // (sigma/epsilon in funct=2 are the full combined values, not scaled by fudgeLJ). - const auto explicit_pairs = moltype.explicitPairs(); - for (auto it = explicit_pairs.constBegin(); it != explicit_pairs.constEnd(); ++it) - { - const auto &pair = it.key(); - const auto &scl = it.value(); - try - { - AtomIdx idx0 = molinfo.atomIdx(pair.atom0()); - AtomIdx idx1 = molinfo.atomIdx(pair.atom1()); - nbpairs.set(idx0, idx1, CLJScaleFactor(scl.first, scl.second)); - } - catch (...) - { - // atom not found — skip silently (already warned during parsing) - } - } +/** Internal function that is used to actually parse the data contained + in the lines of the file */ +void GroTop::parseLines(const QString &path, const PropertyMap &map) { + // first, see if there are any GROMACS defines in the passed map + // and then preprocess the lines to create the fully expanded file to parse + { + QHash defines; - props.setProperty("intrascale", nbpairs); - } - } + try { + const auto p = map["GROMACS_DEFINE"]; - return std::make_tuple(props, errors); - } - catch (const SireError::exception &e) - { - QStringList errors; - errors.append( - QObject::tr("Error getting bond properties for %1. %2: %3").arg(moltype.name()).arg(e.what()).arg(e.why())); + QStringList d; - if (not moltype.warnings().isEmpty()) - { - errors.append("There were warnings parsing this molecule template:"); - errors += moltype.warnings(); - } + if (p.hasValue()) { + d = p.value().asA().toString().split( + ":", Qt::SkipEmptyParts); + } else if (p.source() != "GROMACS_DEFINE") { + d = p.source().split(":", Qt::SkipEmptyParts); + } - return std::make_tuple(Properties(), errors); + for (const auto &define : d) { + auto words = define.split("="); + + if (words.count() == 1) { + defines.insert(words[0].simplified(), "1"); + } else { + defines.insert(words[0].simplified(), words[1].simplified()); + } + } + } catch (...) { } + + // now go through an expand any macros and include the contents of any + // included files + expanded_lines = preprocess(lines(), defines, path, "."); + } + + // now we know that there are no macros to expand, no other files to + // include, and everything should be ok... ;-) + this->interpret(); } -/** This internal function is used to return all of the angle properties - for the passed molecule */ -GroTop::PropsAndErrors GroTop::getAngleProperties(const MoleculeInfo &molinfo, const GroMolType &moltype) const -{ - try - { - const auto R = InternalPotential::symbols().ureyBradley().r(); - const auto THETA = InternalPotential::symbols().angle().theta(); +/** This function is used to create a molecule. Any errors should be written + to the 'errors' QStringList passed as an argument */ +Molecule GroTop::createMolecule(const GroMolType &moltype, QStringList &errors, + const PropertyMap &) const { + try { + MolStructureEditor mol; + + // first go through and create the Molecule layout + //(the atoms are already sorted into Residues) + int cgidx = 1; + ResStructureEditor res; + ChainStructureEditor chain; + CGStructureEditor cgroup; + + auto different_chain = [&](const ChainName &name) { + if (name.isNull() and chain.isEmpty()) + return false; + else if (name.isNull() or chain.isEmpty()) + return true; + else + return name != chain.name(); + }; - QStringList errors; + for (const auto &atom : moltype.atoms()) { + if (cgroup.nAtoms() == 0) { + // this is the first atom in the molecule + cgroup = mol.add(CGName(QString::number(cgidx))); + cgidx += 1; - // add in all of the angle functions - ThreeAtomFunctions angfuncs(molinfo); + if (not atom.chainName().isNull()) { + chain = mol.add(ChainName(atom.chainName())); + res = chain.add(ResNum(atom.residueNumber())); + } else { + res = mol.add(ResNum(atom.residueNumber())); + } - // also any additional Urey-Bradley functions - TwoAtomFunctions ubfuncs(molinfo); + res = res.rename(atom.residueName()); + } else if (different_chain(atom.chainName())) { + // this atom is in a different residue in a different chain + cgroup = mol.add(CGName(QString::number(cgidx))); + cgidx += 1; - const auto angles = moltype.angles(); + if (atom.chainName().isNull()) { + // residue is not in a chain + chain = ChainStructureEditor(); + res = mol.add(ResNum(atom.residueNumber())); + } else { + // residue is in a chain + chain = mol.add(ChainName(atom.chainName())); + res = chain.add(ResNum(atom.residueNumber())); + } - bool has_ub = false; + res = res.rename(atom.residueName()); + } else if (atom.residueNumber() != res.number() or + atom.residueName() != res.name()) { + // this atom is in a different residue + cgroup = mol.add(CGName(QString::number(cgidx))); + cgidx += 1; - for (auto it = angles.constBegin(); it != angles.constEnd(); ++it) - { - const auto &angle = it.key(); - auto potential = it.value(); + res = mol.add(ResNum(atom.residueNumber())); + res = res.rename(atom.residueName()); + } - AtomIdx idx0 = molinfo.atomIdx(angle.atom0()); - AtomIdx idx1 = molinfo.atomIdx(angle.atom1()); - AtomIdx idx2 = molinfo.atomIdx(angle.atom2()); + // add the atom to the residue + auto a = res.add(AtomName(atom.name())); + a = a.renumber(atom.number()); + a = a.reparent(cgroup.name()); + } - if (idx2 < idx0) - { - qSwap(idx0, idx2); - } + return mol.commit(); + } catch (const SireError::exception &e) { + errors.append( + QObject::tr("Could not create the molecule %1. The error was %2: %3.") + .arg(moltype.name()) + .arg(e.what()) + .arg(e.why())); - // do we need to resolve this angle parameter (look up the parameters)? - if (not potential.isResolved()) - { - // look up the atoms in the molecule template - const auto atom0 = moltype.atom(idx0); - const auto atom1 = moltype.atom(idx1); - const auto atom2 = moltype.atom(idx2); - - // get the angle parameter for these atom types - auto new_potential = - this->angle(atom0.bondType(), atom1.bondType(), atom2.bondType(), potential.functionType()); - - if (not new_potential.isResolved()) - { - errors.append(QObject::tr("Cannot find the angle parameters for " - "the angle between atoms %1-%2-%3 (atom types %4-%5-%6, " - "function type %7).") - .arg(atom0.toString()) - .arg(atom1.toString()) - .arg(atom2.toString()) - .arg(atom0.bondType()) - .arg(atom1.bondType()) - .arg(atom2.bondType()) - .arg(potential.functionType())); - continue; - } + if (not moltype.warnings().isEmpty()) { + errors.append(QObject::tr( + "This molecule type had the following parse warnings on read:")); + errors += moltype.warnings(); + } - potential = new_potential; - } + return Molecule(); + } +} - if (potential.isBondAngleCrossTerm()) - { - // extract and add the Urey Bradley term between the 0-2 atoms - auto bondpot = potential.toBondTerm(); +/** This function is used to return atom properties for the passed molecule */ +GroTop::PropsAndErrors +GroTop::getAtomProperties(const MoleculeInfo &molinfo, + const GroMolType &moltype) const { + try { + // create space for all of the properties + AtomStringProperty atom_type(molinfo); + AtomStringProperty bond_type(molinfo); + AtomIntProperty charge_group(molinfo); + AtomCharges atom_chgs(molinfo); + AtomMasses atom_masses(molinfo); + + AtomLJs atom_ljs(molinfo); + AtomElements atom_elements(molinfo); + AtomStringProperty particle_type(molinfo); + + const auto atoms = moltype.atoms(); - auto exp = bondpot.toExpression(R); + QStringList errors; - if (not exp.isZero()) - { - has_ub = true; + bool uses_bondtypes = false; - auto oldfunc = ubfuncs.potential(idx0, idx2); + // loop over each atom and look up parameters + for (int i = 0; i < atoms.count(); ++i) { + auto cgatomidx = molinfo.cgAtomIdx(AtomIdx(i)); - if (not oldfunc.isZero()) - { - ubfuncs.set(idx0, idx2, exp + oldfunc); - } - else - { - ubfuncs.set(idx0, idx2, exp); - } - } + // get the template for this atom (templates in same order as atoms) + const auto atom = atoms.constData()[i]; - // we will only add the angle part here - we will - // need to add the bond part somewhere else - potential = potential.toAngleTerm(); - } + // information from the template + atom_type.set(cgatomidx, atom.atomType()); + bond_type.set(cgatomidx, atom.bondType()); - // now create the angle expression - auto exp = potential.toExpression(THETA); + if (atom.atomType() != atom.bondType()) + uses_bondtypes = true; - if (not exp.isZero()) - { - // add this expression onto any existing expression - auto oldfunc = angfuncs.potential(idx0, idx1, idx2); + charge_group.set(cgatomidx, atom.chargeGroup()); + atom_chgs.set(cgatomidx, atom.charge()); + atom_masses.set(cgatomidx, atom.mass()); - if (not oldfunc.isZero()) - { - angfuncs.set(idx0, idx1, idx2, exp + oldfunc); - } - else - { - angfuncs.set(idx0, idx1, idx2, exp); - } - } - } + // information from the atom type + const auto atmtyp = atom_types.value(atom.atomType()); + + if (atmtyp.isNull()) { + errors.append(QObject::tr("There are no parameters for the atom " + "type '%1', needed by atom '%2'") + .arg(atom.atomType()) + .arg(atom.toString())); + continue; + } else if (atmtyp.hasMassOnly()) { + errors.append( + QObject::tr("The parameters for the atom type '%1' needed " + "by atom '%2' has mass parameters only! '%3'") + .arg(atom.atomType()) + .arg(atom.toString()) + .arg(atmtyp.toString())); + continue; + } + + atom_ljs.set(cgatomidx, atmtyp.ljParameter()); + atom_elements.set(cgatomidx, atmtyp.element()); + particle_type.set(cgatomidx, atmtyp.particleTypeString()); + } - Properties props; - props.setProperty("angle", angfuncs); + Properties props; - if (has_ub) - props.setProperty("urey-bradley", ubfuncs); + props.setProperty("atomtype", atom_type); - return std::make_tuple(props, errors); + if (uses_bondtypes) { + // this forcefield uses a different ato + props.setProperty("bondtype", bond_type); } - catch (const SireError::exception &e) - { - QStringList errors; - errors.append(QObject::tr("Error getting angle properties for %1. %2: %3") - .arg(moltype.name()) - .arg(e.what()) - .arg(e.why())); - if (not moltype.warnings().isEmpty()) - { - errors.append("There were warnings parsing this molecule template:"); - errors += moltype.warnings(); - } + props.setProperty("charge_group", charge_group); + props.setProperty("charge", atom_chgs); + props.setProperty("mass", atom_masses); + props.setProperty("LJ", atom_ljs); + props.setProperty("element", atom_elements); + props.setProperty("particle_type", particle_type); + + return std::make_tuple(props, errors); + } catch (const SireError::exception &e) { + QStringList errors; + errors.append(QObject::tr("Error getting atom properties for %1. %2: %3") + .arg(moltype.name()) + .arg(e.what()) + .arg(e.why())); - return std::make_tuple(Properties(), errors); + if (not moltype.warnings().isEmpty()) { + errors.append("There were warnings parsing this molecule template:"); + errors += moltype.warnings(); } + + return std::make_tuple(Properties(), errors); + } } -/** This internal function is used to return all of the dihedral properties +/** This internal function is used to return all of the bond properties for the passed molecule */ -GroTop::PropsAndErrors GroTop::getDihedralProperties(const MoleculeInfo &molinfo, const GroMolType &moltype) const -{ - try - { - const auto PHI = InternalPotential::symbols().dihedral().phi(); - const auto THETA = InternalPotential::symbols().improper().theta(); +GroTop::PropsAndErrors +GroTop::getBondProperties(const MoleculeInfo &molinfo, + const GroMolType &moltype) const { + try { + const auto R = InternalPotential::symbols().bond().r(); - QStringList errors; + QStringList errors; - // add in all of the dihedral and improper functions - FourAtomFunctions dihfuncs(molinfo); - FourAtomFunctions impfuncs(molinfo); + // add in all of the bond functions, together with the connectivity of the + // molecule + auto connectivity = Connectivity(molinfo).edit(); + connectivity = connectivity.disconnectAll(); + + TwoAtomFunctions bondfuncs(molinfo); + + const auto bonds = moltype.bonds(); + + for (auto it = bonds.constBegin(); it != bonds.constEnd(); ++it) { + const auto &bond = it.key(); + auto potential = it.value(); + + AtomIdx idx0 = molinfo.atomIdx(bond.atom0()); + AtomIdx idx1 = molinfo.atomIdx(bond.atom1()); + + if (idx1 < idx0) { + qSwap(idx0, idx1); + } + + // do we need to resolve this bond parameter (look up the parameters)? + if (not potential.isResolved()) { + // look up the atoms in the molecule template + const auto atom0 = moltype.atom(idx0); + const auto atom1 = moltype.atom(idx1); + + // get the bond parameter for these bond types + auto new_potential = this->bond(atom0.bondType(), atom1.bondType(), + potential.functionType()); + + if (not new_potential.isResolved()) { + errors.append(QObject::tr("Cannot find the bond parameters for " + "the bond between atoms %1-%2 (atom types " + "%3-%4, function type %5).") + .arg(atom0.toString()) + .arg(atom1.toString()) + .arg(atom0.bondType()) + .arg(atom1.bondType()) + .arg(potential.functionType())); + continue; + } + + potential = new_potential; + } + + // add the connection + if (potential.atomsAreBonded()) { + connectivity.connect(idx0, idx1); + } + + // now create the bond expression + auto exp = potential.toExpression(R); + + if (not exp.isZero()) { + // add this expression onto any existing expression + auto oldfunc = bondfuncs.potential(idx0, idx1); + + if (not oldfunc.isZero()) { + bondfuncs.set(idx0, idx1, exp + oldfunc); + } else { + bondfuncs.set(idx0, idx1, exp); + } + } + } + + auto conn = connectivity.commit(); + + Properties props; + props.setProperty("connectivity", conn); + props.setProperty("bond", bondfuncs); + + // if 'generate_pairs' is true, then we need to automatically generate + // the excluded atom pairs, using fudge_qq and fudge_lj for the 1-4 + // interactions + if (generate_pairs) { + if (bonds.isEmpty()) { + // there are no bonds, so there cannot be any intramolecular nonbonded + // energy (don't know how atoms are connected). This likely means + // that this is a solvent molecule, so set the intrascales to 0 + CLJNBPairs nbpairs(molinfo, CLJScaleFactor(0)); + props.setProperty("intrascale", nbpairs); + } else { + CLJNBPairs nbpairs(conn, CLJScaleFactor(fudge_qq, fudge_lj)); + + // Override with any explicitly specified [pairs] funct=2 entries. + // These carry their own fudgeQQ (coulomb scale) and use lj_scl=1.0 + // (sigma/epsilon in funct=2 are the full combined values, not scaled by + // fudgeLJ). + const auto explicit_pairs = moltype.explicitPairs(); + for (auto it = explicit_pairs.constBegin(); + it != explicit_pairs.constEnd(); ++it) { + const auto &pair = it.key(); + const auto &scl = it.value(); + try { + AtomIdx idx0 = molinfo.atomIdx(pair.atom0()); + AtomIdx idx1 = molinfo.atomIdx(pair.atom1()); + nbpairs.set(idx0, idx1, CLJScaleFactor(scl.first, scl.second)); + } catch (...) { + // atom not found — skip silently (already warned during parsing) + } + } + + props.setProperty("intrascale", nbpairs); + } + } + + return std::make_tuple(props, errors); + } catch (const SireError::exception &e) { + QStringList errors; + errors.append(QObject::tr("Error getting bond properties for %1. %2: %3") + .arg(moltype.name()) + .arg(e.what()) + .arg(e.why())); - const auto dihedrals = moltype.dihedrals(); + if (not moltype.warnings().isEmpty()) { + errors.append("There were warnings parsing this molecule template:"); + errors += moltype.warnings(); + } - bool has_any_impropers = false; + return std::make_tuple(Properties(), errors); + } +} - for (auto it = dihedrals.constBegin(); it != dihedrals.constEnd(); ++it) - { - const auto &dihedral = it.key(); - auto potential = it.value(); - - AtomIdx idx0 = molinfo.atomIdx(dihedral.atom0()); - AtomIdx idx1 = molinfo.atomIdx(dihedral.atom1()); - AtomIdx idx2 = molinfo.atomIdx(dihedral.atom2()); - AtomIdx idx3 = molinfo.atomIdx(dihedral.atom3()); - - if (idx3 < idx0) - { - qSwap(idx0, idx3); - qSwap(idx2, idx1); - } +/** This internal function is used to return all of the angle properties + for the passed molecule */ +GroTop::PropsAndErrors +GroTop::getAngleProperties(const MoleculeInfo &molinfo, + const GroMolType &moltype) const { + try { + const auto R = InternalPotential::symbols().ureyBradley().r(); + const auto THETA = InternalPotential::symbols().angle().theta(); - Expression exp; - bool is_improper = false; - - // do we need to resolve this dihedral parameter (look up the parameters)? - if (not potential.isResolved()) - { - // look up the atoms in the molecule template - const auto atom0 = moltype.atom(idx0); - const auto atom1 = moltype.atom(idx1); - const auto atom2 = moltype.atom(idx2); - const auto atom3 = moltype.atom(idx3); - - // get the dihedral parameter for these atom types - could be - // many, as they will be added together - auto resolved = this->dihedrals(atom0.bondType(), atom1.bondType(), atom2.bondType(), atom3.bondType(), - potential.functionType()); - - if (resolved.isEmpty()) - { - errors.append(QObject::tr("Cannot find the dihedral parameters for " - "the dihedral between atoms %1-%2-%3-%4 (atom types %5-%6-%7-%8, " - "function type %9).") - .arg(atom0.toString()) - .arg(atom1.toString()) - .arg(atom2.toString()) - .arg(atom3.toString()) - .arg(atom0.bondType()) - .arg(atom1.bondType()) - .arg(atom2.bondType()) - .arg(atom3.bondType()) - .arg(potential.functionType())); - continue; - } + QStringList errors; - // sum all of the parts together - for (const auto &r : resolved) - { - if (r.isResolved()) - { - if (r.isImproperAngleTerm()) - { - is_improper = true; - exp += r.toImproperExpression(THETA); - } - else - { - exp += r.toExpression(PHI); - } - } - } - } - else - { - // we have a fully-resolved dihedral potential - if (potential.isImproperAngleTerm()) - { - exp = potential.toImproperExpression(THETA); - is_improper = true; - } - else - { - exp = potential.toExpression(PHI); - } - } + // add in all of the angle functions + ThreeAtomFunctions angfuncs(molinfo); - if (not exp.isZero()) - { - if (is_improper) - { - has_any_impropers = true; + // also any additional Urey-Bradley functions + TwoAtomFunctions ubfuncs(molinfo); - // add this expression onto any existing expression - auto oldfunc = impfuncs.potential(idx0, idx1, idx2, idx3); + const auto angles = moltype.angles(); - if (not oldfunc.isZero()) - { - impfuncs.set(idx0, idx1, idx2, idx3, exp + oldfunc); - } - else - { - impfuncs.set(idx0, idx1, idx2, idx3, exp); - } - } - else - { - // add this expression onto any existing expression - auto oldfunc = dihfuncs.potential(idx0, idx1, idx2, idx3); - - if (not oldfunc.isZero()) - { - dihfuncs.set(idx0, idx1, idx2, idx3, exp + oldfunc); - } - else - { - dihfuncs.set(idx0, idx1, idx2, idx3, exp); - } - } - } - } + bool has_ub = false; - Properties props; - props.setProperty("dihedral", dihfuncs); + for (auto it = angles.constBegin(); it != angles.constEnd(); ++it) { + const auto &angle = it.key(); + auto potential = it.value(); - if (has_any_impropers) - props.setProperty("improper", impfuncs); + AtomIdx idx0 = molinfo.atomIdx(angle.atom0()); + AtomIdx idx1 = molinfo.atomIdx(angle.atom1()); + AtomIdx idx2 = molinfo.atomIdx(angle.atom2()); - return std::make_tuple(props, errors); - } - catch (const SireError::exception &e) - { - QStringList errors; - errors.append(QObject::tr("Error getting dihedral properties for %1. %2: %3") - .arg(moltype.name()) - .arg(e.what()) - .arg(e.why())); + if (idx2 < idx0) { + qSwap(idx0, idx2); + } - if (not moltype.warnings().isEmpty()) - { - errors.append("There were warnings parsing this molecule template:"); - errors += moltype.warnings(); + // do we need to resolve this angle parameter (look up the parameters)? + if (not potential.isResolved()) { + // look up the atoms in the molecule template + const auto atom0 = moltype.atom(idx0); + const auto atom1 = moltype.atom(idx1); + const auto atom2 = moltype.atom(idx2); + + // get the angle parameter for these atom types + auto new_potential = + this->angle(atom0.bondType(), atom1.bondType(), atom2.bondType(), + potential.functionType()); + + if (not new_potential.isResolved()) { + errors.append( + QObject::tr( + "Cannot find the angle parameters for " + "the angle between atoms %1-%2-%3 (atom types %4-%5-%6, " + "function type %7).") + .arg(atom0.toString()) + .arg(atom1.toString()) + .arg(atom2.toString()) + .arg(atom0.bondType()) + .arg(atom1.bondType()) + .arg(atom2.bondType()) + .arg(potential.functionType())); + continue; } - return std::make_tuple(Properties(), errors); - } -} + potential = new_potential; + } -/** This internal function is used to return all of the cmap properties - for the passed molecule */ -GroTop::PropsAndErrors GroTop::getCMAPProperties(const MoleculeInfo &molinfo, const GroMolType &moltype) const -{ - try - { - QStringList errors; + if (potential.isBondAngleCrossTerm()) { + // extract and add the Urey Bradley term between the 0-2 atoms + auto bondpot = potential.toBondTerm(); - // add in all of the cmap functions - CMAPFunctions cmapfuncs(molinfo); + auto exp = bondpot.toExpression(R); - const auto cmaps = moltype.cmaps(); + if (not exp.isZero()) { + has_ub = true; - for (auto it = cmaps.constBegin(); it != cmaps.constEnd(); ++it) - { - const auto &cmap = it.key(); - auto potential = it.value(); - - if (potential != "1") - { - errors.append(QObject::tr("The CMAP potential '%1' is not a valid " - "CMAP potential. It should be '1'.") - .arg(potential)); - continue; - } + auto oldfunc = ubfuncs.potential(idx0, idx2); + + if (not oldfunc.isZero()) { + ubfuncs.set(idx0, idx2, exp + oldfunc); + } else { + ubfuncs.set(idx0, idx2, exp); + } + } - AtomIdx idx0 = molinfo.atomIdx(cmap.atom0()); - AtomIdx idx1 = molinfo.atomIdx(cmap.atom1()); - AtomIdx idx2 = molinfo.atomIdx(cmap.atom2()); - AtomIdx idx3 = molinfo.atomIdx(cmap.atom3()); - AtomIdx idx4 = molinfo.atomIdx(cmap.atom4()); + // we will only add the angle part here - we will + // need to add the bond part somewhere else + potential = potential.toAngleTerm(); + } - if (idx4 < idx0) - { - qSwap(idx0, idx4); - qSwap(idx3, idx1); - } + // now create the angle expression + auto exp = potential.toExpression(THETA); - // look up the atoms in the molecule template - const auto atom0 = moltype.atom(idx0); - const auto atom1 = moltype.atom(idx1); - const auto atom2 = moltype.atom(idx2); - const auto atom3 = moltype.atom(idx3); - const auto atom4 = moltype.atom(idx4); - - // get the cmap parameter for these atom types - this returns - // an empty list if there are no matching parameters. We - // only support function type 1 for CMAP potentials - auto resolved = this->cmaps(atom0.atomType(), atom1.atomType(), atom2.atomType(), - atom3.atomType(), atom4.atomType(), 1); - - if (resolved.isEmpty()) - { - errors.append(QObject::tr("Cannot find the cmap parameters for " - "the cmap between atoms %1-%2-%3-%4-%5 (atom types %6-%7-%8-%9-%10, " - "function type 1).") - .arg(atom0.toString()) - .arg(atom1.toString()) - .arg(atom2.toString()) - .arg(atom3.toString()) - .arg(atom4.toString()) - .arg(atom0.atomType()) - .arg(atom1.atomType()) - .arg(atom2.atomType()) - .arg(atom3.atomType()) - .arg(atom4.atomType())); - - continue; - } + if (not exp.isZero()) { + // add this expression onto any existing expression + auto oldfunc = angfuncs.potential(idx0, idx1, idx2); - // we will just use the first CMAP function - cmapfuncs.set(idx0, idx1, idx2, idx3, idx4, resolved[0]); + if (not oldfunc.isZero()) { + angfuncs.set(idx0, idx1, idx2, exp + oldfunc); + } else { + angfuncs.set(idx0, idx1, idx2, exp); } + } + } - Properties props; - props.setProperty("cmap", cmapfuncs); + Properties props; + props.setProperty("angle", angfuncs); - return std::make_tuple(props, errors); - } - catch (const SireError::exception &e) - { - QStringList errors; - errors.append(QObject::tr("Error getting CMAP properties for %1. %2: %3") - .arg(moltype.name()) - .arg(e.what()) - .arg(e.why())); + if (has_ub) + props.setProperty("urey-bradley", ubfuncs); - if (not moltype.warnings().isEmpty()) - { - errors.append("There were warnings parsing this molecule template:"); - errors += moltype.warnings(); - } + return std::make_tuple(props, errors); + } catch (const SireError::exception &e) { + QStringList errors; + errors.append(QObject::tr("Error getting angle properties for %1. %2: %3") + .arg(moltype.name()) + .arg(e.what()) + .arg(e.why())); - return std::make_tuple(Properties(), errors); + if (not moltype.warnings().isEmpty()) { + errors.append("There were warnings parsing this molecule template:"); + errors += moltype.warnings(); } + + return std::make_tuple(Properties(), errors); + } } -/** This function is used to create a molecule. Any errors should be written - to the 'errors' QStringList passed as an argument */ -Molecule GroTop::createMolecule(QString moltype_name, QStringList &errors, const PropertyMap &map) const -{ - // find the molecular template for this molecule - int idx = -1; +/** This internal function is used to return all of the dihedral properties + for the passed molecule */ +GroTop::PropsAndErrors +GroTop::getDihedralProperties(const MoleculeInfo &molinfo, + const GroMolType &moltype) const { + try { + const auto PHI = InternalPotential::symbols().dihedral().phi(); + const auto THETA = InternalPotential::symbols().improper().theta(); - for (int i = 0; i < moltypes.count(); ++i) - { - if (moltypes.constData()[i].name() == moltype_name) - { - idx = i; - break; - } + QStringList errors; + + // add in all of the dihedral and improper functions + FourAtomFunctions dihfuncs(molinfo); + FourAtomFunctions impfuncs(molinfo); + + const auto dihedrals = moltype.dihedrals(); + + bool has_any_impropers = false; + + for (auto it = dihedrals.constBegin(); it != dihedrals.constEnd(); ++it) { + const auto &dihedral = it.key(); + auto potential = it.value(); + + AtomIdx idx0 = molinfo.atomIdx(dihedral.atom0()); + AtomIdx idx1 = molinfo.atomIdx(dihedral.atom1()); + AtomIdx idx2 = molinfo.atomIdx(dihedral.atom2()); + AtomIdx idx3 = molinfo.atomIdx(dihedral.atom3()); + + if (idx3 < idx0) { + qSwap(idx0, idx3); + qSwap(idx2, idx1); + } + + Expression exp; + bool is_improper = false; + + // do we need to resolve this dihedral parameter (look up the parameters)? + if (not potential.isResolved()) { + // look up the atoms in the molecule template + const auto atom0 = moltype.atom(idx0); + const auto atom1 = moltype.atom(idx1); + const auto atom2 = moltype.atom(idx2); + const auto atom3 = moltype.atom(idx3); + + // get the dihedral parameter for these atom types - could be + // many, as they will be added together + auto resolved = this->dihedrals(atom0.bondType(), atom1.bondType(), + atom2.bondType(), atom3.bondType(), + potential.functionType()); + + if (resolved.isEmpty()) { + errors.append(QObject::tr("Cannot find the dihedral parameters for " + "the dihedral between atoms %1-%2-%3-%4 " + "(atom types %5-%6-%7-%8, " + "function type %9).") + .arg(atom0.toString()) + .arg(atom1.toString()) + .arg(atom2.toString()) + .arg(atom3.toString()) + .arg(atom0.bondType()) + .arg(atom1.bondType()) + .arg(atom2.bondType()) + .arg(atom3.bondType()) + .arg(potential.functionType())); + continue; + } + + // sum all of the parts together + for (const auto &r : resolved) { + if (r.isResolved()) { + if (r.isImproperAngleTerm()) { + is_improper = true; + exp += r.toImproperExpression(THETA); + } else { + exp += r.toExpression(PHI); + } + } + } + } else { + // we have a fully-resolved dihedral potential + if (potential.isImproperAngleTerm()) { + exp = potential.toImproperExpression(THETA); + is_improper = true; + } else { + exp = potential.toExpression(PHI); + } + } + + if (not exp.isZero()) { + if (is_improper) { + has_any_impropers = true; + + // add this expression onto any existing expression + auto oldfunc = impfuncs.potential(idx0, idx1, idx2, idx3); + + if (not oldfunc.isZero()) { + impfuncs.set(idx0, idx1, idx2, idx3, exp + oldfunc); + } else { + impfuncs.set(idx0, idx1, idx2, idx3, exp); + } + } else { + // add this expression onto any existing expression + auto oldfunc = dihfuncs.potential(idx0, idx1, idx2, idx3); + + if (not oldfunc.isZero()) { + dihfuncs.set(idx0, idx1, idx2, idx3, exp + oldfunc); + } else { + dihfuncs.set(idx0, idx1, idx2, idx3, exp); + } + } + } + } + + Properties props; + props.setProperty("dihedral", dihfuncs); + + if (has_any_impropers) + props.setProperty("improper", impfuncs); + + return std::make_tuple(props, errors); + } catch (const SireError::exception &e) { + QStringList errors; + errors.append( + QObject::tr("Error getting dihedral properties for %1. %2: %3") + .arg(moltype.name()) + .arg(e.what()) + .arg(e.why())); + + if (not moltype.warnings().isEmpty()) { + errors.append("There were warnings parsing this molecule template:"); + errors += moltype.warnings(); } - if (idx == -1) - { - QStringList typs; + return std::make_tuple(Properties(), errors); + } +} - for (const auto &moltype : moltypes) - { - typs.append(moltype.name()); - } +/** This internal function is used to return all of the cmap properties + for the passed molecule */ +GroTop::PropsAndErrors +GroTop::getCMAPProperties(const MoleculeInfo &molinfo, + const GroMolType &moltype) const { + try { + QStringList errors; + + // add in all of the cmap functions + CMAPFunctions cmapfuncs(molinfo); + + const auto cmaps = moltype.cmaps(); + + for (auto it = cmaps.constBegin(); it != cmaps.constEnd(); ++it) { + const auto &cmap = it.key(); + auto potential = it.value(); + + if (potential != "1") { + errors.append(QObject::tr("The CMAP potential '%1' is not a valid " + "CMAP potential. It should be '1'.") + .arg(potential)); + continue; + } + + AtomIdx idx0 = molinfo.atomIdx(cmap.atom0()); + AtomIdx idx1 = molinfo.atomIdx(cmap.atom1()); + AtomIdx idx2 = molinfo.atomIdx(cmap.atom2()); + AtomIdx idx3 = molinfo.atomIdx(cmap.atom3()); + AtomIdx idx4 = molinfo.atomIdx(cmap.atom4()); + + if (idx4 < idx0) { + qSwap(idx0, idx4); + qSwap(idx3, idx1); + } + + // look up the atoms in the molecule template + const auto atom0 = moltype.atom(idx0); + const auto atom1 = moltype.atom(idx1); + const auto atom2 = moltype.atom(idx2); + const auto atom3 = moltype.atom(idx3); + const auto atom4 = moltype.atom(idx4); + + // get the cmap parameter for these atom types - this returns + // an empty list if there are no matching parameters. We + // only support function type 1 for CMAP potentials + auto resolved = + this->cmaps(atom0.atomType(), atom1.atomType(), atom2.atomType(), + atom3.atomType(), atom4.atomType(), 1); + + if (resolved.isEmpty()) { + errors.append(QObject::tr("Cannot find the cmap parameters for " + "the cmap between atoms %1-%2-%3-%4-%5 (atom " + "types %6-%7-%8-%9-%10, " + "function type 1).") + .arg(atom0.toString()) + .arg(atom1.toString()) + .arg(atom2.toString()) + .arg(atom3.toString()) + .arg(atom4.toString()) + .arg(atom0.atomType()) + .arg(atom1.atomType()) + .arg(atom2.atomType()) + .arg(atom3.atomType()) + .arg(atom4.atomType())); + + continue; + } + + // we will just use the first CMAP function + cmapfuncs.set(idx0, idx1, idx2, idx3, idx4, resolved[0]); + } + + Properties props; + props.setProperty("cmap", cmapfuncs); + + return std::make_tuple(props, errors); + } catch (const SireError::exception &e) { + QStringList errors; + errors.append(QObject::tr("Error getting CMAP properties for %1. %2: %3") + .arg(moltype.name()) + .arg(e.what()) + .arg(e.why())); - errors.append(QObject::tr("There is no molecular template called '%1' " - "in this Gromacs file. Available templates are [ %2 ]") - .arg(moltype_name) - .arg(Sire::toString(typs))); - return Molecule(); + if (not moltype.warnings().isEmpty()) { + errors.append("There were warnings parsing this molecule template:"); + errors += moltype.warnings(); } - const auto moltype = moltypes.constData()[idx]; + return std::make_tuple(Properties(), errors); + } +} - // create the underlying molecule - auto mol = this->createMolecule(moltype, errors, map).edit(); +/** This function is used to create a molecule. Any errors should be written + to the 'errors' QStringList passed as an argument */ +Molecule GroTop::createMolecule(QString moltype_name, QStringList &errors, + const PropertyMap &map) const { + // find the molecular template for this molecule + int idx = -1; - if (mol.nAtoms() == 0) - { - // something went wrong on read - errors.append(QObject::tr("Something went wrong creating a molecule from the template %1.").arg(moltype_name)); - return Molecule(); + for (int i = 0; i < moltypes.count(); ++i) { + if (moltypes.constData()[i].name() == moltype_name) { + idx = i; + break; } + } - mol.rename(moltype_name); - const auto molinfo = mol.info(); + if (idx == -1) { + QStringList typs; - // now get all of the molecule properties - const QVector> funcs = {[&]() - { return getAtomProperties(molinfo, moltype); }, - [&]() - { return getBondProperties(molinfo, moltype); }, - [&]() - { return getAngleProperties(molinfo, moltype); }, - [&]() - { return getDihedralProperties(molinfo, moltype); }, - [&]() - { return getCMAPProperties(molinfo, moltype); }}; - - QVector props(funcs.count()); - auto props_data = props.data(); - - if (usesParallel()) - { - tbb::parallel_for(tbb::blocked_range(0, funcs.count()), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - props_data[i] = funcs.at(i)(); - } }); - } - else - { - for (int i = 0; i < funcs.count(); ++i) - { - props_data[i] = funcs.at(i)(); - } + for (const auto &moltype : moltypes) { + typs.append(moltype.name()); + } + + errors.append( + QObject::tr("There is no molecular template called '%1' " + "in this Gromacs file. Available templates are [ %2 ]") + .arg(moltype_name) + .arg(Sire::toString(typs))); + return Molecule(); + } + + const auto moltype = moltypes.constData()[idx]; + + // create the underlying molecule + auto mol = this->createMolecule(moltype, errors, map).edit(); + + if (mol.nAtoms() == 0) { + // something went wrong on read + errors.append( + QObject::tr( + "Something went wrong creating a molecule from the template %1.") + .arg(moltype_name)); + return Molecule(); + } + + mol.rename(moltype_name); + const auto molinfo = mol.info(); + + // now get all of the molecule properties + const QVector> funcs = { + [&]() { return getAtomProperties(molinfo, moltype); }, + [&]() { return getBondProperties(molinfo, moltype); }, + [&]() { return getAngleProperties(molinfo, moltype); }, + [&]() { return getDihedralProperties(molinfo, moltype); }, + [&]() { return getCMAPProperties(molinfo, moltype); }}; + + QVector props(funcs.count()); + auto props_data = props.data(); + + if (usesParallel()) { + tbb::parallel_for(tbb::blocked_range(0, funcs.count()), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + props_data[i] = funcs.at(i)(); + } + }); + } else { + for (int i = 0; i < funcs.count(); ++i) { + props_data[i] = funcs.at(i)(); } + } - // assemble all of the properties together - for (int i = 0; i < props.count(); ++i) - { - const auto &p = std::get<0>(props.at(i)); - const auto &pe = std::get<1>(props.at(i)); + // assemble all of the properties together + for (int i = 0; i < props.count(); ++i) { + const auto &p = std::get<0>(props.at(i)); + const auto &pe = std::get<1>(props.at(i)); - if (not pe.isEmpty()) - { - errors += pe; - } + if (not pe.isEmpty()) { + errors += pe; + } - for (const auto &key : p.propertyKeys()) - { - const auto mapped = map[key]; + for (const auto &key : p.propertyKeys()) { + const auto mapped = map[key]; - if (mapped.hasValue()) - { - mol.setProperty(key, mapped.value()); - } - else - { - mol.setProperty(mapped, p.property(key)); - } - } + if (mapped.hasValue()) { + mol.setProperty(key, mapped.value()); + } else { + mol.setProperty(mapped, p.property(key)); + } } + } - // finally set the forcefield property - const auto mapped = map["forcefield"]; + // finally set the forcefield property + const auto mapped = map["forcefield"]; - if (mapped.hasValue()) - { - mol.setProperty("forcefield", mapped.value()); - } - else - { - mol.setProperty(mapped, moltype.forcefield()); - } + if (mapped.hasValue()) { + mol.setProperty("forcefield", mapped.value()); + } else { + mol.setProperty(mapped, moltype.forcefield()); + } - return mol.commit(); + return mol.commit(); } -int GroTop::nAtoms() const -{ - return this->startSystem(PropertyMap()).nAtoms(); -} +int GroTop::nAtoms() const { return this->startSystem(PropertyMap()).nAtoms(); } /** Use the data contained in this parser to create a new System of molecules, assigning properties based on the mapping in 'map' */ -System GroTop::startSystem(const PropertyMap &map) const -{ - if (grosys.isEmpty()) - { - // there are no molecules to process - return System(); - } +System GroTop::startSystem(const PropertyMap &map) const { + if (grosys.isEmpty()) { + // there are no molecules to process + return System(); + } - // first, create template molecules for each of the unique molecule types - const auto unique_typs = grosys.uniqueTypes(); + // first, create template molecules for each of the unique molecule types + const auto unique_typs = grosys.uniqueTypes(); - QHash mol_templates; - QHash template_errors; - mol_templates.reserve(unique_typs.count()); + QHash mol_templates; + QHash template_errors; + mol_templates.reserve(unique_typs.count()); - // loop over each unique type, creating the associated molecule and storing - // in mol_templates. If there are any errors, then store them in template_errors - if (usesParallel()) - { - QMutex mutex; + // loop over each unique type, creating the associated molecule and storing + // in mol_templates. If there are any errors, then store them in + // template_errors + if (usesParallel()) { + QMutex mutex; - tbb::parallel_for(tbb::blocked_range(0, unique_typs.count()), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - auto typ = unique_typs[i]; + tbb::parallel_for(tbb::blocked_range(0, unique_typs.count()), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + auto typ = unique_typs[i]; - QStringList errors; - Molecule mol = this->createMolecule(typ, errors, map); + QStringList errors; + Molecule mol = this->createMolecule(typ, errors, map); - QMutexLocker lkr(&mutex); + QMutexLocker lkr(&mutex); - if (not errors.isEmpty()) - { - template_errors.insert(typ, errors); - } + if (not errors.isEmpty()) { + template_errors.insert(typ, errors); + } - mol_templates.insert(typ, mol); - } }); - } - else - { - for (auto typ : unique_typs) - { - QStringList errors; - Molecule mol = this->createMolecule(typ, errors, map); + mol_templates.insert(typ, mol); + } + }); + } else { + for (auto typ : unique_typs) { + QStringList errors; + Molecule mol = this->createMolecule(typ, errors, map); - if (not errors.isEmpty()) - { - template_errors.insert(typ, errors); - } + if (not errors.isEmpty()) { + template_errors.insert(typ, errors); + } - mol_templates.insert(typ, mol); - } + mol_templates.insert(typ, mol); } + } - // next, we see if there were any errors. If there are, then raise an exception - if (not template_errors.isEmpty()) - { - QStringList errors; - - for (auto it = template_errors.constBegin(); it != template_errors.constEnd(); ++it) - { - errors.append(QObject::tr("Error constructing the molecule associated with " - "template '%1' : %2") - .arg(it.key()) - .arg(it.value().join("\n"))); - } + // next, we see if there were any errors. If there are, then raise an + // exception + if (not template_errors.isEmpty()) { + QStringList errors; - throw SireIO::parse_error(QObject::tr("Could not construct a molecule system from the information stored " - "in this Gromacs topology file. Errors include:\n%1") - .arg(errors.join("\n \n")), - CODELOC); + for (auto it = template_errors.constBegin(); + it != template_errors.constEnd(); ++it) { + errors.append( + QObject::tr("Error constructing the molecule associated with " + "template '%1' : %2") + .arg(it.key()) + .arg(it.value().join("\n"))); } - // next, make sure that none of the molecules are empty... - { - QStringList errors; - - for (auto it = mol_templates.constBegin(); it != mol_templates.constEnd(); ++it) - { - if (it.value().isNull()) - { - errors.append(QObject::tr("Error constructing the molecule associated with " - "template '%1' : The molecule is empty!") - .arg(it.key())); - } - } - - if (not errors.isEmpty()) - throw SireIO::parse_error(QObject::tr("Could not construct a molecule system from the information stored " - "in this Gromacs topology file. Errors include:\n%1") - .arg(errors.join("\n \n")), - CODELOC); - } + throw SireIO::parse_error( + QObject::tr( + "Could not construct a molecule system from the information stored " + "in this Gromacs topology file. Errors include:\n%1") + .arg(errors.join("\n \n")), + CODELOC); + } - // now that we have the molecules, we just need to duplicate them - // the correct number of times to create the full system - MoleculeGroup molgroup("all"); + // next, make sure that none of the molecules are empty... + { + QStringList errors; - for (int i = 0; i < grosys.nMolecules(); ++i) - { - molgroup.add(mol_templates.value(grosys[i]).edit().renumber()); + for (auto it = mol_templates.constBegin(); it != mol_templates.constEnd(); + ++it) { + if (it.value().isNull()) { + errors.append( + QObject::tr("Error constructing the molecule associated with " + "template '%1' : The molecule is empty!") + .arg(it.key())); + } } - System system(grosys.name()); - system.add(molgroup); - system.setProperty(map["fileformat"].source(), StringProperty(this->formatName())); - - return system; + if (not errors.isEmpty()) + throw SireIO::parse_error( + QObject::tr("Could not construct a molecule system from the " + "information stored " + "in this Gromacs topology file. Errors include:\n%1") + .arg(errors.join("\n \n")), + CODELOC); + } + + // now that we have the molecules, we just need to duplicate them + // the correct number of times to create the full system + MoleculeGroup molgroup("all"); + + for (int i = 0; i < grosys.nMolecules(); ++i) { + molgroup.add(mol_templates.value(grosys[i]).edit().renumber()); + } + + System system(grosys.name()); + system.add(molgroup); + system.setProperty(map["fileformat"].source(), + StringProperty(this->formatName())); + + return system; } diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index e3842d002..bfe5a1bff 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -23,6 +23,8 @@ organisation on `GitHub `__. * Add convenience function to ``sire.mol.dynamics`` to get current energy trajectory records. +* Fixed parsing of AMBER and GROMACS GLYCAM force field topologies. + `2025.4.0 `__ - February 2026 --------------------------------------------------------------------------------------------- From 067dfa7e20fb87b40146bc36303f196eb65459aa Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 9 Mar 2026 20:51:17 +0000 Subject: [PATCH 3/8] Fix GLYCAM residue reindexing bug. --- corelib/src/libs/SireIO/gro87.cpp | 67 ++++++++++++++++++------------ corelib/src/libs/SireIO/grotop.cpp | 46 ++++++++++++++++---- 2 files changed, 80 insertions(+), 33 deletions(-) diff --git a/corelib/src/libs/SireIO/gro87.cpp b/corelib/src/libs/SireIO/gro87.cpp index adb44d753..24aa5a126 100644 --- a/corelib/src/libs/SireIO/gro87.cpp +++ b/corelib/src/libs/SireIO/gro87.cpp @@ -1889,43 +1889,58 @@ System Gro87::startSystem(const PropertyMap &map) const int ncg = 0; - QSet completed_residues; + // Track used residue numbers to handle topologies where numbering + // restarts (e.g. glycan residues numbered from 1 after a protein + // chain also starting from 1). Duplicate ResNums cause errors when + // looking up residues by number. + // next_unique is always > every number assigned so far, so conflict + // resolution is O(1) rather than a linear scan. + QSet used_resnums; + int next_unique = 0; + auto unique_resnum = [&](int resnum) -> ResNum { + if (!used_resnums.contains(resnum)) + { + used_resnums.insert(resnum); + if (resnum >= next_unique) + next_unique = resnum + 1; + return ResNum(resnum); + } + // conflict: assign next number beyond all previously seen + used_resnums.insert(next_unique); + return ResNum(next_unique++); + }; + + // Track the original residue number/name to detect residue boundaries. + // We compare against the original topology values (not remapped numbers). + // Store editors for the current residue and CutGroup so reparenting uses + // O(1) index lookups rather than O(n) name scans. + int current_orig_resnum = -1; + QString current_resname; + ResStructureEditor current_res; + CGStructureEditor current_cg; for (int i = 0; i < atmnams.count(); ++i) { auto atom = moleditor.add(AtomNum(atmnums[i])); atom = atom.rename(AtomName(atmnams[i])); - const ResNum resnum(resnums[i]); + const int orig_resnum = resnums[i]; + const QString &resnam = resnams[i]; - if (completed_residues.contains(resnum)) + if (orig_resnum != current_orig_resnum || resnam != current_resname) { - auto res = moleditor.residue(resnum); - - if (res.name().value() != resnams[i]) - { - // different residue - res = moleditor.add(resnum); - res = res.rename(ResName(resnams[i])); - ncg += 1; - moleditor.add(CGName(QString::number(ncg))); - } - - atom = atom.reparent(res.index()); - atom = atom.reparent(CGName(QString::number(ncg))); - } - else - { - auto res = moleditor.add(resnum); - res = res.rename(ResName(resnams[i])); - atom = atom.reparent(res.index()); + // new residue + current_res = moleditor.add(unique_resnum(orig_resnum)); + current_res = current_res.rename(ResName(resnam)); + current_orig_resnum = orig_resnum; + current_resname = resnam; ncg += 1; - auto cg = moleditor.add(CGName(QString::number(ncg))); - atom = atom.reparent(cg.index()); - - completed_residues.insert(resnum); + current_cg = moleditor.add(CGName(QString::number(ncg))); } + + atom = atom.reparent(current_res.index()); + atom = atom.reparent(current_cg.index()); } // we have created the molecule - now add in the coordinates/velocities as needed diff --git a/corelib/src/libs/SireIO/grotop.cpp b/corelib/src/libs/SireIO/grotop.cpp index 8d4a4fefe..187e5ba46 100644 --- a/corelib/src/libs/SireIO/grotop.cpp +++ b/corelib/src/libs/SireIO/grotop.cpp @@ -7639,6 +7639,32 @@ Molecule GroTop::createMolecule(const GroMolType &moltype, QStringList &errors, ChainStructureEditor chain; CGStructureEditor cgroup; + // Track used residue numbers to handle topologies where numbering restarts + // (e.g. glycan residues numbered from 1 after a protein chain also starting + // from 1). Duplicate ResNums within a molecule cause duplicate_residue errors. + // next_unique is always > every number assigned so far, so conflict + // resolution is O(1) rather than a linear scan. + QSet used_resnums; + int next_unique = 0; + auto unique_resnum = [&](int resnum) -> ResNum { + if (!used_resnums.contains(resnum)) + { + used_resnums.insert(resnum); + if (resnum >= next_unique) + next_unique = resnum + 1; + return ResNum(resnum); + } + // conflict: assign next number beyond all previously seen + used_resnums.insert(next_unique); + return ResNum(next_unique++); + }; + + // Track the original (topology) residue number of the current residue + // separately, because unique_resnum may assign a different number. The + // "is this a new residue?" check must use the original topology number. + int current_orig_resnum = -1; + ResName current_resname; + auto different_chain = [&](const ChainName &name) { if (name.isNull() and chain.isEmpty()) return false; @@ -7656,12 +7682,14 @@ Molecule GroTop::createMolecule(const GroMolType &moltype, QStringList &errors, if (not atom.chainName().isNull()) { chain = mol.add(ChainName(atom.chainName())); - res = chain.add(ResNum(atom.residueNumber())); + res = chain.add(unique_resnum(atom.residueNumber())); } else { - res = mol.add(ResNum(atom.residueNumber())); + res = mol.add(unique_resnum(atom.residueNumber())); } res = res.rename(atom.residueName()); + current_orig_resnum = atom.residueNumber(); + current_resname = atom.residueName(); } else if (different_chain(atom.chainName())) { // this atom is in a different residue in a different chain cgroup = mol.add(CGName(QString::number(cgidx))); @@ -7670,22 +7698,26 @@ Molecule GroTop::createMolecule(const GroMolType &moltype, QStringList &errors, if (atom.chainName().isNull()) { // residue is not in a chain chain = ChainStructureEditor(); - res = mol.add(ResNum(atom.residueNumber())); + res = mol.add(unique_resnum(atom.residueNumber())); } else { // residue is in a chain chain = mol.add(ChainName(atom.chainName())); - res = chain.add(ResNum(atom.residueNumber())); + res = chain.add(unique_resnum(atom.residueNumber())); } res = res.rename(atom.residueName()); - } else if (atom.residueNumber() != res.number() or - atom.residueName() != res.name()) { + current_orig_resnum = atom.residueNumber(); + current_resname = atom.residueName(); + } else if (atom.residueNumber() != current_orig_resnum or + atom.residueName() != current_resname) { // this atom is in a different residue cgroup = mol.add(CGName(QString::number(cgidx))); cgidx += 1; - res = mol.add(ResNum(atom.residueNumber())); + res = mol.add(unique_resnum(atom.residueNumber())); res = res.rename(atom.residueName()); + current_orig_resnum = atom.residueNumber(); + current_resname = atom.residueName(); } // add the atom to the residue From 0b6f83d396c58d71f5717bb860da399fe42707d1 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Mon, 9 Mar 2026 21:13:22 +0000 Subject: [PATCH 4/8] Mark GLYCAM tests as slow and refactor. --- tests/io/test_amberprm.py | 76 --------------------------------------- tests/io/test_grotop.py | 1 + tests/io/test_prmtop.py | 74 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 76 deletions(-) delete mode 100644 tests/io/test_amberprm.py diff --git a/tests/io/test_amberprm.py b/tests/io/test_amberprm.py deleted file mode 100644 index 30b8138ab..000000000 --- a/tests/io/test_amberprm.py +++ /dev/null @@ -1,76 +0,0 @@ -import sire as sr - -import pytest - - -def test_glycam(tmpdir): - """Test that a topology using the GLYCAM force field (SCEE=1.0, SCNB=1.0) - round-trips correctly through AMBER prm7 format. - - GLYCAM uses full 1-4 interactions (no scaling), so SCEE=1.0 and SCNB=1.0 - for glycan dihedrals. The protein dihedrals use standard AMBER scaling - (SCEE=1.2, SCNB=2.0). Before the fix, CLJScaleFactor(1.0, 1.0) pairs were - silently dropped when building AmberParams, so glycan dihedrals were written - with SCEE=0 and SCNB=0, giving zero 1-4 interactions. - """ - - # Load the GLYCAM topology and coordinates. - mols = sr.load_test_files("glycam.top", "glycam.gro") - - # Write to AMBER prm7 + rst7 format. - d = tmpdir.mkdir("test_glycam_amber") - f = sr.save(mols, d.join("glycam_out"), format=["PRM7", "RST7"]) - - # Parse SCEE_SCALE_FACTOR and SCNB_SCALE_FACTOR from the written prm7. - # Format: 5 values per line, 16 chars each (AmberFormat FLOAT 5 16 8). - scee_values = [] - scnb_values = [] - reading = None - - with open(f[0], "r") as fh: - for line in fh: - if line.startswith("%FLAG SCEE_SCALE_FACTOR"): - reading = "scee" - continue - elif line.startswith("%FLAG SCNB_SCALE_FACTOR"): - reading = "scnb" - continue - elif line.startswith("%FLAG") or line.startswith("%FORMAT"): - if line.startswith("%FLAG"): - reading = None - continue - if reading == "scee": - scee_values.extend(float(x) for x in line.split()) - elif reading == "scnb": - scnb_values.extend(float(x) for x in line.split()) - - assert len(scee_values) > 0, "No SCEE_SCALE_FACTOR entries found" - assert len(scnb_values) > 0, "No SCNB_SCALE_FACTOR entries found" - - # The system has both glycan (SCEE=1.0, SCNB=1.0) and protein - # (SCEE=1.2, SCNB=2.0) dihedrals. Both values must be present. - # Before the fix, all glycan entries would be 0.0. - assert any( - v == pytest.approx(1.0) for v in scee_values - ), "No SCEE=1.0 entries found; GLYCAM glycan dihedrals were not written correctly" - assert any( - v == pytest.approx(1.2, rel=1e-3) for v in scee_values - ), "No SCEE=1.2 entries found; standard AMBER protein dihedrals are missing" - assert any( - v == pytest.approx(1.0) for v in scnb_values - ), "No SCNB=1.0 entries found; GLYCAM glycan dihedrals were not written correctly" - assert any( - v == pytest.approx(2.0, rel=1e-3) for v in scnb_values - ), "No SCNB=2.0 entries found; standard AMBER protein dihedrals are missing" - - # SCEE/SCNB=0.0 is valid and expected — it marks dihedrals that share - # terminal atoms with another dihedral and should not contribute a 1-4 term. - # The pre-fix bug was that ALL glycan dihedral entries were 0.0 because - # CLJScaleFactor(1.0, 1.0) pairs were silently dropped. Having both 1.0 and - # 1.2 present (checked above) confirms the fix is working correctly. - - # Reload and verify the energy is self-consistent after the AMBER roundtrip. - # Before the fix, glycan 1-4 pairs had SCEE=0/SCNB=0, giving zero 1-4 - # interactions and a different energy. - mols2 = sr.load(f) - assert mols2.energy().value() == pytest.approx(mols.energy().value(), rel=1e-3) diff --git a/tests/io/test_grotop.py b/tests/io/test_grotop.py index cf2e51f6e..1014afc06 100644 --- a/tests/io/test_grotop.py +++ b/tests/io/test_grotop.py @@ -377,6 +377,7 @@ def test_grotop_water(tmpdir, water_model): is_vsite = True +@pytest.mark.slow def test_glycam(tmpdir): """Test that a topology using the GLYCAM force field (SCEE=1.0, SCNB=1.0) is read and written correctly. diff --git a/tests/io/test_prmtop.py b/tests/io/test_prmtop.py index 3c790ca3d..fc8edcbc2 100644 --- a/tests/io/test_prmtop.py +++ b/tests/io/test_prmtop.py @@ -258,6 +258,80 @@ def test_reorder_prmtop(reordered_protein): for i in range(3): assert z[i].value() == pytest.approx(coord[i].value(), 1e-3) + +@pytest.mark.slow +def test_glycam(tmpdir): + """Test that a topology using the GLYCAM force field (SCEE=1.0, SCNB=1.0) + round-trips correctly through AMBER prm7 format. + + GLYCAM uses full 1-4 interactions (no scaling), so SCEE=1.0 and SCNB=1.0 + for glycan dihedrals. The protein dihedrals use standard AMBER scaling + (SCEE=1.2, SCNB=2.0). Before the fix, CLJScaleFactor(1.0, 1.0) pairs were + silently dropped when building AmberParams, so glycan dihedrals were written + with SCEE=0 and SCNB=0, giving zero 1-4 interactions. + """ + + # Load the GLYCAM topology and coordinates. + mols = sr.load_test_files("glycam.top", "glycam.gro") + + # Write to AMBER prm7 + rst7 format. + d = tmpdir.mkdir("test_glycam_amber") + f = sr.save(mols, d.join("glycam_out"), format=["PRM7", "RST7"]) + + # Parse SCEE_SCALE_FACTOR and SCNB_SCALE_FACTOR from the written prm7. + # Format: 5 values per line, 16 chars each (AmberFormat FLOAT 5 16 8). + scee_values = [] + scnb_values = [] + reading = None + + with open(f[0], "r") as fh: + for line in fh: + if line.startswith("%FLAG SCEE_SCALE_FACTOR"): + reading = "scee" + continue + elif line.startswith("%FLAG SCNB_SCALE_FACTOR"): + reading = "scnb" + continue + elif line.startswith("%FLAG") or line.startswith("%FORMAT"): + if line.startswith("%FLAG"): + reading = None + continue + if reading == "scee": + scee_values.extend(float(x) for x in line.split()) + elif reading == "scnb": + scnb_values.extend(float(x) for x in line.split()) + + assert len(scee_values) > 0, "No SCEE_SCALE_FACTOR entries found" + assert len(scnb_values) > 0, "No SCNB_SCALE_FACTOR entries found" + + # The system has both glycan (SCEE=1.0, SCNB=1.0) and protein + # (SCEE=1.2, SCNB=2.0) dihedrals. Both values must be present. + # Before the fix, all glycan entries would be 0.0. + assert any( + v == pytest.approx(1.0) for v in scee_values + ), "No SCEE=1.0 entries found; GLYCAM glycan dihedrals were not written correctly" + assert any( + v == pytest.approx(1.2, rel=1e-3) for v in scee_values + ), "No SCEE=1.2 entries found; standard AMBER protein dihedrals are missing" + assert any( + v == pytest.approx(1.0) for v in scnb_values + ), "No SCNB=1.0 entries found; GLYCAM glycan dihedrals were not written correctly" + assert any( + v == pytest.approx(2.0, rel=1e-3) for v in scnb_values + ), "No SCNB=2.0 entries found; standard AMBER protein dihedrals are missing" + + # SCEE/SCNB=0.0 is valid and expected — it marks dihedrals that share + # terminal atoms with another dihedral and should not contribute a 1-4 term. + # The pre-fix bug was that ALL glycan dihedral entries were 0.0 because + # CLJScaleFactor(1.0, 1.0) pairs were silently dropped. Having both 1.0 and + # 1.2 present (checked above) confirms the fix is working correctly. + + # Reload and verify the energy is self-consistent after the AMBER roundtrip. + # Before the fix, glycan 1-4 pairs had SCEE=0/SCNB=0, giving zero 1-4 + # interactions and a different energy. + mols2 = sr.load(f) + assert mols2.energy().value() == pytest.approx(mols.energy().value(), rel=1e-3) + # check that the zinc atoms are bonded to the proteins bonds = mols.bonds("element Zn") From 97de1d494b24b2e905f3ad6307e63d484d5b17ef Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Thu, 12 Mar 2026 16:13:16 +0000 Subject: [PATCH 5/8] Add BioSimSpace helper function to merge intrascale properties. --- corelib/src/libs/SireIO/biosimspace.cpp | 75 +++++++++++++++++++++++++ corelib/src/libs/SireIO/biosimspace.h | 44 +++++++++++++++ wrapper/IO/_IO_free_functions.pypp.cpp | 44 +++++++++++++-- 3 files changed, 159 insertions(+), 4 deletions(-) diff --git a/corelib/src/libs/SireIO/biosimspace.cpp b/corelib/src/libs/SireIO/biosimspace.cpp index 89dd01ca5..96628d6a6 100644 --- a/corelib/src/libs/SireIO/biosimspace.cpp +++ b/corelib/src/libs/SireIO/biosimspace.cpp @@ -32,6 +32,8 @@ #include "SireError/errors.h" +#include "SireMM/cljnbpairs.h" + #include "SireMol/atomelements.h" #include "SireMol/atommasses.h" #include "SireMol/connectivity.h" @@ -39,6 +41,7 @@ #include "SireMol/mgname.h" #include "SireMol/moleditor.h" #include "SireMol/molidx.h" +#include "SireMol/moleculeinfodata.h" #include "SireVol/periodicbox.h" #include "SireVol/triclinicbox.h" @@ -49,6 +52,7 @@ using namespace SireBase; using namespace SireMaths; +using namespace SireMM; using namespace SireMol; using namespace SireUnits; using namespace SireVol; @@ -1754,4 +1758,75 @@ namespace SireIO return Vector(nx, ny, nz); } + SireBase::PropertyList mergeIntrascale(const CLJNBPairs &nb0, + const CLJNBPairs &nb1, + const MoleculeInfoData &merged_info, + const QHash &mol0_merged_mapping, + const QHash &mol1_merged_mapping) + { + // Helper lambda: copy the non-default scaling factors from 'nb' to + // 'nb_merged' according to the provided mapping. Takes nb_merged by + // reference to avoid copies. + auto copyIntrascale = [&](const CLJNBPairs &nb, CLJNBPairs &nb_merged, + const QHash &mapping) + { + const int n = nb.nAtoms(); + + for (int i = 0; i < n; ++i) + { + const AtomIdx ai(i); + + // Get the index of this atom in the merged system. + const AtomIdx merged_ai = mapping.value(ai, AtomIdx(-1)); + + // If this atom hasn't been mapped to the merged system, then we + // can skip it, as any scaling factors involving this atom will + // just use the default. + if (merged_ai == AtomIdx(-1)) + continue; + + for (int j = i; j < n; ++j) + { + const AtomIdx aj(j); + + // Get the scaling factor for this pair of atoms. + const CLJScaleFactor sf = nb.get(ai, aj); + + // This is a non-default scaling factor, so we need to copy + // it across to the merged intrascale object according to + // the mapping. + if (sf.coulomb() != 1.0 or sf.lj() != 1.0) + { + // Get the index of the second atom in the merged system. + const AtomIdx merged_aj = mapping.value(aj, AtomIdx(-1)); + + // Only set the scaling factor if both atoms have been + // mapped to the merged system. If one of the atoms + // hasn't been mapped, then we can just use the default. + if (merged_aj != AtomIdx(-1)) + nb_merged.set(merged_ai, merged_aj, sf); + } + } + } + }; + + // Create the intrascale objects for the merged end-states. + CLJNBPairs intra0(merged_info); + CLJNBPairs intra1(merged_info); + + // Copy the non-default scaling factors from the original intrascale + // objects to the merged intrascale objects according to the provided + // mappings. + copyIntrascale(nb1, intra0, mol1_merged_mapping); + copyIntrascale(nb0, intra0, mol0_merged_mapping); + copyIntrascale(nb0, intra1, mol0_merged_mapping); + copyIntrascale(nb1, intra1, mol1_merged_mapping); + + // Assemble the intrascale objects into a property list to return. + SireBase::PropertyList ret; + ret.append(intra0); + ret.append(intra1); + return ret; + } + } // namespace SireIO diff --git a/corelib/src/libs/SireIO/biosimspace.h b/corelib/src/libs/SireIO/biosimspace.h index 081735cff..6e7f64612 100644 --- a/corelib/src/libs/SireIO/biosimspace.h +++ b/corelib/src/libs/SireIO/biosimspace.h @@ -36,6 +36,12 @@ #include "SireMaths/vector.h" +#include "SireBase/propertylist.h" + +#include "SireMM/cljnbpairs.h" + +#include "SireMol/atomidxmapping.h" +#include "SireMol/moleculeinfodata.h" #include "SireMol/select.h" SIRE_BEGIN_HEADER @@ -372,6 +378,43 @@ namespace SireIO const bool is_lambda1 = false, const PropertyMap &map = PropertyMap()); Vector cross(const Vector &v0, const Vector &v1); + + //! Merge the CLJNBPairs (intrascale) of two molecules into a perturbable + /*! merged molecule's end-state intrascales. + + Expands nb0 from molecule0's atom index space into the merged + molecule's space (preserving actual per-pair scale factors, including + force-field-specific overrides such as GLYCAM funct=2 pairs), then + calls CLJNBPairs::merge to produce intrascale0 and intrascale1. + + \param nb0 + The CLJNBPairs for molecule0 in its original atom index space. + + \param nb1 + The CLJNBPairs for molecule1 in its original atom index space. + + \param merged_info + The MoleculeInfoData for the merged molecule. + + \param mol0_merged_mapping + A hash mapping each AtomIdx in molecule0's original space to + the corresponding AtomIdx in the merged molecule's space. + + \param atom_mapping + The AtomIdxMapping describing how merged-molecule atom indices + correspond to molecule1 atom indices (used by CLJNBPairs::merge). + + \retval [intrascale0, intrascale1] + A PropertyList containing the CLJNBPairs for the lambda=0 and + lambda=1 end states of the merged molecule. + */ + SIREIO_EXPORT SireBase::PropertyList mergeIntrascale( + const SireMM::CLJNBPairs &nb0, + const SireMM::CLJNBPairs &nb1, + const SireMol::MoleculeInfoData &merged_info, + const QHash &mol0_merged_mapping, + const QHash &mol1_merged_mapping); + } // namespace SireIO SIRE_EXPOSE_FUNCTION(SireIO::isAmberWater) @@ -387,6 +430,7 @@ SIRE_EXPOSE_FUNCTION(SireIO::updateCoordinatesAndVelocities) SIRE_EXPOSE_FUNCTION(SireIO::createSodiumIon) SIRE_EXPOSE_FUNCTION(SireIO::createChlorineIon) SIRE_EXPOSE_FUNCTION(SireIO::setCoordinates) +SIRE_EXPOSE_FUNCTION(SireIO::mergeIntrascale) SIRE_END_HEADER diff --git a/wrapper/IO/_IO_free_functions.pypp.cpp b/wrapper/IO/_IO_free_functions.pypp.cpp index 4a5b57a62..e307165f0 100644 --- a/wrapper/IO/_IO_free_functions.pypp.cpp +++ b/wrapper/IO/_IO_free_functions.pypp.cpp @@ -7,6 +7,13 @@ namespace bp = boost::python; +#include "SireBase/propertylist.h" + +#include "SireMM/cljnbpairs.h" + +#include "SireMol/atomidxmapping.h" +#include "SireMol/moleculeinfodata.h" + #include "SireBase/getinstalldir.h" #include "SireBase/parallel.h" @@ -871,16 +878,45 @@ void register_free_functions(){ } { //::SireIO::updateCoordinatesAndVelocities - + typedef ::boost::tuples::tuple< SireSystem::System, QHash< SireMol::MolIdx, SireMol::MolIdx >, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type > ( *updateCoordinatesAndVelocities_function_type )( ::SireSystem::System const &,::SireSystem::System const &,::SireSystem::System const &,::QHash< SireMol::MolIdx, SireMol::MolIdx > const &,bool const,::SireBase::PropertyMap const &,::SireBase::PropertyMap const & ); updateCoordinatesAndVelocities_function_type updateCoordinatesAndVelocities_function_value( &::SireIO::updateCoordinatesAndVelocities ); - - bp::def( + + bp::def( "updateCoordinatesAndVelocities" , updateCoordinatesAndVelocities_function_value , ( bp::arg("original_system"), bp::arg("renumbered_system"), bp::arg("updated_system"), bp::arg("molecule_mapping"), bp::arg("is_lambda1")=(bool const)(false), bp::arg("map0")=SireBase::PropertyMap(), bp::arg("map1")=SireBase::PropertyMap() ) , "Update the coordinates and velocities of original_system with those from\nupdated_system.\n\nPar:am system_original\nThe original system.\n\nPar:am system_renumbered\nThe original system, atoms and residues have been renumbered to be\nunique and in ascending order.\n\nPar:am system_updated\nThe updated system, where molecules may not be in the same order.\n\nPar:am map0\nA dictionary of user-defined molecular property names for system0.\n\nPar:am map1\nA dictionary of user-defined molecular property names for system1.\n\nRetval: system, mapping\nThe system with updated coordinates and velocities and a mapping\nbetween the molecule indices in both systems.\n" ); - + + } + + { //::SireIO::mergeIntrascale + + typedef ::SireBase::PropertyList ( *mergeIntrascale_function_type )( + ::SireMM::CLJNBPairs const &, + ::SireMM::CLJNBPairs const &, + ::SireMol::MoleculeInfoData const &, + ::QHash< SireMol::AtomIdx, SireMol::AtomIdx > const &, + ::QHash< SireMol::AtomIdx, SireMol::AtomIdx > const & ); + mergeIntrascale_function_type mergeIntrascale_function_value( &::SireIO::mergeIntrascale ); + + bp::def( + "mergeIntrascale" + , mergeIntrascale_function_value + , ( bp::arg("nb0"), bp::arg("nb1"), bp::arg("merged_info"), + bp::arg("mol0_merged_mapping"), bp::arg("mol1_merged_mapping") ) + , "Merge the CLJNBPairs (intrascale) of two molecules into the end-state\n" + "intrascales of a perturbable merged molecule.\n" + "\n" + "Uses a two-pass approach to correctly preserve actual per-pair scale factors\n" + "(including GLYCAM funct=2 overrides of 1.0/1.0) without relying on global\n" + "forcefield scale factors. For intrascale0, nb1 is copied first (so ghost\n" + "atoms from mol1 get correct exclusions), then nb0 overwrites (so mapped and\n" + "mol0-unique atoms get correct state-0 values). intrascale1 is built with the\n" + "same logic in reverse.\n" + "\n" + "Returns a PropertyList [intrascale0, intrascale1].\n" ); + } } From ccbd942921de9d99e551fb70107f7b033b99f69b Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 13 Mar 2026 13:42:16 +0000 Subject: [PATCH 6/8] Fix pasting error. --- tests/io/test_prmtop.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/io/test_prmtop.py b/tests/io/test_prmtop.py index fc8edcbc2..3f593385d 100644 --- a/tests/io/test_prmtop.py +++ b/tests/io/test_prmtop.py @@ -258,6 +258,20 @@ def test_reorder_prmtop(reordered_protein): for i in range(3): assert z[i].value() == pytest.approx(coord[i].value(), 1e-3) + # check that the zinc atoms are bonded to the proteins + bonds = mols.bonds("element Zn") + + assert len(bonds) == 16 + + zn = sr.mol.Element("Zn") + + for bond in bonds: + assert bond[0].element() == zn or bond[1].element() == zn + assert bond[0].molecule() == bond[1].molecule() + + assert mols[0]["element Zn"].num_atoms() == 2 + assert mols[1]["element Zn"].num_atoms() == 2 + @pytest.mark.slow def test_glycam(tmpdir): @@ -331,17 +345,3 @@ def test_glycam(tmpdir): # interactions and a different energy. mols2 = sr.load(f) assert mols2.energy().value() == pytest.approx(mols.energy().value(), rel=1e-3) - - # check that the zinc atoms are bonded to the proteins - bonds = mols.bonds("element Zn") - - assert len(bonds) == 16 - - zn = sr.mol.Element("Zn") - - for bond in bonds: - assert bond[0].element() == zn or bond[1].element() == zn - assert bond[0].molecule() == bond[1].molecule() - - assert mols[0]["element Zn"].num_atoms() == 2 - assert mols[1]["element Zn"].num_atoms() == 2 From ff60c0dba5d9d73d377f0005e810a66bda02387a Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 13 Mar 2026 14:31:23 +0000 Subject: [PATCH 7/8] Fix GroMolType streaming operator. --- corelib/src/libs/SireIO/grotop.cpp | 161 +++++++++++++++++------------ 1 file changed, 93 insertions(+), 68 deletions(-) diff --git a/corelib/src/libs/SireIO/grotop.cpp b/corelib/src/libs/SireIO/grotop.cpp index 187e5ba46..e2bcfb3b9 100644 --- a/corelib/src/libs/SireIO/grotop.cpp +++ b/corelib/src/libs/SireIO/grotop.cpp @@ -82,56 +82,62 @@ using namespace SireStream; static const RegisterMetaType r_groatom(NO_ROOT); -QDataStream &operator<<(QDataStream &ds, const GroAtom &atom) { - writeHeader(ds, r_groatom, 3); +QDataStream &operator<<(QDataStream &ds, const GroAtom &atom) +{ + writeHeader(ds, r_groatom, 3); - SharedDataStream sds(ds); + SharedDataStream sds(ds); - sds << atom.atmname << atom.resname << atom.chainname << atom.atmtyp - << atom.bndtyp << atom.atmnum << atom.resnum << atom.chggrp - << atom.chg.to(mod_electron) << atom.mss.to(g_per_mol); + sds << atom.atmname << atom.resname << atom.chainname << atom.atmtyp << atom.bndtyp << atom.atmnum << atom.resnum + << atom.chggrp << atom.chg.to(mod_electron) << atom.mss.to(g_per_mol); - return ds; + return ds; } -QDataStream &operator>>(QDataStream &ds, GroAtom &atom) { - VersionID v = readHeader(ds, r_groatom); +QDataStream &operator>>(QDataStream &ds, GroAtom &atom) +{ + VersionID v = readHeader(ds, r_groatom); - if (v == 3) { - SharedDataStream sds(ds); + if (v == 3) + { + SharedDataStream sds(ds); - double chg, mass; + double chg, mass; - sds >> atom.atmname >> atom.resname >> atom.chainname >> atom.atmtyp >> - atom.bndtyp >> atom.atmnum >> atom.resnum >> atom.chggrp >> chg >> mass; + sds >> atom.atmname >> atom.resname >> atom.chainname >> atom.atmtyp >> atom.bndtyp >> atom.atmnum >> + atom.resnum >> atom.chggrp >> chg >> mass; - atom.chg = chg * mod_electron; - atom.mss = mass * g_per_mol; - } else if (v == 2) { - SharedDataStream sds(ds); + atom.chg = chg * mod_electron; + atom.mss = mass * g_per_mol; + } + else if (v == 2) + { + SharedDataStream sds(ds); - double chg, mass; + double chg, mass; - sds >> atom.atmname >> atom.resname >> atom.atmtyp >> atom.bndtyp >> - atom.atmnum >> atom.resnum >> atom.chggrp >> chg >> mass; + sds >> atom.atmname >> atom.resname >> atom.atmtyp >> atom.bndtyp >> atom.atmnum >> atom.resnum >> + atom.chggrp >> chg >> mass; - atom.chg = chg * mod_electron; - atom.mss = mass * g_per_mol; - } else if (v == 1) { - SharedDataStream sds(ds); + atom.chg = chg * mod_electron; + atom.mss = mass * g_per_mol; + } + else if (v == 1) + { + SharedDataStream sds(ds); - double chg, mass; + double chg, mass; - sds >> atom.atmname >> atom.resname >> atom.atmtyp >> atom.atmnum >> - atom.resnum >> atom.chggrp >> chg >> mass; + sds >> atom.atmname >> atom.resname >> atom.atmtyp >> atom.atmnum >> atom.resnum >> atom.chggrp >> chg >> mass; - atom.bndtyp = atom.atmtyp; - atom.chg = chg * mod_electron; - atom.mss = mass * g_per_mol; - } else - throw version_error(v, "1,2", r_groatom, CODELOC); + atom.bndtyp = atom.atmtyp; + atom.chg = chg * mod_electron; + atom.mss = mass * g_per_mol; + } + else + throw version_error(v, "1,2,3", r_groatom, CODELOC); - return ds; + return ds; } /** Constructor */ @@ -279,52 +285,71 @@ void GroAtom::setMass(SireUnits::Dimension::MolarMass mass) { static const RegisterMetaType r_gromoltyp(NO_ROOT); -QDataStream &operator<<(QDataStream &ds, const GroMolType &moltyp) { - writeHeader(ds, r_gromoltyp, 3); +QDataStream &operator<<(QDataStream &ds, const GroMolType &moltyp) +{ + writeHeader(ds, r_gromoltyp, 4); - SharedDataStream sds(ds); + SharedDataStream sds(ds); - sds << moltyp.nme << moltyp.warns << moltyp.atms0 << moltyp.atms1 - << moltyp.bnds0 << moltyp.bnds1 << moltyp.angs0 << moltyp.angs1 - << moltyp.dihs0 << moltyp.dihs1 << moltyp.cmaps0 << moltyp.cmaps1 - << moltyp.first_atoms0 << moltyp.first_atoms1 << moltyp.ffield0 - << moltyp.ffield1 << moltyp.nexcl0 << moltyp.nexcl1 - << moltyp.is_perturbable; + sds << moltyp.nme << moltyp.warns << moltyp.atms0 << moltyp.atms1 << moltyp.bnds0 << moltyp.bnds1 << moltyp.angs0 + << moltyp.angs1 << moltyp.dihs0 << moltyp.dihs1 + << moltyp.cmaps0 << moltyp.cmaps1 << moltyp.first_atoms0 << moltyp.first_atoms1 << moltyp.ffield0 + << moltyp.ffield1 << moltyp.nexcl0 << moltyp.nexcl1 << moltyp.is_perturbable; - return ds; + return ds; } -QDataStream &operator>>(QDataStream &ds, GroMolType &moltyp) { - VersionID v = readHeader(ds, r_gromoltyp); - - if (v == 1 or v == 2 or v == 3) { - SharedDataStream sds(ds); +QDataStream &operator>>(QDataStream &ds, GroMolType &moltyp) +{ + VersionID v = readHeader(ds, r_gromoltyp); - sds >> moltyp.nme >> moltyp.warns >> moltyp.atms0 >> moltyp.atms1 >> - moltyp.bnds0 >> moltyp.bnds1 >> moltyp.angs0 >> moltyp.angs1 >> - moltyp.dihs0 >> moltyp.dihs1; + if (v == 4) + { + // v4: all fields in correct order, fixing the v3 write/read misalignment + // where ffield was written but not read back. + SharedDataStream sds(ds); - if (v == 3) { - sds >> moltyp.cmaps0 >> moltyp.cmaps1; - } else { - moltyp.cmaps0 = QHash(); - moltyp.cmaps1 = QHash(); + sds >> moltyp.nme >> moltyp.warns >> moltyp.atms0 >> moltyp.atms1 >> moltyp.bnds0 >> moltyp.bnds1 >> + moltyp.angs0 >> moltyp.angs1 >> moltyp.dihs0 >> moltyp.dihs1 >> + moltyp.cmaps0 >> moltyp.cmaps1 >> moltyp.first_atoms0 >> moltyp.first_atoms1 >> + moltyp.ffield0 >> moltyp.ffield1 >> moltyp.nexcl0 >> moltyp.nexcl1 >> moltyp.is_perturbable; } + else if (v == 1 or v == 2 or v == 3) + { + // v1/v2/v3: preserved as-is for backward compatibility. + // Note: v3 streams have a write/read misalignment (ffield was written + // but not read back); existing v3 caches will be corrupt. v4 fixes this. + SharedDataStream sds(ds); - sds >> moltyp.first_atoms0 >> moltyp.first_atoms1; + sds >> moltyp.nme >> moltyp.warns >> moltyp.atms0 >> moltyp.atms1 >> moltyp.bnds0 >> moltyp.bnds1 >> + moltyp.angs0 >> moltyp.angs1 >> moltyp.dihs0 >> moltyp.dihs1; - if (v == 2) - sds >> moltyp.ffield0 >> moltyp.ffield1; - else { - moltyp.ffield0 = MMDetail(); - moltyp.ffield1 = MMDetail(); - } + if (v == 3) + { + sds >> moltyp.cmaps0 >> moltyp.cmaps1; + } + else + { + moltyp.cmaps0 = QHash(); + moltyp.cmaps1 = QHash(); + } - sds >> moltyp.nexcl0 >> moltyp.nexcl1 >> moltyp.is_perturbable; - } else - throw version_error(v, "1,2,3", r_gromoltyp, CODELOC); + sds >> moltyp.first_atoms0 >> moltyp.first_atoms1; - return ds; + if (v == 2) + sds >> moltyp.ffield0 >> moltyp.ffield1; + else + { + moltyp.ffield0 = MMDetail(); + moltyp.ffield1 = MMDetail(); + } + + sds >> moltyp.nexcl0 >> moltyp.nexcl1 >> moltyp.is_perturbable; + } + else + throw version_error(v, "1,2,3,4", r_gromoltyp, CODELOC); + + return ds; } /** Constructor */ From a23a2321225fe3d9482514ed9d8f631456100038 Mon Sep 17 00:00:00 2001 From: Lester Hedges Date: Fri, 13 Mar 2026 15:24:56 +0000 Subject: [PATCH 8/8] Fix errors caused by 1-3 terms in merged molecule. --- corelib/src/libs/SireMM/amberparams.cpp | 4082 +++++++++++------------ corelib/src/libs/SireMM/amberparams.h | 5 + 2 files changed, 1925 insertions(+), 2162 deletions(-) diff --git a/corelib/src/libs/SireMM/amberparams.cpp b/corelib/src/libs/SireMM/amberparams.cpp index 2aa147da0..77001a2c9 100644 --- a/corelib/src/libs/SireMM/amberparams.cpp +++ b/corelib/src/libs/SireMM/amberparams.cpp @@ -80,218 +80,184 @@ using namespace SireUnits::Dimension; static const RegisterMetaType r_bond(NO_ROOT); -QDataStream &operator<<(QDataStream &ds, const AmberBond &bond) -{ - writeHeader(ds, r_bond, 1); +QDataStream &operator<<(QDataStream &ds, const AmberBond &bond) { + writeHeader(ds, r_bond, 1); - ds << bond._k << bond._r0; - return ds; + ds << bond._k << bond._r0; + return ds; } -QDataStream &operator>>(QDataStream &ds, AmberBond &bond) -{ - VersionID v = readHeader(ds, r_bond); +QDataStream &operator>>(QDataStream &ds, AmberBond &bond) { + VersionID v = readHeader(ds, r_bond); - if (v == 1) - { - ds >> bond._k >> bond._r0; - } - else - throw version_error(v, "1", r_bond, CODELOC); + if (v == 1) { + ds >> bond._k >> bond._r0; + } else + throw version_error(v, "1", r_bond, CODELOC); - return ds; + return ds; } /** Construct with the passed bond constant and equilibrium bond length */ -AmberBond::AmberBond(double k, double r0) : _k(k), _r0(r0) -{ -} +AmberBond::AmberBond(double k, double r0) : _k(k), _r0(r0) {} /** Construct from the passed expression */ -AmberBond::AmberBond(const Expression &f, const Symbol &R) : _k(0), _r0(0) -{ - if (f.isZero()) - { - // this is a null bond - _k = 0; - _r0 = 0; - return; - } - - // expression should be of the form "k(r - r0)^2". We need to get the - // factors of R - const auto factors = f.expand(R); - - bool has_k = false; - - QStringList errors; - - double k = 0.0; - double kr0_2 = 0.0; - double kr0 = 0.0; - - for (const auto &factor : factors) - { - if (factor.symbol() == R) - { - if (not factor.power().isConstant()) - { - errors.append(QObject::tr("Power of R must be constant, not %1").arg(factor.power().toString())); - continue; - } - - if (not factor.factor().isConstant()) - { - errors.append(QObject::tr("The value of K in K (R - R0)^2 must be constant. " - "Here it is %1") - .arg(factor.factor().toString())); - continue; - } - - double power = factor.power().evaluate(Values()); - - if (power == 0.0) - { - // this is the constant - kr0_2 += factor.factor().evaluate(Values()); - } - else if (power == 1.0) - { - // this is the -kR0 term - kr0 = factor.factor().evaluate(Values()); - } - else if (power == 2.0) - { - // this is the R^2 term - if (has_k) - { - // we cannot have two R2 factors? - errors.append(QObject::tr("Cannot have two R^2 factors!")); - continue; - } - - k = factor.factor().evaluate(Values()); - has_k = true; - } - else - { - errors.append(QObject::tr("Power of R^2 must equal 2.0, 1.0 or 0.0, not %1").arg(power)); - continue; - } +AmberBond::AmberBond(const Expression &f, const Symbol &R) : _k(0), _r0(0) { + if (f.isZero()) { + // this is a null bond + _k = 0; + _r0 = 0; + return; + } + + // expression should be of the form "k(r - r0)^2". We need to get the + // factors of R + const auto factors = f.expand(R); + + bool has_k = false; + + QStringList errors; + + double k = 0.0; + double kr0_2 = 0.0; + double kr0 = 0.0; + + for (const auto &factor : factors) { + if (factor.symbol() == R) { + if (not factor.power().isConstant()) { + errors.append(QObject::tr("Power of R must be constant, not %1") + .arg(factor.power().toString())); + continue; + } + + if (not factor.factor().isConstant()) { + errors.append( + QObject::tr("The value of K in K (R - R0)^2 must be constant. " + "Here it is %1") + .arg(factor.factor().toString())); + continue; + } + + double power = factor.power().evaluate(Values()); + + if (power == 0.0) { + // this is the constant + kr0_2 += factor.factor().evaluate(Values()); + } else if (power == 1.0) { + // this is the -kR0 term + kr0 = factor.factor().evaluate(Values()); + } else if (power == 2.0) { + // this is the R^2 term + if (has_k) { + // we cannot have two R2 factors? + errors.append(QObject::tr("Cannot have two R^2 factors!")); + continue; } - else - { - errors.append( - QObject::tr("Cannot have a factor that does not include R. %1").arg(factor.symbol().toString())); - } - } - - _k = k; - _r0 = std::sqrt(kr0_2 / k); - - // kr0 should be equal to -2 k r0 - if (std::abs(_k * _r0 + 0.5 * kr0) > 0.001) - { - errors.append(QObject::tr("How can the power of R be %1. It should be 2 x %2 x %3 = %4.") - .arg(kr0) - .arg(_k) - .arg(_r0) - .arg(2 * _k * _r0)); - } - - if (not errors.isEmpty()) - { - throw SireError::incompatible_error( - QObject::tr("Cannot extract an AmberBond with function K ( %1 - R0 )^2 from the " - "expression %2, because\n%3") - .arg(R.toString()) - .arg(f.toString()) - .arg(errors.join("\n")), - CODELOC); - } -} - -AmberBond::AmberBond(const AmberBond &other) : _k(other._k), _r0(other._r0) -{ -} - -AmberBond::~AmberBond() -{ -} - -double AmberBond::operator[](int i) const -{ - i = SireID::Index(i).map(2); - - if (i == 0) - return _k; - else - return _r0; -} -AmberBond &AmberBond::operator=(const AmberBond &other) -{ - _k = other._k; - _r0 = other._r0; - return *this; + k = factor.factor().evaluate(Values()); + has_k = true; + } else { + errors.append( + QObject::tr("Power of R^2 must equal 2.0, 1.0 or 0.0, not %1") + .arg(power)); + continue; + } + } else { + errors.append( + QObject::tr("Cannot have a factor that does not include R. %1") + .arg(factor.symbol().toString())); + } + } + + _k = k; + _r0 = std::sqrt(kr0_2 / k); + + // kr0 should be equal to -2 k r0 + if (std::abs(_k * _r0 + 0.5 * kr0) > 0.001) { + errors.append( + QObject::tr( + "How can the power of R be %1. It should be 2 x %2 x %3 = %4.") + .arg(kr0) + .arg(_k) + .arg(_r0) + .arg(2 * _k * _r0)); + } + + if (not errors.isEmpty()) { + throw SireError::incompatible_error( + QObject::tr("Cannot extract an AmberBond with function K ( %1 - R0 )^2 " + "from the " + "expression %2, because\n%3") + .arg(R.toString()) + .arg(f.toString()) + .arg(errors.join("\n")), + CODELOC); + } +} + +AmberBond::AmberBond(const AmberBond &other) : _k(other._k), _r0(other._r0) {} + +AmberBond::~AmberBond() {} + +double AmberBond::operator[](int i) const { + i = SireID::Index(i).map(2); + + if (i == 0) + return _k; + else + return _r0; +} + +AmberBond &AmberBond::operator=(const AmberBond &other) { + _k = other._k; + _r0 = other._r0; + return *this; } /** Comparison operator */ -bool AmberBond::operator==(const AmberBond &other) const -{ - return _k == other._k and _r0 == other._r0; +bool AmberBond::operator==(const AmberBond &other) const { + return _k == other._k and _r0 == other._r0; } /** Comparison operator */ -bool AmberBond::operator!=(const AmberBond &other) const -{ - return not operator==(other); +bool AmberBond::operator!=(const AmberBond &other) const { + return not operator==(other); } /** Comparison operator */ -bool AmberBond::operator<=(const AmberBond &other) const -{ - return (*this == other) or (*this < other); +bool AmberBond::operator<=(const AmberBond &other) const { + return (*this == other) or (*this < other); } /** Comparison operator */ -bool AmberBond::operator>(const AmberBond &other) const -{ - return not(*this <= other); +bool AmberBond::operator>(const AmberBond &other) const { + return not(*this <= other); } /** Comparison operator */ -bool AmberBond::operator>=(const AmberBond &other) const -{ - return not(*this < other); +bool AmberBond::operator>=(const AmberBond &other) const { + return not(*this < other); } -const char *AmberBond::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); +const char *AmberBond::typeName() { + return QMetaType::typeName(qMetaTypeId()); } -const char *AmberBond::what() const -{ - return AmberBond::typeName(); -} +const char *AmberBond::what() const { return AmberBond::typeName(); } /** Return the energy evaluated from this bond for the passed bond length */ -double AmberBond::energy(double r) const -{ - return _k * SireMaths::pow_2(r - _r0); +double AmberBond::energy(double r) const { + return _k * SireMaths::pow_2(r - _r0); } /** Return an expression to evaluate the energy of this bond, using the passed symbol to represent the bond length */ -Expression AmberBond::toExpression(const Symbol &R) const -{ - return _k * SireMaths::pow_2(R - _r0); +Expression AmberBond::toExpression(const Symbol &R) const { + return _k * SireMaths::pow_2(R - _r0); } -QString AmberBond::toString() const -{ - return QObject::tr("AmberBond( k = %1, r0 = %2 )").arg(_k).arg(_r0); +QString AmberBond::toString() const { + return QObject::tr("AmberBond( k = %1, r0 = %2 )").arg(_k).arg(_r0); } /////////// @@ -300,210 +266,178 @@ QString AmberBond::toString() const static const RegisterMetaType r_angle(NO_ROOT); -QDataStream &operator<<(QDataStream &ds, const AmberAngle &angle) -{ - writeHeader(ds, r_angle, 1); - ds << angle._k << angle._theta0; - return ds; -} - -QDataStream &operator>>(QDataStream &ds, AmberAngle &angle) -{ - VersionID v = readHeader(ds, r_angle); - - if (v == 1) - { - ds >> angle._k >> angle._theta0; - } - else - throw version_error(v, "1", r_angle, CODELOC); - - return ds; -} - -AmberAngle::AmberAngle(double k, double theta0) : _k(k), _theta0(theta0) -{ -} +QDataStream &operator<<(QDataStream &ds, const AmberAngle &angle) { + writeHeader(ds, r_angle, 1); + ds << angle._k << angle._theta0; + return ds; +} + +QDataStream &operator>>(QDataStream &ds, AmberAngle &angle) { + VersionID v = readHeader(ds, r_angle); + + if (v == 1) { + ds >> angle._k >> angle._theta0; + } else + throw version_error(v, "1", r_angle, CODELOC); + + return ds; +} + +AmberAngle::AmberAngle(double k, double theta0) : _k(k), _theta0(theta0) {} + +AmberAngle::AmberAngle(const Expression &f, const Symbol &theta) + : _k(0), _theta0(0) { + if (f.isZero()) { + // this is a null angle + _k = 0; + _theta0 = 0; + return; + } + + // expression should be of the form "k(theta - theta0)^2". We need to get the + // factors of theta + const auto factors = f.expand(theta); + + bool has_k = false; + + QStringList errors; + + double k = 0.0; + double ktheta0_2 = 0.0; + double ktheta0 = 0.0; + + for (const auto &factor : factors) { + if (factor.symbol() == theta) { + if (not factor.power().isConstant()) { + errors.append(QObject::tr("Power of theta must be constant, not %1") + .arg(factor.power().toString())); + continue; + } + + if (not factor.factor().isConstant()) { + errors.append( + QObject::tr("The value of K in K (theta - theta0)^2 must be " + "constant. Here it is %1") + .arg(factor.factor().toString())); + continue; + } + + double power = factor.power().evaluate(Values()); + + if (power == 0.0) { + // this is the constant + ktheta0_2 += factor.factor().evaluate(Values()); + } else if (power == 1.0) { + // this is the -ktheta0 term + ktheta0 = factor.factor().evaluate(Values()); + } else if (power == 2.0) { + // this is the theta^2 term + if (has_k) { + // we cannot have two R2 factors? + errors.append(QObject::tr("Cannot have two theta^2 factors!")); + continue; + } -AmberAngle::AmberAngle(const Expression &f, const Symbol &theta) : _k(0), _theta0(0) -{ - if (f.isZero()) - { - // this is a null angle - _k = 0; - _theta0 = 0; - return; + k = factor.factor().evaluate(Values()); + has_k = true; + } else { + errors.append( + QObject::tr("Power of theta^2 must equal 2.0, 1.0 or 0.0, not %1") + .arg(power)); + continue; + } + } else { + errors.append( + QObject::tr("Cannot have a factor that does not include theta. %1") + .arg(factor.symbol().toString())); } + } - // expression should be of the form "k(theta - theta0)^2". We need to get the - // factors of theta - const auto factors = f.expand(theta); - - bool has_k = false; - - QStringList errors; + _k = k; + _theta0 = std::sqrt(ktheta0_2 / k); - double k = 0.0; - double ktheta0_2 = 0.0; - double ktheta0 = 0.0; - - for (const auto &factor : factors) - { - if (factor.symbol() == theta) - { - if (not factor.power().isConstant()) - { - errors.append(QObject::tr("Power of theta must be constant, not %1").arg(factor.power().toString())); - continue; - } - - if (not factor.factor().isConstant()) - { - errors.append(QObject::tr("The value of K in K (theta - theta0)^2 must be " - "constant. Here it is %1") - .arg(factor.factor().toString())); - continue; - } + // ktheta0 should be equal to -k theta0 + if (std::abs(_k * _theta0 + 0.5 * ktheta0) > 0.001) { + errors.append( + QObject::tr( + "How can the power of theta be %1. It should be 2 x %2 x %3 = %4.") + .arg(ktheta0) + .arg(_k) + .arg(_theta0) + .arg(2 * _k * _theta0)); + } - double power = factor.power().evaluate(Values()); - - if (power == 0.0) - { - // this is the constant - ktheta0_2 += factor.factor().evaluate(Values()); - } - else if (power == 1.0) - { - // this is the -ktheta0 term - ktheta0 = factor.factor().evaluate(Values()); - } - else if (power == 2.0) - { - // this is the theta^2 term - if (has_k) - { - // we cannot have two R2 factors? - errors.append(QObject::tr("Cannot have two theta^2 factors!")); - continue; - } - - k = factor.factor().evaluate(Values()); - has_k = true; - } - else - { - errors.append(QObject::tr("Power of theta^2 must equal 2.0, 1.0 or 0.0, not %1").arg(power)); - continue; - } - } - else - { - errors.append( - QObject::tr("Cannot have a factor that does not include theta. %1").arg(factor.symbol().toString())); - } - } - - _k = k; - _theta0 = std::sqrt(ktheta0_2 / k); - - // ktheta0 should be equal to -k theta0 - if (std::abs(_k * _theta0 + 0.5 * ktheta0) > 0.001) - { - errors.append(QObject::tr("How can the power of theta be %1. It should be 2 x %2 x %3 = %4.") - .arg(ktheta0) - .arg(_k) - .arg(_theta0) - .arg(2 * _k * _theta0)); - } - - if (not errors.isEmpty()) - { - throw SireError::incompatible_error( - QObject::tr("Cannot extract an AmberAngle with function K ( %1 - theta0 )^2 from the " - "expression %2, because\n%3") - .arg(theta.toString()) - .arg(f.toString()) - .arg(errors.join("\n")), - CODELOC); - } + if (not errors.isEmpty()) { + throw SireError::incompatible_error( + QObject::tr("Cannot extract an AmberAngle with function K ( %1 - " + "theta0 )^2 from the " + "expression %2, because\n%3") + .arg(theta.toString()) + .arg(f.toString()) + .arg(errors.join("\n")), + CODELOC); + } } -AmberAngle::AmberAngle(const AmberAngle &other) : _k(other._k), _theta0(other._theta0) -{ -} +AmberAngle::AmberAngle(const AmberAngle &other) + : _k(other._k), _theta0(other._theta0) {} -AmberAngle::~AmberAngle() -{ -} +AmberAngle::~AmberAngle() {} -double AmberAngle::operator[](int i) const -{ - i = SireID::Index(i).map(2); +double AmberAngle::operator[](int i) const { + i = SireID::Index(i).map(2); - if (i == 0) - return _k; - else - return _theta0; + if (i == 0) + return _k; + else + return _theta0; } -AmberAngle &AmberAngle::operator=(const AmberAngle &other) -{ - _k = other._k; - _theta0 = other._theta0; - return *this; +AmberAngle &AmberAngle::operator=(const AmberAngle &other) { + _k = other._k; + _theta0 = other._theta0; + return *this; } -bool AmberAngle::operator==(const AmberAngle &other) const -{ - return _k == other._k and _theta0 == other._theta0; +bool AmberAngle::operator==(const AmberAngle &other) const { + return _k == other._k and _theta0 == other._theta0; } -bool AmberAngle::operator!=(const AmberAngle &other) const -{ - return not operator==(other); +bool AmberAngle::operator!=(const AmberAngle &other) const { + return not operator==(other); } /** Comparison operator */ -bool AmberAngle::operator<=(const AmberAngle &other) const -{ - return (*this == other) or (*this < other); +bool AmberAngle::operator<=(const AmberAngle &other) const { + return (*this == other) or (*this < other); } /** Comparison operator */ -bool AmberAngle::operator>(const AmberAngle &other) const -{ - return not(*this <= other); +bool AmberAngle::operator>(const AmberAngle &other) const { + return not(*this <= other); } /** Comparison operator */ -bool AmberAngle::operator>=(const AmberAngle &other) const -{ - return not(*this < other); +bool AmberAngle::operator>=(const AmberAngle &other) const { + return not(*this < other); } -const char *AmberAngle::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); +const char *AmberAngle::typeName() { + return QMetaType::typeName(qMetaTypeId()); } -const char *AmberAngle::what() const -{ - return AmberAngle::typeName(); -} +const char *AmberAngle::what() const { return AmberAngle::typeName(); } -double AmberAngle::energy(double theta) const -{ - return _k * SireMaths::pow_2(theta - _theta0); +double AmberAngle::energy(double theta) const { + return _k * SireMaths::pow_2(theta - _theta0); } -Expression AmberAngle::toExpression(const Symbol &theta) const -{ - return _k * SireMaths::pow_2(theta - _theta0); +Expression AmberAngle::toExpression(const Symbol &theta) const { + return _k * SireMaths::pow_2(theta - _theta0); } -QString AmberAngle::toString() const -{ - return QObject::tr("AmberAngle( k = %1, theta0 = %2 )").arg(_k).arg(_theta0); +QString AmberAngle::toString() const { + return QObject::tr("AmberAngle( k = %1, theta0 = %2 )").arg(_k).arg(_theta0); } /////////// @@ -512,106 +446,88 @@ QString AmberAngle::toString() const static const RegisterMetaType r_dihpart(NO_ROOT); -QDataStream &operator<<(QDataStream &ds, const AmberDihPart &dih) -{ - writeHeader(ds, r_dihpart, 1); - ds << dih._k << dih._periodicity << dih._phase; - return ds; +QDataStream &operator<<(QDataStream &ds, const AmberDihPart &dih) { + writeHeader(ds, r_dihpart, 1); + ds << dih._k << dih._periodicity << dih._phase; + return ds; } -QDataStream &operator>>(QDataStream &ds, AmberDihPart &dih) -{ - VersionID v = readHeader(ds, r_dihpart); +QDataStream &operator>>(QDataStream &ds, AmberDihPart &dih) { + VersionID v = readHeader(ds, r_dihpart); - if (v == 1) - { - ds >> dih._k >> dih._periodicity >> dih._phase; - } - else - throw version_error(v, "1", r_dihpart, CODELOC); + if (v == 1) { + ds >> dih._k >> dih._periodicity >> dih._phase; + } else + throw version_error(v, "1", r_dihpart, CODELOC); - return ds; + return ds; } -AmberDihPart::AmberDihPart(double k, double periodicity, double phase) : _k(k), _periodicity(periodicity), _phase(phase) -{ -} +AmberDihPart::AmberDihPart(double k, double periodicity, double phase) + : _k(k), _periodicity(periodicity), _phase(phase) {} AmberDihPart::AmberDihPart(const AmberDihPart &other) - : _k(other._k), _periodicity(other._periodicity), _phase(other._phase) -{ -} + : _k(other._k), _periodicity(other._periodicity), _phase(other._phase) {} -AmberDihPart::AmberDihPart::~AmberDihPart() -{ -} +AmberDihPart::AmberDihPart::~AmberDihPart() {} -double AmberDihPart::operator[](int i) const -{ - i = SireID::Index(i).map(3); +double AmberDihPart::operator[](int i) const { + i = SireID::Index(i).map(3); - if (i == 0) - return _k; - else if (i == 1) - return _periodicity; - else - return _phase; + if (i == 0) + return _k; + else if (i == 1) + return _periodicity; + else + return _phase; } -AmberDihPart &AmberDihPart::operator=(const AmberDihPart &other) -{ - _k = other._k; - _periodicity = other._periodicity; - _phase = other._phase; - return *this; +AmberDihPart &AmberDihPart::operator=(const AmberDihPart &other) { + _k = other._k; + _periodicity = other._periodicity; + _phase = other._phase; + return *this; } -bool AmberDihPart::operator==(const AmberDihPart &other) const -{ - return _k == other._k and _periodicity == other._periodicity and _phase == other._phase; +bool AmberDihPart::operator==(const AmberDihPart &other) const { + return _k == other._k and _periodicity == other._periodicity and + _phase == other._phase; } -bool AmberDihPart::operator!=(const AmberDihPart &other) const -{ - return not operator==(other); +bool AmberDihPart::operator!=(const AmberDihPart &other) const { + return not operator==(other); } /** Comparison operator */ -bool AmberDihPart::operator<=(const AmberDihPart &other) const -{ - return (*this == other) or (*this < other); +bool AmberDihPart::operator<=(const AmberDihPart &other) const { + return (*this == other) or (*this < other); } /** Comparison operator */ -bool AmberDihPart::operator>(const AmberDihPart &other) const -{ - return not(*this <= other); +bool AmberDihPart::operator>(const AmberDihPart &other) const { + return not(*this <= other); } /** Comparison operator */ -bool AmberDihPart::operator>=(const AmberDihPart &other) const -{ - return not(*this < other); +bool AmberDihPart::operator>=(const AmberDihPart &other) const { + return not(*this < other); } -const char *AmberDihPart::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); +const char *AmberDihPart::typeName() { + return QMetaType::typeName(qMetaTypeId()); } -const char *AmberDihPart::what() const -{ - return AmberDihPart::typeName(); -} +const char *AmberDihPart::what() const { return AmberDihPart::typeName(); } -double AmberDihPart::energy(double phi) const -{ - return _k * (1 + cos((_periodicity * phi) - _phase)); +double AmberDihPart::energy(double phi) const { + return _k * (1 + cos((_periodicity * phi) - _phase)); } -QString AmberDihPart::toString() const -{ - return QObject::tr("AmberDihPart( k = %1, periodicity = %2, phase = %3 )").arg(_k).arg(_periodicity).arg(_phase); +QString AmberDihPart::toString() const { + return QObject::tr("AmberDihPart( k = %1, periodicity = %2, phase = %3 )") + .arg(_k) + .arg(_periodicity) + .arg(_phase); } /////////// @@ -620,400 +536,344 @@ QString AmberDihPart::toString() const static const RegisterMetaType r_dihedral(NO_ROOT); -QDataStream &operator<<(QDataStream &ds, const AmberDihedral &dih) -{ - writeHeader(ds, r_dihedral, 1); - ds << dih._parts; - return ds; +QDataStream &operator<<(QDataStream &ds, const AmberDihedral &dih) { + writeHeader(ds, r_dihedral, 1); + ds << dih._parts; + return ds; } -QDataStream &operator>>(QDataStream &ds, AmberDihedral &dih) -{ - VersionID v = readHeader(ds, r_dihedral); +QDataStream &operator>>(QDataStream &ds, AmberDihedral &dih) { + VersionID v = readHeader(ds, r_dihedral); - if (v == 1) - { - ds >> dih._parts; - } - else - throw version_error(v, "1", r_dihedral, CODELOC); + if (v == 1) { + ds >> dih._parts; + } else + throw version_error(v, "1", r_dihedral, CODELOC); - return ds; + return ds; } -AmberDihedral::AmberDihedral() -{ -} +AmberDihedral::AmberDihedral() {} -AmberDihedral::AmberDihedral(AmberDihPart part) -{ - _parts = QVector(1, part); +AmberDihedral::AmberDihedral(AmberDihPart part) { + _parts = QVector(1, part); } -AmberDihedral::AmberDihedral(const Expression &f, const Symbol &phi, bool test_ryckaert_bellemans) -{ - if (f.isZero()) - { - // this is a null dihedral - return; - } +AmberDihedral::AmberDihedral(const Expression &f, const Symbol &phi, + bool test_ryckaert_bellemans) { + if (f.isZero()) { + // this is a null dihedral + return; + } - // Copy the expression. - auto f_copy = f; - - // First we check whether the expression can be cast as a Ryckaert-Bellemans - // GromacsDihedral. If so, then we convert the expression to a Fourier series - // representation so that it can be correctly parsed as an AmberDihedral. - if (test_ryckaert_bellemans) - { - try - { - GromacsDihedral gromacs_dihedral = GromacsDihedral(f_copy, phi); - - // This is a Ryckaert-Bellemans dihedral. - if (gromacs_dihedral.functionType() == 3) - { - // Get the dihedral parameters. - auto params = gromacs_dihedral.parameters(); - - // Energy conversion factor. - auto nrg_factor = kJ_per_mol.to(kcal_per_mol); - - // Work out the Fourier series terms. - auto F4 = -nrg_factor * (params[4] / 4.0); - auto F3 = -nrg_factor * (params[3] / 2.0); - auto F2 = nrg_factor * (4.0 * F4 - params[2]); - auto F1 = nrg_factor * (3.0 * F3 - 2.0 * params[1]); - - // Convert the expression to a Fourier series. - f_copy = Expression(0.5 * (F1 * (1 + Cos(phi)) + F2 * (1 - Cos(2 * phi)) + F3 * (1 + Cos(3 * phi)) + - F4 * (1 - Cos(4 * phi)))); - } - } - catch (...) - { - } - } + // Copy the expression. + auto f_copy = f; - // This expression should be a sum of cos terms, plus constant terms. - // The cosine terms can be positive or negative depending on the sign - // of the factor. - QVector> pos_terms; - QVector> neg_terms; - double constant = 0.0; - - QStringList errors; - - // Loop over all terms in the series. - if (f_copy.base().isA()) - { - for (const auto &child : f_copy.base().asA().children()) - { - if (child.isConstant()) - { - // Accumulate the constant factors. - constant += f_copy.factor() * child.evaluate(Values()); - } - else if (child.base().isA()) - { - // Compute the factor and extract the cosine term. - double factor = f_copy.factor() * child.factor(); - auto cos_term = child.base().asA(); - - // Now store a pair, storing the - // positive and negative terms separately. - - if (factor < 0) - neg_terms.append(QPair(factor, cos_term)); - else - pos_terms.append(QPair(factor, cos_term)); - } - } + // First we check whether the expression can be cast as a Ryckaert-Bellemans + // GromacsDihedral. If so, then we convert the expression to a Fourier series + // representation so that it can be correctly parsed as an AmberDihedral. + if (test_ryckaert_bellemans) { + try { + GromacsDihedral gromacs_dihedral = GromacsDihedral(f_copy, phi); + + // This is a Ryckaert-Bellemans dihedral. + if (gromacs_dihedral.functionType() == 3) { + // Get the dihedral parameters. + auto params = gromacs_dihedral.parameters(); + + // Energy conversion factor. + auto nrg_factor = kJ_per_mol.to(kcal_per_mol); + + // Work out the Fourier series terms. + auto F4 = -nrg_factor * (params[4] / 4.0); + auto F3 = -nrg_factor * (params[3] / 2.0); + auto F2 = nrg_factor * (4.0 * F4 - params[2]); + auto F1 = nrg_factor * (3.0 * F3 - 2.0 * params[1]); + + // Convert the expression to a Fourier series. + f_copy = Expression( + 0.5 * (F1 * (1 + Cos(phi)) + F2 * (1 - Cos(2 * phi)) + + F3 * (1 + Cos(3 * phi)) + F4 * (1 - Cos(4 * phi)))); + } + } catch (...) { } + } - // Now loop over the factors and work out what combination adds - // up to the constant term, usings the raw factors, their absolute - // values, or combinations thereof. - - // First add up the positive terms. These always represent standard - // AMBER dihderal terms. - double pos_sum = 0.0; - for (const auto &term : pos_terms) - pos_sum += term.first; - - // Store the number of negative terms. - int num_neg = neg_terms.count(); - - // There are negative factors. - if (num_neg > 0) - { - // The number of ways of combining the factors, using either the - // negative or absolute values of each. - int num_combs = std::pow(2, num_neg); - - // The vector of factors for the current combination. - QVector factors(num_neg); - - QVector temp(num_combs); - for (int i = 0; i < num_combs; ++i) - temp[i] = i; - - bool has_match = false; - - // Now add the negative terms, trying all combinations. - // Abort if we find a combination that matches the constant term. - for (int i = 0; i < num_combs; ++i) - { - // Reset the sum. - double sum = pos_sum; - - // Loop over all terms. - for (int j = 0; j < num_neg; ++j) - { - unsigned int k = temp[i] >> j; - - if (k & 1) - factors[j] = neg_terms[j].first; - else - factors[j] = -neg_terms[j].first; - - // Update the sum. - sum += factors[j]; - } + // This expression should be a sum of cos terms, plus constant terms. + // The cosine terms can be positive or negative depending on the sign + // of the factor. + QVector> pos_terms; + QVector> neg_terms; + double constant = 0.0; - // Woohoo, we've found a combination of terms that match the constant. - if (std::abs(sum - constant) < 0.001) - { - has_match = true; - break; - } - } + QStringList errors; - if (has_match) - { - for (int i = 0; i < num_neg; ++i) - { - // The term factor has been flipped. We need to phase shift - // the cosine term. - if (factors[i] > 0) - { - auto cos_term = neg_terms[i].second.base().asA(); - - // Store the negated factor and shifted cosine term. - neg_terms[i] = QPair(factors[i], Cos(cos_term.argument() + SireMaths::pi)); - } - } - } + // Loop over all terms in the series. + if (f_copy.base().isA()) { + for (const auto &child : f_copy.base().asA().children()) { + if (child.isConstant()) { + // Accumulate the constant factors. + constant += f_copy.factor() * child.evaluate(Values()); + } else if (child.base().isA()) { + // Compute the factor and extract the cosine term. + double factor = f_copy.factor() * child.factor(); + auto cos_term = child.base().asA(); + + // Now store a pair, storing the + // positive and negative terms separately. + + if (factor < 0) + neg_terms.append(QPair(factor, cos_term)); else - { - throw SireError::incompatible_error( - QObject::tr("Cannot extract an Amber-format dihedral expression from '%1' as " - "the expression must be a series of terms of type " - "'k{ 1 + cos[ per %2 - phase ] }'. Errors include\n%3") - .arg(f.toString()) - .arg(phi.toString()) - .arg(errors.join("\n")), - CODELOC); - } + pos_terms.append(QPair(factor, cos_term)); + } } + } - // Create a vector of the cosine terms. - QVector cos_terms; + // Now loop over the factors and work out what combination adds + // up to the constant term, usings the raw factors, their absolute + // values, or combinations thereof. - // First add the positive terms. - for (const auto &term : pos_terms) - cos_terms.append(term.first * term.second); + // First add up the positive terms. These always represent standard + // AMBER dihderal terms. + double pos_sum = 0.0; + for (const auto &term : pos_terms) + pos_sum += term.first; - // Now add the negative terms. - for (const auto &term : neg_terms) - cos_terms.append(term.first * term.second); + // Store the number of negative terms. + int num_neg = neg_terms.count(); - // Next extract all of the data from the cos terms. - QVector> terms; + // There are negative factors. + if (num_neg > 0) { + // The number of ways of combining the factors, using either the + // negative or absolute values of each. + int num_combs = std::pow(2, num_neg); - for (const auto &cos_term : cos_terms) - { - // Term should be of the form 'k cos( periodicity * phi - phase )'. - double k = cos_term.factor(); + // The vector of factors for the current combination. + QVector factors(num_neg); - double periodicity = 0.0; - double phase = 0.0; + QVector temp(num_combs); + for (int i = 0; i < num_combs; ++i) + temp[i] = i; - const auto factors = cos_term.base().asA().argument().expand(phi); + bool has_match = false; - bool ok = true; + // Now add the negative terms, trying all combinations. + // Abort if we find a combination that matches the constant term. + for (int i = 0; i < num_combs; ++i) { + // Reset the sum. + double sum = pos_sum; - for (const auto &factor : factors) - { - if (not factor.power().isConstant()) - { - errors.append(QObject::tr("Power of phi must be constant, not %1").arg(factor.power().toString())); - ok = false; - continue; - } + // Loop over all terms. + for (int j = 0; j < num_neg; ++j) { + unsigned int k = temp[i] >> j; - if (not factor.factor().isConstant()) - { - errors.append(QObject::tr("The value of periodicity must be " - "constant. Here it is %1") - .arg(factor.factor().toString())); - ok = false; - continue; - } + if (k & 1) + factors[j] = neg_terms[j].first; + else + factors[j] = -neg_terms[j].first; - double power = factor.power().evaluate(Values()); + // Update the sum. + sum += factors[j]; + } - if (power == 0.0) - { - // This is the constant phase. - phase += factor.factor().evaluate(Values()); - } - else if (power == 1.0) - { - // This is the periodicity * phi term. - periodicity = factor.factor().evaluate(Values()); - } - else - { - errors.append(QObject::tr("Power of phi must equal 1.0 or 0.0, not %1").arg(power)); - ok = false; - continue; - } - } + // Woohoo, we've found a combination of terms that match the constant. + if (std::abs(sum - constant) < 0.001) { + has_match = true; + break; + } + } - if (ok) - { - terms.append(std::make_tuple(k, periodicity, phase)); + if (has_match) { + for (int i = 0; i < num_neg; ++i) { + // The term factor has been flipped. We need to phase shift + // the cosine term. + if (factors[i] > 0) { + auto cos_term = neg_terms[i].second.base().asA(); + + // Store the negated factor and shifted cosine term. + neg_terms[i] = QPair( + factors[i], Cos(cos_term.argument() + SireMaths::pi)); } + } + } else { + throw SireError::incompatible_error( + QObject::tr( + "Cannot extract an Amber-format dihedral expression from '%1' as " + "the expression must be a series of terms of type " + "'k{ 1 + cos[ per %2 - phase ] }'. Errors include\n%3") + .arg(f.toString()) + .arg(phi.toString()) + .arg(errors.join("\n")), + CODELOC); + } + } + + // Create a vector of the cosine terms. + QVector cos_terms; + + // First add the positive terms. + for (const auto &term : pos_terms) + cos_terms.append(term.first * term.second); + + // Now add the negative terms. + for (const auto &term : neg_terms) + cos_terms.append(term.first * term.second); + + // Next extract all of the data from the cos terms. + QVector> terms; + + for (const auto &cos_term : cos_terms) { + // Term should be of the form 'k cos( periodicity * phi - phase )'. + double k = cos_term.factor(); + + double periodicity = 0.0; + double phase = 0.0; + + const auto factors = cos_term.base().asA().argument().expand(phi); + + bool ok = true; + + for (const auto &factor : factors) { + if (not factor.power().isConstant()) { + errors.append(QObject::tr("Power of phi must be constant, not %1") + .arg(factor.power().toString())); + ok = false; + continue; + } + + if (not factor.factor().isConstant()) { + errors.append(QObject::tr("The value of periodicity must be " + "constant. Here it is %1") + .arg(factor.factor().toString())); + ok = false; + continue; + } + + double power = factor.power().evaluate(Values()); + + if (power == 0.0) { + // This is the constant phase. + phase += factor.factor().evaluate(Values()); + } else if (power == 1.0) { + // This is the periodicity * phi term. + periodicity = factor.factor().evaluate(Values()); + } else { + errors.append(QObject::tr("Power of phi must equal 1.0 or 0.0, not %1") + .arg(power)); + ok = false; + continue; + } } - if (not errors.isEmpty()) - { - throw SireError::incompatible_error( - QObject::tr("Cannot extract an Amber-format dihedral expression from '%1' as " - "the expression must be a series of terms of type " - "'k{ 1 + cos[ per %2 - phase ] }'. Errors include\n%3") - .arg(f.toString()) - .arg(phi.toString()) - .arg(errors.join("\n")), - CODELOC); + if (ok) { + terms.append(std::make_tuple(k, periodicity, phase)); } + } - // Otherwise, add in all of the terms. - if (not terms.isEmpty()) - { - _parts.reserve(terms.count()); + if (not errors.isEmpty()) { + throw SireError::incompatible_error( + QObject::tr( + "Cannot extract an Amber-format dihedral expression from '%1' as " + "the expression must be a series of terms of type " + "'k{ 1 + cos[ per %2 - phase ] }'. Errors include\n%3") + .arg(f.toString()) + .arg(phi.toString()) + .arg(errors.join("\n")), + CODELOC); + } - for (const auto &term : terms) - { - // Remember that the expression uses the negative of the phase ;-) - _parts.append(AmberDihPart(std::get<0>(term), std::get<1>(term), -std::get<2>(term))); - } + // Otherwise, add in all of the terms. + if (not terms.isEmpty()) { + _parts.reserve(terms.count()); + + for (const auto &term : terms) { + // Remember that the expression uses the negative of the phase ;-) + _parts.append(AmberDihPart(std::get<0>(term), std::get<1>(term), + -std::get<2>(term))); } + } } -AmberDihedral::AmberDihedral(const AmberDihedral &other) : _parts(other._parts) -{ -} +AmberDihedral::AmberDihedral(const AmberDihedral &other) + : _parts(other._parts) {} -AmberDihedral::~AmberDihedral() -{ -} +AmberDihedral::~AmberDihedral() {} -AmberDihedral &AmberDihedral::operator+=(const AmberDihPart &part) -{ - _parts.append(part); - return *this; +AmberDihedral &AmberDihedral::operator+=(const AmberDihPart &part) { + _parts.append(part); + return *this; } -AmberDihedral AmberDihedral::operator+(const AmberDihPart &part) const -{ - AmberDihedral ret(*this); - ret += part; - return *this; +AmberDihedral AmberDihedral::operator+(const AmberDihPart &part) const { + AmberDihedral ret(*this); + ret += part; + return *this; } -AmberDihedral &AmberDihedral::operator=(const AmberDihedral &other) -{ - _parts = other._parts; - return *this; +AmberDihedral &AmberDihedral::operator=(const AmberDihedral &other) { + _parts = other._parts; + return *this; } -bool AmberDihedral::operator==(const AmberDihedral &other) const -{ - return _parts == other._parts; +bool AmberDihedral::operator==(const AmberDihedral &other) const { + return _parts == other._parts; } -bool AmberDihedral::operator!=(const AmberDihedral &other) const -{ - return not operator==(other); +bool AmberDihedral::operator!=(const AmberDihedral &other) const { + return not operator==(other); } -const char *AmberDihedral::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); +const char *AmberDihedral::typeName() { + return QMetaType::typeName(qMetaTypeId()); } -const char *AmberDihedral::what() const -{ - return AmberDihedral::typeName(); -} +const char *AmberDihedral::what() const { return AmberDihedral::typeName(); } -AmberDihPart AmberDihedral::operator[](int i) const -{ - if (_parts.isEmpty()) - { - // this is a zero dihedral - i = SireID::Index(i).map(_parts.count()); - return AmberDihPart(); - } - else - { - i = SireID::Index(i).map(_parts.count()); - return _parts[i]; - } +AmberDihPart AmberDihedral::operator[](int i) const { + if (_parts.isEmpty()) { + // this is a zero dihedral + i = SireID::Index(i).map(_parts.count()); + return AmberDihPart(); + } else { + i = SireID::Index(i).map(_parts.count()); + return _parts[i]; + } } -double AmberDihedral::energy(double phi) const -{ - double total = 0; - for (int i = 0; i < _parts.count(); ++i) - { - total += _parts.constData()[i].energy(phi); - } - return total; +double AmberDihedral::energy(double phi) const { + double total = 0; + for (int i = 0; i < _parts.count(); ++i) { + total += _parts.constData()[i].energy(phi); + } + return total; } -Expression AmberDihedral::toExpression(const Symbol &phi) const -{ - Expression ret; +Expression AmberDihedral::toExpression(const Symbol &phi) const { + Expression ret; - for (auto part : _parts) - { - ret += part.k() * (1 + Cos((part.periodicity() * phi) - part.phase())); - } + for (auto part : _parts) { + ret += part.k() * (1 + Cos((part.periodicity() * phi) - part.phase())); + } - return ret; + return ret; } -QString AmberDihedral::toString() const -{ - if (_parts.isEmpty()) - { - return QObject::tr("AmberDihedral( 0 )"); - } +QString AmberDihedral::toString() const { + if (_parts.isEmpty()) { + return QObject::tr("AmberDihedral( 0 )"); + } - QStringList s; - for (int i = 0; i < _parts.count(); ++i) - { - s.append(QObject::tr("k[%1] = %2, periodicity[%1] = %3, phase[%1] = %4") - .arg(i) - .arg(_parts[i].k()) - .arg(_parts[i].periodicity()) - .arg(_parts[i].phase())); - } + QStringList s; + for (int i = 0; i < _parts.count(); ++i) { + s.append(QObject::tr("k[%1] = %2, periodicity[%1] = %3, phase[%1] = %4") + .arg(i) + .arg(_parts[i].k()) + .arg(_parts[i].periodicity()) + .arg(_parts[i].phase())); + } - return QObject::tr("AmberDihedral( %1 )").arg(s.join(", ")); + return QObject::tr("AmberDihedral( %1 )").arg(s.join(", ")); } /////////// @@ -1022,122 +882,96 @@ QString AmberDihedral::toString() const static const RegisterMetaType r_nb14(NO_ROOT); -QDataStream &operator<<(QDataStream &ds, const AmberNB14 &nb) -{ - writeHeader(ds, r_nb14, 1); - ds << nb._cscl << nb._ljscl; - return ds; +QDataStream &operator<<(QDataStream &ds, const AmberNB14 &nb) { + writeHeader(ds, r_nb14, 1); + ds << nb._cscl << nb._ljscl; + return ds; } -QDataStream &operator>>(QDataStream &ds, AmberNB14 &nb) -{ - VersionID v = readHeader(ds, r_nb14); +QDataStream &operator>>(QDataStream &ds, AmberNB14 &nb) { + VersionID v = readHeader(ds, r_nb14); - if (v == 1) - { - ds >> nb._cscl >> nb._ljscl; - } - else - throw version_error(v, "1", r_nb14, CODELOC); + if (v == 1) { + ds >> nb._cscl >> nb._ljscl; + } else + throw version_error(v, "1", r_nb14, CODELOC); - return ds; + return ds; } -AmberNB14::AmberNB14(double cscl, double ljscl) : _cscl(cscl), _ljscl(ljscl) -{ -} +AmberNB14::AmberNB14(double cscl, double ljscl) : _cscl(cscl), _ljscl(ljscl) {} -AmberNB14::AmberNB14(const AmberNB14 &other) : _cscl(other._cscl), _ljscl(other._ljscl) -{ -} +AmberNB14::AmberNB14(const AmberNB14 &other) + : _cscl(other._cscl), _ljscl(other._ljscl) {} -AmberNB14::~AmberNB14() -{ -} +AmberNB14::~AmberNB14() {} -double AmberNB14::operator[](int i) const -{ - i = SireID::Index(i).map(2); +double AmberNB14::operator[](int i) const { + i = SireID::Index(i).map(2); - if (i == 0) - return _cscl; - else - return _ljscl; + if (i == 0) + return _cscl; + else + return _ljscl; } -AmberNB14 &AmberNB14::operator=(const AmberNB14 &other) -{ - _cscl = other._cscl; - _ljscl = other._ljscl; - return *this; +AmberNB14 &AmberNB14::operator=(const AmberNB14 &other) { + _cscl = other._cscl; + _ljscl = other._ljscl; + return *this; } /** Comparison operator */ -bool AmberNB14::operator==(const AmberNB14 &other) const -{ - return _cscl == other._cscl and _ljscl == other._ljscl; +bool AmberNB14::operator==(const AmberNB14 &other) const { + return _cscl == other._cscl and _ljscl == other._ljscl; } /** Comparison operator */ -bool AmberNB14::operator!=(const AmberNB14 &other) const -{ - return not operator==(other); +bool AmberNB14::operator!=(const AmberNB14 &other) const { + return not operator==(other); } /** Comparison operator */ -bool AmberNB14::operator<(const AmberNB14 &other) const -{ - if (_cscl < other._cscl) - { - return true; - } - else if (_cscl == other._cscl) - { - return _ljscl < other._ljscl; - } - else - { - return false; - } +bool AmberNB14::operator<(const AmberNB14 &other) const { + if (_cscl < other._cscl) { + return true; + } else if (_cscl == other._cscl) { + return _ljscl < other._ljscl; + } else { + return false; + } } /** Comparison operator */ -bool AmberNB14::operator<=(const AmberNB14 &other) const -{ - return operator==(other) or operator<(other); +bool AmberNB14::operator<=(const AmberNB14 &other) const { + return operator==(other) or operator<(other); } /** Comparison operator */ -bool AmberNB14::operator>(const AmberNB14 &other) const -{ - return not operator<=(other); +bool AmberNB14::operator>(const AmberNB14 &other) const { + return not operator<=(other); } /** Comparison operator */ -bool AmberNB14::operator>=(const AmberNB14 &other) const -{ - return not operator<(other); +bool AmberNB14::operator>=(const AmberNB14 &other) const { + return not operator<(other); } -const char *AmberNB14::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); +const char *AmberNB14::typeName() { + return QMetaType::typeName(qMetaTypeId()); } -const char *AmberNB14::what() const -{ - return AmberNB14::typeName(); -} +const char *AmberNB14::what() const { return AmberNB14::typeName(); } -QString AmberNB14::toString() const -{ - return QObject::tr("AmberNB14( cscl = %1, ljscl = %2 )").arg(_cscl).arg(_ljscl); +QString AmberNB14::toString() const { + return QObject::tr("AmberNB14( cscl = %1, ljscl = %2 )") + .arg(_cscl) + .arg(_ljscl); } /** Return the value converted to a CLJScaleFactor */ -CLJScaleFactor AmberNB14::toScaleFactor() const -{ - return CLJScaleFactor(_cscl, _ljscl); +CLJScaleFactor AmberNB14::toScaleFactor() const { + return CLJScaleFactor(_cscl, _ljscl); } /////////// @@ -1146,115 +980,91 @@ CLJScaleFactor AmberNB14::toScaleFactor() const static const RegisterMetaType r_nbdihpart(NO_ROOT); -QDataStream &operator<<(QDataStream &ds, const AmberNBDihPart ¶m) -{ - writeHeader(ds, r_nbdihpart, 1); - ds << param.dih << param.nbscl; - return ds; +QDataStream &operator<<(QDataStream &ds, const AmberNBDihPart ¶m) { + writeHeader(ds, r_nbdihpart, 1); + ds << param.dih << param.nbscl; + return ds; } -QDataStream &operator>>(QDataStream &ds, AmberNBDihPart ¶m) -{ - VersionID v = readHeader(ds, r_nbdihpart); +QDataStream &operator>>(QDataStream &ds, AmberNBDihPart ¶m) { + VersionID v = readHeader(ds, r_nbdihpart); - if (v == 1) - { - ds >> param.dih >> param.nbscl; - } - else - throw version_error(v, "1", r_nbdihpart, CODELOC); + if (v == 1) { + ds >> param.dih >> param.nbscl; + } else + throw version_error(v, "1", r_nbdihpart, CODELOC); - return ds; + return ds; } /** Constructor */ -AmberNBDihPart::AmberNBDihPart() -{ -} +AmberNBDihPart::AmberNBDihPart() {} /** Construct from the passed parameters */ -AmberNBDihPart::AmberNBDihPart(const AmberDihPart &dihedral, const AmberNB14 &nb14) : dih(dihedral), nbscl(nb14) -{ -} +AmberNBDihPart::AmberNBDihPart(const AmberDihPart &dihedral, + const AmberNB14 &nb14) + : dih(dihedral), nbscl(nb14) {} /** Copy constructor */ -AmberNBDihPart::AmberNBDihPart(const AmberNBDihPart &other) : dih(other.dih), nbscl(other.nbscl) -{ -} +AmberNBDihPart::AmberNBDihPart(const AmberNBDihPart &other) + : dih(other.dih), nbscl(other.nbscl) {} /** Destructor */ -AmberNBDihPart::~AmberNBDihPart() -{ -} +AmberNBDihPart::~AmberNBDihPart() {} /** Copy assignment operator */ -AmberNBDihPart &AmberNBDihPart::operator=(const AmberNBDihPart &other) -{ - dih = other.dih; - nbscl = other.nbscl; - return *this; +AmberNBDihPart &AmberNBDihPart::operator=(const AmberNBDihPart &other) { + dih = other.dih; + nbscl = other.nbscl; + return *this; } /** Comparison operator */ -bool AmberNBDihPart::operator==(const AmberNBDihPart &other) const -{ - return dih == other.dih and nbscl == other.nbscl; +bool AmberNBDihPart::operator==(const AmberNBDihPart &other) const { + return dih == other.dih and nbscl == other.nbscl; } /** Comparison operator */ -bool AmberNBDihPart::operator!=(const AmberNBDihPart &other) const -{ - return not operator==(other); +bool AmberNBDihPart::operator!=(const AmberNBDihPart &other) const { + return not operator==(other); } /** Comparison operator */ -bool AmberNBDihPart::operator<(const AmberNBDihPart &other) const -{ - if (nbscl < other.nbscl) - { - return true; - } - else if (nbscl == other.nbscl) - { - return dih < other.dih; - } - else - { - return false; - } +bool AmberNBDihPart::operator<(const AmberNBDihPart &other) const { + if (nbscl < other.nbscl) { + return true; + } else if (nbscl == other.nbscl) { + return dih < other.dih; + } else { + return false; + } } /** Comparison operator */ -bool AmberNBDihPart::operator<=(const AmberNBDihPart &other) const -{ - return this->operator==(other) or this->operator<(other); +bool AmberNBDihPart::operator<=(const AmberNBDihPart &other) const { + return this->operator==(other) or this->operator<(other); } /** Comparison operator */ -bool AmberNBDihPart::operator>(const AmberNBDihPart &other) const -{ - return not this->operator<=(other); +bool AmberNBDihPart::operator>(const AmberNBDihPart &other) const { + return not this->operator<=(other); } /** Comparison operator */ -bool AmberNBDihPart::operator>=(const AmberNBDihPart &other) const -{ - return not this->operator<(other); +bool AmberNBDihPart::operator>=(const AmberNBDihPart &other) const { + return not this->operator<(other); } -const char *AmberNBDihPart::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); +const char *AmberNBDihPart::typeName() { + return QMetaType::typeName(qMetaTypeId()); } -const char *AmberNBDihPart::what() const -{ - return AmberNBDihPart::typeName(); -} +const char *AmberNBDihPart::what() const { return AmberNBDihPart::typeName(); } -QString AmberNBDihPart::toString() const -{ - return QObject::tr("AmberNBDihPart( param == %1, scl == %2 )").arg(dih.toString()).arg(nbscl.toString()); +QString AmberNBDihPart::toString() const { + return QObject::tr("AmberNBDihPart( param == %1, scl == %2 )") + .arg(dih.toString()) + .arg(nbscl.toString()); } /////////// @@ -1264,577 +1074,572 @@ QString AmberNBDihPart::toString() const static const RegisterMetaType r_amberparam; /** Serialise to a binary datastream */ -QDataStream &operator<<(QDataStream &ds, const AmberParams &amberparam) -{ - writeHeader(ds, r_amberparam, 3); +QDataStream &operator<<(QDataStream &ds, const AmberParams &amberparam) { + writeHeader(ds, r_amberparam, 3); - SharedDataStream sds(ds); + SharedDataStream sds(ds); - sds << amberparam.molinfo << amberparam.amber_charges << amberparam.amber_ljs << amberparam.amber_masses - << amberparam.amber_elements << amberparam.amber_types << amberparam.born_radii << amberparam.amber_screens - << amberparam.amber_treechains << amberparam.exc_atoms << amberparam.amber_bonds << amberparam.amber_angles - << amberparam.amber_dihedrals << amberparam.amber_impropers << amberparam.amber_nb14s - << amberparam.cmap_funcs << amberparam.radius_set - << amberparam.propmap << static_cast(amberparam); + sds << amberparam.molinfo << amberparam.amber_charges << amberparam.amber_ljs + << amberparam.amber_masses << amberparam.amber_elements + << amberparam.amber_types << amberparam.born_radii + << amberparam.amber_screens << amberparam.amber_treechains + << amberparam.exc_atoms << amberparam.amber_bonds + << amberparam.amber_angles << amberparam.amber_dihedrals + << amberparam.amber_impropers << amberparam.amber_nb14s + << amberparam.cmap_funcs << amberparam.radius_set << amberparam.propmap + << static_cast(amberparam); - return ds; + return ds; } /** Extract from a binary datastream */ -QDataStream &operator>>(QDataStream &ds, AmberParams &amberparam) -{ - VersionID v = readHeader(ds, r_amberparam); - - if (v == 3) - { - SharedDataStream sds(ds); - - sds >> amberparam.molinfo >> amberparam.amber_charges >> amberparam.amber_ljs >> amberparam.amber_masses >> - amberparam.amber_elements >> amberparam.amber_types >> amberparam.born_radii >> amberparam.amber_screens >> - amberparam.amber_treechains >> amberparam.exc_atoms >> amberparam.amber_bonds >> amberparam.amber_angles >> - amberparam.amber_dihedrals >> amberparam.amber_impropers >> amberparam.amber_nb14s >> - amberparam.cmap_funcs >> - amberparam.radius_set >> amberparam.propmap >> static_cast(amberparam); - } - else if (v == 2) - { - SharedDataStream sds(ds); +QDataStream &operator>>(QDataStream &ds, AmberParams &amberparam) { + VersionID v = readHeader(ds, r_amberparam); - amberparam.cmap_funcs.clear(); + if (v == 3) { + SharedDataStream sds(ds); - sds >> amberparam.molinfo >> amberparam.amber_charges >> amberparam.amber_ljs >> amberparam.amber_masses >> - amberparam.amber_elements >> amberparam.amber_types >> amberparam.born_radii >> amberparam.amber_screens >> - amberparam.amber_treechains >> amberparam.exc_atoms >> amberparam.amber_bonds >> amberparam.amber_angles >> - amberparam.amber_dihedrals >> amberparam.amber_impropers >> amberparam.amber_nb14s >> - amberparam.radius_set >> amberparam.propmap >> static_cast(amberparam); - } - else - throw version_error(v, "2,3", r_amberparam, CODELOC); + sds >> amberparam.molinfo >> amberparam.amber_charges >> + amberparam.amber_ljs >> amberparam.amber_masses >> + amberparam.amber_elements >> amberparam.amber_types >> + amberparam.born_radii >> amberparam.amber_screens >> + amberparam.amber_treechains >> amberparam.exc_atoms >> + amberparam.amber_bonds >> amberparam.amber_angles >> + amberparam.amber_dihedrals >> amberparam.amber_impropers >> + amberparam.amber_nb14s >> amberparam.cmap_funcs >> + amberparam.radius_set >> amberparam.propmap >> + static_cast(amberparam); + } else if (v == 2) { + SharedDataStream sds(ds); - return ds; + amberparam.cmap_funcs.clear(); + + sds >> amberparam.molinfo >> amberparam.amber_charges >> + amberparam.amber_ljs >> amberparam.amber_masses >> + amberparam.amber_elements >> amberparam.amber_types >> + amberparam.born_radii >> amberparam.amber_screens >> + amberparam.amber_treechains >> amberparam.exc_atoms >> + amberparam.amber_bonds >> amberparam.amber_angles >> + amberparam.amber_dihedrals >> amberparam.amber_impropers >> + amberparam.amber_nb14s >> amberparam.radius_set >> amberparam.propmap >> + static_cast(amberparam); + } else + throw version_error(v, "2,3", r_amberparam, CODELOC); + + return ds; } /** Null Constructor */ -AmberParams::AmberParams() : ConcreteProperty() -{ -} +AmberParams::AmberParams() + : ConcreteProperty() {} /** Constructor for the passed molecule*/ AmberParams::AmberParams(const MoleculeView &mol, const PropertyMap &map) - : ConcreteProperty() -{ - const auto moldata = mol.data(); + : ConcreteProperty() { + const auto moldata = mol.data(); - const auto param_name = map["parameters"]; + const auto param_name = map["parameters"]; - // if possible, start from the existing parameters and update from there - if (moldata.hasProperty(param_name)) - { - const Property ¶m_prop = moldata.property(param_name); + // if possible, start from the existing parameters and update from there + if (moldata.hasProperty(param_name)) { + const Property ¶m_prop = moldata.property(param_name); - if (param_prop.isA()) - { - this->operator=(param_prop.asA()); + if (param_prop.isA()) { + this->operator=(param_prop.asA()); - if (propmap == map and this->isCompatibleWith(moldata.info())) - { - this->_pvt_updateFrom(moldata); - return; - } - } + if (propmap == map and this->isCompatibleWith(moldata.info())) { + this->_pvt_updateFrom(moldata); + return; + } } + } - // otherwise construct this parameter from scratch - this->operator=(AmberParams()); + // otherwise construct this parameter from scratch + this->operator=(AmberParams()); - molinfo = MoleculeInfo(moldata.info()); - propmap = map; - this->_pvt_createFrom(moldata); + molinfo = MoleculeInfo(moldata.info()); + propmap = map; + this->_pvt_createFrom(moldata); } /** Constructor for the passed molecule*/ -AmberParams::AmberParams(const MoleculeInfo &info) : ConcreteProperty(), molinfo(info) -{ -} +AmberParams::AmberParams(const MoleculeInfo &info) + : ConcreteProperty(), molinfo(info) {} /** Constructor for the passed molecule*/ AmberParams::AmberParams(const MoleculeInfoData &info) - : ConcreteProperty(), molinfo(info) -{ -} + : ConcreteProperty(), molinfo(info) {} /** Copy constructor */ AmberParams::AmberParams(const AmberParams &other) - : ConcreteProperty(), molinfo(other.molinfo), amber_charges(other.amber_charges), - amber_ljs(other.amber_ljs), amber_masses(other.amber_masses), amber_elements(other.amber_elements), - amber_types(other.amber_types), born_radii(other.born_radii), amber_screens(other.amber_screens), - amber_treechains(other.amber_treechains), exc_atoms(other.exc_atoms), amber_bonds(other.amber_bonds), - amber_angles(other.amber_angles), amber_dihedrals(other.amber_dihedrals), amber_impropers(other.amber_impropers), - amber_nb14s(other.amber_nb14s), cmap_funcs(other.cmap_funcs), - radius_set(other.radius_set), propmap(other.propmap) -{ -} + : ConcreteProperty(), molinfo(other.molinfo), + amber_charges(other.amber_charges), amber_ljs(other.amber_ljs), + amber_masses(other.amber_masses), amber_elements(other.amber_elements), + amber_types(other.amber_types), born_radii(other.born_radii), + amber_screens(other.amber_screens), + amber_treechains(other.amber_treechains), exc_atoms(other.exc_atoms), + amber_bonds(other.amber_bonds), amber_angles(other.amber_angles), + amber_dihedrals(other.amber_dihedrals), + amber_impropers(other.amber_impropers), amber_nb14s(other.amber_nb14s), + cmap_funcs(other.cmap_funcs), radius_set(other.radius_set), + propmap(other.propmap), is_perturbable(other.is_perturbable) {} /** Copy assignment operator */ -AmberParams &AmberParams::operator=(const AmberParams &other) -{ - if (this != &other) - { - MoleculeProperty::operator=(other); - molinfo = other.molinfo; - amber_charges = other.amber_charges; - amber_ljs = other.amber_ljs; - amber_masses = other.amber_masses; - amber_elements = other.amber_elements; - amber_types = other.amber_types; - born_radii = other.born_radii; - amber_screens = other.amber_screens; - amber_treechains = other.amber_treechains; - exc_atoms = other.exc_atoms; - amber_bonds = other.amber_bonds; - amber_angles = other.amber_angles; - amber_dihedrals = other.amber_dihedrals; - amber_impropers = other.amber_impropers; - amber_nb14s = other.amber_nb14s; - cmap_funcs = other.cmap_funcs; - radius_set = other.radius_set; - propmap = other.propmap; - } - - return *this; +AmberParams &AmberParams::operator=(const AmberParams &other) { + if (this != &other) { + MoleculeProperty::operator=(other); + molinfo = other.molinfo; + amber_charges = other.amber_charges; + amber_ljs = other.amber_ljs; + amber_masses = other.amber_masses; + amber_elements = other.amber_elements; + amber_types = other.amber_types; + born_radii = other.born_radii; + amber_screens = other.amber_screens; + amber_treechains = other.amber_treechains; + exc_atoms = other.exc_atoms; + amber_bonds = other.amber_bonds; + amber_angles = other.amber_angles; + amber_dihedrals = other.amber_dihedrals; + amber_impropers = other.amber_impropers; + amber_nb14s = other.amber_nb14s; + cmap_funcs = other.cmap_funcs; + radius_set = other.radius_set; + propmap = other.propmap; + is_perturbable = other.is_perturbable; + } + + return *this; } /** Destructor */ -AmberParams::~AmberParams() -{ -} +AmberParams::~AmberParams() {} /** Comparison operator */ -bool AmberParams::operator==(const AmberParams &other) const -{ - return (molinfo == other.molinfo and amber_charges == other.amber_charges and amber_ljs == other.amber_ljs and - amber_masses == other.amber_masses and amber_elements == other.amber_elements and - amber_types == other.amber_types and born_radii == other.born_radii and - amber_screens == other.amber_screens and amber_treechains == other.amber_treechains and - exc_atoms == other.exc_atoms and amber_bonds == other.amber_bonds and amber_angles == other.amber_angles and - amber_dihedrals == other.amber_dihedrals and amber_impropers == other.amber_impropers and - amber_nb14s == other.amber_nb14s and cmap_funcs == other.cmap_funcs and - radius_set == other.radius_set and propmap == other.propmap); +bool AmberParams::operator==(const AmberParams &other) const { + return ( + molinfo == other.molinfo and amber_charges == other.amber_charges and + amber_ljs == other.amber_ljs and amber_masses == other.amber_masses and + amber_elements == other.amber_elements and + amber_types == other.amber_types and born_radii == other.born_radii and + amber_screens == other.amber_screens and + amber_treechains == other.amber_treechains and + exc_atoms == other.exc_atoms and amber_bonds == other.amber_bonds and + amber_angles == other.amber_angles and + amber_dihedrals == other.amber_dihedrals and + amber_impropers == other.amber_impropers and + amber_nb14s == other.amber_nb14s and cmap_funcs == other.cmap_funcs and + radius_set == other.radius_set and propmap == other.propmap); } /** Comparison operator */ -bool AmberParams::operator!=(const AmberParams &other) const -{ - return not AmberParams::operator==(other); +bool AmberParams::operator!=(const AmberParams &other) const { + return not AmberParams::operator==(other); } /** Return the layout of the molecule whose flexibility is contained in this object */ -MoleculeInfo AmberParams::info() const -{ - return molinfo; -} +MoleculeInfo AmberParams::info() const { return molinfo; } /** Set the property map that should be used to find and update properties of the molecule */ -void AmberParams::setPropertyMap(const PropertyMap &map) -{ - propmap = map; -} +void AmberParams::setPropertyMap(const PropertyMap &map) { propmap = map; } /** Return the property map that is used to find and update properties of the molecule */ -const PropertyMap &AmberParams::propertyMap() const -{ - return propmap; -} +const PropertyMap &AmberParams::propertyMap() const { return propmap; } /** Validate this set of parameters. This checks that all of the requirements for an Amber set of parameters are met, e.g. that all Atom indicies are contiguous and in-order, and that all atoms contiguously fill all residues etc. This returns any errors as strings. An empty set of strings indicates that there are no errors */ -QStringList AmberParams::validate() const -{ - return QStringList(); -} +QStringList AmberParams::validate() const { return QStringList(); } /** Validate this set of parameters. In addition to checking that the requirements are met, this also does any work needed to fix problems, if they are fixable. */ -QStringList AmberParams::validateAndFix() -{ - QStringList errors; - - if (not exc_atoms.isEmpty()) - { - Connectivity conn; - bool has_connectivity = false; - - auto new_dihedrals = amber_dihedrals; - auto new_nb14s = amber_nb14s; - auto new_exc = exc_atoms; - - QMutex mutex; - - // All 1-4 scaling factors should match up with actual dihedrals - validate - // that this is the case and fix any problems if we can - tbb::parallel_for(tbb::blocked_range(0, exc_atoms.nGroups()), [&](const tbb::blocked_range &r) - { - for (int icg = r.begin(); icg < r.end(); ++icg) - { - const int nats0 = molinfo.nAtoms(CGIdx(icg)); - - for (int jcg = 0; jcg < exc_atoms.nGroups(); ++jcg) - { - auto group_pairs = exc_atoms.get(CGIdx(icg), CGIdx(jcg)); - - if (group_pairs.isEmpty() and group_pairs.defaultValue() == CLJScaleFactor(1, 1)) +QStringList AmberParams::validateAndFix() { + QStringList errors; + + if (not exc_atoms.isEmpty()) { + Connectivity conn; + bool has_connectivity = false; + + auto new_dihedrals = amber_dihedrals; + auto new_nb14s = amber_nb14s; + auto new_exc = exc_atoms; + + QMutex mutex; + + // All 1-4 scaling factors should match up with actual dihedrals - validate + // that this is the case and fix any problems if we can + tbb::parallel_for( + tbb::blocked_range(0, exc_atoms.nGroups()), + [&](const tbb::blocked_range &r) { + for (int icg = r.begin(); icg < r.end(); ++icg) { + const int nats0 = molinfo.nAtoms(CGIdx(icg)); + + for (int jcg = 0; jcg < exc_atoms.nGroups(); ++jcg) { + auto group_pairs = exc_atoms.get(CGIdx(icg), CGIdx(jcg)); + + if (group_pairs.isEmpty() and + group_pairs.defaultValue() == CLJScaleFactor(1, 1)) { + // non of the pairs of atoms between these two groups are + // bonded + continue; + } + + const int nats1 = molinfo.nAtoms(CGIdx(jcg)); + + // compare all pairs of atoms + for (int i = 0; i < nats0; ++i) { + for (int j = 0; j < nats1; ++j) { + const auto s = group_pairs.get(i, j); + + // Process any non-zero 1-4 pair that isn't purely excluded + // (0,0). This includes both partial-scaling pairs (e.g. + // 0.833, 0.5 for standard AMBER) and full-interaction pairs + // (1.0, 1.0 for GLYCAM SCNB=1.0/SCEE=1.0). + if (not(s.coulomb() == 0.0 and s.lj() == 0.0)) { + const auto atm0 = + molinfo.atomIdx(CGAtomIdx(CGIdx(icg), Index(i))); + const auto atm3 = + molinfo.atomIdx(CGAtomIdx(CGIdx(jcg), Index(j))); + + if (not has_connectivity) { + // have to use the connectivity that is implied by the + // bonds + QMutexLocker lkr(&mutex); + if (not has_connectivity) { + conn = this->connectivity(); + has_connectivity = true; + } + } + + // find the shortest bonded paths between these two atoms + const auto paths = conn.findPaths(atm0, atm3, 4); + + // If the shortest bonded path between these two atoms is + // fewer than 4 atoms (i.e. they are 1-2 or 1-3), + // connectivity always enforces their exclusion from the + // non-bonded calculation regardless of what the intrascale + // says. For perturbable molecules this is expected (a + // ring-closure bond in one end-state can turn a 1-4 pair + // into a 1-3 pair in the other). For non-perturbable + // molecules it likely indicates a topology issue, so warn. { - // non of the pairs of atoms between these two groups are - // bonded + bool has_short_path = false; + for (const auto &path : paths) { + if (path.count() < 4) { + has_short_path = true; + break; + } + } + if (has_short_path) { + if (not is_perturbable) { + QMutexLocker lkr(&mutex); + qWarning().noquote() + << QObject::tr( + "WARNING: Have a 1-4 scaling factor " + "(%1/%2) between atoms %3:%4 and %5:%6 " + "that are fewer than 4 bonds apart. " + "Skipping — connectivity will enforce " + "their exclusion. This may indicate a " + "topology issue.") + .arg(s.coulomb()) + .arg(s.lj()) + .arg(molinfo.name(atm0).value()) + .arg(atm0.value()) + .arg(molinfo.name(atm3).value()) + .arg(atm3.value()); + } continue; + } } - const int nats1 = molinfo.nAtoms(CGIdx(jcg)); + for (const auto &path : paths) { + if (path.count() != 4) { + QMutexLocker lkr(&mutex); + errors.append( + QObject::tr( + "Have a 1-4 scaling factor (%1/%2) " + "between atoms %3:%4 and %5:%6 despite there " + "being no physical " + "dihedral between these two atoms. All 1-4 " + "scaling factors MUST " + "be associated with " + "physical dihedrals. The shortest path is %7") + .arg(s.coulomb()) + .arg(s.lj()) + .arg(molinfo.name(atm0).value()) + .arg(atm0.value()) + .arg(molinfo.name(atm3).value()) + .arg(atm3.value()) + .arg(Sire::toString(path))); + continue; + } + + // convert the atom IDs into a canonical form + auto dih = this->convert( + DihedralID(path[0], path[1], path[2], path[3])); - // compare all pairs of atoms - for (int i = 0; i < nats0; ++i) - { - for (int j = 0; j < nats1; ++j) - { - const auto s = group_pairs.get(i, j); - - // Process any non-zero 1-4 pair that isn't purely excluded (0,0). - // This includes both partial-scaling pairs (e.g. 0.833, 0.5 for standard AMBER) - // and full-interaction pairs (1.0, 1.0 for GLYCAM SCNB=1.0/SCEE=1.0). - if (not(s.coulomb() == 0.0 and s.lj() == 0.0)) - { - const auto atm0 = molinfo.atomIdx(CGAtomIdx(CGIdx(icg), Index(i))); - const auto atm3 = molinfo.atomIdx(CGAtomIdx(CGIdx(jcg), Index(j))); - - if (not has_connectivity) - { - // have to use the connectivity that is implied by the bonds - QMutexLocker lkr(&mutex); - if (not has_connectivity) - { - conn = this->connectivity(); - has_connectivity = true; - } - } - - // find the shortest bonded paths between these two atoms - const auto paths = conn.findPaths(atm0, atm3, 4); - - for (const auto &path : paths) - { - if (path.count() != 4) - { - QMutexLocker lkr(&mutex); - errors.append( - QObject::tr("Have a 1-4 scaling factor (%1/%2) " - "between atoms %3:%4 and %5:%6 despite there being no physical " - "dihedral between these two atoms. All 1-4 scaling factors MUST " - "be associated with " - "physical dihedrals. The shortest path is %7") - .arg(s.coulomb()) - .arg(s.lj()) - .arg(molinfo.name(atm0).value()) - .arg(atm0.value()) - .arg(molinfo.name(atm3).value()) - .arg(atm3.value()) - .arg(Sire::toString(path))); - continue; - } - - // convert the atom IDs into a canonical form - auto dih = this->convert(DihedralID(path[0], path[1], path[2], path[3])); - - // skip if we already have this dihedral - if (new_dihedrals.contains(dih)) - continue; - - // qDebug() << "ADDING NULL DIHEDRAL FOR" << dih.toString(); - - // does this bond involve hydrogen? - //- this relies on "AtomElements" being full - bool contains_hydrogen = false; - - if (not amber_elements.isEmpty()) - { - contains_hydrogen = - (amber_elements.at(molinfo.cgAtomIdx(dih.atom0())).nProtons() < 2) or - (amber_elements.at(molinfo.cgAtomIdx(dih.atom1())).nProtons() < 2) or - (amber_elements.at(molinfo.cgAtomIdx(dih.atom2())).nProtons() < 2) or - (amber_elements.at(molinfo.cgAtomIdx(dih.atom3())).nProtons() < 2); - } - - // create a null dihedral parameter and add this to the set - QMutexLocker lkr(&mutex); - new_dihedrals.insert( - dih, qMakePair(AmberDihedral(Expression(0), Symbol("phi")), contains_hydrogen)); - - // now add in the 1-4 pair - BondID nb14pair = this->convert(BondID(dih.atom0(), dih.atom3())); - - // add them to the list of 14 scale factors - new_nb14s.insert(nb14pair, AmberNB14(s.coulomb(), s.lj())); - - // and remove them from the excluded atoms list - new_exc.set(nb14pair.atom0(), nb14pair.atom1(), CLJScaleFactor(0)); - } - } - } + // skip if we already have this dihedral + if (new_dihedrals.contains(dih)) + continue; + + // qDebug() << "ADDING NULL DIHEDRAL FOR" << + // dih.toString(); + + // does this bond involve hydrogen? + //- this relies on "AtomElements" being full + bool contains_hydrogen = false; + + if (not amber_elements.isEmpty()) { + contains_hydrogen = + (amber_elements.at(molinfo.cgAtomIdx(dih.atom0())) + .nProtons() < 2) or + (amber_elements.at(molinfo.cgAtomIdx(dih.atom1())) + .nProtons() < 2) or + (amber_elements.at(molinfo.cgAtomIdx(dih.atom2())) + .nProtons() < 2) or + (amber_elements.at(molinfo.cgAtomIdx(dih.atom3())) + .nProtons() < 2); + } + + // create a null dihedral parameter and add this to the + // set + QMutexLocker lkr(&mutex); + new_dihedrals.insert( + dih, + qMakePair(AmberDihedral(Expression(0), Symbol("phi")), + contains_hydrogen)); + + // now add in the 1-4 pair + BondID nb14pair = + this->convert(BondID(dih.atom0(), dih.atom3())); + + // add them to the list of 14 scale factors + new_nb14s.insert(nb14pair, + AmberNB14(s.coulomb(), s.lj())); + + // and remove them from the excluded atoms list + new_exc.set(nb14pair.atom0(), nb14pair.atom1(), + CLJScaleFactor(0)); } + } } - } }); + } + } + } + }); - amber_dihedrals = new_dihedrals; - amber_nb14s = new_nb14s; - exc_atoms = new_exc; + amber_dihedrals = new_dihedrals; + amber_nb14s = new_nb14s; + exc_atoms = new_exc; - } // if not exc_atoms.isEmpty() + } // if not exc_atoms.isEmpty() - return errors + this->validate(); + return errors + this->validate(); } -QString AmberParams::toString() const -{ - if (molinfo.nAtoms() == 0) - return QObject::tr("AmberParams::null"); +QString AmberParams::toString() const { + if (molinfo.nAtoms() == 0) + return QObject::tr("AmberParams::null"); - return QObject::tr("AmberParams( nAtoms()=%6 nBonds=%1, nAngles=%2, nDihedrals=%3 " - "nImpropers=%4 n14s=%5 )") - .arg(amber_bonds.count()) - .arg(amber_angles.count()) - .arg(amber_dihedrals.count()) - .arg(amber_impropers.count()) - .arg(amber_nb14s.count()) - .arg(molinfo.nAtoms()); + return QObject::tr( + "AmberParams( nAtoms()=%6 nBonds=%1, nAngles=%2, nDihedrals=%3 " + "nImpropers=%4 n14s=%5 )") + .arg(amber_bonds.count()) + .arg(amber_angles.count()) + .arg(amber_dihedrals.count()) + .arg(amber_impropers.count()) + .arg(amber_nb14s.count()) + .arg(molinfo.nAtoms()); } /** Convert the passed BondID into AtomIdx IDs, sorted in index order */ -BondID AmberParams::convert(const BondID &bond) const -{ - AtomIdx atom0 = info().atomIdx(bond.atom0()); - AtomIdx atom1 = info().atomIdx(bond.atom1()); +BondID AmberParams::convert(const BondID &bond) const { + AtomIdx atom0 = info().atomIdx(bond.atom0()); + AtomIdx atom1 = info().atomIdx(bond.atom1()); - if (atom0.value() <= atom1.value()) - return BondID(atom0, atom1); - else - return BondID(atom1, atom0); + if (atom0.value() <= atom1.value()) + return BondID(atom0, atom1); + else + return BondID(atom1, atom0); } /** Convert the passed AngleID into AtomIdx IDs, sorted in index order */ -AngleID AmberParams::convert(const AngleID &angle) const -{ - AtomIdx atom0 = info().atomIdx(angle.atom0()); - AtomIdx atom1 = info().atomIdx(angle.atom1()); - AtomIdx atom2 = info().atomIdx(angle.atom2()); +AngleID AmberParams::convert(const AngleID &angle) const { + AtomIdx atom0 = info().atomIdx(angle.atom0()); + AtomIdx atom1 = info().atomIdx(angle.atom1()); + AtomIdx atom2 = info().atomIdx(angle.atom2()); - if (atom0.value() <= atom2.value()) - return AngleID(atom0, atom1, atom2); - else - return AngleID(atom2, atom1, atom0); + if (atom0.value() <= atom2.value()) + return AngleID(atom0, atom1, atom2); + else + return AngleID(atom2, atom1, atom0); } /** Convert the passed DihedralID into AtomIdx IDs, sorted in index order */ -DihedralID AmberParams::convert(const DihedralID &dihedral) const -{ - AtomIdx atom0 = info().atomIdx(dihedral.atom0()); - AtomIdx atom1 = info().atomIdx(dihedral.atom1()); - AtomIdx atom2 = info().atomIdx(dihedral.atom2()); - AtomIdx atom3 = info().atomIdx(dihedral.atom3()); - - if (atom0.value() < atom3.value()) - return DihedralID(atom0, atom1, atom2, atom3); - else if (atom0.value() > atom3.value()) - return DihedralID(atom3, atom2, atom1, atom0); - else if (atom1.value() <= atom2.value()) - return DihedralID(atom0, atom1, atom2, atom3); - else - return DihedralID(atom3, atom2, atom1, atom0); +DihedralID AmberParams::convert(const DihedralID &dihedral) const { + AtomIdx atom0 = info().atomIdx(dihedral.atom0()); + AtomIdx atom1 = info().atomIdx(dihedral.atom1()); + AtomIdx atom2 = info().atomIdx(dihedral.atom2()); + AtomIdx atom3 = info().atomIdx(dihedral.atom3()); + + if (atom0.value() < atom3.value()) + return DihedralID(atom0, atom1, atom2, atom3); + else if (atom0.value() > atom3.value()) + return DihedralID(atom3, atom2, atom1, atom0); + else if (atom1.value() <= atom2.value()) + return DihedralID(atom0, atom1, atom2, atom3); + else + return DihedralID(atom3, atom2, atom1, atom0); } /** Convert the passed ImproperID into AtomIdx IDs, sorted in index order */ -ImproperID AmberParams::convert(const ImproperID &improper) const -{ - AtomIdx atom0 = info().atomIdx(improper.atom0()); - AtomIdx atom1 = info().atomIdx(improper.atom1()); - AtomIdx atom2 = info().atomIdx(improper.atom2()); - AtomIdx atom3 = info().atomIdx(improper.atom3()); - - if (atom0.value() < atom3.value()) - return ImproperID(atom0, atom1, atom2, atom3); - else if (atom0.value() > atom3.value()) - return ImproperID(atom3, atom2, atom1, atom0); - else if (atom1.value() <= atom2.value()) - return ImproperID(atom0, atom1, atom2, atom3); - else - return ImproperID(atom3, atom2, atom1, atom0); +ImproperID AmberParams::convert(const ImproperID &improper) const { + AtomIdx atom0 = info().atomIdx(improper.atom0()); + AtomIdx atom1 = info().atomIdx(improper.atom1()); + AtomIdx atom2 = info().atomIdx(improper.atom2()); + AtomIdx atom3 = info().atomIdx(improper.atom3()); + + if (atom0.value() < atom3.value()) + return ImproperID(atom0, atom1, atom2, atom3); + else if (atom0.value() > atom3.value()) + return ImproperID(atom3, atom2, atom1, atom0); + else if (atom1.value() <= atom2.value()) + return ImproperID(atom0, atom1, atom2, atom3); + else + return ImproperID(atom3, atom2, atom1, atom0); } /** Return whether or not this flexibility is compatible with the molecule whose info is in 'molinfo' */ -bool AmberParams::isCompatibleWith(const SireMol::MoleculeInfoData &molinfo) const -{ - return info().UID() == molinfo.UID(); +bool AmberParams::isCompatibleWith( + const SireMol::MoleculeInfoData &molinfo) const { + return info().UID() == molinfo.UID(); } -const char *AmberParams::typeName() -{ - return QMetaType::typeName(qMetaTypeId()); +const char *AmberParams::typeName() { + return QMetaType::typeName(qMetaTypeId()); } /** Return the charges on the atoms */ -AtomCharges AmberParams::charges() const -{ - return amber_charges; -} +AtomCharges AmberParams::charges() const { return amber_charges; } /** Return the atom masses */ -AtomMasses AmberParams::masses() const -{ - return amber_masses; -} +AtomMasses AmberParams::masses() const { return amber_masses; } /** Return the atom elements */ -AtomElements AmberParams::elements() const -{ - return amber_elements; -} +AtomElements AmberParams::elements() const { return amber_elements; } /** Return the atom LJ parameters */ -AtomLJs AmberParams::ljs() const -{ - return amber_ljs; -} +AtomLJs AmberParams::ljs() const { return amber_ljs; } /** Return all of the amber atom types */ -AtomStringProperty AmberParams::amberTypes() const -{ - return amber_types; -} +AtomStringProperty AmberParams::amberTypes() const { return amber_types; } /** Return all of the Born radii of the atoms */ -AtomRadii AmberParams::gbRadii() const -{ - return born_radii; -} +AtomRadii AmberParams::gbRadii() const { return born_radii; } /** Return all of the Born screening parameters for the atoms */ -AtomFloatProperty AmberParams::gbScreening() const -{ - return amber_screens; -} +AtomFloatProperty AmberParams::gbScreening() const { return amber_screens; } /** Return all of the Amber treechain classification for all of the atoms */ -AtomStringProperty AmberParams::treeChains() const -{ - return amber_treechains; -} - -void AmberParams::createContainers() -{ - if (amber_charges.isEmpty()) - { - // set up the objects to hold these parameters - amber_charges = AtomCharges(molinfo); - amber_ljs = AtomLJs(molinfo); - amber_masses = AtomMasses(molinfo); - amber_elements = AtomElements(molinfo); - amber_types = AtomStringProperty(molinfo); - born_radii = AtomRadii(molinfo); - amber_screens = AtomFloatProperty(molinfo); - amber_treechains = AtomStringProperty(molinfo); - } +AtomStringProperty AmberParams::treeChains() const { return amber_treechains; } + +void AmberParams::createContainers() { + if (amber_charges.isEmpty()) { + // set up the objects to hold these parameters + amber_charges = AtomCharges(molinfo); + amber_ljs = AtomLJs(molinfo); + amber_masses = AtomMasses(molinfo); + amber_elements = AtomElements(molinfo); + amber_types = AtomStringProperty(molinfo); + born_radii = AtomRadii(molinfo); + amber_screens = AtomFloatProperty(molinfo); + amber_treechains = AtomStringProperty(molinfo); + } } /** Set the atom parameters for the specified atom to the provided values */ -void AmberParams::add(const AtomID &atom, SireUnits::Dimension::Charge charge, SireUnits::Dimension::MolarMass mass, - const SireMol::Element &element, const SireMM::LJParameter &ljparam, const QString &amber_type, - SireUnits::Dimension::Length born_radius, double screening_parameter, const QString &treechain) -{ - createContainers(); - - CGAtomIdx idx = molinfo.cgAtomIdx(atom); - - amber_charges.set(idx, charge); - amber_ljs.set(idx, ljparam); - amber_masses.set(idx, mass); - amber_elements.set(idx, element); - amber_types.set(idx, amber_type); - born_radii.set(idx, born_radius); - amber_screens.set(idx, screening_parameter); - amber_treechains.set(idx, treechain); +void AmberParams::add(const AtomID &atom, SireUnits::Dimension::Charge charge, + SireUnits::Dimension::MolarMass mass, + const SireMol::Element &element, + const SireMM::LJParameter &ljparam, + const QString &amber_type, + SireUnits::Dimension::Length born_radius, + double screening_parameter, const QString &treechain) { + createContainers(); + + CGAtomIdx idx = molinfo.cgAtomIdx(atom); + + amber_charges.set(idx, charge); + amber_ljs.set(idx, ljparam); + amber_masses.set(idx, mass); + amber_elements.set(idx, element); + amber_types.set(idx, amber_type); + born_radii.set(idx, born_radius); + amber_screens.set(idx, screening_parameter); + amber_treechains.set(idx, treechain); } /** Set the LJ exceptions for the specified atom - this replaces any * existing exceptions */ -void AmberParams::set(const AtomID &atom, const QList &exceptions) -{ - createContainers(); - amber_ljs.set(molinfo.atomIdx(atom).value(), exceptions); +void AmberParams::set(const AtomID &atom, + const QList &exceptions) { + createContainers(); + amber_ljs.set(molinfo.atomIdx(atom).value(), exceptions); } /** Set the LJ exception between atom0 in this set and atom1 in the * passed set of parameters to 'ljparam' */ void AmberParams::set(const AtomID &atom0, const AtomID &atom1, - AmberParams &other, const LJ1264Parameter &ljparam) -{ - createContainers(); - other.createContainers(); + AmberParams &other, const LJ1264Parameter &ljparam) { + createContainers(); + other.createContainers(); - amber_ljs.set(molinfo.atomIdx(atom0).value(), - other.molinfo.atomIdx(atom1).value(), - other.amber_ljs, ljparam); + amber_ljs.set(molinfo.atomIdx(atom0).value(), + other.molinfo.atomIdx(atom1).value(), other.amber_ljs, ljparam); } /** Set the LJ exception between atom0 and atom1 to 'ljparam' */ -void AmberParams::set(const AtomID &atom0, const AtomID &atom1, const LJ1264Parameter &ljparam) -{ - this->set(atom0, atom1, *this, ljparam); +void AmberParams::set(const AtomID &atom0, const AtomID &atom1, + const LJ1264Parameter &ljparam) { + this->set(atom0, atom1, *this, ljparam); } /** Return the connectivity of the molecule implied by the the bonds */ -Connectivity AmberParams::connectivity() const -{ - auto connectivity = Connectivity(molinfo).edit(); +Connectivity AmberParams::connectivity() const { + auto connectivity = Connectivity(molinfo).edit(); - for (auto it = amber_bonds.constBegin(); it != amber_bonds.constEnd(); ++it) - { - connectivity.connect(it.key().atom0(), it.key().atom1()); - } + for (auto it = amber_bonds.constBegin(); it != amber_bonds.constEnd(); ++it) { + connectivity.connect(it.key().atom0(), it.key().atom1()); + } - return connectivity.commit(); + return connectivity.commit(); } /** Set the radius set used by LEAP to assign the Born radii of the atoms. This is just a string that is used to label the radius set in the PRM file */ -void AmberParams::setRadiusSet(const QString &rset) -{ - radius_set = rset; -} +void AmberParams::setRadiusSet(const QString &rset) { radius_set = rset; } /** Return the radius set used by LEAP to assign the Born radii */ -QString AmberParams::radiusSet() const -{ - return radius_set; -} +QString AmberParams::radiusSet() const { return radius_set; } /** Set the excluded atoms of the molecule. This should be a CLJNBPairs with the value equal to 0 for atom0-atom1 pairs that are excluded, and 1 for atom0-atom1 pairs that are to be included in the non-bonded calculation */ -void AmberParams::setExcludedAtoms(const CLJNBPairs &excluded_atoms) -{ - molinfo.assertCompatibleWith(excluded_atoms.info()); - exc_atoms = excluded_atoms; +void AmberParams::setExcludedAtoms(const CLJNBPairs &excluded_atoms) { + molinfo.assertCompatibleWith(excluded_atoms.info()); + exc_atoms = excluded_atoms; } /** Return the excluded atoms of the molecule. The returned @@ -1842,1030 +1647,983 @@ void AmberParams::setExcludedAtoms(const CLJNBPairs &excluded_atoms) is 0 for atom0-atom1 pairs that are to be excluded, and 1 for atom0-atom1 pairs that are to be included in the nonbonded calculation */ -CLJNBPairs AmberParams::excludedAtoms() const -{ - if (exc_atoms.isEmpty()) - { - if (molinfo.nAtoms() <= 3) - { - // everything is bonded, so scale factor is 0 - return CLJNBPairs(molinfo, CLJScaleFactor(0, 0)); - } - else - { - // nothing is explicitly excluded - return CLJNBPairs(molinfo, CLJScaleFactor(1, 1)); - } +CLJNBPairs AmberParams::excludedAtoms() const { + if (exc_atoms.isEmpty()) { + if (molinfo.nAtoms() <= 3) { + // everything is bonded, so scale factor is 0 + return CLJNBPairs(molinfo, CLJScaleFactor(0, 0)); + } else { + // nothing is explicitly excluded + return CLJNBPairs(molinfo, CLJScaleFactor(1, 1)); } - else - return exc_atoms; + } else + return exc_atoms; } /** Return the CLJ nonbonded 1-4 scale factors for the molecule */ -CLJNBPairs AmberParams::cljScaleFactors() const -{ - // start from the set of excluded atoms - CLJNBPairs nbpairs = this->excludedAtoms(); - - // now add in all of the 1-4 nonbonded scale factors - for (auto it = amber_nb14s.constBegin(); it != amber_nb14s.constEnd(); ++it) - { - nbpairs.set(it.key().atom0(), it.key().atom1(), it.value().toScaleFactor()); - } +CLJNBPairs AmberParams::cljScaleFactors() const { + // start from the set of excluded atoms + CLJNBPairs nbpairs = this->excludedAtoms(); + + // now add in all of the 1-4 nonbonded scale factors + for (auto it = amber_nb14s.constBegin(); it != amber_nb14s.constEnd(); ++it) { + nbpairs.set(it.key().atom0(), it.key().atom1(), it.value().toScaleFactor()); + } - return nbpairs; + return nbpairs; } -void AmberParams::add(const BondID &bond, double k, double r0, bool includes_h) -{ - BondID b = convert(bond); - amber_bonds.insert(this->convert(bond), qMakePair(AmberBond(k, r0), includes_h)); +void AmberParams::add(const BondID &bond, double k, double r0, + bool includes_h) { + BondID b = convert(bond); + amber_bonds.insert(this->convert(bond), + qMakePair(AmberBond(k, r0), includes_h)); } -void AmberParams::remove(const BondID &bond) -{ - amber_bonds.remove(this->convert(bond)); +void AmberParams::remove(const BondID &bond) { + amber_bonds.remove(this->convert(bond)); } -AmberBond AmberParams::getParameter(const BondID &bond) const -{ - return amber_bonds.value(this->convert(bond)).first; +AmberBond AmberParams::getParameter(const BondID &bond) const { + return amber_bonds.value(this->convert(bond)).first; } /** Return all of the bond parameters converted to a set of TwoAtomFunctions */ -TwoAtomFunctions AmberParams::bondFunctions(const Symbol &R) const -{ - TwoAtomFunctions funcs(molinfo); +TwoAtomFunctions AmberParams::bondFunctions(const Symbol &R) const { + TwoAtomFunctions funcs(molinfo); - for (auto it = amber_bonds.constBegin(); it != amber_bonds.constEnd(); ++it) - { - funcs.set(it.key(), it.value().first.toExpression(R)); - } + for (auto it = amber_bonds.constBegin(); it != amber_bonds.constEnd(); ++it) { + funcs.set(it.key(), it.value().first.toExpression(R)); + } - return funcs; + return funcs; } /** Return all of the bond parameters converted to a set of TwoAtomFunctions */ -TwoAtomFunctions AmberParams::bondFunctions() const -{ - return bondFunctions(Symbol("r")); +TwoAtomFunctions AmberParams::bondFunctions() const { + return bondFunctions(Symbol("r")); } -void AmberParams::add(const AngleID &angle, double k, double theta0, bool includes_h) -{ - amber_angles.insert(this->convert(angle), qMakePair(AmberAngle(k, theta0), includes_h)); +void AmberParams::add(const AngleID &angle, double k, double theta0, + bool includes_h) { + amber_angles.insert(this->convert(angle), + qMakePair(AmberAngle(k, theta0), includes_h)); } -void AmberParams::remove(const AngleID &angle) -{ - amber_angles.remove(this->convert(angle)); +void AmberParams::remove(const AngleID &angle) { + amber_angles.remove(this->convert(angle)); } -AmberAngle AmberParams::getParameter(const AngleID &angle) const -{ - return amber_angles.value(this->convert(angle)).first; +AmberAngle AmberParams::getParameter(const AngleID &angle) const { + return amber_angles.value(this->convert(angle)).first; } -/** Return all of the angle parameters converted to a set of ThreeAtomFunctions */ -ThreeAtomFunctions AmberParams::angleFunctions(const Symbol &THETA) const -{ - ThreeAtomFunctions funcs(molinfo); +/** Return all of the angle parameters converted to a set of ThreeAtomFunctions + */ +ThreeAtomFunctions AmberParams::angleFunctions(const Symbol &THETA) const { + ThreeAtomFunctions funcs(molinfo); - for (auto it = amber_angles.constBegin(); it != amber_angles.constEnd(); ++it) - { - funcs.set(it.key(), it.value().first.toExpression(THETA)); - } + for (auto it = amber_angles.constBegin(); it != amber_angles.constEnd(); + ++it) { + funcs.set(it.key(), it.value().first.toExpression(THETA)); + } - return funcs; + return funcs; } -/** Return all of the angle parameters converted to a set of ThreeAtomFunctions */ -ThreeAtomFunctions AmberParams::angleFunctions() const -{ - return angleFunctions(Symbol("theta")); +/** Return all of the angle parameters converted to a set of ThreeAtomFunctions + */ +ThreeAtomFunctions AmberParams::angleFunctions() const { + return angleFunctions(Symbol("theta")); } -void AmberParams::add(const DihedralID &dihedral, double k, double periodicity, double phase, bool includes_h) -{ - // convert the dihedral into AtomIdx indicies - DihedralID d = this->convert(dihedral); +void AmberParams::add(const DihedralID &dihedral, double k, double periodicity, + double phase, bool includes_h) { + // convert the dihedral into AtomIdx indicies + DihedralID d = this->convert(dihedral); - // If dihedral already exists, we will append parameters - if (amber_dihedrals.contains(d)) - { - amber_dihedrals[d].first += AmberDihPart(k, periodicity, phase); - } - else - { - amber_dihedrals.insert(d, qMakePair(AmberDihedral(AmberDihPart(k, periodicity, phase)), includes_h)); - } + // If dihedral already exists, we will append parameters + if (amber_dihedrals.contains(d)) { + amber_dihedrals[d].first += AmberDihPart(k, periodicity, phase); + } else { + amber_dihedrals.insert( + d, qMakePair(AmberDihedral(AmberDihPart(k, periodicity, phase)), + includes_h)); + } } -void AmberParams::remove(const DihedralID &dihedral) -{ - amber_dihedrals.remove(this->convert(dihedral)); +void AmberParams::remove(const DihedralID &dihedral) { + amber_dihedrals.remove(this->convert(dihedral)); } -AmberDihedral AmberParams::getParameter(const DihedralID &dihedral) const -{ - return amber_dihedrals.value(this->convert(dihedral)).first; +AmberDihedral AmberParams::getParameter(const DihedralID &dihedral) const { + return amber_dihedrals.value(this->convert(dihedral)).first; } -/** Return all of the dihedral parameters converted to a set of FourAtomFunctions */ -FourAtomFunctions AmberParams::dihedralFunctions(const Symbol &PHI) const -{ - FourAtomFunctions funcs(molinfo); +/** Return all of the dihedral parameters converted to a set of + * FourAtomFunctions */ +FourAtomFunctions AmberParams::dihedralFunctions(const Symbol &PHI) const { + FourAtomFunctions funcs(molinfo); - for (auto it = amber_dihedrals.constBegin(); it != amber_dihedrals.constEnd(); ++it) - { - funcs.set(it.key(), it.value().first.toExpression(PHI)); - } + for (auto it = amber_dihedrals.constBegin(); it != amber_dihedrals.constEnd(); + ++it) { + funcs.set(it.key(), it.value().first.toExpression(PHI)); + } - return funcs; + return funcs; } -/** Return all of the dihedral parameters converted to a set of FourAtomFunctions */ -FourAtomFunctions AmberParams::dihedralFunctions() const -{ - return dihedralFunctions(Symbol("phi")); +/** Return all of the dihedral parameters converted to a set of + * FourAtomFunctions */ +FourAtomFunctions AmberParams::dihedralFunctions() const { + return dihedralFunctions(Symbol("phi")); } -void AmberParams::add(const ImproperID &improper, double k, double periodicity, double phase, bool includes_h) -{ - ImproperID imp = this->convert(improper); +void AmberParams::add(const ImproperID &improper, double k, double periodicity, + double phase, bool includes_h) { + ImproperID imp = this->convert(improper); - if (amber_impropers.contains(imp)) - { - amber_impropers[imp].first += AmberDihPart(k, periodicity, phase); - } - else - { - amber_impropers.insert(imp, qMakePair(AmberDihedral(AmberDihPart(k, periodicity, phase)), includes_h)); - } + if (amber_impropers.contains(imp)) { + amber_impropers[imp].first += AmberDihPart(k, periodicity, phase); + } else { + amber_impropers.insert( + imp, qMakePair(AmberDihedral(AmberDihPart(k, periodicity, phase)), + includes_h)); + } } -void AmberParams::remove(const ImproperID &improper) -{ - amber_impropers.remove(this->convert(improper)); +void AmberParams::remove(const ImproperID &improper) { + amber_impropers.remove(this->convert(improper)); } -AmberDihedral AmberParams::getParameter(const ImproperID &improper) const -{ - return amber_impropers.value(this->convert(improper)).first; +AmberDihedral AmberParams::getParameter(const ImproperID &improper) const { + return amber_impropers.value(this->convert(improper)).first; } -/** Return all of the improper parameters converted to a set of FourAtomFunctions */ -FourAtomFunctions AmberParams::improperFunctions(const Symbol &PHI) const -{ - FourAtomFunctions funcs(molinfo); +/** Return all of the improper parameters converted to a set of + * FourAtomFunctions */ +FourAtomFunctions AmberParams::improperFunctions(const Symbol &PHI) const { + FourAtomFunctions funcs(molinfo); - for (auto it = amber_impropers.constBegin(); it != amber_impropers.constEnd(); ++it) - { - funcs.set(it.key(), it.value().first.toExpression(PHI)); - } + for (auto it = amber_impropers.constBegin(); it != amber_impropers.constEnd(); + ++it) { + funcs.set(it.key(), it.value().first.toExpression(PHI)); + } - return funcs; + return funcs; } -/** Return all of the improper parameters converted to a set of FourAtomFunctions */ -FourAtomFunctions AmberParams::improperFunctions() const -{ - return improperFunctions(Symbol("phi")); +/** Return all of the improper parameters converted to a set of + * FourAtomFunctions */ +FourAtomFunctions AmberParams::improperFunctions() const { + return improperFunctions(Symbol("phi")); } /** Return all of the CMAP functions for the molecule. This will be empty * if there are no CMAP functions for this molecule */ -CMAPFunctions AmberParams::cmapFunctions() const -{ - return this->cmap_funcs; -} +CMAPFunctions AmberParams::cmapFunctions() const { return this->cmap_funcs; } /** Add the passed CMAP parameter for this set of 5 atoms. This will replace * any existing CMAP parameter for this set of atoms */ -void AmberParams::add(const AtomID &atom0, const AtomID &atom1, const AtomID &atom2, - const AtomID &atom3, const AtomID &atom4, - const CMAPParameter &cmap) -{ - if (cmap_funcs.isEmpty()) - { - cmap_funcs = CMAPFunctions(molinfo); - } +void AmberParams::add(const AtomID &atom0, const AtomID &atom1, + const AtomID &atom2, const AtomID &atom3, + const AtomID &atom4, const CMAPParameter &cmap) { + if (cmap_funcs.isEmpty()) { + cmap_funcs = CMAPFunctions(molinfo); + } - cmap_funcs.set(atom0, atom1, atom2, atom3, atom4, cmap); + cmap_funcs.set(atom0, atom1, atom2, atom3, atom4, cmap); } /** Remove the CMAP function from the passed set of 5 atoms */ -void AmberParams::removeCMAP(const AtomID &atom0, const AtomID &atom1, const AtomID &atom2, - const AtomID &atom3, const AtomID &atom4) -{ - if (cmap_funcs.isEmpty()) - { - return; - } +void AmberParams::removeCMAP(const AtomID &atom0, const AtomID &atom1, + const AtomID &atom2, const AtomID &atom3, + const AtomID &atom4) { + if (cmap_funcs.isEmpty()) { + return; + } - cmap_funcs.clear(atom0, atom1, atom2, atom3, atom4); + cmap_funcs.clear(atom0, atom1, atom2, atom3, atom4); - if (cmap_funcs.isEmpty()) - { - cmap_funcs = CMAPFunctions(); - } + if (cmap_funcs.isEmpty()) { + cmap_funcs = CMAPFunctions(); + } } /** Return the CMAP parameter for the passed 5 atoms. This returns a null * parameter if there is no matching CMAP parameter for this set of atoms */ -CMAPParameter AmberParams::getCMAP(const AtomID &atom0, const AtomID &atom1, const AtomID &atom2, - const AtomID &atom3, const AtomID &atom4) const -{ - if (cmap_funcs.isEmpty()) - { - return CMAPParameter(); - } +CMAPParameter AmberParams::getCMAP(const AtomID &atom0, const AtomID &atom1, + const AtomID &atom2, const AtomID &atom3, + const AtomID &atom4) const { + if (cmap_funcs.isEmpty()) { + return CMAPParameter(); + } - return cmap_funcs.parameter(atom0, atom1, atom2, atom3, atom4); + return cmap_funcs.parameter(atom0, atom1, atom2, atom3, atom4); } -void AmberParams::addNB14(const BondID &pair, double cscl, double ljscl) -{ - amber_nb14s.insert(this->convert(pair), AmberNB14(cscl, ljscl)); +void AmberParams::addNB14(const BondID &pair, double cscl, double ljscl) { + amber_nb14s.insert(this->convert(pair), AmberNB14(cscl, ljscl)); } -void AmberParams::removeNB14(const BondID &pair) -{ - amber_nb14s.remove(this->convert(pair)); +void AmberParams::removeNB14(const BondID &pair) { + amber_nb14s.remove(this->convert(pair)); } -AmberNB14 AmberParams::getNB14(const BondID &pair) const -{ - return amber_nb14s.value(this->convert(pair)); +AmberNB14 AmberParams::getNB14(const BondID &pair) const { + return amber_nb14s.value(this->convert(pair)); } /** Add the parameters from 'other' to this set */ -AmberParams &AmberParams::operator+=(const AmberParams &other) -{ - if (not this->isCompatibleWith(other.info()) or propmap != other.propmap) - { - throw SireError::incompatible_error( - QObject::tr("Cannot combine Amber parameters, as the two sets are incompatible!"), CODELOC); - } - - if (not other.amber_charges.isEmpty()) - { - // we overwrite these charges with 'other' - amber_charges = other.amber_charges; - } - - if (not other.exc_atoms.isEmpty()) - { - // we overwrite our excluded atoms with 'other' - exc_atoms = other.exc_atoms; - } - - if (not other.amber_ljs.isEmpty()) - { - // we overwrite these LJs with 'other' - amber_ljs = other.amber_ljs; - } - - if (not other.amber_masses.isEmpty()) - { - // we overwrite these masses with 'other' - amber_masses = other.amber_masses; - } - - if (not other.amber_elements.isEmpty()) - { - // we overwrite these elements with 'other' - amber_elements = other.amber_elements; - } - - if (not other.amber_types.isEmpty()) - { - // we overwrite these types with 'other' - amber_types = other.amber_types; - } - - if (not other.born_radii.isEmpty()) - { - // we overwrite these radii with 'other' - born_radii = other.born_radii; - } - - if (not other.amber_screens.isEmpty()) - { - // we overwrite these screening parameters with 'other' - amber_screens = other.amber_screens; - } - - if (not other.amber_treechains.isEmpty()) - { - // we overwrite these treechain classification with 'other' - amber_treechains = other.amber_treechains; - } - - if (amber_bonds.isEmpty()) - { - amber_bonds = other.amber_bonds; - } - else if (not other.amber_bonds.isEmpty()) - { - for (auto it = other.amber_bonds.constBegin(); it != other.amber_bonds.constEnd(); ++it) - { - amber_bonds.insert(it.key(), it.value()); - } - } - - if (amber_angles.isEmpty()) - { - amber_angles = other.amber_angles; - } - else if (not other.amber_angles.isEmpty()) - { - for (auto it = other.amber_angles.constBegin(); it != other.amber_angles.constEnd(); ++it) - { - amber_angles.insert(it.key(), it.value()); - } - } - - if (amber_dihedrals.isEmpty()) - { - amber_dihedrals = other.amber_dihedrals; - } - else if (not other.amber_dihedrals.isEmpty()) - { - for (auto it = other.amber_dihedrals.constBegin(); it != other.amber_dihedrals.constEnd(); ++it) - { - amber_dihedrals.insert(it.key(), it.value()); - } - } - - if (amber_impropers.isEmpty()) - { - amber_impropers = other.amber_impropers; - } - else if (not other.amber_impropers.isEmpty()) - { - for (auto it = other.amber_impropers.constBegin(); it != other.amber_impropers.constEnd(); ++it) - { - amber_impropers.insert(it.key(), it.value()); - } - } - - if (cmap_funcs.isEmpty()) - { - cmap_funcs = other.cmap_funcs; - } - else if (not other.cmap_funcs.isEmpty()) - { - for (auto param : other.cmap_funcs.parameters()) - { - cmap_funcs.set(param); - } - } - - if (amber_nb14s.isEmpty()) - { - amber_nb14s = other.amber_nb14s; - } - else if (not other.amber_nb14s.isEmpty()) - { - for (auto it = other.amber_nb14s.constBegin(); it != other.amber_nb14s.constEnd(); ++it) - { - amber_nb14s.insert(it.key(), it.value()); - } - } - - if (not other.radius_set.isEmpty()) - { - // overwrite the radius set with other - radius_set = other.radius_set; - } - - return *this; +AmberParams &AmberParams::operator+=(const AmberParams &other) { + if (not this->isCompatibleWith(other.info()) or propmap != other.propmap) { + throw SireError::incompatible_error( + QObject::tr("Cannot combine Amber parameters, as the two sets are " + "incompatible!"), + CODELOC); + } + + if (not other.amber_charges.isEmpty()) { + // we overwrite these charges with 'other' + amber_charges = other.amber_charges; + } + + if (not other.exc_atoms.isEmpty()) { + // we overwrite our excluded atoms with 'other' + exc_atoms = other.exc_atoms; + } + + if (not other.amber_ljs.isEmpty()) { + // we overwrite these LJs with 'other' + amber_ljs = other.amber_ljs; + } + + if (not other.amber_masses.isEmpty()) { + // we overwrite these masses with 'other' + amber_masses = other.amber_masses; + } + + if (not other.amber_elements.isEmpty()) { + // we overwrite these elements with 'other' + amber_elements = other.amber_elements; + } + + if (not other.amber_types.isEmpty()) { + // we overwrite these types with 'other' + amber_types = other.amber_types; + } + + if (not other.born_radii.isEmpty()) { + // we overwrite these radii with 'other' + born_radii = other.born_radii; + } + + if (not other.amber_screens.isEmpty()) { + // we overwrite these screening parameters with 'other' + amber_screens = other.amber_screens; + } + + if (not other.amber_treechains.isEmpty()) { + // we overwrite these treechain classification with 'other' + amber_treechains = other.amber_treechains; + } + + if (amber_bonds.isEmpty()) { + amber_bonds = other.amber_bonds; + } else if (not other.amber_bonds.isEmpty()) { + for (auto it = other.amber_bonds.constBegin(); + it != other.amber_bonds.constEnd(); ++it) { + amber_bonds.insert(it.key(), it.value()); + } + } + + if (amber_angles.isEmpty()) { + amber_angles = other.amber_angles; + } else if (not other.amber_angles.isEmpty()) { + for (auto it = other.amber_angles.constBegin(); + it != other.amber_angles.constEnd(); ++it) { + amber_angles.insert(it.key(), it.value()); + } + } + + if (amber_dihedrals.isEmpty()) { + amber_dihedrals = other.amber_dihedrals; + } else if (not other.amber_dihedrals.isEmpty()) { + for (auto it = other.amber_dihedrals.constBegin(); + it != other.amber_dihedrals.constEnd(); ++it) { + amber_dihedrals.insert(it.key(), it.value()); + } + } + + if (amber_impropers.isEmpty()) { + amber_impropers = other.amber_impropers; + } else if (not other.amber_impropers.isEmpty()) { + for (auto it = other.amber_impropers.constBegin(); + it != other.amber_impropers.constEnd(); ++it) { + amber_impropers.insert(it.key(), it.value()); + } + } + + if (cmap_funcs.isEmpty()) { + cmap_funcs = other.cmap_funcs; + } else if (not other.cmap_funcs.isEmpty()) { + for (auto param : other.cmap_funcs.parameters()) { + cmap_funcs.set(param); + } + } + + if (amber_nb14s.isEmpty()) { + amber_nb14s = other.amber_nb14s; + } else if (not other.amber_nb14s.isEmpty()) { + for (auto it = other.amber_nb14s.constBegin(); + it != other.amber_nb14s.constEnd(); ++it) { + amber_nb14s.insert(it.key(), it.value()); + } + } + + if (not other.radius_set.isEmpty()) { + // overwrite the radius set with other + radius_set = other.radius_set; + } + + return *this; } /** Return a combination of the two passed AmberParams */ -AmberParams AmberParams::operator+(const AmberParams &other) const -{ - AmberParams ret(*this); +AmberParams AmberParams::operator+(const AmberParams &other) const { + AmberParams ret(*this); - ret += other; + ret += other; - return ret; + return ret; } /** Update these parameters from the contents of the passed molecule. This will only work if these parameters are compatible with this molecule */ -void AmberParams::updateFrom(const MoleculeView &molview) -{ - this->assertCompatibleWith(molview); - this->_pvt_updateFrom(molview.data()); +void AmberParams::updateFrom(const MoleculeView &molview) { + this->assertCompatibleWith(molview); + this->_pvt_updateFrom(molview.data()); } -/** Internal function used to grab the property, catching errors and signalling if - the correct property has been found */ +/** Internal function used to grab the property, catching errors and signalling + if the correct property has been found */ template -T getProperty(const PropertyName &prop, const MoleculeData &moldata, bool *found) -{ - if (moldata.hasProperty(prop)) - { - const Property &p = moldata.property(prop); - - if (p.isA()) - { - *found = true; - return p.asA(); - } +T getProperty(const PropertyName &prop, const MoleculeData &moldata, + bool *found) { + if (moldata.hasProperty(prop)) { + const Property &p = moldata.property(prop); + + if (p.isA()) { + *found = true; + return p.asA(); } + } - *found = false; - return T(); + *found = false; + return T(); } -/** Internal function used to guess the masses of atoms based on their element */ -void guessMasses(AtomMasses &masses, const AtomElements &elements, bool *has_masses) -{ - for (int i = 0; i < elements.nCutGroups(); ++i) - { - const CGIdx cg(i); +/** Internal function used to guess the masses of atoms based on their element + */ +void guessMasses(AtomMasses &masses, const AtomElements &elements, + bool *has_masses) { + for (int i = 0; i < elements.nCutGroups(); ++i) { + const CGIdx cg(i); - for (int j = 0; j < elements.nAtoms(cg); ++j) - { - const CGAtomIdx idx(cg, Index(j)); + for (int j = 0; j < elements.nAtoms(cg); ++j) { + const CGAtomIdx idx(cg, Index(j)); - masses.set(idx, elements[idx].mass()); - } + masses.set(idx, elements[idx].mass()); } + } - *has_masses = true; + *has_masses = true; } /** Internal function used to guess the element of atoms based on their name */ -AtomElements guessElements(const MoleculeInfoData &molinfo, bool *has_elements) -{ - AtomElements elements(molinfo); +AtomElements guessElements(const MoleculeInfoData &molinfo, + bool *has_elements) { + AtomElements elements(molinfo); - for (int i = 0; i < elements.nCutGroups(); ++i) - { - const CGIdx cg(i); + for (int i = 0; i < elements.nCutGroups(); ++i) { + const CGIdx cg(i); - for (int j = 0; j < elements.nAtoms(cg); ++j) - { - const CGAtomIdx idx(cg, Index(j)); + for (int j = 0; j < elements.nAtoms(cg); ++j) { + const CGAtomIdx idx(cg, Index(j)); - elements.set(idx, Element::biologicalElement(molinfo.name(idx).value())); - } + elements.set(idx, Element::biologicalElement(molinfo.name(idx).value())); } + } - *has_elements = true; - return elements; + *has_elements = true; + return elements; } /** Construct the hash of bonds */ -void AmberParams::getAmberBondsFrom(const TwoAtomFunctions &funcs, const MoleculeData &moldata, - const QVector &is_hydrogen, const PropertyMap &map) -{ - // get the set of all bond functions - const auto potentials = funcs.potentials(); - - // create temporary space to hold the converted bonds - QVector> bonds(potentials.count()); - auto bonds_data = bonds.data(); - - const auto *potentials_data = potentials.constData(); - - const auto &molinfo = moldata.info(); - - const auto *is_hydrogen_data = is_hydrogen.constData(); - - if (molinfo.nAtoms() != is_hydrogen.count()) - throw SireError::program_bug(QObject::tr("is_hydrogen is wrong!"), CODELOC); - - // convert each of these into an AmberBond - tbb::parallel_for(tbb::blocked_range(0, potentials.count()), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - const auto &potential = potentials_data[i]; - - // convert the atom IDs into a canonical form - BondID bond = this->convert(BondID(potential.atom0(), potential.atom1())); +void AmberParams::getAmberBondsFrom(const TwoAtomFunctions &funcs, + const MoleculeData &moldata, + const QVector &is_hydrogen, + const PropertyMap &map) { + // get the set of all bond functions + const auto potentials = funcs.potentials(); + + // create temporary space to hold the converted bonds + QVector> bonds(potentials.count()); + auto bonds_data = bonds.data(); + + const auto *potentials_data = potentials.constData(); + + const auto &molinfo = moldata.info(); + + const auto *is_hydrogen_data = is_hydrogen.constData(); + + if (molinfo.nAtoms() != is_hydrogen.count()) + throw SireError::program_bug(QObject::tr("is_hydrogen is wrong!"), CODELOC); + + // convert each of these into an AmberBond + tbb::parallel_for( + tbb::blocked_range(0, potentials.count()), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + const auto &potential = potentials_data[i]; + + // convert the atom IDs into a canonical form + BondID bond = + this->convert(BondID(potential.atom0(), potential.atom1())); + + // does this bond involve hydrogen? - this relies on "AtomElements" + // being full + bool contains_hydrogen = + is_hydrogen_data[molinfo.atomIdx(potential.atom0()).value()] or + is_hydrogen_data[molinfo.atomIdx(potential.atom1()).value()]; + + bonds_data[i] = std::make_tuple( + bond, AmberBond(potential.function(), Symbol("r")), + contains_hydrogen); + } + }); - // does this bond involve hydrogen? - this relies on "AtomElements" being full - bool contains_hydrogen = is_hydrogen_data[molinfo.atomIdx(potential.atom0()).value()] or - is_hydrogen_data[molinfo.atomIdx(potential.atom1()).value()]; + // finally add all of these into the amber_bonds hash + amber_bonds.clear(); + amber_bonds.reserve(bonds.count()); - bonds_data[i] = std::make_tuple(bond, AmberBond(potential.function(), Symbol("r")), contains_hydrogen); - } }); + // default to keeping null bonds as this retains + // existing behaviour + bool keep_null_bonds = true; - // finally add all of these into the amber_bonds hash - amber_bonds.clear(); - amber_bonds.reserve(bonds.count()); + const auto keep_null_bonds_prop = map["keep_null_bonds"]; - // default to keeping null bonds as this retains - // existing behaviour - bool keep_null_bonds = true; + if (keep_null_bonds_prop.hasValue()) { + keep_null_bonds = keep_null_bonds_prop.value().asABoolean(); + } - const auto keep_null_bonds_prop = map["keep_null_bonds"]; + for (int i = 0; i < bonds.count(); ++i) { + const auto &amberbond = std::get<1>(bonds_data[i]); - if (keep_null_bonds_prop.hasValue()) - { - keep_null_bonds = keep_null_bonds_prop.value().asABoolean(); - } - - for (int i = 0; i < bonds.count(); ++i) - { - const auto &amberbond = std::get<1>(bonds_data[i]); - - if (amberbond.k() != 0) - { - amber_bonds.insert(std::get<0>(bonds_data[i]), qMakePair(amberbond, std::get<2>(bonds_data[i]))); - } - else if (keep_null_bonds) - { - // include null bonds - create an AmberBond with r0 equal - // to the current bond length - const auto &bondid = std::get<0>(bonds_data[i]); - double r0 = Bond(moldata, bondid).length(map).to(angstrom); - amber_bonds.insert(bondid, qMakePair(AmberBond(0, r0), std::get<2>(bonds_data[i]))); - } + if (amberbond.k() != 0) { + amber_bonds.insert(std::get<0>(bonds_data[i]), + qMakePair(amberbond, std::get<2>(bonds_data[i]))); + } else if (keep_null_bonds) { + // include null bonds - create an AmberBond with r0 equal + // to the current bond length + const auto &bondid = std::get<0>(bonds_data[i]); + double r0 = Bond(moldata, bondid).length(map).to(angstrom); + amber_bonds.insert( + bondid, qMakePair(AmberBond(0, r0), std::get<2>(bonds_data[i]))); } + } } /** Construct the hash of angles */ -void AmberParams::getAmberAnglesFrom(const ThreeAtomFunctions &funcs, const MoleculeData &moldata, - const QVector &is_hydrogen, const PropertyMap &map) -{ - // get the set of all angle functions - const auto potentials = funcs.potentials(); - - // create temporary space to hold the converted angles - QVector> angles(potentials.count()); - auto angles_data = angles.data(); - - const auto &molinfo = moldata.info(); - const auto *is_hydrogen_data = is_hydrogen.constData(); - - if (molinfo.nAtoms() != is_hydrogen.count()) - throw SireError::program_bug(QObject::tr("is_hydrogen is wrong!"), CODELOC); - - // convert each of these into an AmberAngle - tbb::parallel_for(tbb::blocked_range(0, potentials.count()), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - const auto potential = potentials.constData()[i]; - - // convert the atom IDs into a canonical form - AngleID angle = this->convert(AngleID(potential.atom0(), potential.atom1(), potential.atom2())); - - // does this angle involve hydrogen? - this relies on "AtomElements" being full - bool contains_hydrogen = is_hydrogen_data[molinfo.atomIdx(potential.atom0()).value()] or - is_hydrogen_data[molinfo.atomIdx(potential.atom1()).value()] or - is_hydrogen_data[molinfo.atomIdx(potential.atom2()).value()]; - - angles_data[i] = - std::make_tuple(angle, AmberAngle(potential.function(), Symbol("theta")), contains_hydrogen); - } }); - - // finally add all of these into the amber_angles hash - amber_angles.clear(); - amber_angles.reserve(angles.count()); - - // default to keeping null angles as this retains - // existing behaviour - bool keep_null_angles = true; - - const auto keep_null_angles_prop = map["keep_null_angles"]; - - if (keep_null_angles_prop.hasValue()) - { - keep_null_angles = keep_null_angles_prop.value().asABoolean(); - } - - for (int i = 0; i < angles.count(); ++i) - { - const auto &amberangle = std::get<1>(angles_data[i]); - - if (amberangle.k() != 0) - { - amber_angles.insert(std::get<0>(angles_data[i]), qMakePair(amberangle, std::get<2>(angles_data[i]))); - } - else if (keep_null_angles) - { - // include null angles - create an AmberAngle with theta0 equal - // to the current angle size - const auto &angid = std::get<0>(angles_data[i]); - double theta0 = Angle(moldata, angid).size(map).to(radians); - amber_angles.insert(angid, qMakePair(AmberAngle(0, theta0), std::get<2>(angles_data[i]))); +void AmberParams::getAmberAnglesFrom(const ThreeAtomFunctions &funcs, + const MoleculeData &moldata, + const QVector &is_hydrogen, + const PropertyMap &map) { + // get the set of all angle functions + const auto potentials = funcs.potentials(); + + // create temporary space to hold the converted angles + QVector> angles(potentials.count()); + auto angles_data = angles.data(); + + const auto &molinfo = moldata.info(); + const auto *is_hydrogen_data = is_hydrogen.constData(); + + if (molinfo.nAtoms() != is_hydrogen.count()) + throw SireError::program_bug(QObject::tr("is_hydrogen is wrong!"), CODELOC); + + // convert each of these into an AmberAngle + tbb::parallel_for( + tbb::blocked_range(0, potentials.count()), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + const auto potential = potentials.constData()[i]; + + // convert the atom IDs into a canonical form + AngleID angle = this->convert( + AngleID(potential.atom0(), potential.atom1(), potential.atom2())); + + // does this angle involve hydrogen? - this relies on "AtomElements" + // being full + bool contains_hydrogen = + is_hydrogen_data[molinfo.atomIdx(potential.atom0()).value()] or + is_hydrogen_data[molinfo.atomIdx(potential.atom1()).value()] or + is_hydrogen_data[molinfo.atomIdx(potential.atom2()).value()]; + + angles_data[i] = std::make_tuple( + angle, AmberAngle(potential.function(), Symbol("theta")), + contains_hydrogen); } - } -} + }); -/** Construct the hash of dihedrals */ -void AmberParams::getAmberDihedralsFrom(const FourAtomFunctions &funcs, const MoleculeData &moldata, - const QVector &is_hydrogen, const PropertyMap &map) -{ - // get the set of all dihedral functions - const auto potentials = funcs.potentials(); - - // create temporary space to hold the converted dihedrals - QVector> dihedrals(potentials.count()); - auto dihedrals_data = dihedrals.data(); - - const auto &molinfo = moldata.info(); - const auto *is_hydrogen_data = is_hydrogen.constData(); - - if (molinfo.nAtoms() != is_hydrogen.count()) - throw SireError::program_bug(QObject::tr("is_hydrogen is wrong!"), CODELOC); - - // convert each of these into an AmberDihedral - tbb::parallel_for(tbb::blocked_range(0, potentials.count()), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - const auto potential = potentials.constData()[i]; - - // convert the atom IDs into a canonical form - DihedralID dihedral = - this->convert(DihedralID(potential.atom0(), potential.atom1(), potential.atom2(), potential.atom3())); - - // does this bond involve hydrogen? - this relies on "AtomElements" being full - bool contains_hydrogen = is_hydrogen_data[molinfo.atomIdx(potential.atom0()).value()] or - is_hydrogen_data[molinfo.atomIdx(potential.atom1()).value()] or - is_hydrogen_data[molinfo.atomIdx(potential.atom2()).value()] or - is_hydrogen_data[molinfo.atomIdx(potential.atom3()).value()]; - - dihedrals_data[i] = - std::make_tuple(dihedral, AmberDihedral(potential.function(), Symbol("phi")), contains_hydrogen); - } }); - - // finally add all of these into the amber_dihedrals hash - amber_dihedrals.clear(); - amber_dihedrals.reserve(dihedrals.count()); - - for (int i = 0; i < dihedrals.count(); ++i) - { - amber_dihedrals.insert(std::get<0>(dihedrals_data[i]), - qMakePair(std::get<1>(dihedrals_data[i]), std::get<2>(dihedrals_data[i]))); - } -} + // finally add all of these into the amber_angles hash + amber_angles.clear(); + amber_angles.reserve(angles.count()); -/** Construct the hash of impropers */ -void AmberParams::getAmberImpropersFrom(const FourAtomFunctions &funcs, const MoleculeData &moldata, - const QVector &is_hydrogen, const PropertyMap &map) -{ - // get the set of all improper functions - const auto potentials = funcs.potentials(); - - // create temporary space to hold the converted dihedrals - QVector> impropers(potentials.count()); - auto impropers_data = impropers.data(); - - const auto &molinfo = moldata.info(); - const auto *is_hydrogen_data = is_hydrogen.constData(); - - if (molinfo.nAtoms() != is_hydrogen.count()) - throw SireError::program_bug(QObject::tr("is_hydrogen is wrong!"), CODELOC); - - // convert each of these into an AmberDihedral - tbb::parallel_for(tbb::blocked_range(0, potentials.count()), [&](const tbb::blocked_range &r) - { - for (int i = r.begin(); i < r.end(); ++i) - { - const auto potential = potentials.constData()[i]; - - // convert the atom IDs into a canonical form - ImproperID improper = - this->convert(ImproperID(potential.atom0(), potential.atom1(), potential.atom2(), potential.atom3())); - - // does this bond involve hydrogen? - this relies on "AtomElements" being full - bool contains_hydrogen = is_hydrogen_data[molinfo.atomIdx(potential.atom0()).value()] or - is_hydrogen_data[molinfo.atomIdx(potential.atom1()).value()] or - is_hydrogen_data[molinfo.atomIdx(potential.atom2()).value()] or - is_hydrogen_data[molinfo.atomIdx(potential.atom3()).value()]; - - impropers_data[i] = - std::make_tuple(improper, AmberDihedral(potential.function(), Symbol("phi")), contains_hydrogen); - } }); - - // finally add all of these into the amber_dihedrals hash - amber_impropers.clear(); - amber_impropers.reserve(impropers.count()); - - for (int i = 0; i < impropers.count(); ++i) - { - amber_impropers.insert(std::get<0>(impropers_data[i]), - qMakePair(std::get<1>(impropers_data[i]), std::get<2>(impropers_data[i]))); - } -} + // default to keeping null angles as this retains + // existing behaviour + bool keep_null_angles = true; -/** Construct the excluded atom set and 14 NB parameters */ -void AmberParams::getAmberNBsFrom(const CLJNBPairs &nbpairs, const FourAtomFunctions &dihedrals) -{ - // first, copy in the CLJNBPairs from the molecule - exc_atoms = nbpairs; - - // now go through all dihedrals and get the 1-4 scale factors and - // remove them from exc_atoms - const auto potentials = dihedrals.potentials(); - - // create new space to hold the 14 scale factors - QHash new_nb14s; - new_nb14s.reserve(potentials.count()); - - for (int i = 0; i < potentials.count(); ++i) - { - const auto potential = potentials.constData()[i]; - - // convert the atom IDs into a canonical form - BondID nb14pair = this->convert(BondID(potential.atom0(), potential.atom3())); - - const auto molinfo = info(); - - if (not new_nb14s.contains(nb14pair)) - { - // extract the nb14 term from exc_atoms - auto nbscl = nbpairs.get(nb14pair.atom0(), nb14pair.atom1()); - - if (nbscl.coulomb() != 0.0 or nbscl.lj() != 0.0) - { - // add them to the list of 14 scale factors. - // This handles both standard AMBER (e.g. 0.833, 0.5) and - // GLYCAM-style (1.0, 1.0) where SCEE=1.0 and SCNB=1.0. - new_nb14s.insert(nb14pair, AmberNB14(nbscl.coulomb(), nbscl.lj())); - - // and remove them from the excluded atoms list - exc_atoms.set(nb14pair.atom0(), nb14pair.atom1(), CLJScaleFactor(0)); - } - } - } + const auto keep_null_angles_prop = map["keep_null_angles"]; - amber_nb14s = new_nb14s; -} + if (keep_null_angles_prop.hasValue()) { + keep_null_angles = keep_null_angles_prop.value().asABoolean(); + } -/** Create this set of parameters from the passed object */ -void AmberParams::_pvt_createFrom(const MoleculeData &moldata) -{ - // pull out all of the molecular properties - const PropertyMap &map = propmap; - - // first, all of the atom-based properties - bool has_charges, has_ljs, has_masses, has_elements, has_ambertypes; - - amber_charges = getProperty(map["charge"], moldata, &has_charges); - amber_ljs = getProperty(map["LJ"], moldata, &has_ljs); - amber_masses = getProperty(map["mass"], moldata, &has_masses); - amber_elements = getProperty(map["element"], moldata, &has_elements); - amber_types = getProperty(map["ambertype"], moldata, &has_ambertypes); - - if (not has_ambertypes) - { - // first look for the bondtypes property, as OPLS uses this - amber_types = getProperty(map["bondtype"], moldata, &has_ambertypes); - - if (not has_ambertypes) - { - // look for the atomtypes property - amber_types = getProperty(map["atomtype"], moldata, &has_ambertypes); - } - } + for (int i = 0; i < angles.count(); ++i) { + const auto &amberangle = std::get<1>(angles_data[i]); - if (not has_elements) - { - // try to guess the elements from the names and/or masses - amber_elements = guessElements(moldata.info(), &has_elements); + if (amberangle.k() != 0) { + amber_angles.insert(std::get<0>(angles_data[i]), + qMakePair(amberangle, std::get<2>(angles_data[i]))); + } else if (keep_null_angles) { + // include null angles - create an AmberAngle with theta0 equal + // to the current angle size + const auto &angid = std::get<0>(angles_data[i]); + double theta0 = Angle(moldata, angid).size(map).to(radians); + amber_angles.insert( + angid, qMakePair(AmberAngle(0, theta0), std::get<2>(angles_data[i]))); } + } +} - if (not has_masses) - { - // try to guess the masses from the elements - if (has_elements) - { - amber_masses = AtomMasses(moldata.info()); - guessMasses(amber_masses, amber_elements, &has_masses); +/** Construct the hash of dihedrals */ +void AmberParams::getAmberDihedralsFrom(const FourAtomFunctions &funcs, + const MoleculeData &moldata, + const QVector &is_hydrogen, + const PropertyMap &map) { + // get the set of all dihedral functions + const auto potentials = funcs.potentials(); + + // create temporary space to hold the converted dihedrals + QVector> dihedrals( + potentials.count()); + auto dihedrals_data = dihedrals.data(); + + const auto &molinfo = moldata.info(); + const auto *is_hydrogen_data = is_hydrogen.constData(); + + if (molinfo.nAtoms() != is_hydrogen.count()) + throw SireError::program_bug(QObject::tr("is_hydrogen is wrong!"), CODELOC); + + // convert each of these into an AmberDihedral + tbb::parallel_for( + tbb::blocked_range(0, potentials.count()), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + const auto potential = potentials.constData()[i]; + + // convert the atom IDs into a canonical form + DihedralID dihedral = + this->convert(DihedralID(potential.atom0(), potential.atom1(), + potential.atom2(), potential.atom3())); + + // does this bond involve hydrogen? - this relies on "AtomElements" + // being full + bool contains_hydrogen = + is_hydrogen_data[molinfo.atomIdx(potential.atom0()).value()] or + is_hydrogen_data[molinfo.atomIdx(potential.atom1()).value()] or + is_hydrogen_data[molinfo.atomIdx(potential.atom2()).value()] or + is_hydrogen_data[molinfo.atomIdx(potential.atom3()).value()]; + + dihedrals_data[i] = std::make_tuple( + dihedral, AmberDihedral(potential.function(), Symbol("phi")), + contains_hydrogen); } - } + }); - if (not(has_charges and has_ljs and has_masses and has_elements and has_ambertypes)) - { - // it is not possible to create the parameter object if we don't have - // these atom-based parameters - throw SireBase::missing_property( - QObject::tr("Cannot create an AmberParams object for molecule %1 as it is missing " - "necessary atom based properties: has_charges = %2, has_ljs = %3, " - "has_masses = %4, has_elements = %5, has_ambertypes = %6.") - .arg(Molecule(moldata).toString()) - .arg(has_charges) - .arg(has_ljs) - .arg(has_masses) - .arg(has_elements) - .arg(has_ambertypes), - CODELOC); - } - - // now see about the optional born radii and screening parameters - bool has_radii, has_screening, has_treechains; - - born_radii = getProperty(map["gb_radii"], moldata, &has_radii); - amber_screens = getProperty(map["gb_screening"], moldata, &has_screening); - amber_treechains = getProperty(map["treechain"], moldata, &has_treechains); + // finally add all of these into the amber_dihedrals hash + amber_dihedrals.clear(); + amber_dihedrals.reserve(dihedrals.count()); - if (has_radii) - { - // see if there is a label for the source of the GB parameters - bool has_source; - - radius_set = getProperty(map["gb_radius_set"], moldata, &has_source).value(); + for (int i = 0; i < dihedrals.count(); ++i) { + amber_dihedrals.insert(std::get<0>(dihedrals_data[i]), + qMakePair(std::get<1>(dihedrals_data[i]), + std::get<2>(dihedrals_data[i]))); + } +} - if (not has_source) - { - radius_set = "unknown"; +/** Construct the hash of impropers */ +void AmberParams::getAmberImpropersFrom(const FourAtomFunctions &funcs, + const MoleculeData &moldata, + const QVector &is_hydrogen, + const PropertyMap &map) { + // get the set of all improper functions + const auto potentials = funcs.potentials(); + + // create temporary space to hold the converted dihedrals + QVector> impropers( + potentials.count()); + auto impropers_data = impropers.data(); + + const auto &molinfo = moldata.info(); + const auto *is_hydrogen_data = is_hydrogen.constData(); + + if (molinfo.nAtoms() != is_hydrogen.count()) + throw SireError::program_bug(QObject::tr("is_hydrogen is wrong!"), CODELOC); + + // convert each of these into an AmberDihedral + tbb::parallel_for( + tbb::blocked_range(0, potentials.count()), + [&](const tbb::blocked_range &r) { + for (int i = r.begin(); i < r.end(); ++i) { + const auto potential = potentials.constData()[i]; + + // convert the atom IDs into a canonical form + ImproperID improper = + this->convert(ImproperID(potential.atom0(), potential.atom1(), + potential.atom2(), potential.atom3())); + + // does this bond involve hydrogen? - this relies on "AtomElements" + // being full + bool contains_hydrogen = + is_hydrogen_data[molinfo.atomIdx(potential.atom0()).value()] or + is_hydrogen_data[molinfo.atomIdx(potential.atom1()).value()] or + is_hydrogen_data[molinfo.atomIdx(potential.atom2()).value()] or + is_hydrogen_data[molinfo.atomIdx(potential.atom3()).value()]; + + impropers_data[i] = std::make_tuple( + improper, AmberDihedral(potential.function(), Symbol("phi")), + contains_hydrogen); } - } - else - { - radius_set = "unavailable"; - } - - // now lets get the bonded parameters (if they exist...) - bool has_bonds, has_ubs, has_angles, has_dihedrals, has_impropers, has_nbpairs, has_cmaps; - - const auto bonds = getProperty(map["bond"], moldata, &has_bonds); - const auto ub_bonds = getProperty(map["urey_bradley"], moldata, &has_ubs); - const auto angles = getProperty(map["angle"], moldata, &has_angles); - const auto dihedrals = getProperty(map["dihedral"], moldata, &has_dihedrals); - const auto impropers = getProperty(map["improper"], moldata, &has_impropers); - const auto nbpairs = getProperty(map["intrascale"], moldata, &has_nbpairs); - const auto cmaps = getProperty(map["cmap"], moldata, &has_cmaps); - - // get all of the atoms that contain hydrogen - QVector is_hydrogen; + }); - if (has_bonds or has_ubs or has_angles or has_dihedrals or has_impropers) - { - const int natoms = moldata.info().nAtoms(); + // finally add all of these into the amber_dihedrals hash + amber_impropers.clear(); + amber_impropers.reserve(impropers.count()); - is_hydrogen = QVector(natoms, false); - - if (not amber_elements.isEmpty()) - { - auto is_hydrogen_data = is_hydrogen.data(); - - auto elements = amber_elements.toVector(); - - if (elements.count() != natoms) - throw SireError::program_bug(QObject::tr("Wrong elements count!"), CODELOC); - - const auto *elements_data = elements.constData(); + for (int i = 0; i < impropers.count(); ++i) { + amber_impropers.insert(std::get<0>(impropers_data[i]), + qMakePair(std::get<1>(impropers_data[i]), + std::get<2>(impropers_data[i]))); + } +} - for (int i = 0; i < natoms; ++i) - { - is_hydrogen_data[i] = elements_data[i].nProtons() == 1; - } - } - } +/** Construct the excluded atom set and 14 NB parameters */ +void AmberParams::getAmberNBsFrom(const CLJNBPairs &nbpairs, + const FourAtomFunctions &dihedrals) { + // first, copy in the CLJNBPairs from the molecule + exc_atoms = nbpairs; - if (has_cmaps and not cmaps.isEmpty()) - { - cmap_funcs = cmaps; - } - else - { - cmap_funcs = CMAPFunctions(); - } + // now go through all dihedrals and get the 1-4 scale factors and + // remove them from exc_atoms + const auto potentials = dihedrals.potentials(); - QVector> nb_functions; + // create new space to hold the 14 scale factors + QHash new_nb14s; + new_nb14s.reserve(potentials.count()); - if (has_bonds) - { - nb_functions.append([&]() - { getAmberBondsFrom(bonds, moldata, is_hydrogen, map); }); - } + for (int i = 0; i < potentials.count(); ++i) { + const auto potential = potentials.constData()[i]; - if (has_ubs) - { - nb_functions.append([&]() - { getAmberBondsFrom(ub_bonds, moldata, is_hydrogen, map); }); - } + // convert the atom IDs into a canonical form + BondID nb14pair = + this->convert(BondID(potential.atom0(), potential.atom3())); - if (has_angles) - { - nb_functions.append([&]() - { getAmberAnglesFrom(angles, moldata, is_hydrogen, map); }); - } + const auto molinfo = info(); - if (has_dihedrals) - { - nb_functions.append([&]() - { getAmberDihedralsFrom(dihedrals, moldata, is_hydrogen, map); }); - } + if (not new_nb14s.contains(nb14pair)) { + // extract the nb14 term from exc_atoms + auto nbscl = nbpairs.get(nb14pair.atom0(), nb14pair.atom1()); - if (has_impropers) - { - nb_functions.append([&]() - { getAmberImpropersFrom(impropers, moldata, is_hydrogen, map); }); - } + if (nbscl.coulomb() != 0.0 or nbscl.lj() != 0.0) { + // add them to the list of 14 scale factors. + // This handles both standard AMBER (e.g. 0.833, 0.5) and + // GLYCAM-style (1.0, 1.0) where SCEE=1.0 and SCNB=1.0. + new_nb14s.insert(nb14pair, AmberNB14(nbscl.coulomb(), nbscl.lj())); - if (has_nbpairs) - { - nb_functions.append([&]() - { getAmberNBsFrom(nbpairs, dihedrals); }); + // and remove them from the excluded atoms list + exc_atoms.set(nb14pair.atom0(), nb14pair.atom1(), CLJScaleFactor(0)); + } } + } - SireBase::parallel_invoke(nb_functions); - - // ensure that the resulting object is valid - QStringList errors = this->validateAndFix(); + amber_nb14s = new_nb14s; +} - if (not errors.isEmpty()) - { - throw SireError::io_error(QObject::tr("Problem creating the AmberParams object for molecule %1 : %2. " - "Errors include:\n%3") - .arg(moldata.name().value()) - .arg(moldata.number().value()) - .arg(errors.join("\n")), - CODELOC); - } +/** Create this set of parameters from the passed object */ +void AmberParams::_pvt_createFrom(const MoleculeData &moldata) { + // pull out all of the molecular properties + const PropertyMap &map = propmap; + + // check if this is a perturbable molecule + is_perturbable = moldata.hasProperty("is_perturbable") and + moldata.property("is_perturbable").asABoolean(); + + // first, all of the atom-based properties + bool has_charges, has_ljs, has_masses, has_elements, has_ambertypes; + + amber_charges = + getProperty(map["charge"], moldata, &has_charges); + amber_ljs = getProperty(map["LJ"], moldata, &has_ljs); + amber_masses = getProperty(map["mass"], moldata, &has_masses); + amber_elements = + getProperty(map["element"], moldata, &has_elements); + amber_types = getProperty(map["ambertype"], moldata, + &has_ambertypes); + + if (not has_ambertypes) { + // first look for the bondtypes property, as OPLS uses this + amber_types = getProperty(map["bondtype"], moldata, + &has_ambertypes); + + if (not has_ambertypes) { + // look for the atomtypes property + amber_types = getProperty(map["atomtype"], moldata, + &has_ambertypes); + } + } + + if (not has_elements) { + // try to guess the elements from the names and/or masses + amber_elements = guessElements(moldata.info(), &has_elements); + } + + if (not has_masses) { + // try to guess the masses from the elements + if (has_elements) { + amber_masses = AtomMasses(moldata.info()); + guessMasses(amber_masses, amber_elements, &has_masses); + } + } + + if (not(has_charges and has_ljs and has_masses and has_elements and + has_ambertypes)) { + // it is not possible to create the parameter object if we don't have + // these atom-based parameters + throw SireBase::missing_property( + QObject::tr( + "Cannot create an AmberParams object for molecule %1 as it is " + "missing " + "necessary atom based properties: has_charges = %2, has_ljs = %3, " + "has_masses = %4, has_elements = %5, has_ambertypes = %6.") + .arg(Molecule(moldata).toString()) + .arg(has_charges) + .arg(has_ljs) + .arg(has_masses) + .arg(has_elements) + .arg(has_ambertypes), + CODELOC); + } + + // now see about the optional born radii and screening parameters + bool has_radii, has_screening, has_treechains; + + born_radii = getProperty(map["gb_radii"], moldata, &has_radii); + amber_screens = getProperty(map["gb_screening"], moldata, + &has_screening); + amber_treechains = getProperty(map["treechain"], moldata, + &has_treechains); + + if (has_radii) { + // see if there is a label for the source of the GB parameters + bool has_source; + + radius_set = + getProperty(map["gb_radius_set"], moldata, &has_source) + .value(); + + if (not has_source) { + radius_set = "unknown"; + } + } else { + radius_set = "unavailable"; + } + + // now lets get the bonded parameters (if they exist...) + bool has_bonds, has_ubs, has_angles, has_dihedrals, has_impropers, + has_nbpairs, has_cmaps; + + const auto bonds = + getProperty(map["bond"], moldata, &has_bonds); + const auto ub_bonds = + getProperty(map["urey_bradley"], moldata, &has_ubs); + const auto angles = + getProperty(map["angle"], moldata, &has_angles); + const auto dihedrals = + getProperty(map["dihedral"], moldata, &has_dihedrals); + const auto impropers = + getProperty(map["improper"], moldata, &has_impropers); + const auto nbpairs = + getProperty(map["intrascale"], moldata, &has_nbpairs); + const auto cmaps = + getProperty(map["cmap"], moldata, &has_cmaps); + + // get all of the atoms that contain hydrogen + QVector is_hydrogen; + + if (has_bonds or has_ubs or has_angles or has_dihedrals or has_impropers) { + const int natoms = moldata.info().nAtoms(); + + is_hydrogen = QVector(natoms, false); + + if (not amber_elements.isEmpty()) { + auto is_hydrogen_data = is_hydrogen.data(); + + auto elements = amber_elements.toVector(); + + if (elements.count() != natoms) + throw SireError::program_bug(QObject::tr("Wrong elements count!"), + CODELOC); + + const auto *elements_data = elements.constData(); + + for (int i = 0; i < natoms; ++i) { + is_hydrogen_data[i] = elements_data[i].nProtons() == 1; + } + } + } + + if (has_cmaps and not cmaps.isEmpty()) { + cmap_funcs = cmaps; + } else { + cmap_funcs = CMAPFunctions(); + } + + QVector> nb_functions; + + if (has_bonds) { + nb_functions.append( + [&]() { getAmberBondsFrom(bonds, moldata, is_hydrogen, map); }); + } + + if (has_ubs) { + nb_functions.append( + [&]() { getAmberBondsFrom(ub_bonds, moldata, is_hydrogen, map); }); + } + + if (has_angles) { + nb_functions.append( + [&]() { getAmberAnglesFrom(angles, moldata, is_hydrogen, map); }); + } + + if (has_dihedrals) { + nb_functions.append( + [&]() { getAmberDihedralsFrom(dihedrals, moldata, is_hydrogen, map); }); + } + + if (has_impropers) { + nb_functions.append( + [&]() { getAmberImpropersFrom(impropers, moldata, is_hydrogen, map); }); + } + + if (has_nbpairs) { + nb_functions.append([&]() { getAmberNBsFrom(nbpairs, dihedrals); }); + } + + SireBase::parallel_invoke(nb_functions); + + // ensure that the resulting object is valid + QStringList errors = this->validateAndFix(); + + if (not errors.isEmpty()) { + throw SireError::io_error( + QObject::tr( + "Problem creating the AmberParams object for molecule %1 : %2. " + "Errors include:\n%3") + .arg(moldata.name().value()) + .arg(moldata.number().value()) + .arg(errors.join("\n")), + CODELOC); + } } /** Update this set of parameters from the passed object */ -void AmberParams::_pvt_updateFrom(const MoleculeData &moldata) -{ - // for the moment we will just create everything from scratch. - // However, one day we will optimise this and take existing - // data that doesn't need to be regenerated. - PropertyMap oldmap = propmap; - const auto info = molinfo; +void AmberParams::_pvt_updateFrom(const MoleculeData &moldata) { + // for the moment we will just create everything from scratch. + // However, one day we will optimise this and take existing + // data that doesn't need to be regenerated. + PropertyMap oldmap = propmap; + const auto info = molinfo; - this->operator=(AmberParams()); + this->operator=(AmberParams()); - propmap = oldmap; - molinfo = info; + propmap = oldmap; + molinfo = info; - this->_pvt_createFrom(moldata); + this->_pvt_createFrom(moldata); } -PropertyPtr AmberParams::_pvt_makeCompatibleWith(const MoleculeInfoData &newinfo, const AtomMatcher &atommatcher) const -{ - try - { - if (not atommatcher.changesOrder(this->info(), newinfo)) - { - AmberParams ret(*this); - ret.molinfo = MoleculeInfo(newinfo); +PropertyPtr +AmberParams::_pvt_makeCompatibleWith(const MoleculeInfoData &newinfo, + const AtomMatcher &atommatcher) const { + try { + if (not atommatcher.changesOrder(this->info(), newinfo)) { + AmberParams ret(*this); + ret.molinfo = MoleculeInfo(newinfo); - return ret; - } + return ret; + } - QHash matched_atoms = atommatcher.match(this->info(), molinfo); + QHash matched_atoms = + atommatcher.match(this->info(), molinfo); - return this->_pvt_makeCompatibleWith(molinfo, matched_atoms); - } - catch (const SireError::exception &) - { - throw; - return AmberParams(); - } + return this->_pvt_makeCompatibleWith(molinfo, matched_atoms); + } catch (const SireError::exception &) { + throw; + return AmberParams(); + } } -PropertyPtr AmberParams::_pvt_makeCompatibleWith(const MoleculeInfoData &newinfo, - const QHash &map) const -{ - if (map.isEmpty()) - { - AmberParams ret(*this); - ret.molinfo = MoleculeInfo(newinfo); +PropertyPtr +AmberParams::_pvt_makeCompatibleWith(const MoleculeInfoData &newinfo, + const QHash &map) const { + if (map.isEmpty()) { + AmberParams ret(*this); + ret.molinfo = MoleculeInfo(newinfo); - return ret; - } + return ret; + } - throw SireError::incomplete_code("Cannot make compatible if atom order has changed!", CODELOC); + throw SireError::incomplete_code( + "Cannot make compatible if atom order has changed!", CODELOC); } /** Merge this property with another property */ PropertyList AmberParams::merge(const MolViewProperty &other, const AtomIdxMapping &mapping, const QString &ghost, - const SireBase::PropertyMap &map) const -{ - if (not other.isA()) - { - throw SireError::incompatible_error(QObject::tr("Cannot merge %1 with %2 as they are different types.") - .arg(this->what()) - .arg(other.what()), - CODELOC); - } - - SireBase::Console::warning(QObject::tr("Merging %1 properties is not yet implemented. Returning two copies of the original property.") - .arg(this->what())); - - SireBase::PropertyList ret; - - ret.append(*this); - ret.append(*this); - - return ret; + const SireBase::PropertyMap &map) const { + if (not other.isA()) { + throw SireError::incompatible_error( + QObject::tr("Cannot merge %1 with %2 as they are different types.") + .arg(this->what()) + .arg(other.what()), + CODELOC); + } + + SireBase::Console::warning( + QObject::tr("Merging %1 properties is not yet implemented. Returning two " + "copies of the original property.") + .arg(this->what())); + + SireBase::PropertyList ret; + + ret.append(*this); + ret.append(*this); + + return ret; } diff --git a/corelib/src/libs/SireMM/amberparams.h b/corelib/src/libs/SireMM/amberparams.h index 0e6ffc6db..e20d4b46f 100644 --- a/corelib/src/libs/SireMM/amberparams.h +++ b/corelib/src/libs/SireMM/amberparams.h @@ -605,6 +605,11 @@ namespace SireMM /** The property map used (if any) to identify the properties that hold the amber parameters */ SireBase::PropertyMap propmap; + + /** Whether this molecule is perturbable (has an is_perturbable property). + Used in validateAndFix to handle ring-closure 1-3 pairs that may + carry incorrect (1,1) CLJScaleFactors from old-style merges. */ + bool is_perturbable = false; }; #ifndef SIRE_SKIP_INLINE_FUNCTIONS