//
//  Copyright (C) 2004-2018 Greg Landrum and Rational Discovery LLC
//
//   @@ All Rights Reserved @@
//  This file is part of the RDKit.
//  The contents are covered by the terms of the BSD license
//  which is included in the file license.txt, found at the root
//  of the RDKit source tree.
//

#include <RDGeneral/test.h>
#include <iostream>
#include <fstream>

#include <RDGeneral/BoostStartInclude.h>
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <boost/lexical_cast.hpp>
#include <RDGeneral/BoostEndInclude.h>

#include <RDGeneral/Invariant.h>
#include <RDGeneral/RDLog.h>
#include <RDGeneral/utils.h>
#include <RDGeneral/StreamOps.h>

#include <GraphMol/RDKitBase.h>
#include <GraphMol/MolPickler.h>
#include <GraphMol/SmilesParse/SmilesParse.h>
#include <GraphMol/SmilesParse/SmilesWrite.h>
#include <GraphMol/FileParsers/FileParsers.h>
#include <GraphMol/FileParsers/MolSupplier.h>

#include <GraphMol/Descriptors/MolDescriptors.h>
#include <GraphMol/Descriptors/Crippen.h>
#include <GraphMol/Descriptors/Property.h>

#include <GraphMol/PeriodicTable.h>
#include <GraphMol/atomic_data.h>
#include <GraphMol/Descriptors/USRDescriptor.h>

#include <DataStructs/BitVects.h>
#include <DataStructs/BitOps.h>
using namespace RDKit;
using namespace RDKit::Descriptors;

void test1() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test Crippen parameter acquisition."
                        << std::endl;

  const CrippenParamCollection *params = CrippenParamCollection::getParams();
  TEST_ASSERT(params);

  CrippenParams p = *(params->begin());
  TEST_ASSERT(p.idx == 0);
  TEST_ASSERT(p.label == "C1");
  TEST_ASSERT(p.smarts == "[CH4]");

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void test2() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test Crippen calculation." << std::endl;

  ROMol *mol;
  double logp, mr;

  mol = SmilesToMol("C");
  TEST_ASSERT(mol);
  calcCrippenDescriptors(*mol, logp, mr);
  TEST_ASSERT(feq(logp, 0.6361));
  TEST_ASSERT(feq(mr, 6.7310));
  // check singleton functions
  TEST_ASSERT(calcClogP(*mol) == logp);
  TEST_ASSERT(calcMR(*mol) == mr);
  // check that caching works:
  calcCrippenDescriptors(*mol, logp, mr);
  TEST_ASSERT(feq(logp, 0.6361));
  TEST_ASSERT(feq(mr, 6.7310));
  calcCrippenDescriptors(*mol, logp, mr, true, true);
  TEST_ASSERT(feq(logp, 0.6361));
  TEST_ASSERT(feq(mr, 6.7310));

  // check that things work when we don't add Hs:
  calcCrippenDescriptors(*mol, logp, mr, false, true);
  TEST_ASSERT(feq(logp, 0.1441));
  TEST_ASSERT(feq(mr, 2.503));
  delete mol;

  mol = SmilesToMol("C=C");
  TEST_ASSERT(mol);
  calcCrippenDescriptors(*mol, logp, mr, true);
  TEST_ASSERT(feq(logp, 0.8022));
  TEST_ASSERT(feq(mr, 11.2540));
  delete mol;

  mol = SmilesToMol("C#C");
  TEST_ASSERT(mol);
  calcCrippenDescriptors(*mol, logp, mr);
  TEST_ASSERT(feq(logp, 0.2494));
  TEST_ASSERT(feq(mr, 9.8900));
  delete mol;

  mol = SmilesToMol("CO");
  TEST_ASSERT(mol);
  calcCrippenDescriptors(*mol, logp, mr);
  TEST_ASSERT(feq(logp, -0.3915));
  TEST_ASSERT(feq(mr, 8.1428));
  delete mol;

  mol = SmilesToMol("C=O");
  TEST_ASSERT(mol);
  calcCrippenDescriptors(*mol, logp, mr);
  TEST_ASSERT(feq(logp, -0.1849));
  TEST_ASSERT(feq(mr, 7.121));
  delete mol;

  mol = SmilesToMol("C#[O+]");
  TEST_ASSERT(mol);
  calcCrippenDescriptors(*mol, logp, mr);
  TEST_ASSERT(feq(logp, 0.0059));
  TEST_ASSERT(feq(mr, 5.6315));
  delete mol;

  mol = SmilesToMol("C(C)(C)C");
  TEST_ASSERT(mol);
  calcCrippenDescriptors(*mol, logp, mr);
  TEST_ASSERT(feq(logp, 1.6623));
  TEST_ASSERT(feq(mr, 20.512));
  delete mol;

  mol = SmilesToMol("C(C)(C)(C)O");
  TEST_ASSERT(mol);
  calcCrippenDescriptors(*mol, logp, mr);
  TEST_ASSERT(feq(logp, 0.7772));
  TEST_ASSERT(feq(mr, 21.9718));
  delete mol;

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testIssue262() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog)
      << "    Test Issue262: problems with Crippen calculation from pickles."
      << std::endl;

  ROMol *mol, *mol2;
  RWMol *mol3;
  std::string pkl;
  double rlogp, rmr, logp, mr;

  mol = SmilesToMol("c1ncccc1");
  TEST_ASSERT(mol);
  calcCrippenDescriptors(*mol, rlogp, rmr);

  MolPickler::pickleMol(*mol, pkl);

  mol2 = new ROMol(pkl);
  TEST_ASSERT(mol2);
  calcCrippenDescriptors(*mol2, logp, mr);
  TEST_ASSERT(feq(logp, rlogp));
  TEST_ASSERT(feq(mr, rmr));

  mol3 = new RWMol();
  TEST_ASSERT(mol3);
  MolPickler::molFromPickle(pkl, mol3);

  calcCrippenDescriptors(*mol3, logp, mr);
  TEST_ASSERT(feq(logp, rlogp));
  TEST_ASSERT(feq(mr, rmr));

  delete mol;
  delete mol2;
  delete mol3;

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void test3() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test AMW calculation." << std::endl;

  ROMol *mol, *mol2;
  double amw;

  mol = SmilesToMol("C");
  TEST_ASSERT(mol);
  amw = calcAMW(*mol);
  TEST_ASSERT(feq(amw, 16.043, .001));
  amw = calcAMW(*mol, true);
  TEST_ASSERT(feq(amw, 12.011, .001));
  mol2 = MolOps::addHs(*mol);
  amw = calcAMW(*mol2);
  TEST_ASSERT(feq(amw, 16.043, .001));
  amw = calcAMW(*mol2, true);
  TEST_ASSERT(feq(amw, 12.011, .001));
  delete mol;
  delete mol2;

  mol = SmilesToMol("[CH4]");
  TEST_ASSERT(mol);
  amw = calcAMW(*mol);
  TEST_ASSERT(feq(amw, 16.043, .001));
  amw = calcAMW(*mol, true);
  TEST_ASSERT(feq(amw, 12.011, .001));
  delete mol;

  mol = SmilesToMol("C[2H]");
  TEST_ASSERT(mol);
  amw = calcAMW(*mol);
  delete mol;

  TEST_ASSERT(feq(amw, 17.0, .1));

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void test3a() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test Exact MW calculation." << std::endl;

  ROMol *mol, *mol2;
  double mw;

  mol = SmilesToMol("C");
  TEST_ASSERT(mol);
  mw = calcExactMW(*mol);
  TEST_ASSERT(feq(mw, 16.031, .001));
  mw = calcExactMW(*mol, true);
  TEST_ASSERT(feq(mw, 12.000, .001));
  mol2 = MolOps::addHs(*mol);
  mw = calcExactMW(*mol2);
  TEST_ASSERT(feq(mw, 16.031, .001));
  mw = calcExactMW(*mol2, true);
  TEST_ASSERT(feq(mw, 12.000, .001));
  delete mol;
  delete mol2;

  mol = SmilesToMol("[CH4]");
  TEST_ASSERT(mol);
  mw = calcExactMW(*mol);
  TEST_ASSERT(feq(mw, 16.031, .001));
  mw = calcExactMW(*mol, true);
  TEST_ASSERT(feq(mw, 12.000, .001));
  delete mol;

  mol = SmilesToMol("C[2H]");
  TEST_ASSERT(mol);
  mw = calcExactMW(*mol);
  TEST_ASSERT(feq(mw, 17.037, .001));
  mw = calcExactMW(*mol, true);
  TEST_ASSERT(feq(mw, 12.000, .001));
  delete mol;

  mol = SmilesToMol("Cl");
  TEST_ASSERT(mol);
  mw = calcAMW(*mol);
  TEST_ASSERT(feq(mw, 35.453 + 1.008, .001));
  mw = calcExactMW(*mol);
  TEST_ASSERT(feq(mw, 34.9688 + 1.0078, .001));
  delete mol;

  mol = SmilesToMol("[35ClH]");
  TEST_ASSERT(mol);
  mw = calcAMW(*mol);
  TEST_ASSERT(feq(mw, 34.9688 + 1.008, .001));
  mw = calcExactMW(*mol);
  TEST_ASSERT(feq(mw, 34.9688 + 1.0078, .001));
  delete mol;

  mol = SmilesToMol("[36ClH]");
  TEST_ASSERT(mol);
  mw = calcAMW(*mol);
  TEST_ASSERT(feq(mw, 35.9683 + 1.008, .001));
  mw = calcExactMW(*mol);
  TEST_ASSERT(feq(mw, 35.9683 + 1.0078, .001));
  delete mol;

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testLabute() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test Labute ASA descriptors." << std::endl;
  ROMol *mol;
  double asa;

  mol = SmilesToMol("CO");
  asa = calcLabuteASA(*mol);
  TEST_ASSERT(feq(asa, 13.5335, .0001));
  asa = calcLabuteASA(*mol);
  TEST_ASSERT(feq(asa, 13.5335, .0001));
  asa = calcLabuteASA(*mol, true, true);
  TEST_ASSERT(feq(asa, 13.5335, .0001));

  delete mol;
  mol = SmilesToMol("OC(=O)c1ccncc1C(=O)O");
  asa = calcLabuteASA(*mol);
  TEST_ASSERT(feq(asa, 67.2924, .0001));

  delete mol;
  mol = SmilesToMol("C1CCC(c2cccnc2)NC1");
  asa = calcLabuteASA(*mol);
  TEST_ASSERT(feq(asa, 73.0198, .0001));

  delete mol;
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testTPSA() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test TPSA descriptors." << std::endl;

  std::string fName = getenv("RDBASE");
  fName += "/Data/NCI/first_200.tpsa.csv";
  std::ifstream inf(fName.c_str());
  TEST_ASSERT(inf && !inf.bad());

  while (!inf.eof()) {
    std::string inl = getLine(inf);
    boost::trim(inl);
    if (inl.size() == 0 || inl[0] == '#') {
      continue;
    }
    std::vector<std::string> tokens;
    boost::split(tokens, inl, boost::is_any_of(","));
    if (tokens.size() != 2) {
      continue;
    }
    std::string smiles = tokens[0];
    auto oTPSA = boost::lexical_cast<double>(tokens[1]);
    ROMol *mol = SmilesToMol(smiles);
    TEST_ASSERT(mol);
    double nTPSA = calcTPSA(*mol);
    if (!feq(nTPSA, oTPSA, .0001)) {
      std::cerr << " TPSA ERR: " << smiles << " " << oTPSA << " " << nTPSA
                << std::endl;
      std::vector<double> contribs(mol->getNumAtoms());
      getTPSAAtomContribs(*mol, contribs);
      for (unsigned int i = 0; i < mol->getNumAtoms(); ++i) {
        std::cerr << "\t" << i << "\t" << contribs[i] << std::endl;
      }
    }
    TEST_ASSERT(feq(nTPSA, oTPSA, .0001));

    // make sure that adding Hs doesn't affect the value
    // (this was issue 1969745)
    ROMol *mol2 = MolOps::addHs(*mol);
    double hTPSA = calcTPSA(*mol2);
    TEST_ASSERT(feq(nTPSA, hTPSA, .0001));

    delete mol2;
    delete mol;
  }
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

const std::string NonStrictRotProp = "NUM_ROTATABLEBONDS_O";
const std::string StrictRotProp = "NUM_ROTATABLEBONDS";

