01 Binary orbit
In this example, we demonstrate how to:
- Output simulation information on the run
- Estimate properties of binary orbit and config the simulation
- Manually generate binary stars
- Plot orbits with gapping
- Add a background force field
- Integrate elliptic orbits
Circular binary
using AstroIO
using AstroPlot
using AstroPlot.ColorSchemes
using Colors
using CSV, DataFrames
using Plots
pyplot() # Need PyPlot to be installed
using AstroNbodySim
using PhysicalParticles
using Unitful, UnitfulAstro
astro()
mkpathIfNotExist("output")
function plot_orbit_with_gap(sim::Simulation, title::String, gap::Int = 1; resolution = (800,800), kw...)
df = DataFrame(CSV.File(joinpath(sim.config.output.dir, "analysis.csv")))
x = df.x
y = df.y
title *= " of every $(gap) orbit(s)"
count = 0
flip = 0
Head = 1
last_sign = sign(y[1])
plot_x = []
plot_y = []
for i in 1:length(y)
@inbounds new_sign = sign(y[i])
if new_sign != last_sign # half cycle
flip += 1
last_sign = new_sign
if flip % 2 == 0 # full cycle
count += 1
if count % gap == 0 # plot this cycle
push!(plot_x, x[Head:i])
push!(plot_y, y[Head:i])
count = 0 # Reset gap
end
Head = i # start of cycle
end
end
# do nothing
end
p = Plots.plot(plot_x, plot_y,
aspect_ratio = 1, legend = nothing, xlabel = "x [kpc]", ylabel = "y [kpc]",
size = resolution,
)
savefig(p, "output/" * title * ".png")
@info "Total orbits: $(div(flip, 2))"
@info "Steps per orbit: $(length(y) / div(flip, 2))"
@info "Figure has been saved to " * title * ".png"
return df
endanalysers::Dict has full access to simulation data. The key values are functions that return one printable value, and they will be written in analysis.csv. Here we find one of the particle and output its x- and y- coordinates.
p1x(sim::Simulation) = find_particle(sim, 1).Pos.x
p1y(sim::Simulation) = find_particle(sim, 1).Pos.y
analysers = Dict(
"x" => p1x,
"y" => p1y,
)Now we estimate the orbit properties and set up the simulation
G = Constant(uAstro).G
mass = 1.0e10u"Msun"
D = 2.0u"kpc"
binary_orbit_period(mass, D) = 2 * pi * sqrt(0.5 * D^3 / G / mass)
binary_rot_vel(mass, D) = sqrt(G * mass / D / 2.0)
vel = binary_rot_vel(mass, D)
T = binary_orbit_period(mass, D)
@info "Estimated Period: $(T)"
@info "Estimated Rotation Vel: $(vel)"
# Define the two particles
data = StructArray(Star(uAstro, id = i) for i in 1:2);
data.Pos[1] = PVector(+0.5D, 0.0u"kpc", 0.0u"kpc");
data.Pos[2] = PVector(-0.5D, 0.0u"kpc", 0.0u"kpc");
data.Vel[1] = PVector(0.0u"kpc/Gyr", +vel, 0.0u"kpc/Gyr");
data.Vel[2] = PVector(0.0u"kpc/Gyr", -vel, 0.0u"kpc/Gyr");
data.Mass .= mass;
# Define simulations
function compute_dt(a::Number; SofteningLength = 0.0001u"kpc", ErrTolTimestep = 0.025)
return sqrt(2 * ErrTolTimestep * SofteningLength / abs(a))
end
TimeStep = compute_dt(G*mass/D^2)
TimeEnd = T * 1.01
TimeBetweenSnapshots = TimeEnd
circular = Simulation(
deepcopy(data);
analysers,
TimeEnd,
TimeStep,
TimeBetweenSnapshots,
OutputDir = "output/BinaryCircular",
);
run(circular)
plot_orbit_with_gap(circular, "Binary Circular").png)
Massless circular binary with background force field
mass = 0.5e10u"Msun"
R = 1.0u"kpc"
binary_orbit_period(mass, R) = 2 * pi * sqrt(R^3 / G / mass)
binary_rot_vel(mass, R) = sqrt(G * mass / R)
vel = binary_rot_vel(mass, R)
T = binary_orbit_period(mass, R)
@info "Estimated Period: $(T)"
@info "Estimated Rotation Vel: $(vel)"
# The only difference is that the particles have no mass, so they cannot produce any mutual forces
data = StructArray(Star(uAstro, id = i) for i in 1:2);
data.Pos[1] = PVector(+R, 0.0u"kpc", 0.0u"kpc");
data.Pos[2] = PVector(-R, 0.0u"kpc", 0.0u"kpc");
data.Vel[1] = PVector(0.0u"kpc/Gyr", +vel, 0.0u"kpc/Gyr");
data.Vel[2] = PVector(0.0u"kpc/Gyr", -vel, 0.0u"kpc/Gyr");
# Define the background force field
attractor(p::AbstractParticle) = -G * mass / (p.Pos * p.Pos) * normalize(p.Pos) / 1.0u"kpc"
bgforce = Function[attractor]
TimeEnd = T * 1.01
TimeStep = compute_dt(G*mass/R^2)
TimeBetweenSnapshots = TimeEnd
bg = Simulation(
deepcopy(data);
bgforce,
analysers,
TimeEnd,
TimeStep,
TimeBetweenSnapshots,
OutputDir = "output/bgforce",
);
run(bg)
plot_orbit_with_gap(bg, "Binary Circular with bgforce").png)
Elliptic orbit precision
R = 1.0u"kpc"
mass = 1.0e8u"Msun"
e = 0.9
vel = sqrt((1.0-e)*G*mass/R)
@info "Velocity at ap: $(vel)"
a = ellipticSemiMajor(G, mass, R, vel)
@info "Length of semi-major axis: $(a)"
T = ellipticPeriod(G, mass, a)
@info "Period of elliptical orbit: $(T)"
data = StructArray(Star(uAstro, id = i) for i in 1:2)
data.Mass[2] = mass
data.Pos[1] = PVector(R, 0.0u"kpc", 0.0u"kpc")
data.Vel[1] = PVector(0.0u"kpc/Gyr", vel, 0.0u"kpc/Gyr")
NumOrbits = 200
TimeEnd = NumOrbits * T * 1.001
TimeBetweenSnapshots = TimeEnd
# Adaptive timesteps
elliptic_adapt = Simulation(
deepcopy(data);
analysers,
TimeEnd,
ErrTolTimestep = 0.05,
TimeBetweenSnapshots,
ForceSofteningTable = [0.001u"kpc" for i in 1:6],
OutputDir = "output/EllipticOrbitAdapt",
)
run(elliptic_adapt)
df = plot_orbit_with_gap(elliptic_adapt, "Elliptic Orbit (adaptive)", 40, resolution = (600,300))
# Fixed timesteps
Δt = df.time[2:end-1] .- df.time[1:end-2]
elliptic_const = Simulation(
deepcopy(data);
analysers,
TimeEnd,
TimeStep = minimum(Δt) * u"Gyr",
ErrTolTimestep = 0.1,
TimeBetweenSnapshots,
ForceSofteningTable = [0.01u"kpc" for i in 1:6],
OutputDir = "output/EllipticOrbitConst",
)
run(elliptic_const)
df = plot_orbit_with_gap(elliptic_const, "Elliptic Orbit (const)", 40, resolution = (600,300)) of every 40 orbit(s).png)
 of every 40 orbit(s).png)