import copy
import math
import os
import unittest

import numpy

import rdkit.DistanceGeometry as DG
from rdkit import Chem, RDConfig, rdBase
from rdkit.Chem import AllChem, ChemicalForceFields, rdDistGeom, rdMolAlign
from rdkit.Geometry import ComputeSignedDihedralAngle, Point3D
from rdkit.Geometry import rdGeometry as geom
from rdkit.RDLogger import logger

logger = logger()


def feq(v1, v2, tol=1.e-4):
  return abs(v1 - v2) < tol


def lstEq(l1, l2, tol=1.0e-4):
  ln = len(l1)
  if (ln != len(l2)):
    return 0

  for i in range(ln):
    if abs(l1[i] - l2[i]) > tol:
      return 0
  return 1


def compareWithOld(smilesFile, sdFile):
  smiSup = Chem.SmilesMolSupplier(smilesFile, ",", 0, -1)
  sdsup = Chem.SDMolSupplier(sdFile)
  im = 0
  for mol in smiSup:
    cid = rdDistGeom.EmbedMolecule(mol, 10, 1)
    omol = sdsup[im]
    assert cid == 0
    conf = mol.GetConformer(0)
    oconf = omol.GetConformer()
    nat = mol.GetNumAtoms()
    for i in range(nat):
      #atm = mol.GetAtomWithIdx(i)
      #oatm = omol.GetAtomWithIdx(i)
      pos = conf.GetAtomPosition(i)
      opos = oconf.GetAtomPosition(i)
      if not lstEq(pos, opos):
        return 0
    im += 1
  return 1


def compareMatrices(bm1, bm2, map, tol=1.0e-5):
  N = numpy.shape(bm1)[0]
  for i in range(1, N):
    for j in range(i):
      l, m = map[i], map[j]
      if (l < m):
        l, m = m, l
      if (abs(bm1[l, m] - bm2[i, j]) > tol):
        return 0

      if (abs(bm1[m, l] - bm2[j, i]) > tol):
        return 0

  return 1


def compareOrder(smi1, smi2, tol=1.0e-5):
  m1 = Chem.MolFromSmiles(smi1)
  m2 = Chem.MolFromSmiles(smi2)
  bm1 = rdDistGeom.GetMoleculeBoundsMatrix(m1)
  bm2 = rdDistGeom.GetMoleculeBoundsMatrix(m2)
  map = m1.GetSubstructMatch(m2)
  return compareMatrices(bm1, bm2, map, tol)


def computeDist(lst1, lst2):
  res = 0.0
  for i, val in enumerate(lst1):
    res += (val - lst2[i]) * (val - lst2[i])
  res = math.sqrt(res)
  return res


def computeChiralVol(pt1, pt2, pt3, pt4):
  v1 = pt1 - pt4
  v2 = pt2 - pt4
  v3 = pt3 - pt4
  cp = v2.CrossProduct(v3)
  vol = v1.DotProduct(cp)
  return vol


