Fully working mbh for two phase missions!
This commit is contained in:
@@ -20,12 +20,12 @@ module Thesis
|
||||
include("./constants.jl")
|
||||
include("./spacecraft.jl")
|
||||
include("./inner_loop/laguerre-conway.jl")
|
||||
include("./inner_loop/propagator.jl")
|
||||
include("./mission.jl")
|
||||
include("./inner_loop/propagator.jl")
|
||||
include("./conversions.jl")
|
||||
include("./plotting.jl")
|
||||
include("./inner_loop/nlp_solver.jl")
|
||||
# include("./inner_loop/monotonic_basin_hopping.jl")
|
||||
include("./inner_loop/monotonic_basin_hopping.jl")
|
||||
# include("./outer_loop.jl")
|
||||
include("./lamberts.jl")
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
struct LaGuerreConway_Error <: Exception end
|
||||
Base.showerror(io::IO, e::LaGuerreConway_Error) = print(io, "LaGuerre-Conway didn't converge")
|
||||
|
||||
struct ΔVsize_Error <: Exception end
|
||||
struct ΔVsize_Error <: Exception
|
||||
ΔV::Float64
|
||||
end
|
||||
Base.showerror(io::IO, e::ΔVsize_Error) = print(io, "ΔV was too big: $(e.ΔV)")
|
||||
|
||||
struct HitPlanet_Error <: Exception end
|
||||
Base.showerror(io::IO, e::HitPlanet_Error) = print(io, "spacecraft hit the planet...")
|
||||
|
||||
@@ -5,41 +5,22 @@ Generates pareto-distributed random numbers
|
||||
|
||||
This is usually around one to two percent, but sometimes up to ten or so (fat tails)
|
||||
"""
|
||||
function pareto(shape::Tuple{Int, Int}, α::Float64=1.01)
|
||||
s = rand((-1,1), shape)
|
||||
r = rand(Float64, shape)
|
||||
function pareto(shape::Union{Tuple{Int, Int}, Int, Nothing}=nothing; α::Float64=1.01)
|
||||
if shape !== nothing
|
||||
s = rand((-1,1), shape)
|
||||
r = rand(Float64, shape)
|
||||
else
|
||||
s = rand((-1,1))
|
||||
r = rand(Float64)
|
||||
end
|
||||
ϵ = 1e-10
|
||||
return 1 .+ (s./ϵ) * (α - 1.0) ./ (ϵ ./ (ϵ .+ r)).^(-α)
|
||||
end
|
||||
|
||||
function pareto(shape::Int, α::Float64=1.01)
|
||||
s = rand((-1,1), shape)
|
||||
r = rand(Float64, shape)
|
||||
ϵ = 1e-10
|
||||
return 1 .+ (s./ϵ) * (α - 1.0) ./ (ϵ ./ (ϵ .+ r)).^(-α)
|
||||
end
|
||||
|
||||
function pareto(α::Float64=1.01)
|
||||
s = rand((-1,1))
|
||||
r = rand(Float64)
|
||||
ϵ = 1e-10
|
||||
return 1 + (s./ϵ) * (α - 1.0) ./ (ϵ ./ (ϵ .+ r)).^(-α)
|
||||
end
|
||||
|
||||
function pareto_add(α::Float64=1.01)
|
||||
s = rand((-1,1))
|
||||
r = rand(Float64)
|
||||
ϵ = 1e-10
|
||||
return (s./ϵ) * (α - 1.0) ./ (ϵ ./ (ϵ .+ r)).^(-α)
|
||||
end
|
||||
|
||||
"""
|
||||
Returns a random date between two dates
|
||||
"""
|
||||
function gen_date(date_range::Tuple{DateTime, DateTime})
|
||||
l0, lf = date_range
|
||||
l0 + Millisecond(floor(rand()*(lf-l0).value))
|
||||
end
|
||||
Base.rand(l0::DateTime,lf::DateTime) = l0 + Millisecond(floor(rand()*(lf-l0).value))
|
||||
|
||||
"""
|
||||
Perturbs a valid mission with pareto-distributed variables, generating a mission guess
|
||||
@@ -47,7 +28,7 @@ Perturbs a valid mission with pareto-distributed variables, generating a mission
|
||||
function perturb(mission::Mission)
|
||||
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_date = mission.launch_date + Dates.Second(floor(7day * (pareto()-1)))
|
||||
new_launch_v∞ = mission.launch_v∞ .* pareto(3)
|
||||
new_phases = Vector{Phase}()
|
||||
for phase in mission.phases
|
||||
@@ -58,16 +39,7 @@ function perturb(mission::Mission)
|
||||
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
|
||||
mission_guess = Mission_Guess(mission.sc, mission.start_mass, new_launch_date, new_launch_v∞, new_phases)
|
||||
end
|
||||
return mission_guess
|
||||
end
|
||||
@@ -89,7 +61,7 @@ function mission_guess( flybys::Vector{Body},
|
||||
n = 20
|
||||
|
||||
# Determine the launch conditions
|
||||
launch_date = gen_date(launch_window)
|
||||
launch_date = rand(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)
|
||||
|
||||
@@ -142,52 +114,6 @@ function mission_guess( flybys::Vector{Body},
|
||||
return mission_guess
|
||||
end
|
||||
|
||||
"""
|
||||
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∞
|
||||
mass = guess.start_mass
|
||||
current_planet = Earth
|
||||
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 = 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.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 = phase.v∞_out
|
||||
start = state(current_planet, time, v∞_out, mass)
|
||||
end
|
||||
end
|
||||
|
||||
return Mission(guess.sc, guess.start_mass, guess.launch_date, guess.launch_v∞, corrected_phases)
|
||||
end
|
||||
|
||||
"""
|
||||
The cost function for the mission
|
||||
TODO: This will probably move and eventually be passed as an argument
|
||||
"""
|
||||
function cost(mission::Mission, max_C3::Float64, max_v∞::Float64)
|
||||
mass_used = 0.0
|
||||
for phase in mission.phases mass_used += mass_used(sc, phase) end
|
||||
mass_percent = mass_used/mission.sc.dry_mass
|
||||
C3_percent = ( mission.launch_v∞ ⋅ mission.launch_v∞ ) / max_C3
|
||||
v∞_percent = norm(mission.phases[end].v∞_in) / max_v∞
|
||||
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
|
||||
@@ -200,44 +126,50 @@ function mbh( flybys::Vector{Body},
|
||||
max_C3::Float64,
|
||||
max_v∞::Float64,
|
||||
latest_arrival::DateTime,
|
||||
cost_fn::Function,
|
||||
primary::Body=Sun;
|
||||
search_patience::Int=10_000,
|
||||
drill_patience::Int=50,
|
||||
verbose::Bool=false)
|
||||
|
||||
# A convenience function
|
||||
# Convenience Functions
|
||||
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, " ")
|
||||
solve(g::Mission_Guess) = solve_mission(g, launch_window, latest_arrival)
|
||||
cost(m::Mission) = cost_fn(m, max_C3, max_v∞)
|
||||
cost(_::Nothing) = Inf
|
||||
|
||||
# Initialize stuff
|
||||
search_count = 0
|
||||
x_current = Bad_Mission("Starting point")
|
||||
drill_count = 0
|
||||
archive = Vector{Mission}()
|
||||
function log()
|
||||
print("\r ")
|
||||
print("\rsearch: $(search_count) drill: $(drill_count) archive: $(length(archive))\t")
|
||||
end
|
||||
|
||||
# The main loop
|
||||
x_current = nothing
|
||||
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())
|
||||
verbose && log()
|
||||
x_star = 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
|
||||
verbose && log()
|
||||
|
||||
#Perturb to generate a slightly different x_star
|
||||
x_star = inner_loop_solve(perturb(x_basin))
|
||||
x_star = solve(perturb(x_basin))
|
||||
|
||||
# If better than the best, then keep it as current
|
||||
if x_star.converged && cost(x_star) < cost(x_current)
|
||||
@@ -258,6 +190,8 @@ function mbh( flybys::Vector{Body},
|
||||
|
||||
end
|
||||
|
||||
if verbose println() end
|
||||
|
||||
return x_current, archive
|
||||
|
||||
end
|
||||
|
||||
@@ -31,7 +31,7 @@ NOTE: This function will output a proper mission if it succeeded and just re-ret
|
||||
guess otherwise
|
||||
"""
|
||||
function solve_mission( guess::Mission_Guess,
|
||||
launch_window::Vector{DateTime},
|
||||
launch_window::Tuple{DateTime,DateTime},
|
||||
latest_arrival::DateTime;
|
||||
tol=1e-10,
|
||||
verbose::Bool=false,
|
||||
@@ -114,7 +114,7 @@ function solve_mission( guess::Mission_Guess,
|
||||
else
|
||||
g = zeros(num_constraints)
|
||||
optimizer!(g,x)
|
||||
println(g)
|
||||
if verbose println(g) end
|
||||
return guess
|
||||
end
|
||||
|
||||
|
||||
@@ -21,9 +21,6 @@ function prop_one(ΔV_unit::Vector{<:Real},
|
||||
time::Float64,
|
||||
primary::Body=Sun)
|
||||
|
||||
for direction in ΔV_unit
|
||||
abs(direction) <= 1.0 || throw(PropOne_Error(ΔV_unit))
|
||||
end
|
||||
ΔV = max_ΔV(craft.duty_cycle, craft.num_thrusters, craft.max_thrust, time, 0., state[7]) * ΔV_unit
|
||||
halfway = laguerre_conway(state, time/2, primary) + [zeros(3); ΔV]
|
||||
final = laguerre_conway(halfway, time/2, primary)
|
||||
@@ -81,3 +78,24 @@ end
|
||||
Convenience function for propagating a state with no thrust
|
||||
"""
|
||||
prop(x::Vector{Float64}, t::Float64, p::Body=Sun) = prop(zeros(1000,3), [x;1.], no_thrust, t, p)[1]
|
||||
|
||||
"""
|
||||
This is solely for the purposes of getting the final state of a mission or guess
|
||||
"""
|
||||
function prop(m::Mission)
|
||||
time = m.launch_date
|
||||
current_planet = Earth
|
||||
start = state(current_planet, time, m.launch_v∞, m.start_mass)
|
||||
|
||||
final = zeros(7)
|
||||
for phase in m.phases
|
||||
final = prop(phase.thrust_profile, start, m.sc, phase.tof)[2]
|
||||
mass = final[7]
|
||||
current_planet = phase.planet
|
||||
time += Dates.Second(floor(phase.tof))
|
||||
start = state(current_planet, time, phase.v∞_out, mass)
|
||||
end
|
||||
|
||||
return final
|
||||
|
||||
end
|
||||
|
||||
@@ -40,13 +40,6 @@ end
|
||||
Constructor for a mission guess. Generally mission guesses are not converged
|
||||
"""
|
||||
function Mission_Guess(sc::Sc, mass::Float64, date::DateTime, v∞::Vector{Float64}, phases::Vector{Phase})
|
||||
# First do some checks to make sure that it's valid
|
||||
mass_used = 0
|
||||
for phase in phases
|
||||
mass_used += mass_consumption(sc, phase)
|
||||
mass - mass_used > sc.dry_mass || throw(Mass_Error(mass - mass_used))
|
||||
v∞_in, v∞_out = phase.v∞_in, phase.v∞_out
|
||||
end
|
||||
Mission_Guess(sc, mass, date, v∞, phases, false)
|
||||
end
|
||||
|
||||
@@ -95,7 +88,7 @@ function Base.Vector(g::Mission_Guess)
|
||||
return result
|
||||
end
|
||||
|
||||
function lowest_mission_vector(launch_window::Vector{DateTime}, num_phases::Int, n::Int)
|
||||
function lowest_mission_vector(launch_window::Tuple{DateTime,DateTime}, num_phases::Int, n::Int)
|
||||
result = Vector{Float64}()
|
||||
push!(result, Dates.datetime2unix(launch_window[1]))
|
||||
push!(result, -10*ones(3)...)
|
||||
@@ -108,7 +101,7 @@ function lowest_mission_vector(launch_window::Vector{DateTime}, num_phases::Int,
|
||||
return result
|
||||
end
|
||||
|
||||
function highest_mission_vector(launch_window::Vector{DateTime}, mission_length::Float64, num_phases::Int, n::Int)
|
||||
function highest_mission_vector(launch_window::Tuple{DateTime,DateTime}, mission_length::Float64, num_phases::Int, n::Int)
|
||||
result = Vector{Float64}()
|
||||
push!(result, Dates.datetime2unix(launch_window[2]))
|
||||
push!(result, 10*ones(3)...)
|
||||
@@ -148,7 +141,8 @@ function Mission(sc::Sc, mass::Float64, date::DateTime, v∞::Vector{Float64}, p
|
||||
mass > sc.dry_mass || throw(Mass_Error(mass - mass_used))
|
||||
current_planet = phase.planet
|
||||
time += Dates.Second(floor(phase.tof))
|
||||
p_pos = state(current_planet, time)[1:3]
|
||||
start = state(current_planet, time, phase.v∞_out, mass)
|
||||
p_pos = start[1:3]
|
||||
abs(norm(final[1:3] - p_pos)) < √30_000. || throw(Planet_Match_Error(final[1:3], p_pos))
|
||||
v∞_in, v∞_out = phase.v∞_in, phase.v∞_out
|
||||
if phase != phases[end]
|
||||
@@ -156,6 +150,9 @@ function Mission(sc::Sc, mass::Float64, date::DateTime, v∞::Vector{Float64}, p
|
||||
δ = acos( ( v∞_in ⋅ v∞_out ) / ( norm(v∞_in) * norm(v∞_out) ) )
|
||||
periapsis = (phase.planet.μ/(v∞_in ⋅ v∞_in)) * ( 1/sin(δ/2) - 1 )
|
||||
periapsis > 1.1phase.planet.r || throw(HitPlanet_Error())
|
||||
for ΔV in phase.thrust_profile
|
||||
abs(ΔV) <= 1.0 || throw(ΔVsize_Error(ΔV))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user