void testLipinski1() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test Lipinski parameters." << std::endl;

  std::string fName = getenv("RDBASE");
  fName += "/Data/NCI/first_200.props.sdf";
  SDMolSupplier suppl(fName);
  int idx = -1;
  // Figure out which rotatable bond version we are using
  std::string rot_prop = StrictRotProp;
  {
    const bool sanitize = true;
    ROMol *test_mol =
        SmilesToMol("CC(C)(C)c1cc(O)c(cc1O)C(C)(C)C", 0, sanitize);
    if (calcNumRotatableBonds(*test_mol) == 2) {
      rot_prop = NonStrictRotProp;
    }
    delete test_mol;
  }
  while (!suppl.atEnd()) {
    ROMol *mol = nullptr;
    ++idx;
    try {
      mol = suppl.next();
    } catch (...) {
      continue;
    }
    if (!mol) {
      continue;
    }

    unsigned int oVal, nVal;
    std::string foo;

    mol->getProp("NUM_HACCEPTORS", foo);
    oVal = boost::lexical_cast<unsigned int>(foo);
    nVal = calcNumHBA(*mol);
    if (oVal != nVal) {
      std::cerr << "  failed: " << idx << " " << oVal << " " << nVal
                << std::endl;
    }
    TEST_ASSERT(oVal == nVal);

    mol->getProp("NUM_HDONORS", foo);
    oVal = boost::lexical_cast<unsigned int>(foo);
    nVal = calcNumHBD(*mol);
    if (oVal != nVal) {
      std::cerr << "  failed: " << idx << " " << oVal << " " << nVal
                << std::endl;
    }
    TEST_ASSERT(oVal == nVal);

    mol->getProp("NUM_LIPINSKIHDONORS", foo);
    oVal = boost::lexical_cast<unsigned int>(foo);
    nVal = calcLipinskiHBD(*mol);
    if (oVal != nVal) {
      std::cerr << "  failed: " << idx << " " << oVal << " " << nVal
                << std::endl;
    }
    TEST_ASSERT(oVal == nVal);

    mol->getProp("NUM_LIPINSKIHACCEPTORS", foo);
    oVal = boost::lexical_cast<unsigned int>(foo);
    nVal = calcLipinskiHBA(*mol);
    if (oVal != nVal) {
      std::cerr << "  failed: " << idx << " " << oVal << " " << nVal
                << std::endl;
    }
    TEST_ASSERT(oVal == nVal);

    mol->getProp("NUM_RINGS", foo);
    oVal = boost::lexical_cast<unsigned int>(foo);
    nVal = calcNumRings(*mol);
    if (oVal != nVal) {
      std::cerr << "  failed: " << idx << " " << oVal << " " << nVal
                << std::endl;
    }
    TEST_ASSERT(oVal == nVal);

    mol->getProp("NUM_HETEROATOMS", foo);
    oVal = boost::lexical_cast<unsigned int>(foo);
    nVal = calcNumHeteroatoms(*mol);
    if (oVal != nVal) {
      std::cerr << "  failed: " << idx << " " << oVal << " " << nVal
                << std::endl;
    }
    TEST_ASSERT(oVal == nVal);

    mol->getProp(rot_prop, foo);
    oVal = boost::lexical_cast<unsigned int>(foo);
    nVal = calcNumRotatableBonds(*mol);
    if (oVal != nVal) {
      std::cerr << "  failed: " << idx << " " << oVal << " " << nVal
                << " using stored sd prop " << rot_prop << std::endl;
    }
    TEST_ASSERT(oVal == nVal);

    mol->getProp(NonStrictRotProp, foo);
    oVal = boost::lexical_cast<unsigned int>(foo);
    nVal = calcNumRotatableBonds(*mol, NonStrict);
    if (oVal != nVal) {
      std::cerr << "  failed: " << idx << " " << oVal << " " << nVal
                << " using stored sd prop " << rot_prop << std::endl;
    }
    TEST_ASSERT(oVal == nVal);

    mol->getProp(StrictRotProp, foo);
    oVal = boost::lexical_cast<unsigned int>(foo);
    nVal = calcNumRotatableBonds(*mol, Strict);
    if (oVal != nVal) {
      std::cerr << "  failed: " << idx << " " << oVal << " " << nVal
                << " using stored sd prop " << rot_prop << std::endl;
    }
    TEST_ASSERT(oVal == nVal);

    delete mol;
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testVSADescriptors() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test VSA descriptors." << std::endl;

  {
    ROMol *mol;
    std::vector<double> vals;

    mol = SmilesToMol("CO");
    vals = calcSlogP_VSA(*mol);
    TEST_ASSERT(vals.size() == 12);
    for (unsigned int i = 0; i < vals.size(); ++i) {
      switch (i) {
        case 1:
          TEST_ASSERT(feq(vals[i], 12.216, .001));
          break;
        default:
          TEST_ASSERT(feq(vals[i], 0, .001));
      }
    }
    delete mol;

    mol = SmilesToMol("CCO");
    vals = calcSlogP_VSA(*mol);
    TEST_ASSERT(vals.size() == 12);
    for (unsigned int i = 0; i < vals.size(); ++i) {
      switch (i) {
        case 1:
          TEST_ASSERT(feq(vals[i], 11.713, .001));
          break;
        case 4:
          TEST_ASSERT(feq(vals[i], 6.924, .001));
          break;
        default:
          TEST_ASSERT(feq(vals[i], 0, .001));
      }
    }
    delete mol;

    mol = SmilesToMol("Fc1ccccc1");
    vals = calcSlogP_VSA(*mol);
    TEST_ASSERT(vals.size() == 12);
    for (unsigned int i = 0; i < vals.size(); ++i) {
      switch (i) {
        case 3:
          TEST_ASSERT(feq(vals[i], 5.817, .001));
          break;
        case 5:
          TEST_ASSERT(feq(vals[i], 30.332, .001));
          break;
        case 9:
          TEST_ASSERT(feq(vals[i], 4.390, .001));
          break;
        default:
          TEST_ASSERT(feq(vals[i], 0, .001));
      }
    }
    delete mol;
  }

  {
    ROMol *mol;
    std::vector<double> vals;

    mol = SmilesToMol("CO");
    vals = calcSMR_VSA(*mol);
    TEST_ASSERT(vals.size() == 10);
    for (unsigned int i = 0; i < vals.size(); ++i) {
      switch (i) {
        case 0:
          TEST_ASSERT(feq(vals[i], 5.106, .001));
          break;
        case 5:
          TEST_ASSERT(feq(vals[i], 7.110, .001));
          break;
        default:
          TEST_ASSERT(feq(vals[i], 0, .001));
      }
    }
    delete mol;

    mol = SmilesToMol("CCO");
    vals = calcSMR_VSA(*mol);
    TEST_ASSERT(vals.size() == 10);
    for (unsigned int i = 0; i < vals.size(); ++i) {
      switch (i) {
        case 0:
          TEST_ASSERT(feq(vals[i], 5.106, .001));
          break;
        case 4:
          TEST_ASSERT(feq(vals[i], 6.924, .001));
          break;
        case 5:
          TEST_ASSERT(feq(vals[i], 6.607, .001));
          break;
        default:
          TEST_ASSERT(feq(vals[i], 0, .001));
      }
    }
    delete mol;
  }

  {
    ROMol *mol;
    std::vector<double> vals;

    mol = SmilesToMol("CO");
    vals = calcPEOE_VSA(*mol);
    TEST_ASSERT(vals.size() == 14);
    for (unsigned int i = 0; i < vals.size(); ++i) {
      switch (i) {
        case 0:
          TEST_ASSERT(feq(vals[i], 5.106, .001));
          break;
        case 7:
          TEST_ASSERT(feq(vals[i], 7.110, .001));
          break;
        default:
          TEST_ASSERT(feq(vals[i], 0, .001));
      }
    }
    delete mol;

    mol = SmilesToMol("CCO");
    vals = calcPEOE_VSA(*mol);
    TEST_ASSERT(vals.size() == 14);
    for (unsigned int i = 0; i < vals.size(); ++i) {
      switch (i) {
        case 0:
          TEST_ASSERT(feq(vals[i], 5.106, .001));
          break;
        case 6:
          TEST_ASSERT(feq(vals[i], 6.924, .001));
          break;
        case 7:
          TEST_ASSERT(feq(vals[i], 6.607, .001));
          break;
        default:
          TEST_ASSERT(feq(vals[i], 0, .001));
      }
    }
    delete mol;
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testMolFormula() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test molecular formula calculation."
                        << std::endl;

  ROMol *mol, *mol2;
  std::string formula;

  mol = SmilesToMol("C");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "CH4");
  mol2 = MolOps::addHs(*mol);
  formula = calcMolFormula(*mol2);
  TEST_ASSERT(formula == "CH4");
  delete mol;
  delete mol2;

  mol = SmilesToMol("[CH4]");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "CH4");
  delete mol;

  mol = SmilesToMol("CO");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "CH4O");
  mol2 = MolOps::addHs(*mol);
  formula = calcMolFormula(*mol2);
  TEST_ASSERT(formula == "CH4O");
  delete mol;
  delete mol2;

  mol = SmilesToMol("C(=O)N");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "CH3NO");
  mol2 = MolOps::addHs(*mol);
  formula = calcMolFormula(*mol2);
  TEST_ASSERT(formula == "CH3NO");
  delete mol;
  delete mol2;

  mol = SmilesToMol("C(=O)=O");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "CO2");
  delete mol;

  mol = SmilesToMol("C(=O)[O-]");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "CHO2-");
  delete mol;

  mol = SmilesToMol("C([O-])[O-]");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "CH2O2-2");
  delete mol;

  mol = SmilesToMol("C([NH3+])[O-]");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "CH5NO");
  delete mol;

  mol = SmilesToMol("C([NH3+])O");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "CH6NO+");
  delete mol;

  mol = SmilesToMol("C([NH3+])[NH3+]");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "CH8N2+2");
  delete mol;

  // H isotope tests
  mol = SmilesToMol("[2H]C([3H])O");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "CH4O");
  formula = calcMolFormula(*mol, true);
  TEST_ASSERT(formula == "CH2DTO");
  formula = calcMolFormula(*mol, true, false);
  TEST_ASSERT(formula == "CH2[2H][3H]O");
  delete mol;

  // isotope test
  mol = SmilesToMol("[13CH3]C([2H])O");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "C2H6O");
  formula = calcMolFormula(*mol, true);
  TEST_ASSERT(formula == "C[13C]H5DO");
  formula = calcMolFormula(*mol, true, false);
  TEST_ASSERT(formula == "C[13C]H5[2H]O");
  delete mol;

  // isotope test
  mol = SmilesToMol("[13CH3]C[13CH2]C");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "C4H10");
  formula = calcMolFormula(*mol, true);
  TEST_ASSERT(formula == "C2[13C]2H10");
  formula = calcMolFormula(*mol, true, false);
  TEST_ASSERT(formula == "C2[13C]2H10");
  delete mol;

  // order test
  mol = SmilesToMol("[13CH3]C[13CH2]CB(O)O[2H]");
  TEST_ASSERT(mol);
  formula = calcMolFormula(*mol);
  TEST_ASSERT(formula == "C4H11BO2");
  formula = calcMolFormula(*mol, true);
  TEST_ASSERT(formula == "C2[13C]2H10DBO2");
  formula = calcMolFormula(*mol, true, false);
  TEST_ASSERT(formula == "C2[13C]2H10[2H]BO2");
  delete mol;

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testIssue3415534() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test Issue 3415534." << std::endl;

  {
    ROMol *mol = SmilesToMol("CN");
    TEST_ASSERT(mol);
    int nHBD = calcLipinskiHBD(*mol);
    TEST_ASSERT(nHBD == 2);
    delete mol;
  }
  {
    ROMol *mol = SmilesToMol("CNC");
    TEST_ASSERT(mol);
    int nHBD = calcLipinskiHBD(*mol);
    TEST_ASSERT(nHBD == 1);
    delete mol;
  }
  {
    ROMol *mol = SmilesToMol("C[NH3+]");
    TEST_ASSERT(mol);
    int nHBD = calcLipinskiHBD(*mol);
    TEST_ASSERT(nHBD == 3);
    delete mol;
  }
  {
    ROMol *mol = SmilesToMol("CO");
    TEST_ASSERT(mol);
    int nHBD = calcLipinskiHBD(*mol);
    TEST_ASSERT(nHBD == 1);
    delete mol;
  }
  {
    ROMol *mol = SmilesToMol("C[OH2+]");
    TEST_ASSERT(mol);
    int nHBD = calcLipinskiHBD(*mol);
    TEST_ASSERT(nHBD == 2);
    delete mol;
  }
  {
    ROMol *mol = SmilesToMol("COC");
    TEST_ASSERT(mol);
    int nHBD = calcLipinskiHBD(*mol);
    TEST_ASSERT(nHBD == 0);
    delete mol;
  }
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testIssue3433771() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog)
      << "    Test Issue3433771: Bad definition for Crippen atom type O11."
      << std::endl;

  ROMol *mol;
  double logp, mr;

  mol = SmilesToMol("O=C(NC)n1cccc1");
  TEST_ASSERT(mol);
  calcCrippenDescriptors(*mol, logp, mr);
  TEST_ASSERT(feq(logp, 0.6756, .001));

  delete mol;
  mol = SmilesToMol("O=C(n1cccc1)n1cccc1");
  TEST_ASSERT(mol);
  calcCrippenDescriptors(*mol, logp, mr);
  TEST_ASSERT(feq(logp, 1.806, .001));
  delete mol;

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

