Might change the structure again...

This commit is contained in:
Connor
2021-10-05 20:14:39 -06:00
parent 8e8a23c52a
commit cd1806058d
10 changed files with 280 additions and 176 deletions

View File

@@ -41,30 +41,35 @@ function gen_date(date_range::Tuple{DateTime, DateTime})
l0 + Millisecond(floor(rand()*(lf-l0).value))
end
"""
Returns a random amount of time in a range
"""
function gen_period(date_range::Tuple{DateTime, DateTime})
l0, lf = date_range
Millisecond(floor(rand()*(lf-l0).value))
end
"""
Perturbs a valid mission with pareto-distributed variables, generating a mission guess
"""
function perturb(mission::Mission)
new_launch_date = mission.launch_date + Dates.Second(floor(7day * pareto_add()))
new_launch_v∞ = mission.launch_v∞ .* pareto(3)
new_phases = Vector{Phase}()
for phase in mission.phases
new_v∞_in = phase.v∞_in .* pareto(3)
new_δ = phase.δ * pareto()
new_tof = phase.tof * pareto()
new_thrust_profile = phase.thrust_profile .* pareto(size(phase.thrust_profile))
push!(new_phases, Phase(phase.planet, new_v∞_in, new_δ, new_tof, new_thrust_profile))
mission_guess = Bad_Mission("Starting point")
while typeof(mission_guess) == Bad_Mission
new_launch_date = mission.launch_date + Dates.Second(floor(7day * pareto_add()))
new_launch_v∞ = mission.launch_v∞ .* pareto(3)
new_phases = Vector{Phase}()
for phase in mission.phases
new_v∞_in = phase.v∞_in .* pareto(3)
temp_v∞_out = phase.v∞_out .* pareto(3)
new_v∞_out = norm(new_v∞_in) * temp_v∞_out/norm(temp_v∞_out)
new_tof = phase.tof * pareto()
new_thrust_profile = phase.thrust_profile .* pareto(size(phase.thrust_profile))
push!(new_phases, Phase(phase.planet, new_v∞_in, new_v∞_out, new_tof, new_thrust_profile))
end
# TODO: Mission_Guess.validate()
try
mission_guess = Mission_Guess(mission.sc, mission.start_mass, new_launch_date, new_launch_v∞, new_phases)
catch e
if isa(e, Mass_Error) ||isa(e,V∞_Error) || isa(e,HitPlanet_Error)
continue
else
rethrow()
end
end
end
# TODO: Mission_Guess.validate()
Mission_Guess(mission.sc, mission.start_mass, new_launch_date, new_launch_v∞, new_phases)
return mission_guess
end
"""
@@ -78,66 +83,63 @@ function mission_guess( flybys::Vector{Body},
max_v∞_in_mag::Float64,
latest_arrival::DateTime,
primary::Body=Sun )
# TODO: Eventually I can calculate n more intelligently
n = 20
mission_guess = Bad_Mission("Keep trying to generate a guess")
while mission_guess == Bad_Mission("Keep trying to generate a guess")
# TODO: Eventually I can calculate n more intelligently
n = 20
# Determine the launch conditions
launch_date = gen_date(launch_window)
launch_v∞_normalized = rand(-1:0.0001:1, 3)
launch_v∞ = rand(0:0.0001:max_C3_out) * launch_v∞_normalized/norm(launch_v∞_normalized)
# Determine the launch conditions
launch_date = gen_date(launch_window)
launch_v∞_normalized = rand(-1:0.0001:1, 3)
launch_v∞ = rand(0:0.0001:max_C3_out) * launch_v∞_normalized/norm(launch_v∞_normalized)
# Determine the leg lengths
num_phases = length(flybys) - 1
total_tof = 100year
max_tof = (latest_arrival - launch_date).value / 1000
tofs = rand(30day : hour : 0.7max_tof, num_phases)
total_tof = sum(tofs)
while total_tof > max_tof
# Determine the leg lengths
num_phases = length(flybys) - 1
total_tof = 100year
max_tof = (latest_arrival - launch_date).value / 1000
tofs = rand(30day : hour : 0.7max_tof, num_phases)
total_tof = sum(tofs)
end
# For each phase, determine the v∞_in and δ
phases = Vector{Phase}()
for i in 1:num_phases
flyby = flybys[i+1]
v∞_normalized = rand(-1:0.0001:1, 3)
v∞ = rand(0:0.0001:10) * v∞_normalized/norm(v∞_normalized)
δ = rand(0:0.0001:2π)
periapsis = (flyby.μ/(v∞ v∞)) * ( 1/sin(δ/2) - 1 )
while periapsis < flyby.r + 100.
δ = rand(0:0.0001:2π)
periapsis = (flyby.μ/(v∞ v∞)) * ( 1/sin(δ/2) - 1 )
while total_tof > max_tof
tofs = rand(30day : hour : 0.7max_tof, num_phases)
total_tof = sum(tofs)
end
# For each phase, determine the v∞_in and δ
phases = Vector{Phase}()
for i in 1:num_phases
flyby = flybys[i+1]
v∞_in_normalized = rand(-1:0.0001:1, 3)
v∞_in = rand(0:0.0001:10) * v∞_in_normalized/norm(v∞_in_normalized)
v∞_out_normalized = rand(-1:0.0001:1, 3)
v∞_out = norm(v∞_in) * v∞_out_normalized/norm(v∞_out_normalized)
δ = acos( ( v∞_in v∞_out ) / ( norm(v∞_in) * norm(v∞_out) ) )
periapsis = (flyby.μ/(v∞_in v∞_in)) * ( 1/sin(δ/2) - 1 )
while periapsis < flyby.r + 100.
v∞_out_normalized = rand(-1:0.0001:1, 3)
v∞_out = norm(v∞_in) * v∞_out_normalized/norm(v∞_out_normalized)
δ = acos( ( v∞_in v∞_out ) / ( norm(v∞_in) * norm(v∞_out) ) )
periapsis = (flyby.μ/(v∞_in v∞_in)) * ( 1/sin(δ/2) - 1 )
end
thrusts = rand(-1:0.0001:1,(n,3))
push!(phases, Phase(flyby, v∞_in, v∞_out, tofs[i], thrusts))
end
# Finally, determine the arrival v∞
arrival_v∞_normalized = rand(-1:0.0001:1, 3)
arrival_v∞ = rand(0:0.0001:max_v∞_in_mag) * arrival_v∞_normalized/norm(arrival_v∞_normalized)
# And we can construct a mission guess object with these values
try
mission_guess = Mission_Guess( sc, start_mass, launch_date, launch_v∞, phases )
catch e
if isa(e, Mass_Error) ||isa(e,V∞_Error) || isa(e,HitPlanet_Error)
continue
else
rethrow()
end
end
thrusts = rand(-1:0.0001:1,(n,3))
push!(phases, Phase(flyby, v∞, δ, tofs[i], thrusts))
end
# Finally, determine the arrival v∞
arrival_v∞_normalized = rand(-1:0.0001:1, 3)
arrival_v∞ = rand(0:0.0001:max_v∞_in_mag) * arrival_v∞_normalized/norm(arrival_v∞_normalized)
# And we can construct a mission guess object with these values
Mission_Guess( sc, start_mass, launch_date, launch_v∞, phases )
end
"""
A convenience function for calculating mass usage given a certain thrust profile
"""
function mass_consumption(sc::Sc, phase::Phase)
weighted_thrusting_time = 0.0
n = size(phase.thrust_profile)[1]
for i in 1:size(phase.thrust_profile,1)
weighted_thrusting_time += norm(phase.thrust_profile[i,:]) * phase.tof/n
end
return weighted_thrusting_time*sc.mass_flow_rate
end
"""
This attempts to determine v∞_out from v∞_in and the turning angle, assuming we are staying in the
plane of the three planets
"""
function calc_turn(p1::Body, p2::Body, p3::Body, v∞_in::Vector{Float64}, δ::Float64)
return mission_guess
end
"""
@@ -145,25 +147,26 @@ Sequentially calls the NLP solver to attempt to solve based on the initial guess
"""
function inner_loop_solve(guess::Mission_Guess)
v∞_out = guess.launch_v∞
time = utc2et(Dates.format(guess.launch_date,"yyyy-mm-ddTHH:MM:SS"))
mass = guess.start_mass
current_planet = Earth
start = [spkssb(Earth.id, time, "ECLIPJ2000"); 0.0] + [ zeros(3); guess.launch_v∞; guess.start_mass ]
time = utc2et(Dates.format(guess.launch_date,"yyyy-mm-ddTHH:MM:SS"))
start = state(current_planet, time, v∞_out, mass)
corrected_phases = Vector{Phase}()
for i in 1:length(guess.phases)
phase = guess.phases[i]
current_planet = phase.planet
time += phase.tof
goal = spkssb(phase.planet.id, time, "ECLIPJ2000") + [zeros(3); phase.v∞_in]
goal = state(current_planet, time, phase.v∞_in)
result = solve_phase( start, goal, guess.sc, phase.tof, phase.thrust_profile)
result.converged || return Bad_Mission(result.info) # Drop if it's not working
corrected_phase = Phase(phase.planet, phase.v∞_in, phase.δ, phase.tof, result.sol)
corrected_phase = Phase(phase.planet, phase.v∞_in, phase.v∞_out, phase.tof, result.sol)
push!(corrected_phases, corrected_phase)
mass_used = mass_consumption(guess.sc, corrected_phase)
mass -= mass_used
if i != length(guess.phases)
v∞_out = calc_turn(current_planet, phase.planet, guess.phases[i+1].planet, phase.v∞_in, phase.δ)
current_planet = phase.planet
planet_state = [spkssb(current_planet.id, time, "ECLIPJ2000"); 0.0]
start = planet_state + [ zeros(3); v∞_out; start_mass - mass_used ]
v∞_out = phase.v∞_out
start = state(current_planet, time, v∞_out, mass)
end
end
@@ -183,8 +186,13 @@ function cost(mission::Mission, max_C3::Float64, max_v∞::Float64)
return 3mass_percent + C3_percent + v∞_percent
end
cost(_::Bad_Mission) = Inf
"""
This is the main monotonic basin hopping function. There's a lot going on here, but the general idea
is that hopefully you can provide mission parameters and a list of flybys and get the optimal
mission that uses those flybys.
"""
function mbh( flybys::Vector{Body},
sc::Sc,
start_mass::Float64,
@@ -193,49 +201,64 @@ function mbh( flybys::Vector{Body},
max_v∞::Float64,
latest_arrival::DateTime,
primary::Body=Sun;
search_patience::Int=1_000,
drill_patience::Int=50)
# Let's pseudo-code this bitch
#
# First, we need a function (mission_guess below) that randomly generates the:
# - Launch date
# - Launch v∞_out
# - for each phase:
# - tof
# - v∞_in
# - turning angle
#
# Also need a function (inner_loop_solve below) that takes the generated decision vector guess and
# attempts to correct it with the NLP solver. It will either converge or not.
#
# Also need a costing function (may be provided potentially)
#
# Also need a perturb function
#
guess = mission_guess(flybys, sc, start_mass, launch_window, max_C3, max_v∞, latest_arrival, primary)
inner_loop_solve(guess)
# search_count = 0
# x_current = "Bad"
# while search_count < search_patience
# search_count += 1
# drill_count = 0
# x_star = inner_loop_solve(mission_guess())
# if x_star.converged
# if cost(x_star) < cost(x_current)
# x_current = x_star
# while drill_count < drill_patience
# x_star = inner_loop_solve(perturb(x_current))
# if x_star.converged and cost(x_star) < cost(x_current)
# x_current = x_star
# drill_count = 0
# else
# drill_count += 1
# end
# end
# push!(archive, x_current)
# end
# end
#
#
search_patience::Int=10_000,
drill_patience::Int=50,
verbose::Bool=false)
# A convenience function
random_guess() = mission_guess(flybys,sc,start_mass,launch_window,max_C3,max_v∞,latest_arrival,primary)
cost(m::Mission) = cost(m, max_C3, max_v∞)
status(s,d,l) = print("\r\t", "search: ", s, " drill: ", d, " archive length: ", l, " ")
# Initialize stuff
search_count = 0
x_current = Bad_Mission("Starting point")
archive = Vector{Mission}()
# The main loop
if verbose println("Starting Monotonic Basin Hopper") end
while search_count < search_patience
# Intialize an x_star, if it doesn't converge, hop on to the next basin
search_count += 1
drill_count = 0
if verbose status(search_count, drill_count, length(archive)) end
x_star = inner_loop_solve(random_guess())
x_star.converged || continue
x_basin = x_star
# If it does, though, we check to see if it's better than the current
if cost(x_star) < cost(x_current) x_current = x_star end
# Either way, we need to drill into this particular basin, since it's valid
while drill_count < drill_patience
if verbose status(search_count, drill_count, length(archive)) end
#Perturb to generate a slightly different x_star
x_star = inner_loop_solve(perturb(x_basin))
# If better than the best, then keep it as current
if x_star.converged && cost(x_star) < cost(x_current)
x_current = x_star
x_basin = x_star
drill_count = 0
# If better than the best in this particular basin, keep it as basin
elseif x_star.converged && cost(x_star) < cost(x_basin)
x_basin = x_star
drill_count = 0
# Otherwise, keep drilling
else
drill_count += 1
end
end
x_current in archive || push!(archive, x_current)
end
return x_current, archive
end

View File

@@ -39,15 +39,17 @@ function solve_phase( start::Vector{Float64},
lower_x = -1 * ones(3n)
upper_x = ones(3n)
num_constraints = 6
filename = "ipopt_"*string(rand(UInt))
ipopt_options = Dict("constr_viol_tol" => tol,
"acceptable_constr_viol_tol" => 1e-4,
"max_iter" => 10_000,
"max_cpu_time" => 30.,
"print_level" => 0)
# if verbose ipopt_options["print_level"] = 5 end
"print_level" => 0,
"output_file" => filename)
options = Options(solver=IPOPT(ipopt_options),
derivatives=ForwardAD())
x, _, info = minimize(f!, vec(x0), num_constraints, lower_x, upper_x, final[1:6], final[1:6], options)
rm(filename)
if info in [:Solve_Succeeded, :Solved_To_Acceptable_Level]
return Result(true, info, reshape(x,(n,3)))
else