Fully working mbh for two phase missions!

This commit is contained in:
Connor
2021-10-11 21:53:23 -06:00
parent 504d83b390
commit a2c1aedc08
11 changed files with 106 additions and 167 deletions

View File

@@ -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")

View File

@@ -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...")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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