#ifdef RDK_TEST_MULTITHREADED
namespace {
void runblock(const std::vector<ROMol *> &mols, unsigned int count,
              unsigned int idx) {
  for (unsigned int j = 0; j < 1000; j++) {
    for (unsigned int i = 0; i < mols.size(); ++i) {
      if (i % count != idx) {
        continue;
      }
      ROMol *mol = mols[i];
      int nHBD = calcNumHBD(*mol);
      int nHBA = calcNumHBA(*mol);

      int oVal;
      std::string foo;
      mol->getProp("NUM_HACCEPTORS", foo);
      oVal = boost::lexical_cast<int>(foo);
      TEST_ASSERT(oVal == nHBA);
      mol->getProp("NUM_HDONORS", foo);
      oVal = boost::lexical_cast<unsigned int>(foo);
      TEST_ASSERT(oVal == nHBD);

      unsigned int nAmide = calcNumAmideBonds(*mol);
      (void)nAmide;
      double logp, mr;
      calcCrippenDescriptors(*mol, logp, mr);
    }
  }
};
}  // namespace
#include <thread>
#include <future>
void testMultiThread() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test multithreading" << std::endl;

  std::string fName = getenv("RDBASE");
  fName += "/Data/NCI/first_200.props.sdf";
  SDMolSupplier suppl(fName);
  std::cerr << "reading molecules" << std::endl;
  std::vector<ROMol *> mols;
  while (!suppl.atEnd() && mols.size() < 100) {
    ROMol *mol = nullptr;
    try {
      mol = suppl.next();
    } catch (...) {
      continue;
    }
    if (!mol) {
      continue;
    }
    mols.push_back(mol);
  }
  std::vector<std::future<void>> tg;

  std::cerr << "processing" << std::endl;
  unsigned int count = 4;
  for (unsigned int i = 0; i < count; ++i) {
    std::cerr << " launch :" << i << std::endl;
    std::cerr.flush();
    tg.emplace_back(std::async(std::launch::async, runblock, mols, count, i));
  }
  for (auto &fut : tg) {
    fut.get();
  }
  for (auto &mol : mols) {
    delete mol;
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}
#else
void testMultiThread() {}
#endif
void testCrippenContribs() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test Crippen atom type calculations."
                        << std::endl;

  {
    ROMol *mol;
    mol = SmilesToMol("n1ccccc1CO");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());
    std::vector<unsigned int> ts(mol->getNumAtoms());
    std::vector<std::string> ls(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true, &ts, &ls);
    TEST_ASSERT(ts[0] == 59);
    TEST_ASSERT(ts[1] == 25);
    TEST_ASSERT(ts[2] == 25);
    TEST_ASSERT(ts[3] == 25);
    TEST_ASSERT(ts[4] == 25);
    TEST_ASSERT(ts[5] == 28);
    TEST_ASSERT(ts[6] == 17);
    TEST_ASSERT(ts[7] == 69);

    TEST_ASSERT(ls[0] == "N11");
    TEST_ASSERT(ls[7] == "O2");
    delete mol;
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testIssue252() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog)
      << "    Test Issue252: Bad definitions for Crippen atom types."
      << std::endl;

  {
    ROMol *mol;
    mol = SmilesToMol("O=[N+]([O-])C");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], 0.0335, .001));
    TEST_ASSERT(feq(logp[1], -0.3396, .001));
    TEST_ASSERT(feq(logp[2], 0.0335, .001));
    TEST_ASSERT(feq(logp[3], -0.2035, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("CP");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], -0.2035, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("C(C)P");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], -0.2035, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("C(C)(C)P");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], -0.2051, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("C(C)(C)(C)P");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], -0.2051, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("C(=C)c1ccccc1");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], 0.264, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("C(=C)(C)c1ccccc1");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], 0.264, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("C=C");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], 0.1551, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("O=S");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], -0.3339, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("S=O");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], -0.0024, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("C(Cl)C");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], -0.2035, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("C(Cl)(Cl)C");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], -0.2051, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("C(Cl)(Cl)(Cl)C");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], -0.2051, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("C(Cl)c1ccccc1");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], -0.0516, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("C(Cl)(Cl)c1ccccc1");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], 0.1193, .001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("C(Cl)(Cl)(Cl)c1ccccc1");
    TEST_ASSERT(mol);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());

    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[0], -0.0967, .001));
    delete mol;
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testChiVs() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test calculation of ChiVs." << std::endl;

  {
    std::string sdata[] = {"CCCCCC",     "CCC(C)CC", "CC(C)CCC", "CC(C)C(C)C",
                           "CC(C)(C)CC", "CCCCCO",   "CCC(O)CC", "CC(O)(C)CC",
                           "c1ccccc1O",  "CCCl",     "CCBr",     "CCI",
                           "EOS"};
    double ddata[] = {4.828, 4.992, 4.992, 5.155, 5.207, 4.276,
                      4.439, 4.654, 3.834, 2.841, 3.671, 4.242};
    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcChi0v(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }
  {
    std::string sdata[] = {"CCCCCC",     "CCC(C)CC", "CC(C)CCC", "CC(C)C(C)C",
                           "CC(C)(C)CC", "CCCCCO",   "CCC(O)CC", "CC(O)(C)CC",
                           "c1ccccc1O",  "EOS"};

    double ddata[] = {2.914, 2.808, 2.770, 2.643, 2.561,
                      2.523, 2.489, 2.284, 2.134};
    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcChi1v(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }
  {
    std::string sdata[] = {"CCCCCC",     "CCC(C)CC", "CC(C)CCC", "CC(C)C(C)C",
                           "CC(C)(C)CC", "CCCCCO",   "CCC(O)CC", "CC(O)(C)CC",
                           "c1ccccc1O",  "EOS"};

    double ddata[] = {1.707, 1.922, 2.183, 2.488, 2.914,
                      1.431, 1.470, 2.166, 1.336};

    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcChi2v(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }

  {
    std::string sdata[] = {"CCCCCC",     "CCC(C)CC", "CC(C)CCC", "CC(C)C(C)C",
                           "CC(C)(C)CC", "CCCCCO",   "CCC(O)CC", "CC(O)(C)CC",
                           "c1ccccc1O",  "EOS"};

    double ddata[] = {0.957, 1.394, 0.866, 1.333, 1.061,
                      0.762, 0.943, 0.865, 0.756};

    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcChi3v(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }

  {
    std::string sdata[] = {"CCCCCC",     "CCC(C)CC", "CC(C)CCC", "CC(C)C(C)C",
                           "CC(C)(C)CC", "CCCCCO",   "CCC(O)CC", "CC(O)(C)CC",
                           "c1ccccc1O",  "EOS"};

    double ddata[] = {0.500, 0.289, 0.577, 0.000, 0.000,
                      0.362, 0.289, 0.000, 0.428};

    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcChi4v(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testChiNs() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test calculation of ChiNs." << std::endl;

  {
    std::string sdata[] = {"CCCCCC",     "CCC(C)CC", "CC(C)CCC", "CC(C)C(C)C",
                           "CC(C)(C)CC", "CCCCCO",   "CCC(O)CC", "CC(O)(C)CC",
                           "c1ccccc1O",  "CCCl",     "CCBr",     "CCI",
                           "EOS"};
    double ddata[] = {4.828, 4.992, 4.992, 5.155, 5.207, 4.276,
                      4.439, 4.654, 3.834, 2.085, 2.085, 2.085};
    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcChi0n(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }
  {
    std::string sdata[] = {"CCCCCC",     "CCC(C)CC", "CC(C)CCC", "CC(C)C(C)C",
                           "CC(C)(C)CC", "CCCCCO",   "CCC(O)CC", "CC(O)(C)CC",
                           "c1ccccc1O",  "C=S",      "EOS"};

    double ddata[] = {2.914, 2.808, 2.770, 2.643, 2.561,
                      2.523, 2.489, 2.284, 2.134, 0.289};
    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcChi1n(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }
  {
    std::string sdata[] = {"CCCCCC",     "CCC(C)CC", "CC(C)CCC", "CC(C)C(C)C",
                           "CC(C)(C)CC", "CCCCCO",   "CCC(O)CC", "CC(O)(C)CC",
                           "c1ccccc1O",  "EOS"};

    double ddata[] = {1.707, 1.922, 2.183, 2.488, 2.914,
                      1.431, 1.470, 2.166, 1.336};

    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcChi2n(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }

  {
    std::string sdata[] = {"CCCCCC",     "CCC(C)CC", "CC(C)CCC", "CC(C)C(C)C",
                           "CC(C)(C)CC", "CCCCCO",   "CCC(O)CC", "CC(O)(C)CC",
                           "c1ccccc1O",  "EOS"};

    double ddata[] = {0.957, 1.394, 0.866, 1.333, 1.061,
                      0.762, 0.943, 0.865, 0.756};

    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcChi3n(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }

  {
    std::string sdata[] = {"CCCCCC",     "CCC(C)CC", "CC(C)CCC", "CC(C)C(C)C",
                           "CC(C)(C)CC", "CCCCCO",   "CCC(O)CC", "CC(O)(C)CC",
                           "c1ccccc1O",  "EOS"};

    double ddata[] = {0.500, 0.289, 0.577, 0.000, 0.000,
                      0.362, 0.289, 0.000, 0.428};

    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcChi4n(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testHallKierAlpha() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test calculation of HallKierAlpha."
                        << std::endl;

  {
    std::string sdata[] = {"C=O",
                           "CCC1(CC)C(=O)NC(=O)N(C)C1=O",
                           "OCC(O)C(O)C(O)C(O)CO",
                           "OCC1OC(O)C(O)C(O)C1O",
                           "Fc1c[nH]c(=O)[nH]c1=O",
                           "OC1CNC(C(=O)O)C1",
                           "CCCc1[nH]c(=S)[nH]c(=O)c1",
                           "CN(CCCl)CCCl",
                           "CBr",
                           "CI",
                           "EOS"};
    double ddata[] = {
        -0.3300, -1.3900, -0.2400, -0.2400, -1.3900,
        -0.6100, -0.9000, 0.5400,  0.480,   0.730,
    };
    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcHallKierAlpha(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testKappa1() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test calculation of Kappa1." << std::endl;

  {
    std::string sdata[] = {
        "C12CC2C3CC13",      "C1CCC12CC2",        "C1CCCCC1",          "CCCCCC",
        "CCC(C)C1CCC(C)CC1", "CC(C)CC1CCC(C)CC1", "CC(C)C1CCC(C)CCC1", "EOS"};
    double ddata[] = {2.344, 3.061, 4.167, 6.000, 9.091, 9.091, 9.091};
    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcKappa1(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testKappa2() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test calculation of Kappa2." << std::endl;

  {
    std::string sdata[] = {"[C+2](C)(C)(C)(C)(C)C",
                           "[C+](C)(C)(C)(C)(CC)",
                           "C(C)(C)(C)(CCC)",
                           "CC(C)CCCC",
                           "CCCCCCC",
                           "CCCCCC",
                           "CCCCCCC",
                           "C1CCCC1",
                           "C1CCCC1C",
                           "C1CCCCC1",
                           "C1CCCCCC1",
                           "CCCCC",
                           "CC=CCCC",
                           "C1=CN=CN1",
                           "c1ccccc1",
                           "c1cnccc1",
                           "n1ccncc1",
                           "CCCCF",
                           "CCCCCl",
                           "CCCCBr",
                           "CCC(C)C1CCC(C)CC1",
                           "CC(C)CC1CCC(C)CC1",
                           "CC(C)C1CCC(C)CCC1",
                           "EOS"};
    double ddata[] = {0.667000, 1.240000, 2.344400, 4.167000, 6.000000,
                      5.000000, 6.000000, 1.440000, 1.633000, 2.222000,
                      3.061000, 4.000000, 4.740000, 0.884000, 1.606000,
                      1.552000, 1.500000, 3.930000, 4.290000, 4.480000,
                      4.133000, 4.133000, 4.133000};
    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcKappa2(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}
void testKappa3() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test calculation of Kappa3." << std::endl;

  {
    std::string sdata[] = {
        "C[C+](C)(C)(C)C(C)(C)C", "CCC(C)C(C)(C)(CC)", "CCC(C)CC(C)CC",
        "CC(C)CCC(C)CC",          "CC(C)CCCC(C)C",     "CCC(C)C1CCC(C)CC1",
        "CC(C)CC1CCC(C)CC1",      "CC(C)C1CCC(C)CCC1", "EOS"};
    double ddata[] = {2.000000, 2.380000, 4.500000, 5.878000,
                      8.000000, 2.500000, 3.265000, 2.844000};
    unsigned int idx = 0;
    while (sdata[idx] != "EOS") {
      ROMol *mol;
      mol = SmilesToMol(sdata[idx]);
      TEST_ASSERT(mol);
      double v = calcKappa3(*mol);
      TEST_ASSERT(feq(v, ddata[idx], 0.002));
      ++idx;
      delete mol;
    }
  }
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testRingDescriptors() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test ring descriptors" << std::endl;

  std::string fName = getenv("RDBASE");
  fName += "/Code/GraphMol/Descriptors/test_data/aid466.trunc.sdf";
  SDMolSupplier suppl(fName);
  while (!suppl.atEnd()) {
    ROMol *mol = suppl.next();
    TEST_ASSERT(mol);
    unsigned int iv;
    mol->getProp("NumRings", iv);
    TEST_ASSERT(iv == calcNumRings(*mol));
    mol->getProp("NumAromaticRings", iv);
    TEST_ASSERT(iv == calcNumAromaticRings(*mol));
    mol->getProp("NumSaturatedRings", iv);
    TEST_ASSERT(iv == calcNumSaturatedRings(*mol));
    mol->getProp("NumAromaticHeterocycles", iv);
    TEST_ASSERT(iv == calcNumAromaticHeterocycles(*mol));
    mol->getProp("NumAromaticCarbocycles", iv);
    TEST_ASSERT(iv == calcNumAromaticCarbocycles(*mol));
    mol->getProp("NumSaturatedHeterocycles", iv);
    TEST_ASSERT(iv == calcNumSaturatedHeterocycles(*mol));
    mol->getProp("NumSaturatedCarbocycles", iv);
    TEST_ASSERT(iv == calcNumSaturatedCarbocycles(*mol));
    mol->getProp("NumAliphaticRings", iv);
    TEST_ASSERT(iv == calcNumAliphaticRings(*mol));
    mol->getProp("NumAliphaticHeterocycles", iv);
    TEST_ASSERT(iv == calcNumAliphaticHeterocycles(*mol));
    mol->getProp("NumAliphaticCarbocycles", iv);
    TEST_ASSERT(iv == calcNumAliphaticCarbocycles(*mol));
    mol->getProp("NumHeterocycles", iv);
    TEST_ASSERT(iv == calcNumHeterocycles(*mol));

    delete mol;
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testMiscCountDescriptors() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test other count descriptors." << std::endl;

  {
    ROMol *mol;
    mol = SmilesToMol("OCCO");
    TEST_ASSERT(feq(calcFractionCSP3(*mol), 1.0, 0.001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("OO");
    TEST_ASSERT(feq(calcFractionCSP3(*mol), 0.0, 0.001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("OC=CO");
    TEST_ASSERT(feq(calcFractionCSP3(*mol), 0.0, 0.001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("CCC=C");
    TEST_ASSERT(feq(calcFractionCSP3(*mol), 0.5, 0.001));
    delete mol;
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testMQNs() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test MQN" << std::endl;

  {
    unsigned int tgt[42] = {98, 0,  4,  0, 0,  1,  0,  3,  9, 5, 4, 124, 29, 3,
                            0,  66, 35, 0, 25, 30, 21, 2,  2, 0, 0, 6,   12, 6,
                            0,  70, 26, 0, 0,  0,  2,  16, 0, 0, 0, 0,   10, 5};
    // Figure out which rotatable bond version we are using
    //  and update the test accordingly
    {
      const bool sanitize = true;
      ROMol *test_mol =
          SmilesToMol("CC(C)(C)c1cc(O)c(cc1O)C(C)(C)C", 0, sanitize);
      if (calcNumRotatableBonds(*test_mol) == 2) {
        tgt[18] = 26;
      }
      delete test_mol;
    }
    std::vector<unsigned int> accum(42, 0);

    std::string fName = getenv("RDBASE");
    fName += "/Code/GraphMol/Descriptors/test_data/aid466.trunc.sdf";
    SDMolSupplier suppl(fName);
    while (!suppl.atEnd()) {
      ROMol *mol = suppl.next();
      TEST_ASSERT(mol);
      std::vector<unsigned int> v = calcMQNs(*mol);
      TEST_ASSERT(v.size() == 42);
      for (unsigned int i = 0; i < 42; ++i) {
        accum[i] += v[i];
      }
      delete mol;
    }
    for (unsigned int i = 0; i < 42; ++i) {
      if (accum[i] != tgt[i]) {
        std::cerr << " !! " << i << " " << accum[i] << "!=" << tgt[i]
                  << std::endl;
      }
      TEST_ASSERT(accum[i] == tgt[i]);
    }
  }
  {  // github #623
    ROMol *mol;
    mol = SmilesToMol("CC*");
    TEST_ASSERT(mol);
    std::vector<unsigned int> v = calcMQNs(*mol);
    TEST_ASSERT(v[11] == 2);
    delete mol;
  }
  {  // github #623
    ROMol *mol;
    mol = SmilesToMol("[2H][2H]");
    TEST_ASSERT(mol);
    std::vector<unsigned int> v = calcMQNs(*mol);
    TEST_ASSERT(v[11] == 0);
    delete mol;
  }
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testGitHubIssue56() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test GitHub Issue 56." << std::endl;

  {
    ROMol *mol;
    mol = SmilesToMol("[H+]");
    double mw;
    mw = calcAMW(*mol);
    TEST_ASSERT(feq(mw, 1.008, 0.001));
    mw = calcExactMW(*mol);
    TEST_ASSERT(feq(mw, 1.0078, 0.001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("[2H+]");
    double mw;
    mw = calcAMW(*mol);
    TEST_ASSERT(feq(mw, 2.014, 0.001));
    mw = calcExactMW(*mol);
    TEST_ASSERT(feq(mw, 2.014, 0.001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("[H]");
    double mw;
    mw = calcAMW(*mol);
    TEST_ASSERT(feq(mw, 1.008, 0.001));
    mw = calcExactMW(*mol);
    TEST_ASSERT(feq(mw, 1.0078, 0.001));
    delete mol;
  }
  {
    ROMol *mol;
    mol = SmilesToMol("[2H]");
    double mw;
    mw = calcAMW(*mol);
    TEST_ASSERT(feq(mw, 2.014, 0.001));
    mw = calcExactMW(*mol);
    TEST_ASSERT(feq(mw, 2.014, 0.001));
    delete mol;
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testGitHubIssue92() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog)
      << "    Test Github92: Bad Crippen atom type for pyrrole H." << std::endl;

  {
    RWMol *mol;
    mol = SmilesToMol("c1cccn1[H]", 0, 0);
    TEST_ASSERT(mol);
    MolOps::sanitizeMol(*mol);
    TEST_ASSERT(mol->getNumAtoms() == 6);
    std::vector<double> logp(mol->getNumAtoms());
    std::vector<double> mr(mol->getNumAtoms());
    getCrippenAtomContribs(*mol, logp, mr, true);
    TEST_ASSERT(feq(logp[5], 0.2142, .001));
    delete mol;
  }
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testGitHubIssue463() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog)
      << "    Test Github362: order dependence  in Kier-Hall descriptors."
      << std::endl;

  {  // start with the hall-kier delta values:
    RWMol *mol;
    mol = SmilesToMol("O=C(Nc1nccs1)NC(C1CC1)C");
    TEST_ASSERT(mol);
    TEST_ASSERT(mol->getNumAtoms() == 14);
    unsigned int order[] = {0, 11, 8, 7, 2, 4, 5, 13, 10, 12, 9, 3, 1, 6};
    std::vector<unsigned int> nVect(
        order, order + sizeof(order) / sizeof(unsigned int));
    ROMol *nm = MolOps::renumberAtoms(*mol, nVect);
    TEST_ASSERT(nm);

    std::vector<double> hkds(mol->getNumAtoms());
    Descriptors::detail::hkDeltas(*mol, hkds, true);
    std::vector<double> nhkds(mol->getNumAtoms());
    Descriptors::detail::hkDeltas(*nm, nhkds, true);

    for (unsigned int j = 0; j < mol->getNumAtoms(); ++j) {
      TEST_ASSERT(feq(hkds[nVect[j]], nhkds[j]));
    }

    delete mol;
    delete nm;
  }

  {  // now chiNv values, where the problem was reported:

    RWMol *mol;
    mol = SmilesToMol("O=C(Nc1nccs1)NC(C1CC1)C");
    TEST_ASSERT(mol);
    TEST_ASSERT(mol->getNumAtoms() == 14);
    unsigned int order[] = {0, 11, 8, 7, 2, 4, 5, 13, 10, 12, 9, 3, 1, 6};
    std::vector<unsigned int> nVect(
        order, order + sizeof(order) / sizeof(unsigned int));
    ROMol *nm = MolOps::renumberAtoms(*mol, nVect);
    TEST_ASSERT(nm);

    double cv = calcChi3v(*mol);
    double ncv = calcChi3v(*nm);

    TEST_ASSERT(feq(cv, ncv));

    delete mol;
    delete nm;
  }

  {  // now chiNn values, where the problem was reported:

    RWMol *mol;
    mol = SmilesToMol("O=C(Nc1nccs1)NC(C1CC1)C");
    TEST_ASSERT(mol);
    TEST_ASSERT(mol->getNumAtoms() == 14);
    unsigned int order[] = {0, 11, 8, 7, 2, 4, 5, 13, 10, 12, 9, 3, 1, 6};
    std::vector<unsigned int> nVect(
        order, order + sizeof(order) / sizeof(unsigned int));
    ROMol *nm = MolOps::renumberAtoms(*mol, nVect);
    TEST_ASSERT(nm);

    double cv = calcChi3n(*mol);
    double ncv = calcChi3n(*nm);

    TEST_ASSERT(feq(cv, ncv));

    delete mol;
    delete nm;
  }

  {  // the root cause was handling of rings
    RWMol *mol;
    mol = SmilesToMol("C1CC1");
    TEST_ASSERT(mol);
    TEST_ASSERT(mol->getNumAtoms() == 3);
    std::vector<double> hkds(mol->getNumAtoms());
    Descriptors::detail::hkDeltas(*mol, hkds, true);

    double cv = calcChi3v(*mol);
    TEST_ASSERT(feq(hkds[0], hkds[1]));
    TEST_ASSERT(feq(hkds[1], hkds[2]));
    TEST_ASSERT(feq(cv, hkds[0] * hkds[0] * hkds[0]));

    delete mol;
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testSpiroAndBridgeheads() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog)
      << "    Test calculation of spiro and bridgehead counts." << std::endl;

  {
    RWMol *mol;
    mol = SmilesToMol("C1CC2CCC1CC2");
    TEST_ASSERT(mol);

    unsigned int nSpiro = Descriptors::calcNumSpiroAtoms(*mol);
    TEST_ASSERT(nSpiro == 0);

    unsigned int nBridgehead = Descriptors::calcNumBridgeheadAtoms(*mol);
    TEST_ASSERT(nBridgehead == 2);

    delete mol;
  }

  {
    RWMol *mol;
    mol = SmilesToMol("C1CCC2(C1)CC1CCC2CC1");
    TEST_ASSERT(mol);

    unsigned int nSpiro = Descriptors::calcNumSpiroAtoms(*mol);
    TEST_ASSERT(nSpiro == 1);

    unsigned int nBridgehead = Descriptors::calcNumBridgeheadAtoms(*mol);
    TEST_ASSERT(nBridgehead == 2);

    delete mol;
  }

  {
    RWMol *mol;
    mol = SmilesToMol("CC1(C)CC2(C)CCC1(C)CC2");
    TEST_ASSERT(mol);

    unsigned int nSpiro = Descriptors::calcNumSpiroAtoms(*mol);
    TEST_ASSERT(nSpiro == 0);

    unsigned int nBridgehead = Descriptors::calcNumBridgeheadAtoms(*mol);
    TEST_ASSERT(nBridgehead == 2);

    delete mol;
  }

  {  // test the atoms parameter
    RWMol *mol;
    mol = SmilesToMol("C1CCC2(C1)CC1CCC2CC1");
    TEST_ASSERT(mol);

    std::vector<unsigned int> atoms;

    unsigned int nSpiro = Descriptors::calcNumSpiroAtoms(*mol, &atoms);
    TEST_ASSERT(nSpiro == 1);
    TEST_ASSERT(atoms.size() == nSpiro);
    TEST_ASSERT(std::find(atoms.begin(), atoms.end(), 3) != atoms.end());

    atoms.clear();
    unsigned int nBridgehead =
        Descriptors::calcNumBridgeheadAtoms(*mol, &atoms);
    TEST_ASSERT(nBridgehead == 2);
    TEST_ASSERT(atoms.size() == nBridgehead);
    TEST_ASSERT(std::find(atoms.begin(), atoms.end(), 9) != atoms.end());
    TEST_ASSERT(std::find(atoms.begin(), atoms.end(), 6) != atoms.end());

    delete mol;
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testGitHubIssue694() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog)
      << "    Test Github694: ExactMolWt ignoring the mass of the electron"
      << std::endl;

  {
    ROMol *mol = SmilesToMol("[35Cl]");
    TEST_ASSERT(mol);
    double mw = calcExactMW(*mol);
    TEST_ASSERT(
        feq(mw, PeriodicTable::getTable()->getMassForIsotope(17, 35), .000001));
    delete mol;
    mol = SmilesToMol("[35Cl-]");
    TEST_ASSERT(mol);
    mw = calcExactMW(*mol);
    TEST_ASSERT(feq(mw,
                    PeriodicTable::getTable()->getMassForIsotope(17, 35) +
                        constants::electronMass,
                    .000001));
    delete mol;
    mol = SmilesToMol("[35Cl+]");
    TEST_ASSERT(mol);
    mw = calcExactMW(*mol);
    TEST_ASSERT(feq(mw,
                    PeriodicTable::getTable()->getMassForIsotope(17, 35) -
                        constants::electronMass,
                    .000001));
    delete mol;
    mol = SmilesToMol("[35Cl+2]");
    TEST_ASSERT(mol);
    mw = calcExactMW(*mol);
    TEST_ASSERT(feq(mw,
                    PeriodicTable::getTable()->getMassForIsotope(17, 35) -
                        2 * constants::electronMass,
                    .000001));
    delete mol;
  }
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testProperties() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Testing Properties and KitchenSink"
                        << std::endl;

  {
    std::vector<std::string> all_names = Properties::getAvailableProperties();

    Properties sink;
    std::vector<std::string> names = sink.getPropertyNames();

    TEST_ASSERT(names == all_names);

    RWMol *mol;
    mol = SmilesToMol("C1CCC2(C1)CC1CCC2CC1");
    std::vector<double> props = sink.computeProperties(*mol);

    std::vector<double> res;
    for (const auto& prop : all_names) {
      std::vector<std::string> props;
      props.push_back(prop);
      Properties property(props);
      std::vector<double> computedProps = property.computeProperties(*mol);
      TEST_ASSERT(computedProps.size() == 1);
      res.push_back(computedProps[0]);
    }

    TEST_ASSERT(props == res);
    delete mol;
  }
  {
    std::vector<std::string> names;
    names.emplace_back("NumSpiroAtoms");
    names.emplace_back("NumBridgeheadAtoms");
    Properties sink(names);
    std::vector<std::string> sink_names = sink.getPropertyNames();
    TEST_ASSERT(names == sink_names);
    std::vector<double> res;
    res.push_back(1.);
    res.push_back(2.);

    RWMol *mol;
    mol = SmilesToMol("C1CCC2(C1)CC1CCC2CC1");
    TEST_ASSERT(mol);
    // Test annotation as well
    std::vector<double> props = sink.computeProperties(*mol, true);

    TEST_ASSERT(props == res);
    TEST_ASSERT(mol->getProp<double>("NumSpiroAtoms") == 1.);
    TEST_ASSERT(mol->getProp<double>("NumBridgeheadAtoms") == 2.);
    delete mol;

    mol = SmilesToMol("C1CCC2(C1)CC1CCC2CC1");
    TEST_ASSERT(mol);
    // Test annotation as well
    sink.annotateProperties(*mol);
    TEST_ASSERT(mol->getProp<double>("NumSpiroAtoms") == 1.);
    TEST_ASSERT(mol->getProp<double>("NumBridgeheadAtoms") == 2.);
    delete mol;
  }

  {
    try {
      std::vector<std::string> names;
      names.emplace_back("FakeName");
      Properties sink(names);
      TEST_ASSERT(0);  // should throw
    } catch (KeyErrorException &) {
      BOOST_LOG(rdErrorLog)
          << "---Caught keyerror (bad property name)---" << std::endl;
    }
  }
}

void testPropertyQueries() {
  RWMol *mol;
  mol = SmilesToMol("C1CCC2(C1)CC1CCC2CC1");
  {
    PROP_RANGE_QUERY *query = makePropertyRangeQuery("exactmw", 50, 300);
    TEST_ASSERT(query->Match(*mol));
    delete query;
  }
  {
    PROP_RANGE_QUERY *query = makePropertyRangeQuery("exactmw", 1000, 10300);
    TEST_ASSERT(!query->Match(*mol));
    delete query;
  }

  {
    auto pq = std::unique_ptr<PROP_EQUALS_QUERY>(
        makePropertyQuery<PROP_EQUALS_QUERY>("exactmw", calcExactMW(*mol)));
    TEST_ASSERT(pq->Match(*mol));

    pq = std::unique_ptr<PROP_EQUALS_QUERY>(
        makePropertyQuery<PROP_EQUALS_QUERY>("NumHBA", calcNumHBA(*mol)));
    TEST_ASSERT(pq->Match(*mol));

    pq =
        std::unique_ptr<PROP_EQUALS_QUERY>(makePropertyQuery<PROP_EQUALS_QUERY>(
            "lipinskiHBA", calcLipinskiHBA(*mol)));
    TEST_ASSERT(pq->Match(*mol));
  }
  delete mol;
}

void testStereoCounting() {
  const bool debugParse = false;
  const bool sanitize = false;
  ROMol *m = SmilesToMol("NC(C)(F)C(=O)O", debugParse, sanitize);
  TEST_ASSERT(!m->hasProp(common_properties::_StereochemDone));

  std::vector<std::string> names;
  names.emplace_back("NumAtomStereoCenters");
  names.emplace_back("NumUnspecifiedAtomStereoCenters");
  Properties prop(names);

  try {
    prop.computeProperties(*m);
    TEST_ASSERT(0);  // didn't catch exception
  } catch (ValueErrorException &) {
    BOOST_LOG(rdErrorLog) << "---Caught stereo value error---" << std::endl;
  }

  delete m;
  m = SmilesToMol("NC(C)(F)C(=O)O", sanitize);
  std::vector<double> res = prop.computeProperties(*m);
  TEST_ASSERT(res[0] == 1);
  TEST_ASSERT(res[1] == 1);
  delete m;
}

void testUSRDescriptor() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test USR Descriptor" << std::endl;
  std::vector<double> descriptor(12);

  // no conformers
  ROMol *mol = SmilesToMol("C1CCCCC1");
  bool ok = false;
  try {
    USR(*mol, descriptor);
  } catch (ConformerException &) {
    ok = true;
  }
  delete mol;
  TEST_ASSERT(ok);

  // number of atoms < 3
  mol = SmilesToMol("CC");
  ok = false;
  try {
    USR(*mol, descriptor);
  } catch (ValueErrorException &) {
    ok = true;
  }
  delete mol;
  TEST_ASSERT(ok);

  // DESCRIPTOR
  // comparing to results produced by Adrian Schreyer's code
  // http://hg.adrianschreyer.eu/usrcat/src/70e075d93cd25370e7ef93301d0e28d49a0851c2/usrcat/geometry.py?at=default
  double refValues[12] = {2.37938524,  0.62181927, -0.89089872, 2.63773456,
                          1.1577952,   -0.6937349, 3.38248245,  1.59816952,
                          -0.72933115, 3.38248245, 1.59816952,  -0.72933115};
  std::vector<double> refUSR(refValues,
                             refValues + sizeof(refValues) / sizeof(double));
  std::string rdbase = getenv("RDBASE");
  std::string fname1 =
      rdbase + "/Code/GraphMol/Descriptors/test_data/cyclohexane.mol";
  mol = MolFileToMol(fname1, true, false, true);
  std::vector<double> myUSR(12);
  USR(*mol, myUSR);
  for (unsigned int i = 0; i < myUSR.size(); ++i) {
    TEST_ASSERT(feq(myUSR[i], refUSR[i]));
  }
  delete mol;
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testUSRScore() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test USR Score" << std::endl;
  // SCORE
  // descriptors and reference score from JCC (2007), 28, 1711-1723.
  double m1[12] = {4.44, 2.98,  1.04,   4.55, 4.70,  0.23,
                   8.30, 16.69, -22.97, 7.37, 15.64, 0.51};
  double m2[12] = {4.39, 3.11,  1.36,   4.50, 4.44,  0.09,
                   8.34, 16.78, -23.20, 7.15, 16.52, 0.13};
  std::vector<double> d1(m1, m1 + sizeof(m1) / sizeof(double));
  std::vector<double> d2(m2, m2 + sizeof(m2) / sizeof(double));
  std::vector<double> weights(1, 1.0);
  TEST_ASSERT(feq(calcUSRScore(d1, d2, weights), 0.812, 0.001));

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testUSRCATDescriptor() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test USRCAT Descriptor" << std::endl;
  std::vector<double> descriptor(12);

  // no conformers
  ROMol *mol = SmilesToMol("C1CCCCC1");
  bool ok = false;
  try {
    USR(*mol, descriptor);
  } catch (ConformerException &) {
    ok = true;
  }
  TEST_ASSERT(ok);

  // number of atoms < 3
  mol = SmilesToMol("CC");
  ok = false;
  try {
    USR(*mol, descriptor);
  } catch (ValueErrorException &) {
    ok = true;
  }
  TEST_ASSERT(ok);

  // DESCRIPTOR
  // comparing to results produced by Adrian Schreyer's code
  // http://hg.adrianschreyer.eu/usrcat/src/70e075d93cd2?at=default
  double refValues[60] = {
      2.37938524, 0.62181927, -0.89089872, 2.63773456, 1.1577952,  -0.6937349,
      3.38248245, 1.59816952, -0.72933115, 3.38248245, 1.59816952, -0.72933115,
      1.50000000, 0.00000000, -0.77827171, 1.86602540, 1.00893468, -0.89059233,
      3.02358914, 1.02718486, -0.64081261, 3.02358914, 1.02718486, -0.64081261,
      0.00000000, 0.00000000, 0.00000000,  0.00000000, 0.00000000, 0.00000000,
      0.00000000, 0.00000000, 0.00000000,  0.00000000, 0.00000000, 0.00000000,
      0.00000000, 0.00000000, 0.00000000,  0.00000000, 0.00000000, 0.00000000,
      0.00000000, 0.00000000, 0.00000000,  0.00000000, 0.00000000, 0.00000000,
      0.00000000, 0.00000000, 0.00000000,  0.00000000, 0.00000000, 0.00000000,
      0.00000000, 0.00000000, 0.00000000,  0.00000000, 0.00000000, 0.00000000};

  std::vector<double> refUSR(refValues,
                             refValues + sizeof(refValues) / sizeof(double));
  std::string rdbase = getenv("RDBASE");
  std::string fname1 =
      rdbase + "/Code/GraphMol/Descriptors/test_data/cyclohexane.mol";
  mol = MolFileToMol(fname1, true, false, true);
  std::vector<std::vector<unsigned int>> atomIds;
  std::vector<double> myUSR(12);
  USRCAT(*mol, myUSR, atomIds);
  for (unsigned int i = 0; i < myUSR.size(); ++i) {
    TEST_ASSERT(feq(myUSR[i], refUSR[i]));
  }

  atomIds.resize(4);
  unsigned int h[6] = {0, 1, 2, 3, 4, 5};
  std::vector<unsigned int> hydrophobic(h,
                                        h + sizeof(h) / sizeof(unsigned int));
  atomIds[0] = hydrophobic;
  USRCAT(*mol, myUSR, atomIds);
  for (unsigned int i = 0; i < myUSR.size(); ++i) {
    TEST_ASSERT(feq(myUSR[i], refUSR[i]));
  }

  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testGithub1702() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog) << "    Test Github #1702: AUTOCORR2D.h not installed "
                           "unless RDK_BUILD_DESCRIPTORS3D but is required"
                        << std::endl;
  std::vector<double> descriptor(12);

  ROMol *mol = SmilesToMol("C1CCCCC1");
  std::vector<double> dvals;
  AUTOCORR2D(*mol, dvals);
  TEST_ASSERT(dvals.size() == 192);
  delete mol;
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testGithub1973() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog)
      << "    Test Github #1973: support P and S terms for TPSA calculation"
      << std::endl;

  {
    // Some examples from the original publication, including the non S and P
    // values calculated with RDKit (which match what's in table 3 of the
    // original publication) and then values with S and P contributions added by
    // hand.
    std::vector<std::string> smiles = {
        "Cc1ccccc1N1C(=O)c2cc(S(N)(=O)=O)c(Cl)cc2NC1C", "O=C(O)P(=O)(O)O",
        "O=C(O)c1cc(N=Nc2ccc(S(=O)(=O)Nc3ccccn3)cc2)ccc1O"};
    std::vector<double> orig_tpsa = {92.50, 94.83, 141.31};
    std::vector<double> new_tpsa = {100.88, 104.64, 149.69};
    for (unsigned int i = 0; i < smiles.size(); ++i) {
      std::unique_ptr<ROMol> mol(SmilesToMol(smiles[i]));
      TEST_ASSERT(mol);
      auto oTPSA = calcTPSA(*mol);
      TEST_ASSERT(feq(oTPSA, orig_tpsa[i], 0.01));
      auto nTPSA = calcTPSA(*mol, true, true);
      // std::cerr << smiles[i] << " " << new_tpsa[i] << " " << nTPSA <<
      // std::endl;
      TEST_ASSERT(feq(nTPSA, new_tpsa[i], 0.01));
    }
  }
  {
    // Some examples constructed by hand
    std::vector<std::string> smiles = {"c1ccccc1S", "c1cscc1",    "CC(=S)C",
                                       "CSC",       "CS(=O)C",    "CP(C)C",
                                       "CP=O",      "CP(C)(C)=O", "C[PH](C)=O"};
    std::vector<double> orig_tpsa = {0,   0,     0,     0,    17.07,
                                     0.0, 17.07, 17.07, 17.07};
    std::vector<double> new_tpsa = {38.8,  28.24, 32.09, 25.30, 36.28,
                                    13.59, 51.21, 26.88, 40.54};
    for (unsigned int i = 0; i < smiles.size(); ++i) {
      std::unique_ptr<ROMol> mol(SmilesToMol(smiles[i]));
      TEST_ASSERT(mol);
      auto oTPSA = calcTPSA(*mol);
      TEST_ASSERT(feq(oTPSA, orig_tpsa[i], 0.01));
      auto nTPSA = calcTPSA(*mol, true, true);
      // std::cerr << smiles[i] << " " << new_tpsa[i] << " " << nTPSA <<
      // std::endl;
      TEST_ASSERT(feq(nTPSA, new_tpsa[i], 0.01));
    }
  }
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

void testGithub2948() {
  BOOST_LOG(rdErrorLog) << "-------------------------------------" << std::endl;
  BOOST_LOG(rdErrorLog)
      << "    Test Github #2948: Empty molecule has non-zero LabuteASA"
      << std::endl;

  {
    ROMol m;
    auto asa = calcLabuteASA(m);
    TEST_ASSERT(feq(asa, 0, 0.0001))
  }
  BOOST_LOG(rdErrorLog) << "  done" << std::endl;
}

//-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
//
//-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
int main() {
  RDLog::InitLogs();
#if 1
  test1();
  test2();
  testIssue262();
  test3();
  test3a();
  testLabute();
  testTPSA();
  testLipinski1();
  testVSADescriptors();
  testMolFormula();
  testIssue3415534();
  testIssue3433771();
  testMultiThread();
  testIssue252();
  testChiVs();
  testChiNs();
  testHallKierAlpha();
  testKappa1();
  testKappa2();
  testKappa3();
  testCrippenContribs();
  testRingDescriptors();
  testMiscCountDescriptors();
  testMQNs();
  testGitHubIssue56();
  testGitHubIssue92();
  testGitHubIssue463();
  testSpiroAndBridgeheads();
  testGitHubIssue694();
  testProperties();
  testPropertyQueries();
  testStereoCounting();
  testUSRDescriptor();
  testGithub1702();
#endif
  testGithub1973();
  testGithub2948();
}


/*!@file
 * @copyright This code is licensed under the 3-clause BSD license.
 *   Copyright ETH Zurich, Laboratory of Physical Chemistry, Reiher Group.
 *   See LICENSE.txt for details.
 */

#include <boost/test/unit_test.hpp>

#include <algorithm>
#include <vector>
#include <cassert>
#include <functional>
#include <numeric>

#include "Molassembler/Shapes/Data.h"
#include "Molassembler/Shapes/Properties.h"

#include "Molassembler/Stereopermutation/Manipulation.h"
#include "Molassembler/Stereopermutation/RotationEnumerator.h"

#include "Molassembler/Temple/Functional.h"
#include "Molassembler/Temple/Random.h"
#include "Molassembler/Temple/Stringify.h"
#include "Molassembler/Temple/constexpr/LogicalOperatorTests.h"

using namespace Scine::Molassembler;
using namespace Stereopermutations;

template<typename T>
std::ostream& operator << (std::ostream& os, const std::vector<T>& vector) {
  for(unsigned i = 0; i < vector.size(); i++) {
    os << vector[i];
    if(i != vector.size() - 1) {
      os << ", ";
    }
  }

  return os;
}

Shapes::Vertex operator "" _v(unsigned long long v) {
  return Shapes::Vertex(v);
}

// create instances of all symmetries with monodentate ligands
BOOST_AUTO_TEST_CASE(StereopermutationInstantiation, *boost::unit_test::label("Stereopermutations")) {
  for(const auto& shape: Shapes::allShapes) {
    const unsigned S = Shapes::size(shape);
    if(S > 8) {
      continue;
    }

    Stereopermutation testStereopermutation(std::vector<char>(S, 'A'));
  }
}

BOOST_AUTO_TEST_CASE(StereopermutationRotation, *boost::unit_test::label("Stereopermutations")) {
  // Octahedron ship-screw like cis-cis-cis
  const Stereopermutation asymm {
    {'A', 'B', 'C', 'D', 'E', 'F'},
    {{0_v, 1_v}, {2_v, 4_v}, {3_v, 5_v}}
  };

  std::vector<Shapes::Vertex> C4z {{3_v, 0_v, 1_v, 2_v, 4_v, 5_v}};

  const Stereopermutation expected {
    {'D', 'A', 'B', 'C', 'E', 'F'},
    {{0_v, 5_v}, {1_v, 2_v}, {3_v, 4_v}}
  };

  const Stereopermutation rotated = asymm.applyPermutation(C4z);

  BOOST_CHECK_MESSAGE(
    rotated == expected,
    "Expected rotation of " << asymm.toString() << " with C4z in octahedron to\n"
    << expected.toString() << ", got\n" << rotated.toString()
  );
}

BOOST_AUTO_TEST_CASE(RotationCorrectness, *boost::unit_test::label("Stereopermutations")) {
  Stereopermutation testCase {
    {'A', 'A', 'C', 'D', 'B', 'B'},
    {{0_v, 5_v}, {1_v, 4_v}}
  };

  auto isAorB = [](const char& test) -> bool {
    return (test == 'A' || test == 'B');
  };

  const auto allRotations = generateAllRotations(testCase, Shapes::Shape::Octahedron);
  // Make sure the links still refer to A or B characters
  for(const auto& rotation : allRotations) {
    BOOST_CHECK(
      Temple::all_of(
        rotation.links,
        [&](const auto& link) -> bool {
          return (
            isAorB(rotation.characters.at(link.first))
            && isAorB(rotation.characters.at(link.second))
          );
        }
      )
    );
  }
}

BOOST_AUTO_TEST_CASE(OctahedralSymmetryCorrectness, *boost::unit_test::label("Stereopermutations")) {
  Stereopermutation octahedralInstance(
    {'A', 'B', 'C', 'D', 'E', 'F'}
  );

  BOOST_CHECK_EQUAL(
    generateAllRotations(octahedralInstance, Shapes::Shape::Octahedron).size(),
    24 // 4 C4 cases on each of 6 A position selections
  );
}

BOOST_AUTO_TEST_CASE(BugfixTests, *boost::unit_test::label("Stereopermutations")) {
  Stereopermutation a {
    {'A', 'A', 'A', 'B', 'B', 'B'},
    {{2_v, 3_v}, {1_v, 4_v}, {0_v, 5_v}}
  };
  Stereopermutation b {
    {'A', 'A', 'B', 'A', 'B', 'B'},
    {{3_v, 5_v}, {1_v, 2_v}, {0_v, 4_v}}
  };

  // one and only one of the following can be true for any Stereopermutations a and b
  BOOST_CHECK(Temple::testLogicalOperators(a, b));

  BOOST_CHECK(rotationallySuperimposable(a, b, Shapes::Shape::Octahedron));
  BOOST_CHECK(rotationallySuperimposable(b, a, Shapes::Shape::Octahedron));

  /* Contrived example of two that have inconsistent logical operators, just
   * reordered op pairs. Will evaluate == but also < w/ current impl.
   */
  Stereopermutation c {
    {'A', 'A', 'A', 'B', 'B', 'B'},
    {{2_v, 3_v}, {1_v, 4_v}, {0_v, 5_v}}
  };
  Stereopermutation d {
    {'A', 'A', 'A', 'B', 'B', 'B'},
    {{1_v, 4_v}, {2_v, 3_v}, {0_v, 5_v}}
  };

  BOOST_CHECK(Temple::testLogicalOperators(c, d));
  BOOST_CHECK(rotationallySuperimposable(c, d, Shapes::Shape::Octahedron));
  BOOST_CHECK(rotationallySuperimposable(d, c, Shapes::Shape::Octahedron));
}

using Characters = Stereopermutation::CharacterOccupation;
using PairSet = std::vector<
  std::pair<unsigned, unsigned>
>;

struct TestCase {
  Characters characters;
  PairSet pairs;
  unsigned expectedUnique;
};

Stereopermutation::OrderedLinks makeLinks(const PairSet& pairs) {
  return Temple::map(
    pairs,
    [](const std::pair<unsigned, unsigned>& v) -> Stereopermutation::Link {
      return std::make_pair(
        Shapes::Vertex(v.first),
        Shapes::Vertex(v.second)
      );
    }
  );
}

void runTestsWithCounts(
  const Shapes::Shape& shape,
  const std::vector<TestCase>& testCases
) {
  for(const auto& testCase: testCases) {
    // instantiate
    const Stereopermutation stereopermutation {
      testCase.characters,
      makeLinks(testCase.pairs)
    };

    // The count of uniques in the tests are all without trans-arranged pairs!
    const auto unique = uniques(
      stereopermutation,
      shape,
      true // remove trans arranged pairs
    );

    // std::cout << stereopermutation.toString() << " unique weights: " << Temple::stringify(unique.weights) << "\n";

    BOOST_CHECK_MESSAGE(
      unique.list.size() == testCase.expectedUnique,
      "Mismatch: Expected " << testCase.expectedUnique
        << " stereopermutations for\n" << stereopermutation.toString() << " in " << Shapes::name(shape) << " shape, got "
        << unique.list.size() << " stereopermutations\n"
    );
  }
}

/* Tetrahedron tests */
BOOST_AUTO_TEST_CASE(MonodentateTetrahedral, *boost::unit_test::label("Stereopermutations")) {
  runTestsWithCounts(
    Shapes::Shape::Tetrahedron,
    {
      { // AAAA
        Characters(4, 'A'),
        {},
        1
      },
      { // A3B
        Characters({'A', 'A', 'A', 'B'}),
        {},
        1
      },
      { // A2B2
        Characters({'A', 'A', 'B', 'B'}),
        {},
        1
      },
      { // A2BC
        Characters({'A', 'A', 'B', 'C'}),
        {},
        1
      },
      { // ABCD
        Characters({'A', 'B', 'C', 'D'}),
        {},
        2
      },
    }
  );
}

/* Square Planar tests */
BOOST_AUTO_TEST_CASE(MonodentateSquarePlanar, *boost::unit_test::label("Stereopermutations")) {
  runTestsWithCounts(
    Shapes::Shape::Square,
    {
      { // M_A
        Characters(4, 'A'),
        {},
        1
      },
      { // M_A3B
        Characters({'A', 'A', 'A', 'B'}),
        {},
        1
      },
      { // M_A2B2
        Characters({'A', 'A', 'B', 'B'}),
        {},
        2
      },
      { // M_A2BC
        Characters({'A', 'A', 'B', 'C'}),
        {},
        2
      },
      { // M_ABCD
        Characters({'A', 'B', 'C', 'D'}),
        {},
        3
      },
    }
  );
}

/* Octahedron tests */
/* Expected values taken from
 * Miessler, Gary L., Tarr, Donald A.: Inorganic Chemistry, Third Edition.
 * Do not know whether these are correct! They are "all calculated using a
 * computer program [...]."
 * The reference however is useful: WE Bennett, Inorg. Chem. 1969
 */
BOOST_AUTO_TEST_CASE(MonodentateOctahedron, *boost::unit_test::label("Stereopermutations")) {
  runTestsWithCounts(
    Shapes::Shape::Octahedron,
    {
      {
        Characters(6, 'A'),
        {},
        1
      },
      {
        Characters({'A', 'A', 'A', 'A', 'A', 'B'}),
        {},
        1
      },
      {
        Characters({'A', 'A', 'A', 'A', 'B', 'B'}),
        {},
        2
      },
      {
        Characters({'A', 'A', 'A', 'B', 'B', 'B'}),
        {},
        2
      },
      {
        Characters({'A', 'A', 'A', 'A', 'B', 'C'}),
        {},
        2
      },
      {
        Characters({'A', 'A', 'A', 'B', 'B', 'C'}),
        {},
        3
      },
      {
        Characters({'A', 'A', 'B', 'B', 'C', 'C'}),
        {},
        6
      },
      {
        Characters({'A', 'A', 'A', 'B', 'C', 'D'}),
        {},
        5
      },
      {
        Characters({'A', 'A', 'B', 'B', 'C', 'D'}),
        {},
        8
      },
      {
        Characters({'A', 'A', 'B', 'C', 'D', 'E'}),
        {},
        15
      },
      {
        Characters({'A', 'B', 'C', 'D', 'E', 'F'}),
        {},
        30
      }
    }
  );
}

BOOST_AUTO_TEST_CASE(MultidentateOctahedron, *boost::unit_test::label("Stereopermutations")) {
  runTestsWithCounts(
    Shapes::Shape::Octahedron,
    {
      { // M(A-A)_3
        Characters({'A', 'A', 'A', 'A', 'A', 'A'}),
        PairSet({{0, 1}, {2, 3}, {4, 5}}),
        2
        /* NOTE: besides the two cis-cis-cis enantiomers, there are
         * cis-cis-trans and trans-trans-trans isomers, these are removed by
         * default!
         */
      },
      { // M(A-B)_3
        Characters({'A', 'B', 'A', 'B', 'A', 'B'}),
        PairSet({{0, 1}, {2, 3}, {4, 5}}),
        4
      },
      { // M(A-B)_2 CD
        Characters({'A', 'B', 'A', 'B', 'C', 'D'}),
        PairSet({{0, 1}, {2, 3}}),
        11
      },
      { // M(A-A)(B-C)DE
        Characters({'A', 'A', 'B', 'C', 'D', 'E'}),
        PairSet({{0, 1}, {2, 3}}),
        10
      },
      { // M(A-B)(C-D)EF
        Characters({'A', 'B', 'C', 'D', 'E', 'F'}),
        PairSet({{0, 1}, {2, 3}}),
        20
      },
      { // M(A-B-A)CDE
        Characters({'A', 'B', 'A', 'C', 'D', 'E'}),
        PairSet({{0, 1}, {1, 2}}),
        9
      },
      { // M(A-B-C)_2
        Characters({'A', 'B', 'C', 'A', 'B', 'C'}),
        PairSet({{0, 1}, {1, 2}, {3, 4}, {4, 5}}),
        11
      },
      { // M(A-B-B-A)CD
        Characters({'A', 'B', 'B', 'A', 'C', 'D'}),
        PairSet({{0, 1}, {1, 2}, {2, 3}}),
        7
      },
      { // M(A-B-C-B-A)D
        Characters({'A', 'B', 'C', 'B', 'A', 'D'}),
        PairSet({{0, 1}, {1, 2}, {2, 3}, {3, 4}}),
        7
      }
    }
  );
}

BOOST_AUTO_TEST_CASE(OctahedralCaseWithoutTransRemoval, *boost::unit_test::label("Stereopermutations")) {
  const auto shape = Shapes::Shape::Octahedron;
  const auto characters = Characters(6, 'A');
  const auto pairs = PairSet({{0, 1}, {2, 3}, {4, 5}});

  const Stereopermutation stereopermutation {characters, makeLinks(pairs)};
  const auto unique = uniques(stereopermutation, shape);
  BOOST_CHECK_EQUAL(unique.list.size(), 4);
}

BOOST_AUTO_TEST_CASE(numUnlinkedStereopermutationsTest, *boost::unit_test::label("Stereopermutations")) {
  // Crosscheck number of unlinked stereopermutations with shapes
  for(const Shapes::Shape shape : Shapes::allShapes) {
    const unsigned S = Shapes::size(shape);
    if(S > 8) {
      continue;
    }

    std::vector<char> characters (S);
    for(unsigned nIdentical = 1; nIdentical < S; ++nIdentical) {
      // Populate characters for construction of a Stereopermutation
      for(unsigned i = 0; i < characters.size(); ++i) {
        characters.at(i) = 'A' + std::max(
          0,
          static_cast<int>(i) - static_cast<int>(nIdentical) + 1
        );
      }

      assert(
        std::count(
          std::begin(characters),
          std::end(characters),
          'A'
        ) == nIdentical
      );

      const Stereopermutation initialStereopermutation {characters};

      const unsigned uniquesCount = uniques(initialStereopermutation, shape).list.size();

      // shapes prediction
      const unsigned chemicalSymmetriesCount = Shapes::Properties::numUnlinkedStereopermutations(
        shape,
        nIdentical
      );

      BOOST_CHECK_MESSAGE(
        uniquesCount == chemicalSymmetriesCount,
        "stereopermutation and shapes differ in unique "
        << "stereopermutation counts for " << Shapes::name(shape)
        << " and " << nIdentical << " ligands: " << uniquesCount << " vs "
        << chemicalSymmetriesCount
      );
    }
  }
}


#include "deposit_service_impl.hpp"

#include "repositories/account_repository.hpp"
#include "repositories/deposit_repository.hpp"

#include "model/savings_deposit.hpp"
#include "model/fixed_deposit.hpp"
#include "model/current_deposit.hpp"
#include "model/exceptions_messages.hpp"

#include <boost/format.hpp>
#include <boost/uuid/uuid_io.hpp>

namespace Service {
namespace Impl {

    struct Withdrawer : public Model::DepositVisitor {

        static void withdraw(Model::Deposit& deposit, double amount) {
            Withdrawer withdrawer;
            withdrawer.ammount_ = amount;
            deposit.accept(withdrawer);
        }

        void visit(Model::SavingsDeposit& savings) override {
            double currentBalance = savings.getBalance();
            double minBalance = savings.getMinBalance();

            if(currentBalance - ammount_ < minBalance) {
                throw std::domain_error(Model::Exceptions::minBalanceViolation);
            }

            savings.withdraw(ammount_);
        }

        void visit(Model::FixedDeposit& entity) override {
            throw std::domain_error(Model::Exceptions::wrongFixedDepositOperation);
        }

        void visit(Model::CurrentDeposit& current) override {
            double currentBalance = current.getBalance();
            double newBalance = currentBalance - ammount_;

            if(newBalance < 0){
                double overdraftLimit = current.getOverdraftLimit();
                double allowedSum = currentBalance > 0 ? overdraftLimit : overdraftLimit + currentBalance;

                if(std::abs(newBalance) > allowedSum){
                    throw std::domain_error(Model::Exceptions::ovedraftOverflow);
                }
            }

            current.withdraw(ammount_);
        }

        double ammount_ = 0.0;
    };

    struct YearProcessor : public Model::DepositVisitor {
        static void process(Model::Deposit& deposit) {
            YearProcessor processor;
            deposit.accept(processor);
        }

        void visit(Model::SavingsDeposit& savings) override {
            savings.refill(savings.getBalance() * (savings.getRateOfInterest() / 100.0));
        }

        void visit(Model::FixedDeposit& entity) override {
            static double maturityInterest = 0.2;
            double maturityCharge = entity.getMaturity() * maturityInterest;
            entity.refill(entity.getBalance() * ((entity.getRateOfInterest() + maturityCharge) / 100.0));
            entity.mature();
        }

        void visit(Model::CurrentDeposit& current) override {
            // Current deposit has 0 rate of interest. The holder pays operational charge
            static const double operationalChargeInterest = 0.2;
            double operationalCharge = current.getOverdraftLimit() * operationalChargeInterest;
            current.withdraw(operationalCharge);
        }
    };

    DepositServiceImpl::DepositServiceImpl(
            Repository::DepositRepository& depositRepo, Repository::AccountRepository& accountRepo)
    : BaseClass(depositRepo), accountRepo_(accountRepo)
    {}

    Model::EntityID
    DepositServiceImpl::createCurrentDeposit(const Model::EntityID& ownerId, double initialBalance,
                                                             double overDraftLimit) {
        using namespace Model;
        auto owner = accountRepo_.resolveEntity(ownerId);

        if(!owner){
            throw std::domain_error((boost::format(Exceptions::unknownAccount) % boost::uuids::to_string(ownerId)).str());
        }

        return create<Model::CurrentDeposit>(generator_(), overDraftLimit, initialBalance, *owner);
    }

    Model::EntityID
    DepositServiceImpl::createSavingsDeposit(const Model::EntityID& ownerId, double initialBalance, double minBalance,
                                             double rateOfInterest) {
        using namespace Model;

        auto owner = accountRepo_.resolveEntity(ownerId);

        if(!owner){
            throw std::domain_error((boost::format(Exceptions::unknownAccount) % boost::uuids::to_string(ownerId)).str());
        }

        return create<Model::SavingsDeposit>(generator_(), minBalance, initialBalance, rateOfInterest, *owner);
    }

    Model::EntityID
    DepositServiceImpl::createFixedDeposit(const Model::EntityID& ownerId, double initialBalance, double rateOfInterest) {
        using namespace Model;

        auto owner = accountRepo_.resolveEntity(ownerId);
        if(!owner){
            throw std::domain_error((boost::format(Exceptions::unknownAccount) % boost::uuids::to_string(ownerId)).str());
        }

        return create<Model::FixedDeposit>(generator_(), *owner, initialBalance, rateOfInterest);
    }

    boost::optional<const Model::SavingsDeposit&> DepositServiceImpl::getSavingsDeposit(const Model::EntityID& depositId) const {
        return constCast(repository_.findSavingsDeposit(depositId));
    }

    boost::optional<const Model::CurrentDeposit&> DepositServiceImpl::getCurrentDeposit(
            const Model::EntityID &depositId) const {
        return constCast(repository_.findCurrentDeposit(depositId));
    }

    boost::optional<const Model::FixedDeposit&> DepositServiceImpl::getFixedDeposit(
            const Model::EntityID &depositId) const {
        return constCast(repository_.findFixedDeposit(depositId));
    }

    void DepositServiceImpl::refill(const Model::EntityID& depositID, double amount) {
        using namespace Model;

        if(amount <= 0) {
            throw std::domain_error(Model::Exceptions::negativeValue);
        }

        auto deposit = repository_.resolveEntity(depositID);

        if(!deposit) {
            throw std::domain_error((boost::format(Exceptions::unknownDeposit) % boost::uuids::to_string(depositID)).str());
        }

        if(Model::DepositCast<FixedDeposit>().cast(*deposit)){
            throw std::domain_error(Exceptions::wrongFixedDepositOperation);
        }

        deposit->refill(amount);
    }

    double DepositServiceImpl::getRateOfInterest(const Model::EntityID& depositId) const {
        auto deposit = repository_.resolveEntity(depositId);

        if(!deposit) {
            throw std::domain_error((boost::format(Model::Exceptions::unknownDeposit) % boost::uuids::to_string(depositId)).str());
        }

        return deposit->getRateOfInterest();
    }

    double DepositServiceImpl::getBalance(const Model::EntityID& depositId) const {
        auto deposit = repository_.resolveEntity(depositId);

        if(!deposit) {
            throw std::domain_error((boost::format(Model::Exceptions::unknownDeposit) % boost::uuids::to_string(depositId)).str());
        }

        return deposit->getBalance();
    }

    void DepositServiceImpl::withdraw(const Model::EntityID& depositId, double amount) {
        if(amount <= 0) {
            throw std::domain_error(Model::Exceptions::negativeValue);
        }

        auto deposit = repository_.resolveEntity(depositId);

        if(!deposit) {
            throw std::domain_error((boost::format(Model::Exceptions::unknownDeposit) % boost::uuids::to_string(depositId)).str());
        }

        Withdrawer::withdraw(*deposit, amount);
    }

    void DepositServiceImpl::setOverdraftLimit(const Model::EntityID& depositId, double overdraftLimit) {
        using namespace Model;

        auto deposit = repository_.resolveEntity(depositId);

        if(!deposit) {
            throw std::domain_error((boost::format(Exceptions::unknownDeposit) % boost::uuids::to_string(depositId)).str());
        }

        auto currentDeposit = Model::DepositCast<CurrentDeposit>().cast(*deposit);

        if(!currentDeposit){
            throw std::domain_error((boost::format(Exceptions::depositExpected) % "Current").str());
        }

        currentDeposit->setOverdraftLimit(overdraftLimit);
    }

    double DepositServiceImpl::closeDeposit(const Model::EntityID& depositId) {
        auto deposit = repository_.resolveEntity(depositId);

        if(!deposit) {
            throw std::domain_error((boost::format(Model::Exceptions::unknownDeposit) % boost::uuids::to_string(depositId)).str());
        }

        double balance = deposit->getBalance();
        repository_.remove(depositId);

        return balance;
    }

    void DepositServiceImpl::onBankPeriodPassed(const Model::EntityID& depositId) {
        auto deposit = repository_.resolveEntity(depositId);

        if(!deposit) {
            throw std::domain_error((boost::format(Model::Exceptions::unknownDeposit) % boost::uuids::to_string(depositId)).str());
        }

        YearProcessor::process(*deposit);
    }

    int DepositServiceImpl::getDepositsAmount() const {
        return repository_.getSize();
    }
}
}


#include <jlcxx/jlcxx.hpp>
#include <Eigen/Dense>

namespace jlcxx
{
  // Match the Eigen Matrix type, skipping the default parameters
  template<typename ScalarT, int Rows, int Cols>
  struct BuildParameterList<Eigen::Matrix<ScalarT, Rows, Cols>>
  {
    // We set the parameterlist to int64_t, to avoid having to wrap all parameters in Int32() in Julia
    typedef ParameterList<ScalarT, std::integral_constant<int64_t, Rows>, std::integral_constant<int64_t, Cols>> type;
  };
}

JULIA_CPP_MODULE_BEGIN(registry)
  using namespace jlcxx;

  Module& types = registry.create_module("EigenCpp");

  // Add some fixed-size matrices
  types.add_type<Parametric<TypeVar<1>, TypeVar<2>, TypeVar<3>>>("Matrix")
    .apply<Eigen::Matrix<double, 4, 4>, Eigen::Matrix<double, 4, 2>, Eigen::Matrix<double, 4, 1>>([&types](auto wrapped)
  {
    typedef typename decltype(wrapped)::type WrappedT;
    typedef typename WrappedT::Scalar ScalarT;
    typedef remove_const_ref<decltype(std::declval<WrappedT>()+std::declval<WrappedT>())> SumT;

    types.add_type<SumT>("Sum_" + std::string(typeid(SumT).name()));

    types.method("setConstant", [](WrappedT& eigen_mat, const ScalarT val)
    {
      eigen_mat.setConstant(val);
    });

    types.method("print", [](const WrappedT& eigen_mat)
    {
      std::cout << eigen_mat << std::endl;
    });

    types.method("convert", [](SingletonType<ArrayRef<ScalarT, 2>>, WrappedT& eigen_mat)
    {
      return ArrayRef<ScalarT, 2>(eigen_mat.data(), eigen_mat.rows(), eigen_mat.cols());
    });

    types.method("getindex", [](const WrappedT& eigen_mat, const int64_t row, const int64_t col)
    {
      return eigen_mat(row-1,col-1);
    });

    types.method("setindex!", [](WrappedT& eigen_mat, const ScalarT val, const int64_t row, const int64_t col)
    {
      eigen_mat(row-1,col-1) = val;
    });

    types.method("+", [](const WrappedT& mat1, const WrappedT& mat2)
    {
      return create<SumT>(mat1 + mat2);
    });

    types.method("convert", [](SingletonType<WrappedT>, const SumT& sum)
    {
      return create<WrappedT>(sum);
    });

    types.method("==", [](const WrappedT& eigen_mat, const ArrayRef<ScalarT, WrappedT::ColsAtCompileTime==1?1:2>& julia_mat)
    {
      return eigen_mat == Eigen::Map<const WrappedT>(julia_mat.data());
    });
  });

  // Mat-vec product for (4x4)*(4x1)
  typedef remove_const_ref<decltype(std::declval<Eigen::Matrix<double, 4, 4>>()*std::declval<Eigen::Matrix<double, 4, 1>>())> ProductT;
  types.add_type<ProductT>("Product_" + std::string(typeid(ProductT).name()));

  types.method("*", [](const Eigen::Matrix<double, 4, 4>& mat, const Eigen::Matrix<double, 4, 1>& vec)
  {
    return create<ProductT>(mat * vec);
  });

  types.method("+", [](const Eigen::Matrix<double, 4, 1>& mat, const ProductT& prod)
  {
    return create<Eigen::Matrix<double, 4, 1>>(mat+prod);
  });

  // in-place y += A*x
  types.method("plus_assign_product!", [](const Eigen::Matrix<double, 4, 4>& mat, const Eigen::Matrix<double, 4, 1>& vec, Eigen::Matrix<double, 4, 1>& result)
  {
    result += mat*vec;
  });
JULIA_CPP_MODULE_END


#include <boost/random/generate_canonical.hpp>


#include <boost/graph/ssca_graph_generator.hpp>


#pragma once

#include <vector>

#include <Eigen/Core>
#include <Eigen/Sparse>

#include <autodiff/autodiff_types.hpp>
#include <physics/rigid_body.hpp>
#include <utils/eigen_ext.hpp>

namespace ipc::rigid {

class RigidBodyAssembler {
public:
    RigidBodyAssembler() {}
    ~RigidBodyAssembler() {}

    /// @brief inits assembler to use this set of rigid-bodies
    void init(const std::vector<RigidBody>& rbs);

    // World Vertices Functions
    // --------------------------------------------------------------------

    template <typename T>
    MatrixX<T> world_vertices(
        const std::vector<MatrixMax3<T>>& rotations,
        const std::vector<VectorMax3<T>>& positions) const;
    template <typename T>
    MatrixX<T> world_vertices(const Poses<T>& poses) const;
    template <typename T>
    MatrixX<T> world_vertices(const VectorX<T>& dofs) const
    {
        return world_vertices(Pose<T>::dofs_to_poses(dofs, dim()));
    }

    Eigen::MatrixXd
    world_vertices(const RigidBody::Step step = RigidBody::CURRENT_STEP) const;
    Eigen::MatrixXd world_vertices_t0() const
    {
        return world_vertices(RigidBody::PREVIOUS_STEP);
    }
    Eigen::MatrixXd world_vertices_t1() const
    {
        return world_vertices(RigidBody::CURRENT_STEP);
    }

    template <typename T>
    VectorMax3<T> world_vertex(const Pose<T>& pose, const int vertex_idx) const;
    template <typename T>
    VectorMax3<T>
    world_vertex(const Poses<T>& poses, const int vertex_idx) const;

    Eigen::MatrixXd world_velocities() const;

    /// @brief Derivatives of the vertices with repect to rigid body DOF.
    ///
    /// Store the jacobian and hessian in a condensed form where only the
    /// rigid DOF of the body the vertex belongs to is computed.
    ///
    /// @returns The vertices of all rigid bodies as a \f$n \times {2, 3}\f$
    /// matrix.
    Eigen::MatrixXd world_vertices_diff(
        const PosesD& poses,
        Eigen::MatrixXd& jac,
        Eigen::MatrixXd& hess,
        bool compute_jac,
        bool compute_hess) const;

    /// @brief Derivatives of the vertices with repect to rigid body DOF.
    ///
    /// Store the jacobian and hessian in a condensed form where only the
    /// rigid DOF of the body the vertex belongs to is computed.
    ///
    /// @returns The vertices of all rigid bodies as a \f$n \times {2, 3}\f$
    /// matrix.
    Eigen::MatrixXd world_vertices_diff(
        const Eigen::VectorXd& dof,
        Eigen::MatrixXd& jac,
        Eigen::MatrixXd& hess,
        bool compute_jac,
        bool compute_hess) const
    {
        return world_vertices_diff(
            PoseD::dofs_to_poses(dof, dim()), jac, hess, compute_jac,
            compute_hess);
    }

    void global_to_local_vertex(
        const long global_vertex_id,
        long& rigid_body_id,
        long& local_vertex_id) const;
    void global_to_local_edge(
        const long global_edge_id,
        long& rigid_body_id,
        long& local_edge_id) const;
    void global_to_local_face(
        const long global_face_id,
        long& rigid_body_id,
        long& local_face_id) const;

    // Ridig Body CM Functions
    // --------------------------------------------------------------------
    /// @brief assemble rigid body poses to a single vector
    PosesD rb_poses(const bool previous = false) const;
    PosesD rb_poses_t0() const { return rb_poses(true); }
    PosesD rb_poses_t1() const { return rb_poses(false); }
    /// @brief set rigid body poses
    void set_rb_poses(const PosesD& poses);

    // --------------------------------------------------------------------

    long num_vertices() const { return m_body_vertex_id.back(); }
    long num_edges() const { return m_body_edge_id.back(); }
    long num_faces() const { return m_body_face_id.back(); }
    size_t num_bodies() const { return m_rbs.size(); }
    size_t count_kinematic_bodies() const;
    int dim() const { return m_rbs.size() ? m_rbs[0].dim() : 0; }

    long vertex_id_to_body_id(long vi) const
    {
        return m_vertex_to_body_map(vi);
    }
    long edge_id_to_body_id(long ei) const
    {
        return m_vertex_to_body_map(m_edges(ei, 0));
    }
    long face_id_to_body_id(long fi) const
    {
        return m_vertex_to_body_map(m_faces(fi, 0));
    }

    const RigidBody& vertex_id_to_body(long vi) const
    {
        return m_rbs[vertex_id_to_body_id(vi)];
    }
    const RigidBody& edge_id_to_body(long ei) const
    {
        return m_rbs[edge_id_to_body_id(ei)];
    }
    const RigidBody& face_id_to_body(long fi) const
    {
        return m_rbs[face_id_to_body_id(fi)];
    }

    const Eigen::VectorXi& group_ids() const { return m_vertex_group_ids; }

    /// Get a vector of body ids where each body is close to at least one
    /// other body.
    std::vector<std::pair<int, int>> close_bodies(
        const PosesD& poses_t0,
        const PosesD& poses_t1,
        const double inflation_radius) const;
    std::vector<std::pair<int, int>> close_bodies_brute_force(
        const PosesD& poses_t0,
        const PosesD& poses_t1,
        const double inflation_radius) const;
    std::vector<std::pair<int, int>> close_bodies_bvh(
        const PosesD& poses_t0,
        const PosesD& poses_t1,
        const double inflation_radius) const;
    std::vector<std::pair<int, int>> close_bodies_hash_grid(
        const PosesD& poses_t0,
        const PosesD& poses_t1,
        const double inflation_radius) const;

    /// Get the ith rigid body
    const RigidBody& operator[](size_t i) const { return m_rbs[i]; }
    /// Get the ith rigid body
    RigidBody& operator[](size_t i) { return m_rbs[i]; }

    std::vector<RigidBody> m_rbs;

    /// @brief The starting index of each RB in the global vertices
    std::vector<long> m_body_vertex_id;
    /// @brief The starting index of each RB in the global edges
    std::vector<long> m_body_edge_id;
    /// @brief The starting index of each RB in the global faces
    std::vector<long> m_body_face_id;

    double average_edge_length; ///< Average edge length

    /// @brief average value of mass matrix diagonal (including inertia)
    double average_mass;

    /// @brief indexes for vertices
    Eigen::VectorXi m_vertex_to_body_map;

    /// @brief re-indexed edges of the whole system
    Eigen::MatrixXi m_edges;

    /// @brief re-indexed faces of the whole system
    Eigen::MatrixXi m_faces;

    /// @brief re-indexed faces to edges of the whole system
    Eigen::MatrixXi m_faces_to_edges;

    /// @brief re-indexed faces to edges of the whole system
    std::vector<size_t> m_codim_edges_to_edges;

    /// @brief mass_matrix of the rigid bodies dof
    DiagonalMatrixXd m_rb_mass_matrix;

    /// @brief flags for rb degrees of freedom
    VectorXb is_rb_dof_fixed;

    /// @brief flag for vertices degrees of freedom (used for visualization)
    MatrixXb is_dof_fixed;

protected:
    /// @brief Group ids per vertex
    Eigen::VectorXi m_vertex_group_ids;
};

} // namespace ipc::rigid

#include "rigid_body_assembler.tpp"