Commit b0dacea0 authored by Andreas Krämer's avatar Andreas Krämer
Browse files

Changed forcefield to not use dispersion corrections for CHARMM

parent 1d3804df
...@@ -377,7 +377,7 @@ for values in sorted(uniqueCmaps, key=lambda x: uniqueCmaps[x]): ...@@ -377,7 +377,7 @@ for values in sorted(uniqueCmaps, key=lambda x: uniqueCmaps[x]):
for map in cmaps: for map in cmaps:
print(' <Torsion map="%d" class1="%s" class2="%s" class3="%s" class4="%s" class5="%s"/>' % (uniqueCmaps[map.values], map.classes[0], map.classes[1], map.classes[2], map.classes[3], map.classes[4])) print(' <Torsion map="%d" class1="%s" class2="%s" class3="%s" class4="%s" class5="%s"/>' % (uniqueCmaps[map.values], map.classes[0], map.classes[1], map.classes[2], map.classes[3], map.classes[4]))
print(' </CMAPTorsionForce>') print(' </CMAPTorsionForce>')
print(' <NonbondedForce coulomb14scale="1.0" lj14scale="1.0">') print(' <NonbondedForce coulomb14scale="1.0" lj14scale="1.0" useDispersionCorrection="False">')
for type in atomTypes: for type in atomTypes:
print(' <Atom type="%d" charge="%g" sigma="1.0" epsilon="1.0"/>' % (type.type, type.charge)) print(' <Atom type="%d" charge="%g" sigma="1.0" epsilon="1.0"/>' % (type.type, type.charge))
print(' </NonbondedForce>') print(' </NonbondedForce>')
...@@ -464,6 +464,7 @@ print("""] ...@@ -464,6 +464,7 @@ print("""]
customNonbondedForce = mm.CustomNonbondedForce('4*eps*((sig/r)^12-(sig/r)^6); eps=epsilon(type1, type2); sig=sigma(type1, type2)') customNonbondedForce = mm.CustomNonbondedForce('4*eps*((sig/r)^12-(sig/r)^6); eps=epsilon(type1, type2); sig=sigma(type1, type2)')
customNonbondedForce.setNonbondedMethod(min(nonbondedForce.getNonbondedMethod(), 2)) customNonbondedForce.setNonbondedMethod(min(nonbondedForce.getNonbondedMethod(), 2))
customNonbondedForce.setUseLongRangeCorrection(False)
customNonbondedForce.addTabulatedFunction('epsilon', mm.Discrete2DFunction(numAtomClasses, numAtomClasses, epsilon)) customNonbondedForce.addTabulatedFunction('epsilon', mm.Discrete2DFunction(numAtomClasses, numAtomClasses, epsilon))
customNonbondedForce.addTabulatedFunction('sigma', mm.Discrete2DFunction(numAtomClasses, numAtomClasses, sigma)) customNonbondedForce.addTabulatedFunction('sigma', mm.Discrete2DFunction(numAtomClasses, numAtomClasses, sigma))
customNonbondedForce.addPerParticleParameter('type') customNonbondedForce.addPerParticleParameter('type')
......
...@@ -102920,7 +102920,7 @@ ...@@ -102920,7 +102920,7 @@
<Torsion map="6" type1="C" type2="NH1" type3="CT" type4="C" type5="NH1"/> <Torsion map="6" type1="C" type2="NH1" type3="CT" type4="C" type5="NH1"/>
<Torsion map="7" type1="C" type2="N" type3="CT2" type4="C" type5="N"/> <Torsion map="7" type1="C" type2="N" type3="CT2" type4="C" type5="N"/>
</CMAPTorsionForce> </CMAPTorsionForce>
<NonbondedForce coulomb14scale="1.0" lj14scale="1.0"> <NonbondedForce coulomb14scale="1.0" lj14scale="1.0" useDispersionCorrection="False">
<UseAttributeFromResidue name="charge"/> <UseAttributeFromResidue name="charge"/>
<Atom epsilon="0.0" sigma="1.0" type="H"/> <Atom epsilon="0.0" sigma="1.0" type="H"/>
<Atom epsilon="0.0" sigma="1.0" type="HC"/> <Atom epsilon="0.0" sigma="1.0" type="HC"/>
...@@ -103335,7 +103335,7 @@ ...@@ -103335,7 +103335,7 @@
<Atom epsilon="0.0" sigma="1.0" type="NE"/> <Atom epsilon="0.0" sigma="1.0" type="NE"/>
<Atom epsilon="0.0" sigma="1.0" type="DUM"/> <Atom epsilon="0.0" sigma="1.0" type="DUM"/>
</NonbondedForce> </NonbondedForce>
<LennardJonesForce lj14scale="1.0"> <LennardJonesForce lj14scale="1.0" useDispersionCorrection="False">
<Atom epsilon="0.192464" sigma="0.04000135244450124" type="H"/> <Atom epsilon="0.192464" sigma="0.04000135244450124" type="H"/>
<Atom epsilon="0.192464" sigma="0.04000135244450124" type="HC"/> <Atom epsilon="0.192464" sigma="0.04000135244450124" type="HC"/>
<Atom epsilon="0.092048" sigma="0.2351972615890496" type="HA"/> <Atom epsilon="0.092048" sigma="0.2351972615890496" type="HA"/>
...@@ -7200,7 +7200,7 @@ ...@@ -7200,7 +7200,7 @@
<Torsion map="2" class1="CD2O1A" class2="ND3A3" class3="CD31C" class4="CD2O1A" class5="ND2A2"/> <Torsion map="2" class1="CD2O1A" class2="ND3A3" class3="CD31C" class4="CD2O1A" class5="ND2A2"/>
<Torsion map="2" class1="CD2O1A" class2="ND3A3" class3="CD31C" class4="CD2O1A" class5="ND3A3"/> <Torsion map="2" class1="CD2O1A" class2="ND3A3" class3="CD31C" class4="CD2O1A" class5="ND3A3"/>
</CMAPTorsionForce> </CMAPTorsionForce>
<NonbondedForce coulomb14scale="1.0" lj14scale="1.0"> <NonbondedForce coulomb14scale="1.0" lj14scale="1.0" useDispersionCorrection="False">
<Atom type="0" charge="1.71639" sigma="1.0" epsilon="1.0"/> <Atom type="0" charge="1.71639" sigma="1.0" epsilon="1.0"/>
<Atom type="1" charge="-1.11466" sigma="1.0" epsilon="1.0"/> <Atom type="1" charge="-1.11466" sigma="1.0" epsilon="1.0"/>
<Atom type="2" charge="0.55733" sigma="1.0" epsilon="1.0"/> <Atom type="2" charge="0.55733" sigma="1.0" epsilon="1.0"/>
...@@ -11370,6 +11370,7 @@ sigma14 = [ ...@@ -11370,6 +11370,7 @@ sigma14 = [
   
customNonbondedForce = mm.CustomNonbondedForce('4*eps*((sig/r)^12-(sig/r)^6); eps=epsilon(type1, type2); sig=sigma(type1, type2)') customNonbondedForce = mm.CustomNonbondedForce('4*eps*((sig/r)^12-(sig/r)^6); eps=epsilon(type1, type2); sig=sigma(type1, type2)')
customNonbondedForce.setNonbondedMethod(min(nonbondedForce.getNonbondedMethod(), 2)) customNonbondedForce.setNonbondedMethod(min(nonbondedForce.getNonbondedMethod(), 2))
customNonbondedForce.setUseLongRangeCorrection(False)
customNonbondedForce.setCutoffDistance(nonbondedForce.getCutoffDistance()) customNonbondedForce.setCutoffDistance(nonbondedForce.getCutoffDistance())
customNonbondedForce.addTabulatedFunction('epsilon', mm.Discrete2DFunction(numAtomClasses, numAtomClasses, epsilon)) customNonbondedForce.addTabulatedFunction('epsilon', mm.Discrete2DFunction(numAtomClasses, numAtomClasses, epsilon))
customNonbondedForce.addTabulatedFunction('sigma', mm.Discrete2DFunction(numAtomClasses, numAtomClasses, sigma)) customNonbondedForce.addTabulatedFunction('sigma', mm.Discrete2DFunction(numAtomClasses, numAtomClasses, sigma))
...@@ -37,6 +37,7 @@ import os ...@@ -37,6 +37,7 @@ import os
import itertools import itertools
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
import math import math
import warnings
from math import sqrt, cos from math import sqrt, cos
from copy import deepcopy from copy import deepcopy
from collections import defaultdict from collections import defaultdict
...@@ -2273,10 +2274,11 @@ class NonbondedGenerator(object): ...@@ -2273,10 +2274,11 @@ class NonbondedGenerator(object):
SCALETOL = 1e-5 SCALETOL = 1e-5
def __init__(self, forcefield, coulomb14scale, lj14scale): def __init__(self, forcefield, coulomb14scale, lj14scale, useDispersionCorrection):
self.ff = forcefield self.ff = forcefield
self.coulomb14scale = coulomb14scale self.coulomb14scale = coulomb14scale
self.lj14scale = lj14scale self.lj14scale = lj14scale
self.useDispersionCorrection = useDispersionCorrection
self.params = ForceField._AtomTypeParameters(forcefield, 'NonbondedForce', 'Atom', ('charge', 'sigma', 'epsilon')) self.params = ForceField._AtomTypeParameters(forcefield, 'NonbondedForce', 'Atom', ('charge', 'sigma', 'epsilon'))
def registerAtom(self, parameters): def registerAtom(self, parameters):
...@@ -2285,15 +2287,31 @@ class NonbondedGenerator(object): ...@@ -2285,15 +2287,31 @@ class NonbondedGenerator(object):
@staticmethod @staticmethod
def parseElement(element, ff): def parseElement(element, ff):
existing = [f for f in ff._forces if isinstance(f, NonbondedGenerator)] existing = [f for f in ff._forces if isinstance(f, NonbondedGenerator)]
if 'useDispersionCorrection' in element.attrib:
useDispersionCorrection = bool(eval(element.attrib.get('useDispersionCorrection')))
else:
useDispersionCorrection = None
if len(existing) == 0: if len(existing) == 0:
generator = NonbondedGenerator(ff, float(element.attrib['coulomb14scale']), float(element.attrib['lj14scale'])) generator = NonbondedGenerator(
ff,
float(element.attrib['coulomb14scale']),
float(element.attrib['lj14scale']),
useDispersionCorrection
)
ff.registerGenerator(generator) ff.registerGenerator(generator)
else: else:
# Multiple <NonbondedForce> tags were found, probably in different files. Simply add more types to the existing one. # Multiple <NonbondedForce> tags were found, probably in different files. Simply add more types to the existing one.
generator = existing[0] generator = existing[0]
if abs(generator.coulomb14scale - float(element.attrib['coulomb14scale'])) > NonbondedGenerator.SCALETOL or \ if (abs(generator.coulomb14scale - float(element.attrib['coulomb14scale'])) > NonbondedGenerator.SCALETOL
abs(generator.lj14scale - float(element.attrib['lj14scale'])) > NonbondedGenerator.SCALETOL: or abs(generator.lj14scale - float(element.attrib['lj14scale'])) > NonbondedGenerator.SCALETOL
):
raise ValueError('Found multiple NonbondedForce tags with different 1-4 scales') raise ValueError('Found multiple NonbondedForce tags with different 1-4 scales')
if (
generator.useDispersionCorrection is not None
and useDispersionCorrection is not None
and generator.useDispersionCorrection != useDispersionCorrection
):
raise ValueError('Found multiple NonbondedForce tags with different useDispersionCorrection settings.')
generator.params.parseDefinitions(element) generator.params.parseDefinitions(element)
def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args): def createForce(self, sys, data, nonbondedMethod, nonbondedCutoff, args):
...@@ -2317,7 +2335,18 @@ class NonbondedGenerator(object): ...@@ -2317,7 +2335,18 @@ class NonbondedGenerator(object):
if 'ewaldErrorTolerance' in args: if 'ewaldErrorTolerance' in args:
force.setEwaldErrorTolerance(args['ewaldErrorTolerance']) force.setEwaldErrorTolerance(args['ewaldErrorTolerance'])
if 'useDispersionCorrection' in args: if 'useDispersionCorrection' in args:
force.setUseDispersionCorrection(bool(args['useDispersionCorrection'])) lrc_keyword = bool(args['useDispersionCorrection'])
if self.useDispersionCorrection is not None and lrc_keyword != self.useDispersionCorrection:
warnings.warn(
"Conflicting settings for useDispersionCorrection in createSystem() and forcefield file. "
"Using the one specified in createSystem()."
)
force.setUseDispersionCorrection(lrc_keyword)
elif self.useDispersionCorrection is not None:
force.setUseDispersionCorrection(self.useDispersionCorrection)
else:
# by default
force.setUseDispersionCorrection(True)
sys.addForce(force) sys.addForce(force)
def postprocessSystem(self, sys, data, args): def postprocessSystem(self, sys, data, args):
...@@ -2334,10 +2363,11 @@ parsers["NonbondedForce"] = NonbondedGenerator.parseElement ...@@ -2334,10 +2363,11 @@ parsers["NonbondedForce"] = NonbondedGenerator.parseElement
class LennardJonesGenerator(object): class LennardJonesGenerator(object):
"""A NBFix generator to construct the L-J force with NBFIX implemented as a lookup table""" """A NBFix generator to construct the L-J force with NBFIX implemented as a lookup table"""
def __init__(self, forcefield, lj14scale): def __init__(self, forcefield, lj14scale, useDispersionCorrection):
self.ff = forcefield self.ff = forcefield
self.nbfixTypes = {} self.nbfixTypes = {}
self.lj14scale = lj14scale self.lj14scale = lj14scale
self.useDispersionCorrection = useDispersionCorrection
self.ljTypes = ForceField._AtomTypeParameters(forcefield, 'LennardJonesForce', 'Atom', ('sigma', 'epsilon')) self.ljTypes = ForceField._AtomTypeParameters(forcefield, 'LennardJonesForce', 'Atom', ('sigma', 'epsilon'))
def registerNBFIX(self, parameters): def registerNBFIX(self, parameters):
...@@ -2356,14 +2386,28 @@ class LennardJonesGenerator(object): ...@@ -2356,14 +2386,28 @@ class LennardJonesGenerator(object):
@staticmethod @staticmethod
def parseElement(element, ff): def parseElement(element, ff):
existing = [f for f in ff._forces if isinstance(f, LennardJonesGenerator)] existing = [f for f in ff._forces if isinstance(f, LennardJonesGenerator)]
if 'useDispersionCorrection' in element.attrib:
useDispersionCorrection = bool(eval(element.attrib.get('useDispersionCorrection')))
else:
useDispersionCorrection = None
if len(existing) == 0: if len(existing) == 0:
generator = LennardJonesGenerator(ff, float(element.attrib['lj14scale'])) generator = LennardJonesGenerator(
ff,
float(element.attrib['lj14scale']),
useDispersionCorrection=useDispersionCorrection
)
ff.registerGenerator(generator) ff.registerGenerator(generator)
else: else:
# Multiple <LennardJonesForce> tags were found, probably in different files # Multiple <LennardJonesForce> tags were found, probably in different files
generator = existing[0] generator = existing[0]
if abs(generator.lj14scale - float(element.attrib['lj14scale'])) > NonbondedGenerator.SCALETOL: if abs(generator.lj14scale - float(element.attrib['lj14scale'])) > NonbondedGenerator.SCALETOL:
raise ValueError('Found multiple LennardJonesForce tags with different 1-4 scales') raise ValueError('Found multiple LennardJonesForce tags with different 1-4 scales')
if (
generator.useDispersionCorrection is not None
and useDispersionCorrection is not None
and generator.useDispersionCorrection != useDispersionCorrection
):
raise ValueError('Found multiple LennardJonesForce tags with different useDispersionCorrection settings.')
for LJ in element.findall('Atom'): for LJ in element.findall('Atom'):
generator.registerLennardJones(LJ.attrib) generator.registerLennardJones(LJ.attrib)
for Nbfix in element.findall('NBFixPair'): for Nbfix in element.findall('NBFixPair'):
...@@ -2434,12 +2478,24 @@ class LennardJonesGenerator(object): ...@@ -2434,12 +2478,24 @@ class LennardJonesGenerator(object):
if args['switchDistance'] is not None: if args['switchDistance'] is not None:
self.force.setUseSwitchingFunction(True) self.force.setUseSwitchingFunction(True)
self.force.setSwitchingDistance(args['switchDistance']) self.force.setSwitchingDistance(args['switchDistance'])
if 'useDispersionCorrection' in args:
lrc_keyword = bool(args['useDispersionCorrection'])
if self.useDispersionCorrection is not None and lrc_keyword != self.useDispersionCorrection:
warnings.warn(
"Conflicting settings for useDispersionCorrection in createSystem() and forcefield file. "
"Using the one specified in createSystem()."
)
self.force.setUseLongRangeCorrection(lrc_keyword)
elif self.useDispersionCorrection is not None:
self.force.setUseLongRangeCorrection(self.useDispersionCorrection)
else:
# by default
self.force.setUseLongRangeCorrection(True)
# Add the particles # Add the particles
for atom in data.atoms: for atom in data.atoms:
self.force.addParticle((typeToMergedType[data.atomType[atom]],)) self.force.addParticle((typeToMergedType[data.atomType[atom]],))
self.force.setUseLongRangeCorrection(bool(args.get('useLongRangeCorrection', False)))
self.force.setCutoffDistance(nonbondedCutoff) self.force.setCutoffDistance(nonbondedCutoff)
sys.addForce(self.force) sys.addForce(self.force)
......
...@@ -6,6 +6,7 @@ from simtk.unit import * ...@@ -6,6 +6,7 @@ from simtk.unit import *
import simtk.openmm.app.element as elem import simtk.openmm.app.element as elem
import simtk.openmm.app.forcefield as forcefield import simtk.openmm.app.forcefield as forcefield
import math import math
import textwrap
try: try:
from cStringIO import StringIO from cStringIO import StringIO
except ImportError: except ImportError:
...@@ -50,16 +51,77 @@ class TestForceField(unittest.TestCase): ...@@ -50,16 +51,77 @@ class TestForceField(unittest.TestCase):
for f in forces)) for f in forces))
def test_DispersionCorrection(self): def test_DispersionCorrection(self):
"""Test to make sure the nonbondedCutoff parameter is passed correctly.""" """Test to make sure that the dispersion/long-range correction is set properly."""
top = Topology()
for useDispersionCorrection in [True, False]: chain = top.addChain()
system = self.forcefield1.createSystem(self.pdb1.topology,
nonbondedCutoff=2*nanometer,
useDispersionCorrection=useDispersionCorrection)
for lrc in (True, False):
xml = textwrap.dedent(
"""
<ForceField>
<LennardJonesForce lj14scale="0.3" useDispersionCorrection="{lrc}">
<Atom type="A" sigma="1" epsilon="0.1"/>
<Atom type="B" sigma="2" epsilon="0.2"/>
<NBFixPair type1="A" type2="B" sigma="2.5" epsilon="1.1"/>
</LennardJonesForce>
<NonbondedForce coulomb14scale="0.833333" lj14scale="0.5" useDispersionCorrection="{lrc2}">
<Atom type="A" sigma="0.315" epsilon="0.635"/>
</NonbondedForce>
</ForceField>
"""
)
ff = ForceField(StringIO(xml.format(lrc=lrc, lrc2=lrc)))
system = ff.createSystem(top)
checked_nonbonded = False
checked_custom = False
for force in system.getForces(): for force in system.getForces():
if isinstance(force, NonbondedForce): if isinstance(force, NonbondedForce):
self.assertEqual(useDispersionCorrection, force.getUseDispersionCorrection()) self.assertEqual(force.getUseDispersionCorrection(), lrc)
checked_nonbonded = True
elif isinstance(force, CustomNonbondedForce):
self.assertEqual(force.getUseLongRangeCorrection(), lrc)
checked_custom = True
self.assertTrue(checked_nonbonded and checked_custom)
# check that the keyword argument overwrites xml input
lrc_kwarg = not lrc
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
system2 = ff.createSystem(top, useDispersionCorrection=lrc_kwarg)
self.assertTrue(len(w) == 2)
assert "conflict" in str(w[-1].message).lower()
checked_nonbonded = False
checked_custom = False
for force in system2.getForces():
if isinstance(force, NonbondedForce):
self.assertEqual(force.getUseDispersionCorrection(), lrc_kwarg)
checked_nonbonded = True
elif isinstance(force, CustomNonbondedForce):
self.assertEqual(force.getUseLongRangeCorrection(), lrc_kwarg)
checked_custom = True
self.assertTrue(checked_nonbonded and checked_custom)
# check that no warning is generated when useDispersionCorrection is not in the xml file
xml = textwrap.dedent(
"""
<ForceField>
<LennardJonesForce lj14scale="0.3">
<Atom type="A" sigma="1" epsilon="0.1"/>
<Atom type="B" sigma="2" epsilon="0.2"/>
<NBFixPair type1="A" type2="B" sigma="2.5" epsilon="1.1"/>
</LennardJonesForce>
<NonbondedForce coulomb14scale="0.833333" lj14scale="0.5">
<Atom type="A" sigma="0.315" epsilon="0.635"/>
</NonbondedForce>
</ForceField>
"""
)
ff = ForceField(StringIO(xml))
system = ff.createSystem(top)
for lrc_kwarg in [True, False]:
with warnings.catch_warnings():
warnings.simplefilter("error")
system2 = ff.createSystem(top, useDispersionCorrection=lrc_kwarg)
def test_Cutoff(self): def test_Cutoff(self):
"""Test to make sure the nonbondedCutoff parameter is passed correctly.""" """Test to make sure the nonbondedCutoff parameter is passed correctly."""
...@@ -227,7 +289,7 @@ class TestForceField(unittest.TestCase): ...@@ -227,7 +289,7 @@ class TestForceField(unittest.TestCase):
angles = forcefield.HarmonicAngleGenerator(ff) angles = forcefield.HarmonicAngleGenerator(ff)
angles.registerAngle({'class1':'HW', 'class2':'OW', 'class3':'HW', 'angle':1.82421813418*radians, 'k':836.8*kilojoules_per_mole/radian}) angles.registerAngle({'class1':'HW', 'class2':'OW', 'class3':'HW', 'angle':1.82421813418*radians, 'k':836.8*kilojoules_per_mole/radian})
ff.registerGenerator(angles) ff.registerGenerator(angles)
nonbonded = forcefield.NonbondedGenerator(ff, 0.833333, 0.5) nonbonded = forcefield.NonbondedGenerator(ff, 0.833333, 0.5, True)
nonbonded.registerAtom({'type':'tip3p-O', 'charge':-0.834, 'sigma':0.31507524065751241*nanometers, 'epsilon':0.635968*kilojoules_per_mole}) nonbonded.registerAtom({'type':'tip3p-O', 'charge':-0.834, 'sigma':0.31507524065751241*nanometers, 'epsilon':0.635968*kilojoules_per_mole})
nonbonded.registerAtom({'type':'tip3p-H', 'charge':0.417, 'sigma':1*nanometers, 'epsilon':0*kilojoules_per_mole}) nonbonded.registerAtom({'type':'tip3p-H', 'charge':0.417, 'sigma':1*nanometers, 'epsilon':0*kilojoules_per_mole})
ff.registerGenerator(nonbonded) ff.registerGenerator(nonbonded)
...@@ -712,7 +774,7 @@ class TestForceField(unittest.TestCase): ...@@ -712,7 +774,7 @@ class TestForceField(unittest.TestCase):
<Atom name="SOD" type="SOD"/> <Atom name="SOD" type="SOD"/>
</Residue> </Residue>
</Residues> </Residues>
<LennardJonesForce lj14scale="1.0"> <LennardJonesForce lj14scale="1.0" useDispersionCorrection="False">
<Atom type="CLA" sigma="0.404468018036" epsilon="0.6276"/> <Atom type="CLA" sigma="0.404468018036" epsilon="0.6276"/>
<Atom type="SOD" sigma="0.251367073323" epsilon="0.1962296"/> <Atom type="SOD" sigma="0.251367073323" epsilon="0.1962296"/>
<NBFixPair type1="CLA" type2="SOD" sigma="0.33239431" epsilon="0.350933"/> <NBFixPair type1="CLA" type2="SOD" sigma="0.33239431" epsilon="0.350933"/>
...@@ -940,5 +1002,6 @@ class AmoebaTestForceField(unittest.TestCase): ...@@ -940,5 +1002,6 @@ class AmoebaTestForceField(unittest.TestCase):
diff = norm(f1-f2) diff = norm(f1-f2)
self.assertTrue(diff < 0.1 or diff/norm(f1) < 1e-3) self.assertTrue(diff < 0.1 or diff/norm(f1) < 1e-3)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment