One of my current research interests is in the geometry of constant-mean-curvature time-like submanifolds of Minkowski space. A special case of this is the study of cosmic strings under the Nambu-Goto action. Mathematically the classical (as opposed to quantum) behavior of such strings are quite well understood, by combining the works of theoretical physicists since the 80s (especially those of Jens Hoppe and collaborators) together with recent mathematical developments (such as the work of Nguyen and Tian and that of Jerrard, Novaga, and Orlandi). To get a better handle on what is going on with these results, and especially to provide some visual aids, I've tried to produce some simulations of the dynamics of closed cosmic strings. (This was also an opportunity for me to practice code-writing in Julia and learn a bit about the best practices for performance and optimization in that language.)
The code
After some false starts, here are some reasonably stable code.
MeanCurvature3.jl
:
function MC_corr3!(position::Array{Float64,2}, prev_vel::Array{Float64,2}, next_vel::Array{Float64,2}, result::Array{Float64,2}, dt::Float64)
# We will assume that data is stored in the format point(coord,number), so as a 3x1000 array or something.
num_points = size(position,2)
num_dims = size(position,1)
curr_vel = zeros(num_dims)
curr_vs = zeros(num_dims)
curr_ps = zeros(num_dims)
curr_pss = zeros(num_dims)
pred_vel = zeros(num_dims)
agreement = true
for col = 1:num_points #Outer loop is column
if col == 1
prev_col = num_points
next_col = 2
elseif col == num_points
prev_col = num_points - 1
next_col = 1
else
prev_col = col -1
next_col = col + 1
end
for row = 1:num_dims
curr_vel[row] = (next_vel[row,col] + prev_vel[row,col])/2
curr_vs[row] = (next_vel[row,next_col] + prev_vel[row,next_col] - next_vel[row,prev_col] - prev_vel[row,prev_col])/4
curr_ps[row] = (position[row,next_col] - position[row,prev_col])/2
curr_pss[row] = position[row,next_col] + position[row,prev_col] - 2*position[row,col]
end
beta = (1 + dot(curr_vel,curr_vel))^(1/2)
sigma = dot(curr_ps,curr_ps)
psvs = dot(curr_ps,curr_vs)
bvvs = dot(curr_vs,curr_vel) / (beta^2)
pssps = dot(curr_pss,curr_ps)
for row in 1:num_dims
result[row,col] = curr_pss[row] / (sigma * beta) - curr_ps[row] * pssps / (sigma^2 * beta) - curr_vel[row] * psvs / (sigma * beta) - curr_ps[row] * bvvs / (sigma * beta)
pred_vel[row] = prev_vel[row,col] + dt * result[row,col]
end
agreement = agreement && isapprox(next_vel[:,col], pred_vel, rtol=sqrt(eps(Float64)))
end
return agreement
end
function find_next_vel!(position::Array{Float64,2}, prev_vel::Array{Float64,2}, next_vel::Array{Float64,2}, dt::Float64; max_tries::Int64=50)
tries = 1
result = zeros(next_vel)
agreement = MC_corr3!(position,prev_vel,next_vel,result,dt)
for j in 1:size(next_vel,2), i in 1:size(next_vel,1)
next_vel[i,j] = prev_vel[i,j] + result[i,j]*dt
end
while !agreement && tries < max_tries
agreement = MC_corr3!(position,prev_vel,next_vel,result,dt)
for j in 1:size(next_vel,2), i in 1:size(next_vel,1)
next_vel[i,j] = prev_vel[i,j] + result[i,j]*dt
end
tries +=1
end
return tries, agreement
end
This first file does the heavy lifting of solving the evolution equation. The scheme is a semi-implicit finite difference scheme. The function MC_Corr3
takes as input the current position, the previous velocity, and the next velocity, and computes the correct current acceleration. The function find_next_vel
iterates MC_Corr3
until the computed acceleration agrees (up to numerical errors) with the input previous and next velocities.
Or, in notations:
MC_Corr3: ( x[t], v[t-1], v[t+1] ) --> Delta-v[t]
and find_next_vel
iterates MC_Corr3
until
Delta-v[t] == (v[t+1] - v[t-1]) / 2
The code in this file is also where the performance matters the most, and I spent quite some time experimenting with different algorithms to find one with most reasonable speed.
InitialData3.jl
:
function make_ellipse(a::Float64,b::Float64, n::Int64, extra_dims::Int64=1) # a,b are relative lengths of x and y axes
s = linspace(0,2π * (n-1)/n, n)
if extra_dims == 0
return vcat(transpose(a*cos(s)), transpose(b*sin(s)))
elseif extra_dims > 0
return vcat(transpose(a*cos(s)), transpose(b*sin(s)), zeros(extra_dims,n))
else
error("extra_dims must be non-negative")
end
end
function perturb_data!(data::Array{Float64,2}, coeff::Vector{Float64}, num_modes::Int64)
# num_modes is the number of modes
# coeff are the relative sizes of the perturbations
numpts = size(data,2)
for j in 2:num_modes
rcoeff = rand(length(coeff),2)
for pt in 1:numpts
theta = 2j * π * pt / numpts
for d in 1:length(coeff)
data[d,pt] += ( (rcoeff[d,1] - 0.5) * cos(theta) + (rcoeff[d,2] - 0.5) * sin(theta)) * coeff[d] / j^2
end
end
end
nothing
end
This file just sets up the initial data. Note that in principle the number of ambient spatial dimensions is arbitrary.
GraphCode3.jl
:
using Plots
pyplot(size=(1920,1080), reuse=true)
function plot_data2D(filename_prefix::ASCIIString, filename_offset::Int64, titlestring::ASCIIString, data::Array{Float64,2}, additional_data...)
x_max = 1.5
y_max = 1.5
plot(transpose(data)[:,1], transpose(data)[:,2] , xlims=(-x_max,x_max), ylims=(-y_max,y_max), title=titlestring)
if length(additional_data) > 0
for i in 1:length(additional_data)
plot!(transpose(additional_data[i][1,:]), transpose(additional_data[i][2,:]))
end
end
png(filename_prefix*dec(filename_offset,5)*".png")
nothing
end
function plot_data3D(filename_prefix::ASCIIString, filename_offset::Int64, titlestring::ASCIIString, data::Array{Float64,2}, additional_data...)
x_max = 1.5
y_max = 1.5
z_max = 0.9
tdata = transpose(data)
plot(tdata[:,1], tdata[:,2],tdata[:,3], xlims=(-x_max,x_max), ylims=(-y_max,y_max),zlims=(-z_max,z_max), title=titlestring)
if length(additional_data) > 0
for i in 1:length(additional_data)
tdata = transpose(additional_data[i])
plot!(tdata[:,1], tdata[:,2], tdata[:,3])
end
end
png(filename_prefix*dec(filename_offset,5)*".png")
nothing
end
This file provides some wrapper commands for generating the plots.
NonNewton3.jl
:
include("InitialData3.jl")
include("MeanCurvature3.jl")
include("GraphCode3.jl")
num_pts = 3000
default_timestep = 0.01 / num_pts
max_time = 3
plot_every_ts = 1500
my_data = make_ellipse(1.0,1.0,num_pts,0)
perturb_data!(my_data, [1.0,1.0], 15)
this_vel = zeros(my_data)
next_vel = zeros(my_data)
for t = 0:floor(Int64,max_time / default_timestep)
num_tries, agreement = find_next_vel!(my_data, this_vel,next_vel,default_timestep)
if !agreement
warn("Time $(t*default_timestep): failed to converge when finding next_vel.")
warn("Dumping information:")
max_beta = 1.0
max_col = 1
for col in 1:size(my_data,2)
beta = (1 + dot(next_vel[:,col], next_vel[:,col]))^(1/2)
if beta > max_beta
max_beta = beta
max_col = col
end
end
warn(" Beta attains maximum at position $max_col")
warn(" Beta = $max_beta")
warn(" Position = ", my_data[:,max_col])
prevcol = max_col - 1
nextcol = max_col + 1
if max_col == 1
prevcol = size(my_data,2)
elseif max_col == size(my_data,2)
nextcol = 1
end
warn(" Deltas")
warn(" Left: ", my_data[:,max_col] - my_data[:,prevcol])
warn(" Right: ", my_data[:,nextcol] - my_data[:,max_col])
warn(" Previous velocity: ", this_vel[:,max_col])
warn(" Putative next velocity: ", next_vel[:,max_col])
warn("Quitting...")
break
end
for col in 1:size(my_data,2)
beta = (1 + dot(next_vel[:,col], next_vel[:,col]))^(1/2)
for row in 1:size(my_data,1)
my_data[row,col] += next_vel[row,col] * default_timestep / beta
this_vel[row,col] = next_vel[row,col]
end
if beta > 1e7
warn("time: ", t * default_timestep)
warn("Almost null... beta = ", beta)
warn("current position = ", my_data[:,col])
warn("current Deltas")
prevcol = col - 1
nextcol = col + 1
if col == 1
prevcol = size(my_data,2)
elseif col == size(my_data,2)
nextcol = 1
end
warn(" Left: ", my_data[:,col] - my_data[:,prevcol])
warn(" Right: ", my_data[:,nextcol] - my_data[:,col])
end
end
if t % plot_every_ts ==0
plot_data2D("3Dtest", div(t,plot_every_ts), @sprintf("elapsed: %0.4f",t*default_timestep), my_data, make_ellipse(cos(t*default_timestep), cos(t*default_timestep),100,0))
info("Frame $(t/plot_every_ts): used $num_tries tries.")
end
end
And finally the main file. Mostly it just ties the other files together to produce the plots using the simulation code; there are some diagnostics included for me to keep an eye on the output.
The results
First thing to do is to run a sanity check against explicit solutions. In rotational symmetry, the solution to the cosmic string equations can be found analytically. As you can see below the simulation closely replicates the explicit solution in this case.
The video ends when the simulation stopped. The simulation stopped because a singularity has formed; in this video the singularity can be seen as the collapse of the string to a single point.
Next we can play around with a more complicated initial configuration.
In this video the blue curve is the closed cosmic string, which starts out as a random perturbation of the circle with zero initial speed. The string contracts with acceleration determined by the Nambu-Goto action. The simulation ends when a singularity has formed. It is perhaps a bit hard to see directly where the singularity happened. The diagnostic messages, however, help in this regard. From it we know that the onset of singularity can be seen in the final frame:
The highlighted region is getting quite pointy. In fact, that is accompanied with the "corner" picking up infinite acceleration (in other words, experiencing an infinite force). The mathematical singularity corresponds to something unreasonable happening in the physics.
To make it easier to see the "speed" at which the curve is moving, the following videos show the string along with its "trail". This first one again shows how a singularity can happen as the curve gets gradually more bent, eventually forming a corner.
This next one does a good job emphasizing the "wave" nature of the motion.
The closed cosmic strings behave like a elastic band. The string, overall, wants to contract to a point. Small undulations along the string however are propagated like traveling waves. Both of these tendencies can be seen quite clearly in the above video. That the numerical solver can solve "past" the singular point is a happy accident; while theoretically the solutions can in fact be analytically continued past the singular points, the renormalization process involved in this continuation is numerically unstable and we shouldn't be able to see it on the computer most of the time.
The next video also emphasizes the wave nature of the motion. In addition to the traveling waves, pay attention to the bottom left of the video. Initially the string is almost straight there. This total lack of curvature is a stationary configuration for the string, and so initially there is absolutely no acceleration of that segment of the string. The curvature from the left and right of that segment slowly intrudes on the quiescent piece until the whole thing starts moving.
The last video for this post is a simulation when the ambient space is 3 dimensional. The motion of the string, as you can see, becomes somewhat more complicated. When the ambient space is 2 dimensional a point either accelerates or decelerates based on the local (signed) curvature of the string. But when the ambient space is 3 dimensional, the curvature is now a vector and this additional degree of freedom introduces complications into the behavior. For example, when the ambient space is 2 dimensional it is known that all closed cosmic strings become singular in finite time. But in 3 dimensions there are many closed cosmic strings that vibrate in place without every becoming singular. The video below is one that does however become singular. In addition to a fading trail to help visualize the speed of the curve, this plot also includes the shadows: projections of the curve onto the three coordinate planes.