class TestCase(unittest.TestCase):

  def setUp(self):
    pass

  def _test0Cdk2(self):
    fileN = os.path.join(RDConfig.RDBaseDir, 'Code', 'GraphMol', 'DistGeomHelpers', 'test_data',
                         'cis_trans_cases.csv')

    ofile = os.path.join(RDConfig.RDBaseDir, 'Code', 'GraphMol', 'DistGeomHelpers', 'test_data',
                         'embedDistOpti.sdf')
    self.assertTrue(compareWithOld(fileN, ofile))

  def test1Small(self):
    #writer = Chem.SDWriter("test.sdf")
    # single double and tripple atoms cases should not fail
    mol = Chem.MolFromSmiles('O')
    rdDistGeom.EmbedMolecule(mol, 10, 1)
    conf = mol.GetConformer()
    self.assertTrue(lstEq(conf.GetAtomPosition(0), [0.0, 0.0, 0.0]))
    # writer.write(mol)

    mol = Chem.MolFromSmiles('CO')
    rdDistGeom.EmbedMolecule(mol, 10, 1)
    conf = mol.GetConformer()
    self.assertTrue(lstEq(conf.GetAtomPosition(0), [0.69192, 0.0, 0.0]))
    self.assertTrue(lstEq(conf.GetAtomPosition(1), [-0.69192, 0.0, 0.0]))
    # writer.write(mol)

    mol = Chem.MolFromSmiles('CCC')
    rdDistGeom.EmbedMolecule(mol, 10, 1)
    conf = mol.GetConformer()
    self.assertTrue(lstEq(conf.GetAtomPosition(0), [-1.21676, -0.2989, 0.0]))
    self.assertTrue(lstEq(conf.GetAtomPosition(1), [-0.00604, 0.59337, 0.0]))
    self.assertTrue(lstEq(conf.GetAtomPosition(2), [1.22281, -0.29446, 0.0]))
    # writer.write(mol)

    mol = Chem.MolFromSmiles('O=C=O')
    rdDistGeom.EmbedMolecule(mol, 10, 1)
    conf = mol.GetConformer()

    # writer.write(mol)
    self.assertTrue(lstEq(conf.GetAtomPosition(0), [-1.237578, -0.000110, 0.0]))
    self.assertTrue(lstEq(conf.GetAtomPosition(1), [-0.003500, 0.000027, 0.0]))
    self.assertTrue(lstEq(conf.GetAtomPosition(2), [1.241078, 0.000137, 0.0]))

    mol = Chem.MolFromSmiles('C=C=C=C')
    rdDistGeom.EmbedMolecule(mol, 10, 1, useExpTorsionAnglePrefs=False, useBasicKnowledge=False)
    conf = mol.GetConformer()

    # writer.write(mol)

    d1 = computeDist(conf.GetAtomPosition(0), conf.GetAtomPosition(1))
    self.assertTrue(feq(d1, 1.31, 0.01))
    d2 = computeDist(conf.GetAtomPosition(0), conf.GetAtomPosition(2))
    self.assertTrue(feq(d2, 2.59, 0.05))
    d3 = computeDist(conf.GetAtomPosition(0), conf.GetAtomPosition(3))
    self.assertTrue(feq(d3, 3.84, 0.1))
    d4 = computeDist(conf.GetAtomPosition(1), conf.GetAtomPosition(2))
    self.assertTrue(feq(d4, 1.29, 0.01))
    d5 = computeDist(conf.GetAtomPosition(1), conf.GetAtomPosition(3))
    self.assertTrue(feq(d5, 2.54, 0.1))
    d6 = computeDist(conf.GetAtomPosition(2), conf.GetAtomPosition(3))
    self.assertTrue(feq(d6, 1.31, 0.01))

  def test2Utils(self):
    mol = Chem.MolFromSmiles('CC')
    bm = rdDistGeom.GetMoleculeBoundsMatrix(mol)
    self.assertTrue(bm[1, 0] > 0)
    self.assertTrue(bm[0, 1] > 0)
    self.assertTrue(bm[0, 1] >= bm[1, 0])
    self.assertTrue(bm[1, 0] < 1.510)
    self.assertTrue(bm[0, 1] > 1.510)

  def test3MultiConf(self):
    mol = Chem.MolFromSmiles("CC(C)(C)c(cc12)n[n]2C(=O)/C=C(N1)/COC")
    cids = rdDistGeom.EmbedMultipleConfs(mol, 10, maxAttempts=30, randomSeed=100,
                                         useExpTorsionAnglePrefs=False, useBasicKnowledge=False)
    energies = [
      116.330, 106.246, 109.816, 104.890, 93.060, 140.803, 139.253, 95.820, 123.591, 108.655
    ]
    nenergies = []
    for cid in cids:
      ff = ChemicalForceFields.UFFGetMoleculeForceField(mol, 10.0, cid)
      ee = ff.CalcEnergy()
      nenergies.append(ee)
    # print(['%.3f' % x for x in nenergies])
    # print(nenergies)
    self.assertTrue(lstEq(energies, nenergies, tol=1e-2))

  def test4OrderDependence(self):
    self.assertTrue(compareOrder("CC(C)(C)C(=O)NC(C1)CC(N2C)CCC12",
                                 "CN1C2CCC1CC(NC(=O)C(C)(C)C)C2"))
    # issue 230
    self.assertTrue(compareOrder("C#CC(C)(C)N(CN1)C\\N=C/1SC", "CSC1=NCN(C(C)(C)C#C)CN1"))
    # issue 232
    self.assertTrue(compareOrder("CC(C)(C)C(=O)NC(C1)CC(N2C)CCC12",
                                 "CN1C2CCC1CC(NC(=O)C(C)(C)C)C2"))

  def test5Issue285(self):
    m = Chem.MolFromSmiles('CNC=O')
    cs = rdDistGeom.EmbedMultipleConfs(m, 10)
    for i, ci in enumerate(cs):
      for j in range(i + 1, len(cs)):
        cj = cs[j]
        self.assertTrue(Chem.MolToMolBlock(m, confId=ci) != Chem.MolToMolBlock(m, confId=cj))

  def test6RmsPruning(self):
    smiles = [
      'CC(C)CC(NC(C1[N+]CCC1)=O)C([O-])=O', 'CC(NC(CO)C(O)c1ccc([N+]([O-])=O)cc1)=O',
      'CC([N+])C(NC(C)C(N1C(C=O)CCC1)=O)=O', 'CC(NC1C(O)C=C(C([O-])=O)OC1C(O)C(O)CO)=O',
      'CCCC=C(NC(C1CC1(C)C)=O)C([O-])=O', 'OCC(O)C(O)C(Cn1c2c(cc(C)c(C)c2)nc-2c(=O)[nH]c(=O)nc12)O'
    ]

    nconfs = []
    expected = [3, 2, 7, 6, 3, 3]
    for smi in smiles:
      mol = Chem.MolFromSmiles(smi)
      cids = rdDistGeom.EmbedMultipleConfs(mol, 50, maxAttempts=30, randomSeed=100,
                                           pruneRmsThresh=1.5)
      nconfs.append(len(cids))

    d = [abs(x - y) for x, y in zip(expected, nconfs)]
    # print(nconfs)
    self.assertTrue(max(d) <= 1)

    # previous settings
    params = rdDistGeom.ETKDG()
    params.randomSeed = 100
    params.maxIterations = 30
    params.pruneRmsThresh = 1.5
    params.useSymmetryForPruning = False
    nconfs = []
    expected = [4, 5, 5, 6, 7, 3]
    for smi in smiles:
      mol = Chem.MolFromSmiles(smi)
      cids = rdDistGeom.EmbedMultipleConfs(mol, 50, params)
      nconfs.append(len(cids))

    d = [abs(x - y) for x, y in zip(expected, nconfs)]
    # print(nconfs)
    self.assertTrue(max(d) <= 1)

  def test6Chirality(self):
    # turn on chirality and we should get chiral volume that is pretty consistent and
    # positive
    tgtVol = 13.0
    smiles = "Cl[C@](C)(F)Br"
    mol = Chem.MolFromSmiles(smiles)
    cids = rdDistGeom.EmbedMultipleConfs(mol, 30, maxAttempts=30, randomSeed=100)
    self.assertTrue(len(cids) == 30)
    for cid in cids:
      conf = mol.GetConformer(cid)
      vol = computeChiralVol(conf.GetAtomPosition(0), conf.GetAtomPosition(2),
                             conf.GetAtomPosition(3), conf.GetAtomPosition(4))
      self.assertTrue(abs(vol - tgtVol) < 1)

    # turn of chirality and now we should see both chiral forms
    smiles = "ClC(C)(F)Br"
    mol = Chem.MolFromSmiles(smiles)
    cids = rdDistGeom.EmbedMultipleConfs(mol, 30, maxAttempts=30, randomSeed=120)
    self.assertTrue(len(cids) == 30)
    nPos = 0
    nNeg = 0
    for cid in cids:
      conf = mol.GetConformer(cid)
      vol = computeChiralVol(conf.GetAtomPosition(0), conf.GetAtomPosition(2),
                             conf.GetAtomPosition(3), conf.GetAtomPosition(4))
      self.assertTrue(abs(vol - tgtVol) < 1 or abs(vol + tgtVol) < 1)
      if vol < 0:
        nNeg += 1
      else:
        nPos += 1
    self.assertTrue(nPos > 0)
    self.assertTrue(nNeg > 0)

    tgtVol = 3.0
    for i in range(10):
      smiles = "Cl[C@H](F)Br"
      mol = Chem.MolFromSmiles(smiles)
      ci = rdDistGeom.EmbedMolecule(mol, 30, (i + 1) * 10)
      conf = mol.GetConformer(ci)
      vol = computeChiralVol(conf.GetAtomPosition(0), conf.GetAtomPosition(1),
                             conf.GetAtomPosition(2), conf.GetAtomPosition(3))
      self.assertTrue(abs(vol - tgtVol) < 1, "%s %s" % (vol, tgtVol))

    tgtVol = 3.5
    expected = [-3.62, -3.67, -3.72, 3.91, 3.95, 3.98, 3.90, 3.94, 3.98, 3.91]
    nPos = 0
    nNeg = 0
    for i in range(30):
      smiles = "ClC(F)Br"
      mol = Chem.MolFromSmiles(smiles)
      ci = rdDistGeom.EmbedMolecule(mol, 30, (i + 1) * 10)
      conf = mol.GetConformer(ci)
      vol = computeChiralVol(conf.GetAtomPosition(0), conf.GetAtomPosition(1),
                             conf.GetAtomPosition(2), conf.GetAtomPosition(3))
      self.assertTrue(abs(vol - tgtVol) < 1 or abs(vol + tgtVol) < 1)
      if vol < 0:
        nNeg += 1
      else:
        nPos += 1

    self.assertTrue(nPos > 0)
    self.assertTrue(nNeg > 0)

    smiles = "Cl[C@H](F)Br"
    m = Chem.MolFromSmiles(smiles)
    mol = Chem.AddHs(m)
    cids = rdDistGeom.EmbedMultipleConfs(mol, 10, maxAttempts=30, randomSeed=100)
    self.assertTrue(len(cids) == 10)
    tgtVol = 10.5
    for cid in cids:
      conf = mol.GetConformer(cid)
      vol = computeChiralVol(conf.GetAtomPosition(0), conf.GetAtomPosition(2),
                             conf.GetAtomPosition(3), conf.GetAtomPosition(4))
      self.assertTrue(abs(vol - tgtVol) < 2.)

    # let's try a little more complicated system
    expectedV1 = -2.0
    expectedV2 = -2.9

    for i in range(5):
      smi = "C1=CC=C(C=C1)[C@H](OC1=C[NH]N=C1)C(=O)[NH]C[C@H](Cl)C1=CC=NC=C1"
      mol = Chem.MolFromSmiles(smi)
      ci = rdDistGeom.EmbedMolecule(mol, randomSeed=(i + 1) * 15)
      self.assertTrue(ci >= 0)
      ff = ChemicalForceFields.UFFGetMoleculeForceField(mol, 10.0, ci)
      ff.Minimize()

      conf = mol.GetConformer(ci)
      vol1 = computeChiralVol(conf.GetAtomPosition(6), conf.GetAtomPosition(3),
                              conf.GetAtomPosition(7), conf.GetAtomPosition(13))
      self.assertTrue(abs(vol1 - expectedV1) < 1 or abs(vol1 + expectedV1) < 1)
      if vol1 < 0:
        nNeg += 1
      else:
        nPos += 1

      vol2 = computeChiralVol(conf.GetAtomPosition(17), conf.GetAtomPosition(16),
                              conf.GetAtomPosition(18), conf.GetAtomPosition(19))
      self.assertTrue(abs(vol2 - expectedV2) < 1 or abs(vol2 + expectedV2) < 1)

    # remove the chiral specification and we should see other chiral
    # forms of the compound
    expectedV1 = 2.0  # [-2.30, -2.31, -2.30,  2.30, -1.77]
    expectedV2 = 2.8  # [2.90,  2.89,  2.69, -2.90, -2.93]

    self.assertTrue(nPos > 0)
    self.assertTrue(nNeg > 0)
    for i in range(5):
      smi = "C1=CC=C(C=C1)C(OC1=C[NH]N=C1)C(=O)[NH]CC(Cl)C1=CC=NC=C1"
      mol = Chem.MolFromSmiles(smi)
      ci = rdDistGeom.EmbedMolecule(mol, 30, (i + 1) * 10)
      ff = ChemicalForceFields.UFFGetMoleculeForceField(mol, 10.0, ci)
      ff.Minimize()

      conf = mol.GetConformer(ci)
      vol1 = computeChiralVol(conf.GetAtomPosition(6), conf.GetAtomPosition(3),
                              conf.GetAtomPosition(7), conf.GetAtomPosition(13))
      vol2 = computeChiralVol(conf.GetAtomPosition(17), conf.GetAtomPosition(16),
                              conf.GetAtomPosition(18), conf.GetAtomPosition(19))
      self.assertTrue(abs(abs(vol1) - expectedV1) < 1.0)
      self.assertTrue(abs(abs(vol2) - expectedV2) < 1.0)

  def test7ConstrainedEmbedding(self):
    ofile = os.path.join(RDConfig.RDBaseDir, 'Code', 'GraphMol', 'DistGeomHelpers', 'test_data',
                         'constrain1.sdf')
    suppl = Chem.SDMolSupplier(ofile)
    ref = next(suppl)
    probe = copy.deepcopy(ref)

    cMap = {}
    for i in range(5):
      cMap[i] = ref.GetConformer().GetAtomPosition(i)
    ci = rdDistGeom.EmbedMolecule(probe, coordMap=cMap, randomSeed=23)
    self.assertTrue(ci > -1)
    algMap = list(zip(range(5), range(5)))
    ssd = rdMolAlign.AlignMol(probe, ref, atomMap=algMap)
    self.assertTrue(ssd < 0.1)

  def test8MultiThreadMultiConf(self):
    if (rdBase.rdkitBuild.split('|')[2] != "MINGW"):
      ENERGY_TOLERANCE = 1.0e-6
      MSD_TOLERANCE = 1.0e-6
    else:
      ENERGY_TOLERANCE = 1.0
      MSD_TOLERANCE = 1.0e-5
    mol = Chem.AddHs(Chem.MolFromSmiles("CC(C)(C)c(cc12)n[n]2C(=O)/C=C(N1)/COC"))
    cids = rdDistGeom.EmbedMultipleConfs(mol, 200, maxAttempts=30, randomSeed=100)
    energies = []
    for cid in cids:
      ff = ChemicalForceFields.UFFGetMoleculeForceField(mol, 10.0, cid)
      ee = ff.CalcEnergy()
      energies.append(ee)

    mol2 = Chem.AddHs(Chem.MolFromSmiles("CC(C)(C)c(cc12)n[n]2C(=O)/C=C(N1)/COC"))
    cids2 = rdDistGeom.EmbedMultipleConfs(mol2, 200, maxAttempts=30, randomSeed=100, numThreads=4)
    self.assertTrue(lstEq(cids, cids2))
    nenergies = []
    for cid in cids2:
      ff = ChemicalForceFields.UFFGetMoleculeForceField(mol2, 10.0, cid)
      ee = ff.CalcEnergy()
      nenergies.append(ee)

    self.assertTrue(lstEq(energies, nenergies, tol=ENERGY_TOLERANCE))

    for cid in cids:
      msd = 0.0
      for i in range(mol.GetNumAtoms()):
        msd += (mol.GetConformer().GetAtomPosition(i) -
                mol2.GetConformer().GetAtomPosition(i)).LengthSq()
      msd /= mol.GetNumAtoms()
      self.assertTrue(msd < MSD_TOLERANCE)

  def _compareConfs(self, mol, ref, molConfId, refConfId):
    self.assertEqual(mol.GetNumAtoms(), ref.GetNumAtoms())
    molConf = mol.GetConformer(molConfId)
    refConf = ref.GetConformer(refConfId)
    for i in range(mol.GetNumAtoms()):
      mp = molConf.GetAtomPosition(i)
      rp = refConf.GetAtomPosition(i)
      self.assertAlmostEqual((mp - rp).Length(), 0.0, 3)

  def test9EmbedParams(self):
    mol = Chem.AddHs(Chem.MolFromSmiles('OCCC'))
    fn = os.path.join(RDConfig.RDBaseDir, 'Code', 'GraphMol', 'DistGeomHelpers', 'test_data',
                      'simple_torsion.dg.mol')
    ref = Chem.MolFromMolFile(fn, removeHs=False)
    params = rdDistGeom.EmbedParameters()
    params.randomSeed = 42
    self.assertEqual(rdDistGeom.EmbedMolecule(mol, params), 0)
    self._compareConfs(mol, ref, 0, 0)

    fn = os.path.join(RDConfig.RDBaseDir, 'Code', 'GraphMol', 'DistGeomHelpers', 'test_data',
                      'simple_torsion.etdg.mol')
    ref = Chem.MolFromMolFile(fn, removeHs=False)
    params = rdDistGeom.EmbedParameters()
    params.randomSeed = 42
    params.useExpTorsionAnglePrefs = True
    self.assertEqual(rdDistGeom.EmbedMolecule(mol, params), 0)
    self._compareConfs(mol, ref, 0, 0)
    params = rdDistGeom.ETDG()
    params.randomSeed = 42
    self.assertEqual(rdDistGeom.EmbedMolecule(mol, params), 0)
    self._compareConfs(mol, ref, 0, 0)

    fn = os.path.join(RDConfig.RDBaseDir, 'Code', 'GraphMol', 'DistGeomHelpers', 'test_data',
                      'simple_torsion.etkdg.mol')
    ref = Chem.MolFromMolFile(fn, removeHs=False)
    params = rdDistGeom.EmbedParameters()
    params.randomSeed = 42
    params.useExpTorsionAnglePrefs = True
    params.useBasicKnowledge = True
    self.assertEqual(rdDistGeom.EmbedMolecule(mol, params), 0)
    self._compareConfs(mol, ref, 0, 0)
    params = rdDistGeom.ETKDG()
    params.randomSeed = 42
    self.assertEqual(rdDistGeom.EmbedMolecule(mol, params), 0)
    self._compareConfs(mol, ref, 0, 0)

    fn = os.path.join(RDConfig.RDBaseDir, 'Code', 'GraphMol', 'DistGeomHelpers', 'test_data',
                      'simple_torsion.kdg.mol')
    ref = Chem.MolFromMolFile(fn, removeHs=False)
    params = rdDistGeom.EmbedParameters()
    params.randomSeed = 42
    params.useBasicKnowledge = True
    self.assertEqual(rdDistGeom.EmbedMolecule(mol, params), 0)
    self._compareConfs(mol, ref, 0, 0)
    params = rdDistGeom.KDG()
    params.randomSeed = 42
    self.assertEqual(rdDistGeom.EmbedMolecule(mol, params), 0)
    self._compareConfs(mol, ref, 0, 0)

  def test10ETKDGv2(self):
    mol = Chem.AddHs(Chem.MolFromSmiles('n1cccc(C)c1ON'))
    fn = os.path.join(RDConfig.RDBaseDir, 'Code', 'GraphMol', 'DistGeomHelpers', 'test_data',
                      'torsion.etkdg.v2.mol')
    ref = Chem.MolFromMolFile(fn, removeHs=False)
    params = rdDistGeom.ETKDGv2()
    params.randomSeed = 42
    self.assertEqual(rdDistGeom.EmbedMolecule(mol, params), 0)
    self._compareConfs(mol, ref, 0, 0)

  def assertDeterministicWithSeed(self, seed):
    input_mol = Chem.MolFromSmiles('CN(Cc1cnc2nc(N)nc(N)c2n1)c1ccc(C(=O)NC(CCC(=O)O)C(=O)O)cc1')

    params = AllChem.ETKDG()
    params.pruneRmsThresh = -1.0  # skip internal RMSD pruning
    if seed is not None:
      params.randomSeed = seed

    firstMol = Chem.AddHs(input_mol)
    firstIds = AllChem.EmbedMultipleConfs(firstMol, 11, params)

    secondMol = Chem.AddHs(input_mol)
    secondIds = AllChem.EmbedMultipleConfs(secondMol, 11, params)

    self.assertEqual(list(firstIds), list(secondIds))
    self.assertEqual(firstMol.GetNumConformers(), secondMol.GetNumConformers())

    nonDeterministic = False
    for confIdx in range(firstMol.GetNumConformers()):

      firstConf = firstMol.GetConformer(confIdx)
      secondConf = secondMol.GetConformer(confIdx)

      firstPositions = firstConf.GetPositions()
      secondPositions = secondConf.GetPositions()

      d = firstPositions - secondPositions
      rmsd = numpy.sqrt(numpy.sum(d * d))
      if seed >= 0:
        self.assertEqual(rmsd, 0.0)
      elif rmsd != 0.0:
        nonDeterministic = True
    if seed < 0:
      self.assertTrue(nonDeterministic)

  def testETKDGIsDeterministic(self):
    self.assertDeterministicWithSeed(-1)  # not deterministic
    self.assertDeterministicWithSeed(0)  # deterministic
    self.assertDeterministicWithSeed(1)  # deterministic
    # as large as we can go without overflowing since 11 * 195225786 should not overflow the int
    self.assertDeterministicWithSeed(195225786)
    self.assertDeterministicWithSeed(195225787)  # one higher seed will overflow though
    # another large seeds that shouldn't overflow internals and make them non-deterministic
    self.assertDeterministicWithSeed(0x1CEB00DA)

  def testGithub1763(self):
    mol = Chem.MolFromSmiles('CCCCC')
    bm1 = rdDistGeom.GetMoleculeBoundsMatrix(mol)
    bm2 = rdDistGeom.GetMoleculeBoundsMatrix(mol, doTriangleSmoothing=False)
    self.assertTrue(bm1[0, 4] < bm2[0, 4])

  def testGithub2057(self):
    # ensure that ETKDG is the default Embedder
    mol = Chem.AddHs(Chem.MolFromSmiles('OCCC'))
    fn = os.path.join(RDConfig.RDBaseDir, 'Code', 'GraphMol', 'DistGeomHelpers', 'test_data',
                      'simple_torsion.etkdg.mol')
    ref = Chem.MolFromMolFile(fn, removeHs=False)
    self.assertEqual(rdDistGeom.EmbedMolecule(mol, randomSeed=42), 0)
    self._compareConfs(mol, ref, 0, 0)

  def testProvidingBoundsMatrix(self):
    m1 = Chem.MolFromSmiles("C1CCC1C")
    bm1 = rdDistGeom.GetMoleculeBoundsMatrix(m1)
    bm1[0, 3] = 1.21
    bm1[3, 0] = 1.20
    bm1[2, 3] = 1.21
    bm1[3, 2] = 1.20
    bm1[4, 3] = 1.21
    bm1[3, 4] = 1.20
    DG.DoTriangleSmoothing(bm1)
    ps = rdDistGeom.EmbedParameters()
    ps.useRandomCoords = True
    ps.SetBoundsMat(bm1)
    ps.randomSeed = 0xf00d
    self.assertEqual(rdDistGeom.EmbedMolecule(m1, ps), 0)
    conf = m1.GetConformer()
    self.assertAlmostEqual((conf.GetAtomPosition(3) - conf.GetAtomPosition(0)).Length(), 1.2,
                           delta=0.05)
    self.assertAlmostEqual((conf.GetAtomPosition(3) - conf.GetAtomPosition(2)).Length(), 1.2,
                           delta=0.05)
    self.assertAlmostEqual((conf.GetAtomPosition(3) - conf.GetAtomPosition(4)).Length(), 1.2,
                           delta=0.05)

  def testProvidingCPCI(self):
    """
        test for a ring molecule, repeated generating a conformer with and without enforcing 
        an additional +ve interaction between a pair of non-bonded atoms (termed CPCI, 
        custom pairwise charge-like interaciton), in every iteration, applying CPCI should
        yield a conformer where this pair of atoms are further apart.
        """
    for i in range(5):
      ps = rdDistGeom.EmbedParameters()
      ps.randomSeed = i
      ps.useBasicKnowledge = True
      ps.useRandomCoords = False
      m1 = Chem.MolFromSmiles("C1CCCC1C")
      self.assertEqual(rdDistGeom.EmbedMolecule(m1, ps), 0)

      m2 = Chem.MolFromSmiles("C1CCCC1C")
      ps = rdDistGeom.EmbedParameters()
      ps.randomSeed = i
      ps.useRandomCoords = False
      ps.useBasicKnowledge = True
      ps.SetCPCI({(0, 3): 0.9})
      self.assertEqual(rdDistGeom.EmbedMolecule(m2, ps), 0)

      conf1 = m1.GetConformer()
      conf2 = m2.GetConformer()
      self.assertTrue((conf2.GetAtomPosition(3) -
                       conf2.GetAtomPosition(0)).Length() > (conf1.GetAtomPosition(3) -
                                                             conf1.GetAtomPosition(0)).Length())

  def testScaleBoundsMatForce(self):
    """
        for pentane, set a target distance for the 1-5 distance, and generate conformers with changing weights for (all) the atom pair distance restraints,
        the conformer with the stronger weight for the atom pairs will always have a 1-5 distance closer to the target value than that with the weaker weight.
        """
    target = 4
    for i in range(5):
      ps = rdDistGeom.EmbedParameters()
      ps.randomSeed = i
      ps.useBasicKnowledge = True
      ps.useRandomCoords = False
      m1 = Chem.MolFromSmiles("CCCCC")
      bm1 = rdDistGeom.GetMoleculeBoundsMatrix(m1)
      bm1[0, 4] = target
      bm1[4, 0] = target
      DG.DoTriangleSmoothing(bm1)
      ps.boundsMatForceScaling = 0.1
      ps.SetBoundsMat(bm1)
      self.assertEqual(rdDistGeom.EmbedMolecule(m1, ps), 0)

      m2 = Chem.MolFromSmiles("CCCCC")
      ps = rdDistGeom.EmbedParameters()
      ps.randomSeed = i
      ps.useBasicKnowledge = True
      ps.useRandomCoords = False
      ps.boundsMatForceScaling = 10
      ps.SetBoundsMat(bm1)
      self.assertEqual(rdDistGeom.EmbedMolecule(m2, ps), 0)

      conf1 = m1.GetConformer()
      conf2 = m2.GetConformer()
      self.assertTrue(
        abs((conf2.GetAtomPosition(4) - conf2.GetAtomPosition(0)).Length() -
            target) < abs((conf1.GetAtomPosition(4) - conf1.GetAtomPosition(0)).Length() - target))

  def testETKDGv3amide(self):
    """
        test for a macrocycle molecule, ETKDGv3 samples trans amide
    """

    def get_atom_mapping(mol, smirks="[O:1]=[C:2]@;-[NX3:3]-[H:4]"):
      qmol = Chem.MolFromSmarts(smirks)
      ind_map = {}
      for atom in qmol.GetAtoms():
        map_num = atom.GetAtomMapNum()
        if map_num:
          ind_map[map_num - 1] = atom.GetIdx()
      map_list = [ind_map[x] for x in sorted(ind_map)]
      matches = list()
      for match in mol.GetSubstructMatches(qmol, uniquify=False):
        mas = [match[x] for x in map_list]
        matches.append(tuple(mas))
      return matches

    smiles = "C1CCC(=O)NCCCCCC(=O)NC1"
    smiles_mol = Chem.MolFromSmiles(smiles)
    mol = Chem.AddHs(smiles_mol)
    params = AllChem.ETKDGv3()
    params.randomSeed = 0
    AllChem.EmbedMolecule(mol, params)
    conf = mol.GetConformer(0)
    for torsion in get_atom_mapping(mol):
      a1, a2, a3, a4 = [conf.GetAtomPosition(i) for i in torsion]
      self.assertAlmostEqual(abs(ComputeSignedDihedralAngle(a1, a2, a3, a4)), 3.14, delta=0.1)

  def testGetTorsionBonds(self):
    m = Chem.AddHs(Chem.MolFromSmiles('CCCC'))
    ts = rdDistGeom.GetExperimentalTorsions(m)
    self.assertEqual(len(ts), 1)
    self.assertEqual(ts[0]["bondIndex"], 1)
    self.assertEqual(ts[0]["torsionIndex"], 229)
    self.assertEqual(ts[0]["smarts"], '[!#1:1][CX4H2:2]!@;-[CX4H2:3][!#1:4]')
    self.assertEqual(list(ts[0]["V"]), [0.0, 0.0, 4.0, 0.0, 0.0, 0.0])
    self.assertEqual(list(ts[0]["signs"]), [1, 1, 1, 1, 1, 1])
    self.assertEqual(list(ts[0]["atomIndices"]), [0, 1, 2, 3])
    params = rdDistGeom.ETKDGv3()
    ts = rdDistGeom.GetExperimentalTorsions(m, params)
    self.assertEqual(len(ts), 1)
    self.assertEqual(ts[0]["bondIndex"], 1)
    self.assertEqual(ts[0]["smarts"], '[!#1:1][CX4H2:2]!@;-[CX4H2:3][!#1:4]')
    self.assertEqual(ts[0]["torsionIndex"], 229)
    self.assertEqual(list(ts[0]["V"]), [0.0, 0.0, 4.0, 0.0, 0.0, 0.0])
    self.assertEqual(list(ts[0]["signs"]), [1, 1, 1, 1, 1, 1])
    self.assertEqual(list(ts[0]["atomIndices"]), [0, 1, 2, 3])

  def testTrackFailures(self):
    params = AllChem.ETKDGv3()
    params.trackFailures = True
    params.maxIterations = 50
    params.randomSeed = 42
    mol = Chem.MolFromSmiles('C=CC1=C(N)Oc2cc1c(-c1cc(C(C)O)cc(=O)cc1C1NCC(=O)N1)c(OC)c2OC')
    mol = Chem.AddHs(mol)
    AllChem.EmbedMolecule(mol, params)
    cnts = params.GetFailureCounts()
    self.assertGreater(cnts[AllChem.EmbedFailureCauses.INITIAL_COORDS], 5)
    self.assertGreater(cnts[AllChem.EmbedFailureCauses.ETK_MINIMIZATION], 10)

  def testCoordMap(self):
    mol = Chem.AddHs(Chem.MolFromSmiles("OCCC"))
    ps = rdDistGeom.EmbedParameters()
    coordMap = {0: geom.Point3D(0, 0, 0), \
                1: geom.Point3D(0, 0, 1.5),
                2: geom.Point3D(0, 1.5, 1.5)
                }
    ps.SetCoordMap(coordMap)
    ps.randomSeed = 42
    rdDistGeom.EmbedMolecule(mol, ps)

    conf = mol.GetConformer()
    v1 = conf.GetAtomPosition(0) - conf.GetAtomPosition(1)
    v2 = conf.GetAtomPosition(2) - conf.GetAtomPosition(1)
    angle = v1.AngleTo(v2)
    self.assertAlmostEqual(angle, math.pi / 2.0, delta=0.15)

    # make sure that we can call that a second time
    coordMap = {0: geom.Point3D(0, 0, 0), \
                1: geom.Point3D(1.5, 0, 0),
                2: geom.Point3D(1.5, 1.5, 0)
                }
    ps.SetCoordMap(coordMap)
    ps.randomSeed = 42
    rdDistGeom.EmbedMolecule(mol, ps)

    conf = mol.GetConformer()
    v1 = conf.GetAtomPosition(0) - conf.GetAtomPosition(1)
    v2 = conf.GetAtomPosition(2) - conf.GetAtomPosition(1)
    angle = v1.AngleTo(v2)
    self.assertAlmostEqual(angle, math.pi / 2.0, delta=0.15)

  def testSymmetrizeTerminal(self):
    mol = Chem.AddHs(Chem.MolFromSmiles("FCC(=O)O"))
    ps = rdDistGeom.ETKDGv3()
    ps.randomSeed = 0xc0ffee
    ps.pruneRmsThresh = 0.5
    cids = rdDistGeom.EmbedMultipleConfs(mol, 50, ps)
    self.assertEqual(len(cids), 1)
    ps.symmetrizeConjugatedTerminalGroupsForPruning = False
    cids = rdDistGeom.EmbedMultipleConfs(mol, 50, ps)
    self.assertGreater(len(cids), 1)

  def testSetattr(self):
    mol = Chem.MolFromSmiles("CCC")
    bm = rdDistGeom.GetMoleculeBoundsMatrix(mol)
    ps = rdDistGeom.EmbedParameters()
    ps.randomSeed = 0xc0ffee
    ps.SetBoundsMat(bm)
    with self.assertRaises(AttributeError):
      ps.wrongName = 1234
    with self.assertRaises(AttributeError):
      ps.wrongName(1234)


  def testEmbedParamsToJSON(self):
    ps = rdDistGeom.KDG()
    json = rdDistGeom.EmbedParametersToJSON(ps)
    goal = '{"basinThresh":"5","boundsMatForceScaling":"1","boxSizeMult":"2","clearConfs":"true","embedFragmentsSeparately":"true","enableSequentialRandomSeeds":"false","enforceChirality":"true","ETversion":"1","forceTransAmides":"true","ignoreSmoothingFailures":"false","maxIterations":"0","numThreads":"1","numZeroFail":"1","onlyHeavyAtomsForRMS":"true","optimizerForceTol":"0.001","pruneRmsThresh":"-1","randNegEig":"true","randomSeed":"-1","symmetrizeConjugatedTerminalGroupsForPruning":"true","timeout":"0","trackFailures":"false","useBasicKnowledge":"true","useExpTorsionAnglePrefs":"false","useMacrocycle14config":"false","useMacrocycleTorsions":"false","useRandomCoords":"false","useSmallRingTorsions":"false","useSymmetryForPruning":"true","verbose":"false"}'
    self.assertEqual(
      json,
      goal
    )
    p = Point3D(1.1,2.2,3.3)
    ps.SetCoordMap({3:p})
    json = rdDistGeom.EmbedParametersToJSON(ps)
    goal = '{"basinThresh":"5","boundsMatForceScaling":"1","boxSizeMult":"2","clearConfs":"true","embedFragmentsSeparately":"true","enableSequentialRandomSeeds":"false","enforceChirality":"true","ETversion":"1","forceTransAmides":"true","ignoreSmoothingFailures":"false","maxIterations":"0","numThreads":"1","numZeroFail":"1","onlyHeavyAtomsForRMS":"true","optimizerForceTol":"0.001","pruneRmsThresh":"-1","randNegEig":"true","randomSeed":"-1","symmetrizeConjugatedTerminalGroupsForPruning":"true","timeout":"0","trackFailures":"false","useBasicKnowledge":"true","useExpTorsionAnglePrefs":"false","useMacrocycle14config":"false","useMacrocycleTorsions":"false","useRandomCoords":"false","useSmallRingTorsions":"false","useSymmetryForPruning":"true","verbose":"false","coordMap":{"3":["1.100000","2.200000","3.300000"]}}'
    self.assertEqual(json,goal)
    mol = Chem.AddHs(Chem.MolFromSmiles("O"))
    bm = rdDistGeom.GetMoleculeBoundsMatrix(mol)
    ps.SetBoundsMat(bm)
    goal = '{"basinThresh":"5","boundsMatForceScaling":"1","boxSizeMult":"2","clearConfs":"true","embedFragmentsSeparately":"true","enableSequentialRandomSeeds":"false","enforceChirality":"true","ETversion":"1","forceTransAmides":"true","ignoreSmoothingFailures":"false","maxIterations":"0","numThreads":"1","numZeroFail":"1","onlyHeavyAtomsForRMS":"true","optimizerForceTol":"0.001","pruneRmsThresh":"-1","randNegEig":"true","randomSeed":"-1","symmetrizeConjugatedTerminalGroupsForPruning":"true","timeout":"0","trackFailures":"false","useBasicKnowledge":"true","useExpTorsionAnglePrefs":"false","useMacrocycle14config":"false","useMacrocycleTorsions":"false","useRandomCoords":"false","useSmallRingTorsions":"false","useSymmetryForPruning":"true","verbose":"false","coordMap":{"3":["1.100000","2.200000","3.300000"]},"boundsMatrix":[["0","1.0002542040013616","1.0002542040013616"],["0.98025420400136154","0","1.6573654663221247"],["0.98025420400136154","1.5773654663221246","0"]]}'
    json = rdDistGeom.EmbedParametersToJSON(ps)
    self.assertEqual(json,goal)

if __name__ == '__main__':
  unittest.main()
