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