Skip to content

Commit 39df815

Browse files
committed
Finish implementation of 'thermalize'
1 parent d608582 commit 39df815

2 files changed

Lines changed: 219 additions & 7 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ Key:
5353

5454
First development round (v0.1.0, current):
5555
- Internal workings figured out, Swift code written
56-
- C API implemented
56+
- C API drafted
5757
- Constant forces
5858

5959
Second development round (v0.2.0, in ~weeks):
6060
- Inner workings tested
61-
- Python API implemented
61+
- C, Python API implemented
6262
- Constant torques
6363

6464
Third development round (v1.0.0, in ~months):

Sources/MM4/ForceField/MM4ForceField+Thermalize.swift

Lines changed: 217 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// Created by Philip Turner on 10/21/23.
66
//
77

8+
import OpenMM
9+
810
extension MM4ForceField {
911
/// Create random thermal velocities, while conserving the total (bulk)
1012
/// momentum of each rigid body.
@@ -45,6 +47,157 @@ extension MM4ForceField {
4547
// Notes about angular momentum:
4648
// https://www.physicsforums.com/threads/how-can-angular-velocity-be-independent-of-the-choice-of-origin.986098/#:~:text=Both%20the%20angular%20momentum%20and,the%20angular%20velocity%20does%20not.
4749

50+
// Using reordered indices.
51+
let descriptor = MM4StateDescriptor()
52+
descriptor.positions = true
53+
descriptor.velocities = true
54+
let originalState = state(descriptor: descriptor)
55+
let positions = originalState.positions!
56+
let bulkVelocities = originalState.velocities!
57+
58+
// Using reordered indices.
59+
latestContext.context.setVelocitiesToTemperature(temperature)
60+
descriptor.positions = false
61+
let thermalState = state(descriptor: descriptor)
62+
let thermalVelocities = thermalState.velocities!
63+
64+
// Using reordered indices.
65+
var anchorStatuses = [Bool](repeating: false, count: system.atomCount)
66+
for anchor in _anchors {
67+
anchorStatuses[Int(anchor)] = true
68+
}
69+
70+
var activeRigidBodies: [Int]
71+
if let rigidBodies {
72+
activeRigidBodies = rigidBodies
73+
} else {
74+
activeRigidBodies = []
75+
for rigidBodyID in system.parameters.rigidBodies.indices {
76+
activeRigidBodies.append(rigidBodyID)
77+
}
78+
}
79+
80+
// Set the system's velocities to these at the end of the function. Note,
81+
// these are in reordered indices, while the 'velocities' property uses
82+
// original indices. Do not set these using the 'velocities' property.
83+
var newVelocities: [SIMD3<Float>] = bulkVelocities
84+
85+
// Don't forget to always map atom IDs to reordered IDs!
86+
for rigidBodyID in activeRigidBodies {
87+
// Last anchor ID is reordered.
88+
let range = system.parameters.rigidBodies[rigidBodyID]
89+
var anchorCount = 0
90+
var lastAnchorID: Int32 = -1
91+
for originalID in range {
92+
let atomID = system.reorderedIndices[Int(originalID)]
93+
if anchorStatuses[Int(atomID)] {
94+
anchorCount += 1
95+
lastAnchorID = atomID
96+
}
97+
}
98+
99+
let atoms = system.parameters.atoms
100+
var totalMass: Double = .zero
101+
var centerOfMass: SIMD3<Float>
102+
if anchorCount == 1 {
103+
// Reading with a reordered index.
104+
centerOfMass = positions[Int(lastAnchorID)]
105+
} else {
106+
var positionSum: SIMD3<Double> = .zero
107+
for originalID in range {
108+
let atomID = system.reorderedIndices[Int(originalID)]
109+
let mass = Double(atoms.masses[Int(originalID)])
110+
let position = positions[Int(atomID)]
111+
totalMass += mass
112+
positionSum += mass * SIMD3<Double>(position)
113+
}
114+
centerOfMass = SIMD3<Float>(positionSum / totalMass)
115+
}
116+
117+
// Find the original/thermalized linear/angular velocities, while
118+
// accumulating the moment of inertia.
119+
var bulkMomentum: SIMD3<Double> = .zero
120+
var thermalMomentum: SIMD3<Double> = .zero
121+
var bulkAngularMomentum: SIMD3<Double> = .zero
122+
var thermalAngularMomentum: SIMD3<Double> = .zero
123+
var rotationalInertia: RotationalInertia = .init()
124+
for originalID in range {
125+
let atomID = system.reorderedIndices[Int(originalID)]
126+
let mass = Double(atoms.masses[Int(originalID)])
127+
let position = positions[Int(atomID)]
128+
let bulkVelocity = bulkVelocities[Int(atomID)]
129+
let thermalVelocity = thermalVelocities[Int(atomID)]
130+
bulkMomentum += mass * SIMD3(bulkVelocity)
131+
thermalMomentum += mass * SIMD3(thermalVelocity)
132+
133+
// From Wikipedia:
134+
// https://en.wikipedia.org/wiki/Rigid_body_dynamics#Linear_and_angular_momentum
135+
//
136+
// L = m * (R - R_cm) cross d/dt (R - R_cm)
137+
// assume R_cm is stationary
138+
// L = m * (R - R_cm) cross v
139+
let relativePosition = position - centerOfMass
140+
let bulkAngularVelocity = cross(relativePosition, bulkVelocity)
141+
let thermalAngularVelocity = cross(relativePosition, thermalVelocity)
142+
bulkAngularMomentum += mass * SIMD3(bulkAngularVelocity)
143+
thermalAngularMomentum += mass * SIMD3(thermalAngularVelocity)
144+
rotationalInertia.append(mass: mass, relativePosition: relativePosition)
145+
}
146+
147+
// If the system has >1 anchor points, suppress the angular velocity.
148+
if anchorCount > 1 {
149+
bulkAngularMomentum = .zero
150+
}
151+
152+
// Matrix:
153+
// L = I * w
154+
// (I^{-1}) L = w
155+
// w = angular velocity
156+
//
157+
// Resulting vector:
158+
// w_x: angular velocity around x-axis (YZ plane)
159+
// w_y: angular velocity around y-axis (ZX plane)
160+
// w_z: angular velocity around z-axis (XY plane)
161+
//
162+
// Convert into a linear velocity for each particle:
163+
// v = w cross r
164+
let inverse = rotationalInertia.inverse
165+
func project(momentum: SIMD3<Double>) -> SIMD3<Float> {
166+
var output: SIMD3<Double> = .zero
167+
output += momentum.x * inverse.0
168+
output += momentum.y * inverse.1
169+
output += momentum.z * inverse.2
170+
return SIMD3<Float>(output)
171+
}
172+
173+
let bulkVelocity = bulkMomentum / totalMass
174+
let thermalVelocity = thermalMomentum / totalMass
175+
let velocityCorrection = -SIMD3<Float>(thermalVelocity - bulkVelocity)
176+
177+
let bulkAngularVelocity = project(momentum: bulkAngularMomentum)
178+
let thermalAngularVelocity = project(momentum: thermalAngularMomentum)
179+
let angularVelocityCorrection = -SIMD3<Float>(
180+
thermalAngularVelocity - bulkAngularVelocity)
181+
182+
// Apply the correction to rotational and angular velocity.
183+
for originalID in range {
184+
let atomID = system.reorderedIndices[Int(originalID)]
185+
let position = positions[Int(atomID)]
186+
var velocity = thermalVelocities[Int(atomID)]
187+
velocity += velocityCorrection
188+
189+
let relativePosition = position - centerOfMass
190+
velocity += cross(angularVelocityCorrection, relativePosition)
191+
newVelocities[Int(atomID)] = velocity
192+
}
193+
}
194+
195+
// Set the system's velocities to the new ones, reverting changes to rigid
196+
// bodies that shouldn't be thermalized.
197+
let array = OpenMM_Vec3Array(size: system.atomCount)
198+
for (index, velocity) in newVelocities.enumerated() {
199+
array[index] = SIMD3<Double>(velocity)
200+
}
48201
}
49202
}
50203

