Skip to content

Commit 3c84274

Browse files
authored
Replace eigendecomposition in rotation_vector with direct Rodrigues extraction (+ fix near-identity NaN) (#81)
## Summary In order to improve performance I propose three fixes : - `rotation_vector` : use of Rodrigues formula to leverage knowledge of orthogonality for our matrices instead of blindly use `eigen`, which runs an iterative eigensolver and allocates, when the closed-form Rodrigues extraction needs only a trace and a few subtractions.. - 'R_bodyframe' : declaration of the variable so we are not creating a new vector at each steps but just redirecting the stored one. - 'flush' : removed the flush from `make_step!` but keep the one in `write_phi_frame` which is enough. To prevent ill defined operation while using the closed-form Rodrigues extraction which divide by `sin θ` : - `sin θ > √eps` general case, safe to divide. - `sin θ ≈ 0, cos θ > 0` θ ≈ 0 (near identity), return the zero vector. - `sin θ ≈ 0, cos θ ≤ 0` θ ≈ π, recover the axis from the symmetric part. EDIT : One can run this code to verify the validity of the closed form, especially at the boundaries. ```julia using LinearAlgebra using StaticArrays using Printf # eigen based function rotation_vector_eigen(R::SMatrix{3,3,T}) where {T} cos_θ = clamp((tr(R) - 1) / 2, -one(T), one(T)) vals, vecs = eigen(Matrix(R)) idx = argmin(abs.(real.(vals) .- 1)) n = real.(vecs[:, idx]) n = SVector{3,T}(n[1], n[2], n[3]) n = n / norm(n) n_skew = SMatrix{3,3,T}(0, n[3], -n[2], -n[3], 0, n[1], n[2], -n[1], 0) sin_θ = clamp(-tr(n_skew * R) / 2, -one(T), one(T)) θ = atan(sin_θ, cos_θ) return θ * n end # closed form function rotation_vector_closed(R::SMatrix{3,3,T}) where {T} cos_θ = clamp((tr(R) - 1) / 2, -one(T), one(T)) ax = R[3,2] - R[2,3] ay = R[1,3] - R[3,1] az = R[2,1] - R[1,2] sin_θ = sqrt(ax^2 + ay^2 + az^2) / 2 if sin_θ > sqrt(eps(T)) θ = atan(sin_θ, cos_θ) inv_2s = θ / (2 * sin_θ) return SVector{3,T}(ax * inv_2s, ay * inv_2s, az * inv_2s) elseif cos_θ > 0 return zero(SVector{3,T}) else # θ ≈ π if R[1,1] >= R[2,2] && R[1,1] >= R[3,3] nx = sqrt(max((R[1,1] + 1) / 2, zero(T))) n = SVector{3,T}(nx, (R[1,2]+R[2,1])/(4nx), (R[1,3]+R[3,1])/(4nx)) elseif R[2,2] >= R[3,3] ny = sqrt(max((R[2,2] + 1) / 2, zero(T))) n = SVector{3,T}((R[1,2]+R[2,1])/(4ny), ny, (R[2,3]+R[3,2])/(4ny)) else nz = sqrt(max((R[3,3] + 1) / 2, zero(T))) n = SVector{3,T}((R[1,3]+R[3,1])/(4nz), (R[2,3]+R[3,2])/(4nz), nz) end θ = atan(sin_θ, cos_θ) return θ * n / norm(n) end end # rotation vector to rotation matrix using Rodrigues function rot_matrix(axis::SVector{3,T}, θ::T) where {T} n = axis / norm(axis) K = SMatrix{3,3,T}(0, n[3], -n[2], -n[3], 0, n[1], n[2], -n[1], 0) return SMatrix{3,3,T}(1I) + sin(θ)*K + (1 - cos(θ))*(K*K) end same(a, b; atol) = isapprox(a, b; atol=atol) || isapprox(a, -b; atol=atol) const T = Float64 const AXES = [SVector{3,T}(1,0,0), SVector{3,T}(1,1,0), SVector{3,T}(1,-2,3), SVector{3,T}(-1,0.5,2)] # one can choose whatever axis ok = true # Tcheck aggrement for general case println("CASE 1 eigen vs closed form, θ ∈ [0.1, 3.0]") @printf("%-16s %-7s %-12s %-12s %-11s\n", "axis", "θ", "‖closed‖", "‖eigen‖", "|Δ|") worst1 = 0.0 for axn in AXES, θ in (0.1, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0) R = rot_matrix(axn, T(θ)) # def rotation matrix from axis and θ vc = rotation_vector_closed(R) # extract rotation vector from closed form method ve = rotation_vector_eigen(R) # from eigen method d = min(maximum(abs.(vc .- ve)), maximum(abs.(vc .+ ve))) global worst1 = max(worst1, d) global ok &= isapprox(norm(vc), θ; atol=1e-8) && same(vc, ve; atol=1e-6) @printf("%-16s %-7.2f %-12.6f %-12.6f %-11.2e\n", string(round.(axn; digits=1)), θ, norm(vc), norm(ve), d) end @printf("\nworst |Δ| (mod sign): %.2e\n\n", worst1) # Tcheck aggrement for case near θ = 0 println("CASE 2 near identity") @printf("%-16s %-7s %-24s %-24s\n", "axis", "θ", "eigen", "closed") worst2 = 0.0 for axn in AXES, θ in (1e-3, 1e-6, 1e-9) R = rot_matrix(axn, T(θ)) vc = rotation_vector_closed(R) ve = rotation_vector_eigen(R) global worst2 = max(worst2, norm(vc - ve), 0.0) global ok &= all(isfinite, vc) && all(isfinite, ve) @printf("%-16s %-7.0e %-24s %-24s\n", string(round.(axn; digits=1)), θ, string(round.(ve; digits=10)), string(round.(vc; digits=10))) end @printf("\nworst |Δ| near 0: %.2e\n\n", worst2) # Tcheck aggrement case near θ = π println("CASE 3 near θ = π : magnitude and axis") @printf("%-16s %-26s %-26s\n", "true n̂", "eigen n̂", "closed n̂") for axn in AXES n = axn / norm(axn) R = rot_matrix(axn, T(π) - 1e-7) ve = rotation_vector_eigen(R); vc = rotation_vector_closed(R) global ok &= isapprox(norm(vc), π; atol=1e-3) && same(vc/norm(vc), SVector{3,T}(n); atol=1e-2) @printf("%-16s %-26s %-26s\n", string(round.(n; digits=3)), string(round.(ve ./ norm(ve); digits=3)), string(round.(vc ./ norm(vc); digits=3))) end println("\nboth give |Φ| = θ ≈ π and the correct axis.\n") println(ok ? "PASSED : closed form matches eigen across all cases." : "FAILED") ```
1 parent 72f98b8 commit 3c84274

1 file changed

Lines changed: 45 additions & 42 deletions

File tree

src/rotation.jl

Lines changed: 45 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -27,54 +27,60 @@ function body_frame(r1::SVector{3,T}, r2::SVector{3,T}, r3::SVector{3,T}, L::T)
2727
return hcat(e1, e2, e3) # SMatrix{3,3,T}
2828
end
2929

30-
function get_all_body_frames(system::Molecules)
31-
L = system.box[1] # cubic box
32-
T = typeof(L)
33-
R = Vector{SMatrix{3,3,T,9}}(undef, system.Nmol) # pre-allocate
30+
function get_all_body_frames!(R_bodyframe, system::Molecules)
31+
L = system.box[1]
3432
pos = system.position
35-
@inbounds for m in 1:system.Nmol # iterate over the number of molecules
36-
s = system.start_mol[m] # first bead index for the considered molecule
37-
R[m] = body_frame(pos[s], pos[s+1], pos[s+2], L)
33+
@inbounds for m in 1:system.Nmol
34+
s = system.start_mol[m]
35+
R_bodyframe[m] = body_frame(pos[s], pos[s+1], pos[s+2], L)
3836
end
39-
return R
4037
end
4138

4239
function rotation_vector(R::SMatrix{3,3,T}) where {T}
43-
4440
cos_θ = clamp((tr(R) - 1) / 2, -one(T), one(T))
4541

46-
vals, vecs = eigen(R) # eigenvalues and eigenvectors of R
47-
idx = argmin(abs.(real.(vals) .- 1)) # find the idx corresponding to eigenvalue = 1
48-
n = real.(vecs[:, idx]) # extract the rotation axis vector
49-
n = SVector{3,T}(n[1], n[2], n[3])
50-
51-
n_skew = SMatrix{3,3,T}(
52-
0, n[3], -n[2],
53-
-n[3], 0, n[1],
54-
n[2], -n[1], 0
55-
) # skew matrix for n vector
56-
57-
# Rodrigues formula
58-
sin_θ = clamp(-tr(n_skew * R) / 2, -one(T), one(T))
42+
ax = R[3,2] - R[2,3]
43+
ay = R[1,3] - R[3,1]
44+
az = R[2,1] - R[1,2]
45+
sin_θ = sqrt(ax^2 + ay^2 + az^2) / 2
5946

60-
# theta
61-
θ = atan(sin_θ, cos_θ)
62-
63-
return θ * n
47+
if sin_θ > sqrt(eps(T)) # guard sin_θ ≈ 0
48+
θ = atan(sin_θ, cos_θ)
49+
inv_2s = θ / (2 * sin_θ)
50+
return SVector{3,T}(ax * inv_2s, ay * inv_2s, az * inv_2s)
51+
52+
elseif cos_θ > 0 # guard θ ≈ 0, near identity
53+
return zero(SVector{3,T})
54+
55+
else # guard θ ≈ π
56+
if R[1,1] >= R[2,2] && R[1,1] >= R[3,3]
57+
nx = sqrt(max((R[1,1] + 1) / 2, zero(T)))
58+
n = SVector{3,T}(nx, (R[1,2] + R[2,1]) / (4nx), (R[1,3] + R[3,1]) / (4nx))
59+
elseif R[2,2] >= R[3,3]
60+
ny = sqrt(max((R[2,2] + 1) / 2, zero(T)))
61+
n = SVector{3,T}((R[1,2] + R[2,1]) / (4ny), ny, (R[2,3] + R[3,2]) / (4ny))
62+
else
63+
nz = sqrt(max((R[3,3] + 1) / 2, zero(T)))
64+
n = SVector{3,T}((R[1,3] + R[3,1]) / (4nz), (R[2,3] + R[3,2]) / (4nz), nz)
65+
end
66+
θ = atan(sin_θ, cos_θ)
67+
return θ * n / norm(n)
68+
end
6469
end
6570

6671
##### Definition of the RotationState ie: the accumulating variables #####
6772
# mutable because we need to update it
6873
########## ##########
6974

7075
mutable struct RotationState{T}
71-
R_ref::Vector{Vector{SMatrix{3,3,T,9}}}
76+
R_ref::Vector{Vector{SMatrix{3,3,T,9}}}
77+
R_bodyframe::Vector{SMatrix{3,3,T,9}}
7278
Φ_acc::Vector{Vector{SVector{3,T}}}
7379
initialized::Bool
7480
end
7581

7682
# Start empty, filled during initialise when we first see the system
77-
RotationState{T}() where {T} = RotationState{T}([], [], false)
83+
RotationState{T}() where {T} = RotationState{T}([], [], [], false)
7884

7985
##### The Rotation computation algorithm #####
8086
########## ##########
@@ -98,23 +104,21 @@ end
98104
########## ##########
99105

100106
function Arianna.initialise(algorithm::ComputeRotation, simulation::Simulation)
101-
102107
for c in eachindex(simulation.chains)
103108
system = simulation.chains[c]
104109
state = algorithm.states[c]
105110
T = typeof(system.temperature)
106111
N_mol = system.Nmol
107112
n_θ = length(algorithm.theta_T)
108113

109-
# compute initial body frames
110-
R_all = get_all_body_frames(system)
114+
state.R_bodyframe = Vector{SMatrix{3,3,T,9}}(undef, N_mol) # allocate once
115+
get_all_body_frames!(state.R_bodyframe, system) # fill state.R_bodyframe
111116

112-
# fill state
113-
state.R_ref = [copy(R_all) for _ in 1:n_θ]
114-
state.Φ_acc = [[zero(SVector{3,T}) for _ in 1:N_mol] for _ in 1:n_θ]
115-
state.initialized = true
116-
117-
resize!(system.Φ, n_θ) # a posteriori as we know n_θ
117+
state.R_ref = [copy(state.R_bodyframe) for _ in 1:n_θ]
118+
state.Φ_acc = [[zero(SVector{3,T}) for _ in 1:N_mol] for _ in 1:n_θ]
119+
state.initialized = true
120+
121+
resize!(system.Φ, n_θ)
118122
for k in 1:n_θ
119123
system.Φ[k] = [zero(SVector{3,T}) for _ in 1:N_mol]
120124
end
@@ -125,20 +129,19 @@ end
125129
########## ##########
126130

127131
function Arianna.make_step!(simulation::Simulation, algorithm::ComputeRotation)
128-
129132
tcollect(eachindex(simulation.chains) |> Transducers.Map(c -> begin
130133
system = simulation.chains[c]
131134
state = algorithm.states[c]
132135
N_mol = system.Nmol
133-
R_all = get_all_body_frames(system)
136+
get_all_body_frames!(state.R_bodyframe, system) # no new vector created updates state.R_bodyframe
134137
for (k, θ_T) in enumerate(algorithm.theta_T)
135138
for m in 1:N_mol
136-
dR = state.R_ref[k][m]' * R_all[m]
139+
dR = state.R_ref[k][m]' * state.R_bodyframe[m]
137140
Φ_current = rotation_vector(dR)
138141
Φ_total = state.Φ_acc[k][m] + Φ_current
139142
if norm(Φ_current) > θ_T
140143
state.Φ_acc[k][m] = Φ_total
141-
state.R_ref[k][m] = R_all[m]
144+
state.R_ref[k][m] = state.R_bodyframe[m]
142145
end
143146
system.Φ[k][m] = Φ_total
144147
end
@@ -157,7 +160,7 @@ function write_phi_frame(file::IOStream, t::Int, N_mol::Int, Φs::Vector{<:SVect
157160
for m in 1:N_mol
158161
println(file, "$m $(Φs[m][1]) $(Φs[m][2]) $(Φs[m][3])")
159162
end
160-
flush(file)
163+
#flush(file) not flushing at everyframe
161164
end
162165

163166
struct StorePhiTrajectories <: AriannaAlgorithm

0 commit comments

Comments
 (0)