def DetermineSolvationParameters ( system, buffervalues = _BUFFERVALUES, log = logFile, molarities = _MOLARITIES, solventdensity = _WATERDENSITY, solventmass = _WATERMASS ): """Determine some solvation parameters for a system.""" # . Get the system's extents (before and after reorientation). masses = system.atoms.GetItemAttributes ( "mass" ) ( origin0, extents0 ) = system.coordinates3.EnclosingOrthorhombicBox ( ) coordinates3 = Clone ( system.coordinates3 ) coordinates3.ToPrincipalAxes ( weights = masses ) ( origin1, extents1 ) = coordinates3.EnclosingOrthorhombicBox ( ) # . Get the system's charge and mass. charge = sum ( system.atoms.GetItemAttributes ( "formalCharge" ) ) mmcharge = sum ( system.AtomicCharges ( ) ) mass = sum ( masses ) # . Convert the mass to milligrams. mass *= UNITS_MASS_AMU_TO_KG * 1000.0 # . Calculate the solvent volume in A^3. solventvolume = solventmass * ( UNITS_MASS_AMU_TO_KG * 1.0e+30 ) / solventdensity # . Basic printing. if LogFileActive ( log ): if math.fabs ( float ( charge ) - mmcharge ) > _CHARGETOLERANCE: log.Paragraph ( "Total formal and MM charges differ. MM charge = {:.3f}.".format ( mmcharge ) ) log.Paragraph ( "Total formal charge of system = {:.1f}.".format ( float ( charge ) ) ) # . Detailed printing. # . Initialization. nbuffers = len ( buffervalues ) if nbuffers > 0: # . Loop over extents. for ( extents, tag ) in ( ( extents0, "Original" ), ( extents1, "Reoriented" ) ): # . Get the extents. x = extents[0] y = extents[1] z = extents[2] # . Get the buffer data. data = [ [] for i in range ( 4 ) ] for buffer in buffervalues: data[0].append ( x + buffer ) data[1].append ( y + buffer ) data[2].append ( z + buffer ) data[3].append ( ( x + buffer ) * ( y + buffer ) * ( z + buffer ) ) # . Table header. if LogFileActive ( log ): table = log.GetTable ( columns = [ 30 ] + nbuffers * [ 20 ] ) table.Start ( ) table.Title ( "Solvation Information for " + tag + " Coordinates in an Orthorhombic Box" ) table.Heading ( "Property" ) table.Heading ( "Buffers (Angstroms)", columnSpan = nbuffers ) table.Heading ( "" ) for buffer in buffervalues: table.Heading ( "{:.2f}".format ( buffer ) ) # . Box data. for ( values, property ) in zip ( data, ( "X (A)", "Y", "Z", "Volume (A^3)" ) ): table.Entry ( "Box " + property, alignment = "l" ) for value in values: table.Entry ( "{:.2f}".format ( value ) ) # . Number of solvent molecules in solvent-only box. table.Entry ( "Molecules in Pure Solvent Box", alignment = "l" ) for v in data[3]: table.Entry ( "{:d}".format ( int ( round ( v / solventvolume ) ) ) ) # . System concentration. table.Entry ( "System Concentration (mg/l)", alignment = "l" ) for v in data[3]: table.Entry ( "{:.2f}".format ( mass / ( 1.0e-27 * v ) ) ) # . Number of ions in box for a given molarity - with the constraint that the total charge (system + ions) is zero. for molarity in molarities: table.Entry ( "Ions {:.3f} M".format ( molarity ), alignment = "l" ) for v in data[3]: nnegative = npositive = int ( round ( molarity * v * ( CONSTANT_AVOGADRO_NUMBER * 1.0e-27 ) ) ) if charge > 0: nnegative += abs ( charge ) elif charge < 0: npositive += abs ( charge ) table.Entry ( "+{:d}/-{:d}".format ( npositive, nnegative ) ) # . Finish up. table.Stop ( ) # . Spherical systems. # . Radii. radius0 = max ( extents0 ) / 2.0 radius1 = max ( extents1 ) / 2.0 if nbuffers > 0: # . Loop over extents. for ( radius, tag ) in ( ( radius0, "Original" ), ( radius1, "Reoriented" ) ): # . Get the buffer data. data = [ [] for i in range ( 2 ) ] for buffer in buffervalues: r = radius + buffer data[0].append ( r ) data[1].append ( 4.0 * math.pi * r**3 / 3.0 ) # . Table header. if LogFileActive ( log ): table = log.GetTable ( columns = [ 35 ] + nbuffers * [ 20 ] ) table.Start ( ) table.Title ( "Solvation Information for " + tag + " Coordinates in a Sphere" ) table.Heading ( "Property" ) table.Heading ( "Buffers (Angstroms)", columnSpan = nbuffers ) table.Heading ( "" ) for buffer in buffervalues: table.Heading ( "{:.2f}".format ( buffer ) ) # . Box data. for ( values, property ) in zip ( data, ( "Radius (A)", "Volume (A^3)" ) ): table.Entry ( "Sphere " + property, alignment = "l" ) for value in values: table.Entry ( "{:.2f}".format ( value ) ) # . Number of solvent molecules in solvent-only box. table.Entry ( "Molecules in Pure Solvent Sphere", alignment = "l" ) for v in data[1]: table.Entry ( "{:d}".format ( int ( round ( v / solventvolume ) ) ) ) # . System concentration. table.Entry ( "System Concentration (mg/l)", alignment = "l" ) for v in data[1]: table.Entry ( "{:.2f}".format ( mass / ( 1.0e-27 * v ) ) ) # . Number of ions in box for a given molarity - with the constraint that the total charge (system + ions) is zero. for molarity in molarities: table.Entry ( "Ions {:.3f} M".format ( molarity ), alignment = "l" ) for v in data[1]: nnegative = npositive = int ( round ( molarity * v * ( CONSTANT_AVOGADRO_NUMBER * 1.0e-27 ) ) ) if charge > 0: nnegative += abs ( charge ) elif charge < 0: npositive += abs ( charge ) table.Entry ( "+{:d}/-{:d}".format ( npositive, nnegative ) ) # . Finish up. table.Stop ( )
def BuildCubicSolventBox ( molecule, nmolecules, log = logFile, moleculesize = None, excludeHydrogens = True, QRANDOMROTATION = True, randomNumberGenerator = None, scalesafety = 1.1 ): """Build a cubic solvent box.""" # . Get the number of molecules in each direction. nlinear = int ( math.ceil ( math.pow ( float ( nmolecules ), 1.0 / 3.0 ) ) ) # . Get the indices of the occupied sites. sites = sample ( range ( nlinear**3 ), nmolecules ) sites.sort ( ) # . Get the number of atoms in the molecule and an appropriate selection. natoms = len ( molecule.atoms ) selection = Selection.FromIterable ( range ( natoms ) ) # . Get a copy of molecule's coordinates (reorientated). coordinates3 = Clone ( molecule.coordinates3 ) coordinates3.ToPrincipalAxes ( ) # . Get the molecule size depending upon the input options. # . A molecule size has been specified. if moleculesize is not None: size = moleculesize # . Determine the size of the molecule as the diagonal distance across its enclosing orthorhombic box. else: radii = molecule.atoms.GetItemAttributes ( "vdwRadius" ) if excludeHydrogens: atomicNumbers = molecule.atoms.GetItemAttributes ( "atomicNumber" ) for ( i, atomicNumber ) in enumerate ( molecule.atoms.GetItemAttributes ( "atomicNumber" ) ): if atomicNumber == 1: radii[i] = 0.0 ( origin, extents ) = coordinates3.EnclosingOrthorhombicBox ( radii = radii ) size = extents.Norm2 ( ) * scalesafety # . Create the new system - temporarily resetting the coordinates. temporary3 = molecule.coordinates3 molecule.coordinates3 = coordinates3 solvent = MergeByAtom ( nmolecules * [ molecule ] ) molecule.coordinates3 = temporary3 # . Set the system symmetry. solvent.DefineSymmetry ( crystalClass = CrystalClassCubic ( ), a = size * float ( nlinear ) ) # . Set up for random rotations. if QRANDOMROTATION: if randomNumberGenerator is None: randomNumberGenerator = RandomNumberGenerator.WithRandomSeed ( ) rotation = Matrix33.Null ( ) # . Loop over the box sites. origin = 0.5 * float ( 1 - nlinear ) * size n = 0 translation = Vector3.Null ( ) for i in range ( nlinear ): translation[0] = origin + size * float ( i ) for j in range ( nlinear ): translation[1] = origin + size * float ( j ) for k in range ( nlinear ): if len ( sites ) == 0: break translation[2] = origin + size * float ( k ) # . Check for an occupied site. if sites[0] == n: sites.pop ( 0 ) # . Randomly rotate the coordinates. if QRANDOMROTATION: rotation.RandomRotation ( randomNumberGenerator ) solvent.coordinates3.Rotate ( rotation, selection = selection ) # . Translate the coordinates. solvent.coordinates3.Translate ( translation, selection = selection ) # . Increment the selection for the next molecule. selection.Increment ( natoms ) n += 1 # . Do some printing. if LogFileActive ( log ): summary = log.GetSummary ( ) summary.Start ( "Cubic Solvent Box Summary" ) summary.Entry ( "Number of Molecules", "{:d}" .format ( nmolecules ) ) summary.Entry ( "Density (kg m^-3)", "{:.3f}".format ( SystemDensity ( solvent ) ) ) summary.Entry ( "Box Side", "{:.3f}".format ( solvent.symmetryParameters.a ) ) summary.Entry ( "Molecule Size", "{:.3f}".format ( size ) ) summary.Stop ( ) # . Return the cubic system. return solvent
def CalculateSolvationParameters ( system, bufferValue = 0.0, geometry = "Orthorhombic", ionicStrength = 0.0, reorientSolute = False ): """Check the solvation parameters for a system.""" # . Get the system charge. charge = sum ( system.atoms.GetItemAttributes ( "formalCharge" ) ) absoluteCharge = abs ( charge ) # . Get the system extents. if reorientSolute: masses = system.atoms.GetItemAttributes ( "mass" ) coordinates3 = Clone ( system.coordinates3 ) coordinates3.ToPrincipalAxes ( weights = masses ) else: coordinates3 = system.coordinates3 ( origin, extents ) = coordinates3.EnclosingOrthorhombicBox ( ) extents.AddScalar ( bufferValue ) # . Get solvated system dimensions. r = x = y = z = 0.0 if geometry == "Cubic": x = y = z = max ( extents ) ; elif geometry == "Orthorhombic": x = extents[0] ; y = extents[1] ; z = extents[2] elif geometry == "Spherical": r = max ( extents ) / 2.0 elif geometry == "Tetragonal (X=Y)": x = y = max ( extents[0], extents[1] ) ; z = extents[2] elif geometry == "Tetragonal (Y=Z)": y = z = max ( extents[1], extents[2] ) ; x = extents[0] elif geometry == "Tetragonal (Z=X)": z = x = max ( extents[2], extents[0] ) ; y = extents[1] # . Get the minimum values. minimumR = max ( 0.0, r - bufferValue ) minimumX = max ( 0.0, x - bufferValue ) minimumY = max ( 0.0, y - bufferValue ) minimumZ = max ( 0.0, z - bufferValue ) # . Get the volume. if geometry == "Spherical": v = 4.0 * math.pi * r**3 / 3.0 else: v = x * y * z # . Determine the number of anions and cations given the ionic strength. minimumNumberAnions = 0 minimumNumberCations = 0 numberAnions = 0 numberCations = 0 if ionicStrength > 0.0: numberAnions = numberCations = int ( round ( ionicStrength * v * ( CONSTANT_AVOGADRO_NUMBER * 1.0e-27 ) ) ) if charge > 0: minimumNumberAnions = absoluteCharge numberAnions += absoluteCharge elif charge < 0: minimumNumberCations = absoluteCharge numberCations += absoluteCharge # . Finish up. solvationParameters = { "minimumNumberAnions" : minimumNumberAnions , "minimumNumberCations" : minimumNumberCations, "minimumRadius" : minimumR, "minimumX" : minimumX, "minimumY" : minimumY, "minimumZ" : minimumZ, "numberAnions" : numberAnions , "numberCations" : numberCations, "radius" : r, "x" : x, "y" : y, "z" : z } return solvationParameters
def HardSphereIonMobilities(molecule, nreflections=30, ntrajectories=600000, randomNumberGenerator=None, temperature=298.0, log=logFile): """Calculate ion mobilities with a hard-sphere model.""" # . Get the atom data. hsradii = _GetHardSphereRadii(molecule.atoms) masses = molecule.atoms.GetItemAttributes("mass") totalmass = masses.Sum() # . Get initial coordinates, move to center of mass and convert to metres. xyz0 = Clone(molecule.coordinates3) xyz0.TranslateToCenter(weights=masses) xyz0.Scale(1.0e-10) # . Get the mass constant. massHe = PeriodicTable.Element(2).mass massconstant = _MASSCONSTANT * math.sqrt((1.0 / massHe) + (1.0 / totalmass)) # . Get the random number generator. if randomNumberGenerator is None: randomNumberGenerator = RandomNumberGenerator.WithRandomSeed() rotation = Matrix33.Null() # . Initialize some calculation variables. cof = Real1DArray.WithExtent(nreflections) cof.Set(0.0) crof = Real1DArray.WithExtent(nreflections) crof.Set(0.0) crb = 0.0 mreflections = 0 # . Loop over the trajectories. for it in range(ntrajectories): # . Randomly rotate the coordinate set. rotation.RandomRotation(randomNumberGenerator) xyz = Clone(xyz0) xyz.Rotate(rotation) # . Loop over the collisions. QCOLLISION = False for ir in range(nreflections): # . Initial collision - at a random point in the yz plane along the x-axis. if ir == 0: (origin, extents) = xyz.EnclosingOrthorhombicBox(radii=hsradii) yzarea = extents[1] * extents[2] yc = origin[1] + extents[1] * randomNumberGenerator.NextReal() zc = origin[2] + extents[2] * randomNumberGenerator.NextReal() xaxis = Vector3.WithValues(1.0, 0.0, 0.0) # . Subsequent collisions - always along the x-axis. else: yc = 0.0 zc = 0.0 # . Initialization. ic = -1 # . The index of the colliding particle. xc = origin[0] + extents[0] # . The largest x-coordinate. # . Loop over particles. for (i, h) in enumerate(hsradii): # . After the first collision only x-values > 0 are allowed. if (ir == 0) or (xyz[i, 0] > 1.0e-16): # . yd and zd are the coordinates of the impact points for the ith atom # . with respect to its own coordinates (if such a point exists). # . dev is the impact parameter. h2 = h * h y = yc - xyz[i, 1] z = zc - xyz[i, 2] yz2 = y * y + z * z # . If there is a collision with the ith atom, check to see if it occurs before previous collisions. if yz2 < h2: x = xyz[i, 0] - math.sqrt(h2 - yz2) if x < xc: xc = x ic = i # . Check mreflections. if ir >= mreflections: mreflections = ir + 1 # . There was a collision. if ic >= 0: QCOLLISION = True # . Translate the coordinates so that the collision point is at the origin. xyz.Translate(Vector3.WithValues(-xc, -yc, -zc)) # . Rotate the coordinates so that the outgoing vector is along the x-axis. h = xyz.GetRow( ic ) # . Normalized vector from the collision point to the ic-th atom. h.Normalize(tolerance=1.0e-20) axis = Vector3.WithValues( 0.0, h[2], -h[1]) # . Normalized axis of rotation. axis.Normalize(tolerance=1.0e-20) alpha = math.pi - 2.0 * math.acos(h[0]) # . Angle of rotation. rotation.RotationAboutAxis(alpha, axis) xyz.Rotate(rotation) rotation.ApplyTo(xaxis) # . Calculate the cosine of the angle between the incoming vector and the normal to a plane, # . the reflection from which would be equivalent to the accumulated reflection. # . This is equal to h[0] when ir = 0. cof[ir] = math.cos(0.5 * (math.pi - math.acos(xaxis[0]))) # . Check outgoing. # . Get the outgoing vector (the ingoing vector is always [1,0,0]). out = Vector3.WithValues(1.0 - 2.0 * h[0] * h[0], -2.0 * h[0] * h[1], -2.0 * h[0] * h[2]) rotation.ApplyTo(out) out[0] -= 1.0 if out.Norm2() > 1.0e-6: print( "Invalid Rotation: {:10.3f} {:10.3f} {:10.3f}.".format( out[0], out[1], out[2])) # . There was no collision. else: # . Top up the remaining elements of cof with the last valid value of cof. if ir == 0: t = 0.0 else: t = cof[ir - 1] for i in range(ir, nreflections): cof[i] = t # . Exit. break # . End of collisions. # . Projection approximation. if QCOLLISION: crb += yzarea # . Hard-sphere approximation. for ir in range(nreflections): crof[ir] += yzarea * cof[ir] * cof[ir] # . End of trajectories. crof.Scale(2.0 / float(ntrajectories)) pacs = crb / float(ntrajectories) pamob = massconstant / (pacs * math.sqrt(temperature)) hscs = crof[mreflections - 1] hsmob = massconstant / (hscs * math.sqrt(temperature)) # . Output results. if LogFileActive(log): summary = log.GetSummary() summary.Start("Hard-Sphere Ion Mobilities") summary.Entry("MC Trajectories", "{:d}".format(ntrajectories)) summary.Entry("Reflection Limit", "{:d}".format(nreflections)) summary.Entry("PA Mobility", "{:.4g}".format(pamob)) summary.Entry("PA Cross-Section", "{:.4g}".format(pacs * 1.0e+20)) summary.Entry("HS Mobility", "{:.4g}".format(hsmob)) summary.Entry("HS Cross-Section", "{:.4g}".format(hscs * 1.0e+20)) summary.Entry("Max. Reflections", "{:d}".format(mreflections)) summary.Stop() # . Finish up. results = { "MC Trajectories": ntrajectories, "Reflection Limit": nreflections, "PA Mobility": pamob, "PA Cross-Section": pacs * 1.0e+20, "HS Mobility": hsmob, "HS Cross-Section": hscs * 1.0e+20, "Maximum Reflections": mreflections } return results