-
Notifications
You must be signed in to change notification settings - Fork 1
on-the-fly computation of MSAD for trimers #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
eb72bf8
19fd098
36838de
c19c937
6bb1a4f
42be0d7
c3e1e24
6163750
f0e90fd
3ecaa61
2d82dca
86108a4
c2f848d
eab3084
39a863d
e25b791
e98224d
ee390e9
1f4527c
2071047
dc6aa12
8efb845
8efddc8
e2d510f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,233 @@ | ||
| # msad.jl | ||
| # | ||
| # On-the-fly Mean Squared Angular Displacement (MSAD) tracker. | ||
| # Implements three methods: | ||
| # - Integral : accumulates incremental rotation vectors every step | ||
| # - Threshold : relays the reference frame when rotation exceeds θ_T | ||
| # - Euler : compares directly to initial frame, computed on log schedule | ||
|
|
||
| using LinearAlgebra | ||
| using StaticArrays | ||
|
|
||
| ##### Basic operations for rotation inspired fromo our python post processing ##### | ||
| ########## ########## | ||
|
|
||
| function body_frame(r1::SVector{3,T}, r2::SVector{3,T}, r3::SVector{3,T}, L::T) where {T} | ||
| e1 = r2 - r1 | ||
| v = r3 - r1 | ||
| # minimum image convention | ||
| e1 = e1 - round.(e1 / L) .* L | ||
| v = v - round.( v / L) .* L | ||
| # Gram-Schmidt basis right handed | ||
| e1 = e1 / norm(e1) | ||
| e2 = cross(e1, v) | ||
| e2 = e2 / norm(e2) | ||
| e3 = cross(e1, e2) | ||
| # hcat stacks column vectors → 3×3 matrix | ||
| return hcat(e1, e2, e3) # SMatrix{3,3,T} | ||
| end | ||
|
|
||
| function get_all_body_frames(system::Molecules) | ||
| L = system.box[1] # cubic box | ||
| T = typeof(L) | ||
| R = Vector{SMatrix{3,3,T,9}}(undef, system.Nmol) # pre-allocate | ||
| pos = system.position | ||
| @inbounds for m in 1:system.Nmol # iterate over the number of molecules | ||
| s = system.start_mol[m] # first bead index for the considered molecule | ||
| R[m] = body_frame(pos[s], pos[s+1], pos[s+2], L) | ||
| end | ||
| return R | ||
| end | ||
|
|
||
| function rotation_vector(R::SMatrix{3,3,T}) where {T} | ||
|
|
||
| cos_θ = clamp((tr(R) - 1) / 2, -one(T), one(T)) | ||
|
|
||
| vals, vecs = eigen(R) # eigenvalues and eigenvectors of R | ||
| idx = argmin(abs.(real.(vals) .- 1)) # find the idx corresponding to eigenvalue = 1 | ||
| n = real.(vecs[:, idx]) # extract the rotation axis vector | ||
| n = SVector{3,T}(n[1], n[2], n[3]) | ||
|
|
||
| n_skew = SMatrix{3,3,T}( | ||
| 0, n[3], -n[2], | ||
| -n[3], 0, n[1], | ||
| n[2], -n[1], 0 | ||
| ) # skew matrix for n vector | ||
|
|
||
| # Rodrigues formula | ||
| sin_θ = clamp(-tr(n_skew * R) / 2, -one(T), one(T)) | ||
|
|
||
| # theta | ||
| θ = atan(sin_θ, cos_θ) | ||
|
|
||
| return θ * n | ||
| end | ||
|
Comment on lines
+42
to
+64
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This works, but it's not the best option is
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with what is said in Sec IIC. However, taking n as the eigenvector associated with the eigenvalue 1 is misleading because n and -n are solutions. Both values are taken randomly when extracting eigenvectors and this lead to "false" rotation of ±π. Using the skew matrix we remove this ambiguity. Then should I add a case where theta is close to pi?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure why would this be a problem ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that it is irrelevant for the condition check in th threshold method. However, when doing the accumulation sum(θn) we will accumulate random sign flip no? And then norm(ɸ) does not represent the true rotation. When I was validating my operation in python post process I had to use skew symmetric part to define n instead of eigenvectors.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When you do the calculation you indeed find two solutions
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK yes you're right sorry... I am changing for the eigenvector rn.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No worries! There should be an implementation here : https://github.com/romainljsimon/MoonWalk.jl
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just one question : when using the eigenvector/value decomposition Julia might need to store things in memory. Isn't that going to slow down the simulation?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey, yes it is a slower but safer implementation! |
||
|
|
||
| ##### Definition of the MSAD State ie: the accumulating variables ##### | ||
| # mutable because we need to update it | ||
| ########## ########## | ||
|
|
||
| mutable struct MSADState{T} | ||
| R0::Vector{SMatrix{3,3,T,9}} # (Nmol,3,3) initial body frames at t=0 | ||
| R_prev::Vector{SMatrix{3,3,T,9}} # (Nmol,3,3) body frames at previous time | ||
| phi_integral::Vector{SVector{3,T}} # (Nmol,3) Accumulated vector for integral method | ||
| R_ref_thresh::Vector{SMatrix{3,3,T,9}} # (Nmol,3,3) reference body frames for each molecule in the threshold method | ||
| phi_acc::Vector{SVector{3,T}} # (Nmol,3) Accumulated vector for threshold method | ||
| initialized::Bool | ||
| end | ||
|
|
||
| # Start empty, filled during initialise when we first see the system | ||
| MSADState{T}() where {T} = MSADState{T}([], [], [], [], [], false) | ||
|
|
||
| ##### The MSAD computation algorithm ##### | ||
| ########## ########## | ||
|
|
||
| mutable struct MSADTracker{T} <: AriannaAlgorithm | ||
| states::Vector{MSADState{T}} | ||
| theta_T::T | ||
| compute_schedule::Vector{Int} | ||
| output_schedule::Vector{Int} | ||
| paths_integral::Vector{String} | ||
| paths_thresh::Vector{String} | ||
| files_integral::Vector{IOStream} | ||
| files_thresh::Vector{IOStream} | ||
| end | ||
|
|
||
| function MSADTracker(chains; | ||
| theta_T::Float64=π/4, | ||
| output_schedule::Vector{Int}=Int[], | ||
| path::String=".", | ||
| scheduler::Vector{Int}=Int[], | ||
| kwargs...) | ||
|
|
||
| compute_schedule = scheduler | ||
|
|
||
| if !isempty(compute_schedule) && !isempty(output_schedule) | ||
| @assert all(t in compute_schedule for t in output_schedule) """ | ||
| output_schedule contains steps not in compute_schedule. | ||
| You cannot write output at a step where make_step! was not called. | ||
| """ | ||
| end | ||
|
|
||
| n = length(chains) | ||
| states = [MSADState{Float64}() for _ in 1:n] | ||
| dirs = [joinpath(path, "chains", "$c") for c in 1:n] | ||
|
|
||
| paths_integral = [joinpath(d, "phi_integral.dat") for d in dirs] | ||
| paths_thresh = [joinpath(d, "phi_thresh.dat") for d in dirs] | ||
|
|
||
| files_integral = Vector{IOStream}(undef, n) | ||
| files_thresh = Vector{IOStream}(undef, n) | ||
|
|
||
| return MSADTracker{Float64}(states, theta_T, compute_schedule, | ||
| output_schedule, paths_integral, paths_thresh, | ||
| files_integral, files_thresh) | ||
| end | ||
|
|
||
| ##### Function to write phi frame ##### | ||
| ########## ########## | ||
|
|
||
| function write_phi_frame(file::IOStream, t::Int, N_mol::Int, | ||
| phis::Vector{<:SVector}) | ||
| println(file, N_mol) | ||
| println(file, "t=$t") | ||
| for m in 1:N_mol | ||
| println(file, "$m $(phis[m][1]) $(phis[m][2]) $(phis[m][3])") | ||
| end | ||
| flush(file) | ||
| end | ||
|
|
||
| ##### Initialisation of the simulation ##### | ||
| # called once at t = 0, set the system, the mutable struct and the storing paths | ||
| ########## ########## | ||
|
|
||
| function Arianna.initialise(algorithm::MSADTracker, simulation::Simulation) | ||
|
|
||
| for c in eachindex(simulation.chains) | ||
| system = simulation.chains[c] | ||
| state = algorithm.states[c] | ||
| T = typeof(system.temperature) | ||
|
|
||
| # create output directory if it doesn't exist | ||
| mkpath(dirname(algorithm.paths_integral[c])) | ||
|
|
||
| # open output files | ||
| algorithm.files_integral[c] = open(algorithm.paths_integral[c], "w") | ||
| algorithm.files_thresh[c] = open(algorithm.paths_thresh[c], "w") | ||
|
|
||
| # compute initial body frames | ||
| R_all = get_all_body_frames(system) | ||
| N_mol = system.Nmol | ||
|
|
||
| # fill state | ||
| state.R0 = copy(R_all) | ||
| state.R_prev = copy(R_all) | ||
| state.phi_integral = [zero(SVector{3,T}) for _ in 1:N_mol] | ||
| state.R_ref_thresh = copy(R_all) | ||
| state.phi_acc = [zero(SVector{3,T}) for _ in 1:N_mol] | ||
| state.initialized = true | ||
|
|
||
| # write t=0, all phi vectors are zero | ||
| write_phi_frame(algorithm.files_integral[c], 0, N_mol, | ||
| state.phi_integral) | ||
| write_phi_frame(algorithm.files_thresh[c], 0, N_mol, | ||
| [zero(SVector{3,T}) for _ in 1:N_mol]) | ||
| end | ||
| end | ||
|
|
||
| ##### Finalise the simulation ##### | ||
| # close output to not keep unwritten data in memory | ||
| ########## ########## | ||
|
|
||
| function Arianna.finalise(tracker::MSADTracker, ::Simulation) | ||
| for c in eachindex(tracker.files_integral) | ||
| close(tracker.files_integral[c]) | ||
| close(tracker.files_thresh[c]) | ||
| end | ||
| end | ||
| ##### Make a step in simulation ##### | ||
| ########## ########## | ||
|
|
||
| function Arianna.make_step!(simulation::Simulation, algorithm::MSADTracker) | ||
| t = simulation.t | ||
|
|
||
| for c in eachindex(simulation.chains) | ||
| system = simulation.chains[c] | ||
| state = algorithm.states[c] | ||
| N_mol = system.Nmol | ||
|
|
||
| # compute current body frames shared by all three methods | ||
| R_all = get_all_body_frames(system) | ||
|
|
||
| # Integral update | ||
| for m in 1:N_mol | ||
| dR = state.R_prev[m]' * R_all[m] | ||
| state.phi_integral[m] = state.phi_integral[m] + rotation_vector(dR) | ||
| end | ||
|
|
||
| # Threshold update | ||
| phi_current = Vector{SVector{3,eltype(algorithm.theta_T)}}(undef, N_mol) | ||
| phi_total = Vector{SVector{3,eltype(algorithm.theta_T)}}(undef, N_mol) | ||
| for m in 1:N_mol | ||
| dR = state.R_ref_thresh[m]' * R_all[m] | ||
| phi_current[m] = rotation_vector(dR) | ||
| phi_total[m] = state.phi_acc[m] + phi_current[m] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This variable is redundant, as it can be recomputed any time from the other two
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, plus I don't think creating two empty vectors is necessary. I guess one could write: dR = state.R_ref_thresh[m]' * R_all[m]
phi_current = rotation_vector(dR)
if norm(phi_current) >= tracker.theta_T
state.phi_acc[m] = state.phi_acc[m] + phi_current
state.R_ref_thresh[m] = R_all[m]
end
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. Then I also changed
by
in the output for the threshold method |
||
| if norm(phi_current[m]) >= algorithm.theta_T | ||
| state.phi_acc[m] = phi_total[m] | ||
| state.R_ref_thresh[m] = R_all[m] | ||
| end | ||
| end | ||
|
|
||
| state.R_prev .= R_all | ||
|
|
||
| # output if time match output schedule | ||
| if t in algorithm.output_schedule | ||
|
|
||
| # Integral | ||
| write_phi_frame(algorithm.files_integral[c],t,N_mol,state.phi_integral) | ||
|
|
||
| # Threshold | ||
| write_phi_frame(algorithm.files_thresh[c],t,N_mol,phi_total) | ||
| end | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| using StaticArrays, LinearAlgebra, BenchmarkTools | ||
|
|
||
| function rotation_vector_skew(R::SMatrix{3,3,T}) where {T} | ||
| cos_θ = clamp((tr(R) - 1) / 2, -one(T), one(T)) | ||
| skew = (R - R') / 2 | ||
| sin_n = SVector(skew[3,2], skew[1,3], skew[2,1]) | ||
| sin_θ = norm(sin_n) | ||
| θ = atan(sin_θ, cos_θ) | ||
| if sin_θ > 1e-6 | ||
| return (θ / sin_θ) * sin_n | ||
| elseif cos_θ > 0 | ||
| return sin_n | ||
| else | ||
| RpI = R + one(R) | ||
| c1 = SVector(RpI[1,1], RpI[2,1], RpI[3,1]) | ||
| c2 = SVector(RpI[1,2], RpI[2,2], RpI[3,2]) | ||
| c3 = SVector(RpI[1,3], RpI[2,3], RpI[3,3]) | ||
| n = norm(c1) >= norm(c2) ? c1 : c2 | ||
| n = norm(n) >= norm(c3) ? n : c3 | ||
| return π * (n / norm(n)) | ||
| end | ||
| end | ||
|
|
||
| function rotation_vector_eig(R::SMatrix{3,3,T}) where {T} | ||
| cos_θ = clamp((tr(R) - 1) / 2, -one(T), one(T)) | ||
| vals, vecs = eigen(R) | ||
| idx = argmin(abs.(real.(vals) .- 1)) | ||
| n = real.(vecs[:, idx]) | ||
| n = SVector{3,T}(n[1], n[2], n[3]) | ||
| 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 | ||
|
|
||
| # 90° rotation around z — normal case | ||
| R = SMatrix{3,3,Float64}(0.0, 1.0, 0.0, -1.0, 0.0, 0.0, 0.0, 0.0, 1.0) | ||
|
|
||
| println("=== Skew-symmetric ===") | ||
| @btime rotation_vector_skew($R) | ||
|
|
||
| println("=== Eigenvector ===") | ||
| @btime rotation_vector_eig($R) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you want to commit this change ?