@@ -66,15 +219,74 @@ func cross<T: BinaryFloatingPoint & SIMDScalar>(
66219
///
67220
/// Source: [Stack Overflow](https://stackoverflow.com/a/18504573)
68221
struct RotationalInertia {
69-
/// The position to compute rotation around.
70-
var origin: SIMD3<Float>
71-
72222
/// The accumulator for the rigid body's moment of inertia.
73223
var columns: (SIMD3<Double>, SIMD3<Double>, SIMD3<Double>)
74224

75225
/// Initialize a moment of inertia with zero mass.
76-
init(origin: SIMD3<Float>) {
77-
self.origin = origin
226+
init() {
78227
self.columns = (.zero, .zero, .zero)
79228
}
229+
230+
/// Add an atom to the accumulator.
231+
mutating func append(mass: Double, relativePosition: SIMD3<Float>) {
232+
// From Wikipedia:
233+
// https://en.wikipedia.org/wiki/Rigid_body_dynamics#Mass_properties
234+
//
235+
// I_R = m * (I (S^T S) - S S^T)
236+
// where S is the column vector R - R_cm
237+
let STS = (relativePosition * relativePosition).sum()
238+
var column0 = SIMD3(STS, 0, 0)
239+
var column1 = SIMD3(0, STS, 0)
240+
var column2 = SIMD3(0, 0, STS)
241+
column0 -= relativePosition.x * relativePosition
242+
column1 -= relativePosition.y * relativePosition
243+
column2 -= relativePosition.z * relativePosition
244+
245+
// Convert to FP64 before adding to the accumulator. The matrix is
246+
// symmetric, so it doesn't matter whether you mix up the rows and columns.
247+
columns.0 += mass * SIMD3<Double>(column0)
248+
columns.1 += mass * SIMD3<Double>(column1)
249+
columns.2 += mass * SIMD3<Double>(column2)
250+
}
251+
252+
// The matrix is symmetric, but not exactly orthonormal. Inversion is not a
253+
// simple transpose operation.
254+
var inverse: (SIMD3<Double>, SIMD3<Double>, SIMD3<Double>) {
255+
// Source: https://stackoverflow.com/a/18504573
256+
//
257+
// double det = m(0, 0) * (m(1, 1) * m(2, 2) - m(2, 1) * m(1, 2)) -
258+
// m(0, 1) * (m(1, 0) * m(2, 2) - m(1, 2) * m(2, 0)) +
259+
// m(0, 2) * (m(1, 0) * m(2, 1) - m(1, 1) * m(2, 0));
260+
let determinant =
261+
columns.0[0]*(columns.1[1]*columns.2[2]-columns.2[1]*columns.1[2]) -
262+
columns.0[1]*(columns.1[0]*columns.2[2]-columns.1[2]*columns.2[0]) +
263+
columns.0[2]*(columns.1[0]*columns.2[1]-columns.1[1]*columns.2[0])
264+
let invdet = 1 / determinant
265+
266+
// minv(0, 0) = (m(1, 1) * m(2, 2) - m(2, 1) * m(1, 2)) * invdet;
267+
// minv(0, 1) = (m(0, 2) * m(2, 1) - m(0, 1) * m(2, 2)) * invdet;
268+
// minv(0, 2) = (m(0, 1) * m(1, 2) - m(0, 2) * m(1, 1)) * invdet;
269+
let result00 = (columns.1[1]*columns.2[2]-columns.2[1]*columns.1[2])*invdet
270+
let result01 = (columns.0[2]*columns.2[1]-columns.0[1]*columns.2[2])*invdet
271+
let result02 = (columns.0[1]*columns.1[2]-columns.0[2]*columns.1[1])*invdet
272+
273+
// minv(1, 0) = (m(1, 2) * m(2, 0) - m(1, 0) * m(2, 2)) * invdet;
274+
// minv(1, 1) = (m(0, 0) * m(2, 2) - m(0, 2) * m(2, 0)) * invdet;
275+
// minv(1, 2) = (m(1, 0) * m(0, 2) - m(0, 0) * m(1, 2)) * invdet;
276+
let result10 = (columns.1[2]*columns.2[0]-columns.1[0]*columns.2[2])*invdet
277+
let result11 = (columns.0[0]*columns.2[2]-columns.0[2]*columns.2[0])*invdet
278+
let result12 = (columns.1[0]*columns.0[2]-columns.0[0]*columns.1[2])*invdet
279+
280+
// minv(2, 0) = (m(1, 0) * m(2, 1) - m(2, 0) * m(1, 1)) * invdet;
281+
// minv(2, 1) = (m(2, 0) * m(0, 1) - m(0, 0) * m(2, 1)) * invdet;
282+
// minv(2, 2) = (m(0, 0) * m(1, 1) - m(1, 0) * m(0, 1)) * invdet;
283+
let result20 = (columns.1[0]*columns.2[1]-columns.2[0]*columns.1[1])*invdet
284+
let result21 = (columns.2[0]*columns.0[1]-columns.0[0]*columns.2[1])*invdet
285+
let result22 = (columns.0[0]*columns.1[1]-columns.1[0]*columns.0[1])*invdet
286+
287+
let column0 = SIMD3(result00, result10, result20)
288+
let column1 = SIMD3(result01, result11, result21)
289+
let column2 = SIMD3(result02, result12, result22)
290+
return (column0, column1, column2)
291+
}
80292
}

0 commit comments

Comments
 (0)