Update for ky
This commit is contained in:
@@ -5,7 +5,7 @@ module Thesis
|
||||
using PlotlyJS
|
||||
using Distributed
|
||||
using SPICE
|
||||
using Dates: DateTime, Millisecond, Dates
|
||||
using Dates: DateTime, Millisecond, Dates, Second, format, datetime2unix, unix2datetime
|
||||
|
||||
try
|
||||
furnsh("../../spice_files/naif0012.tls")
|
||||
@@ -15,18 +15,18 @@ module Thesis
|
||||
furnsh("spice_files/de430.bsp")
|
||||
end
|
||||
|
||||
include("./errors.jl")
|
||||
include("./bodies.jl")
|
||||
include("./constants.jl")
|
||||
include("./spacecraft.jl")
|
||||
include("./inner_loop/laguerre-conway.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("./outer_loop.jl")
|
||||
include("./lamberts.jl")
|
||||
include("./types/errors.jl")
|
||||
include("./types/bodies.jl")
|
||||
include("./utilities/constants.jl")
|
||||
include("./types/spacecraft.jl")
|
||||
include("./utilities/laguerre-conway.jl")
|
||||
include("./types/mission.jl")
|
||||
include("./utilities/propagator.jl")
|
||||
include("./utilities/conversions.jl")
|
||||
include("./utilities/plotting.jl")
|
||||
include("./nlp_solver.jl")
|
||||
include("./mbh.jl")
|
||||
include("./genetic_algorithm.jl")
|
||||
include("./utilities/lamberts.jl")
|
||||
|
||||
end
|
||||
|
||||
36
julia/src/genetic_algorithm.jl
Normal file
36
julia/src/genetic_algorithm.jl
Normal file
@@ -0,0 +1,36 @@
|
||||
export outer_loop
|
||||
|
||||
function outer_loop()
|
||||
|
||||
# Ok, so here's the pseudocode
|
||||
# generate a population randomly -> rand(sequence)
|
||||
# run mbh and find cost for each member of population -> mbh.(population)
|
||||
# loop
|
||||
# loop (number of children)
|
||||
# randomly choose some parents from pool -> shuffle(pool)[1:n]
|
||||
# choose best member = mom -> sort(pool)[1]
|
||||
# randomly choose some parents from pool again -> shuffle(pool)[1:n]
|
||||
# choose best member = dad -> sort(pool)[1]
|
||||
# take the first and last random number of entries from mom and middle from dad -> mate()
|
||||
# insert new child into population -> push!()
|
||||
# loop (number of mutated)
|
||||
# randomly choose some parents from the pool -> shuffle(pool)[1:n]
|
||||
# best member = retained -> sort(pool)[1]
|
||||
# copy retained -> mutated and possibly mutate each chromosome -> mutate()
|
||||
# insert mutated into population -> push!()
|
||||
# loop (number of elite)
|
||||
# insert the top individuals into the pool -> push!(new_pool, sort(pool)[1:n])
|
||||
# run mbh and evaluate all members of new pool
|
||||
# return the best
|
||||
#
|
||||
# Functions Needed:
|
||||
# rand(flyby_sequence)
|
||||
# mate(flyby_sequence, flyby_sequence)
|
||||
# mutate(flyby_sequence)
|
||||
# snapshot(generation)
|
||||
#
|
||||
|
||||
println("And so the end begins")
|
||||
|
||||
end
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
using Dates
|
||||
|
||||
export inner_loop
|
||||
|
||||
"""
|
||||
This is it. The outer function call for the inner loop. After this is done,
|
||||
there's only the outer loop left to do. And that's pretty easy.
|
||||
"""
|
||||
function inner_loop(launch_date::DateTime,
|
||||
craft::Sc,
|
||||
start_mass::Float64,
|
||||
phases::Vector{Phase};
|
||||
min_flyby::Float64=1000.,
|
||||
mbh_specs=nothing,
|
||||
verbose=false)
|
||||
|
||||
# First we need to do some quick checks that the mission is well formed
|
||||
for i in 1:length(phases)
|
||||
if i == 1
|
||||
@assert phases[i].from_planet == "Earth"
|
||||
else
|
||||
# Check that the planet is valid
|
||||
if phases[i].from_planet ∉ keys(μs)
|
||||
error("Planet is not valid: ", phases[i].from_planet)
|
||||
# Check that there is only one flyby planet
|
||||
elseif phases[i].from_planet != phases[i-1].to_planet
|
||||
fromP, toP = phases[i].from_planet, phases[i-1].to_planet
|
||||
error("Planets don't match up: (phase $(i)) $(fromP) / (phase $(i-1)) $(toP)")
|
||||
# Check that v∞_in == v∞_out
|
||||
elseif !isapprox(norm(phases[i].v∞_outgoing), norm(phases[i-1].v∞_incoming))
|
||||
norm_incoming = norm(phases[i-1].v∞_incoming)
|
||||
norm_outgoing = norm(phases[i].v∞_outgoing)
|
||||
error("""Norms of vectors aren't equal:
|
||||
Phase $(i-1): $(phases[i-1].v∞_incoming) / norm: $(norm_incoming)
|
||||
Phase $(i): $(phases[i].v∞_outgoing) / norm: $(norm_outgoing)""")
|
||||
end
|
||||
# Check that the approach is not too low
|
||||
v∞ = norm(phases[i].v∞_outgoing)
|
||||
δ = acos((phases[i].v∞_outgoing ⋅ phases[i-1].v∞_incoming)/v∞^2)
|
||||
flyby = μs[phases[i].from_planet]/v∞^2 * (1/sin(δ/2) - 1)
|
||||
true_min = rs[phases[i].from_planet] + min_flyby
|
||||
flyby <= true_min || error("Flyby too low from phase $(i-1) to $(i): $(flyby) / $(true_min)")
|
||||
end
|
||||
end
|
||||
|
||||
time = utc2et(Dates.format(launch_date,"yyyy-mm-ddTHH:MM:SS"))
|
||||
thrust_profiles = Vector{Matrix{Float64}}()
|
||||
|
||||
for phase in phases
|
||||
planet1_state = [spkssb(ids[phase.from_planet], time, "ECLIPJ2000"); 0.0]
|
||||
time += phase.time_of_flight
|
||||
planet2_state = [spkssb(ids[phase.to_planet], time, "ECLIPJ2000"); 0.0]
|
||||
start = planet1_state + [0., 0., 0., phase.v∞_outgoing..., start_mass]
|
||||
final = planet2_state + [0., 0., 0., phase.v∞_incoming..., start_mass]
|
||||
println(start)
|
||||
println(final)
|
||||
# TODO: Come up with improved method of calculating "n"
|
||||
if mbh_specs === nothing
|
||||
best = mbh(start, final, craft, μs["Sun"], 0.0, phase.time_of_flight, 20,
|
||||
verbose=verbose)[1]
|
||||
else
|
||||
sil, dil = mbh_specs
|
||||
best = mbh(start, final, craft, μs["Sun"], 0.0, phase.time_of_flight, 20,
|
||||
verbose=verbose, search_patience_lim=sil, drill_patience_lim=dil)[1]
|
||||
end
|
||||
push!(thrust_profiles, best.zero)
|
||||
end
|
||||
return thrust_profiles
|
||||
|
||||
end
|
||||
@@ -28,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()-1)))
|
||||
new_launch_date = mission.launch_date + Second(floor(7day * (pareto()-1)))
|
||||
new_launch_v∞ = mission.launch_v∞ .* pareto(3)
|
||||
new_phases = Vector{Phase}()
|
||||
for phase in mission.phases
|
||||
@@ -57,7 +57,7 @@ function mission_guess( flybys::Vector{Body},
|
||||
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
|
||||
n = 40
|
||||
|
||||
# Determine the launch conditions
|
||||
launch_date = rand(launch_window...)
|
||||
@@ -114,6 +114,16 @@ function mission_guess( flybys::Vector{Body},
|
||||
return mission_guess
|
||||
end
|
||||
|
||||
function record(m::Mission, planets::Vector{Body})
|
||||
t = Dates.now()
|
||||
abbrev = join([ planet.name[1] for planet in planets ])
|
||||
mkdir("archive/$(abbrev)_$(t)")
|
||||
store(m, "archive/$(abbrev)_$(t)/mission")
|
||||
p = plot(m, title="$(abbrev) Trajectory")
|
||||
savefig(p,"archive/$(abbrev)_$(t)/plot.html")
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
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
|
||||
@@ -134,11 +144,12 @@ function mbh( flybys::Vector{Body},
|
||||
|
||||
# Convenience Functions
|
||||
random_guess() = mission_guess(flybys,sc,start_mass,launch_window,max_C3,max_v∞,latest_arrival)
|
||||
solve(g::Mission_Guess) = solve_mission(g, launch_window, latest_arrival)
|
||||
cost(m::Mission) = cost_fn(m, max_C3, max_v∞)
|
||||
cost(m::Union{Mission,Mission_Guess}) = cost_fn(m, max_C3, max_v∞)
|
||||
cost(_::Nothing) = Inf
|
||||
solve(g::Mission_Guess) = solve_mission(g, launch_window, latest_arrival, max_C3, max_v∞)
|
||||
|
||||
# Initialize stuff
|
||||
x_current = nothing
|
||||
search_count = 0
|
||||
drill_count = 0
|
||||
archive = Vector{Mission}()
|
||||
@@ -152,7 +163,6 @@ function mbh( flybys::Vector{Body},
|
||||
end
|
||||
|
||||
# The main loop
|
||||
x_current = nothing
|
||||
while search_count < search_patience
|
||||
|
||||
# Intialize an x_star, if it doesn't converge, hop on to the next basin
|
||||
@@ -189,7 +199,10 @@ function mbh( flybys::Vector{Body},
|
||||
end
|
||||
end
|
||||
|
||||
x_current in archive || push!(archive, x_current)
|
||||
if x_current ∉ archive
|
||||
push!(archive, x_current)
|
||||
record(x_current, flybys)
|
||||
end
|
||||
|
||||
# If in test mode, we don't need to actually optimize. Just grab the first valid basin-best
|
||||
if test
|
||||
@@ -25,6 +25,10 @@ function constraint_bounds(guess::Mission_Guess)
|
||||
push!(high_constraint, guess.start_mass - guess.sc.dry_mass)
|
||||
end
|
||||
end
|
||||
|
||||
push!(low_constraint, 0., 0.)
|
||||
push!(high_constraint, 1e3, 1e3)
|
||||
|
||||
return low_constraint, high_constraint
|
||||
|
||||
end
|
||||
@@ -36,10 +40,12 @@ guess otherwise
|
||||
"""
|
||||
function solve_mission( guess::Mission_Guess,
|
||||
launch_window::Tuple{DateTime,DateTime},
|
||||
latest_arrival::DateTime;
|
||||
latest_arrival::DateTime,
|
||||
max_C3::Float64,
|
||||
max_v∞::Float64;
|
||||
tol=1e-12,
|
||||
verbose::Bool=false,
|
||||
print_level=0 )
|
||||
print_level=0)
|
||||
|
||||
# First we define our starting point
|
||||
x0 = Vector(guess)
|
||||
@@ -51,9 +57,10 @@ function solve_mission( guess::Mission_Guess,
|
||||
# Establish initial conditions
|
||||
v∞_out = x[2:4]
|
||||
current_planet = Earth
|
||||
launch_date = Dates.unix2datetime(x[1])
|
||||
time = utc2et(Dates.format(launch_date,"yyyy-mm-ddTHH:MM:SS"))
|
||||
launch_date = unix2datetime(x[1])
|
||||
time = utc2et(format(launch_date,"yyyy-mm-ddTHH:MM:SS"))
|
||||
start = state(current_planet, time, v∞_out, guess.start_mass)
|
||||
final = zeros(7)
|
||||
|
||||
# Now, for each phase we must require:
|
||||
# - That the ending state matches (all legs)
|
||||
@@ -88,13 +95,16 @@ function solve_mission( guess::Mission_Guess,
|
||||
else
|
||||
g[8*(length(flybys)-1)+7] = final[7] - guess.sc.dry_mass
|
||||
end
|
||||
g[8*(length(flybys)-1)+8] = max_C3 - norm(x[2:4])^2
|
||||
g[8*(length(flybys)-1)+9] = max_v∞ - norm(v∞_in)
|
||||
i += 1
|
||||
end
|
||||
|
||||
return 1.0
|
||||
|
||||
catch e
|
||||
|
||||
if isa(e,LaGuerreConway_Error)
|
||||
if isa(e,LaGuerreConway_Error) || isa(e, Mass_Error)
|
||||
g[1:8*(length(flybys)-1)+7] .= 1e10
|
||||
return 1e10
|
||||
else
|
||||
@@ -104,16 +114,16 @@ function solve_mission( guess::Mission_Guess,
|
||||
end
|
||||
end
|
||||
|
||||
max_time = Dates.datetime2unix(latest_arrival) - Dates.datetime2unix(launch_window[1])
|
||||
max_time = datetime2unix(latest_arrival) - datetime2unix(launch_window[1])
|
||||
lower_x = lowest_mission_vector(launch_window, length(guess.phases), n)
|
||||
upper_x = highest_mission_vector(launch_window, max_time, length(guess.phases), n)
|
||||
num_constraints = 8*(length(guess.phases)-1) + 7
|
||||
num_constraints = 8*(length(guess.phases)-1) + 9
|
||||
g_low, g_high = constraint_bounds(guess)
|
||||
ipopt_options = Dict("constr_viol_tol" => tol,
|
||||
"acceptable_constr_viol_tol" => 100tol,
|
||||
"bound_relax_factor" => 0.,
|
||||
"max_iter" => 100_000,
|
||||
"max_cpu_time" => 5. * length(guess.phases),
|
||||
"max_cpu_time" => 3. * length(guess.phases),
|
||||
"print_level" => print_level)
|
||||
options = Options(solver=IPOPT(ipopt_options), derivatives=ForwardFD())
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
using Random, Dates
|
||||
|
||||
export gen_decision_vector
|
||||
|
||||
"""
|
||||
Returns a random date between two dates
|
||||
"""
|
||||
function gen_date(date_range::Vector{DateTime})
|
||||
l0, lf = date_range
|
||||
l0 + Dates.Millisecond(floor(rand()*(lf-l0).value))
|
||||
end
|
||||
|
||||
"""
|
||||
Returns a random amount of time in a range
|
||||
"""
|
||||
function gen_period(date_range::Vector{DateTime})
|
||||
l0, lf = date_range
|
||||
Dates.Millisecond(floor(rand()*(lf-l0).value))
|
||||
end
|
||||
|
||||
"""
|
||||
So ideally, this should generate a nice random decision vector, given the constraints.
|
||||
Everything that you need to produce a vector of phases
|
||||
|
||||
Start with an empty vector of the right size
|
||||
You need:
|
||||
- launch_date
|
||||
- 3 components v∞_out for Earth
|
||||
- and then up to four flybys which contain:
|
||||
- a planet
|
||||
- three components v∞_in
|
||||
- turning angle (in ecliptic)
|
||||
- tof to planet
|
||||
and finally, the ending planet is held fixed
|
||||
"""
|
||||
function gen_decision_vector(launch_range::Vector{DateTime},
|
||||
target::String,
|
||||
arrival_deadline::DateTime)
|
||||
phases = Vector{Phase}()
|
||||
launch_date = gen_date(launch_range)
|
||||
v∞_out = 20rand(Float64,3) .- 10.
|
||||
|
||||
# Generate the planets (or null flybys)
|
||||
planets = [ ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"];
|
||||
repeat(["None"],8) ] # Just as likely to get a planet as no flyby
|
||||
long_flybys = [ "Earth"; filter(x -> x != "None", rand(planets, 4)); target ]
|
||||
# This will cut the flybys off if the target shows up early
|
||||
flybys = long_flybys[1:findfirst(x->x==target, long_flybys)]
|
||||
|
||||
time = launch_date
|
||||
for i in 1:length(flybys)-1
|
||||
v∞_in = 20rand(Float64,3) .- 10. # Generate the v∞_in components
|
||||
tof = gen_period([time,arrival_deadline])
|
||||
time += tof
|
||||
push!(phases,Phase(flybys[i],flybys[i+1], tof.value/1000, v∞_out, v∞_in))
|
||||
v∞_out_base = rand(Float64,3) .- 0.5
|
||||
v∞_out = norm(v∞_in) * v∞_out_base/norm(v∞_out_base)
|
||||
end
|
||||
|
||||
return phases
|
||||
|
||||
end
|
||||
|
||||
"""
|
||||
This is the binary crossover function, implemented as detailed in Englander.
|
||||
It chooses the first n and last m phases from
|
||||
"""
|
||||
function crossover()
|
||||
end
|
||||
@@ -13,7 +13,7 @@ end
|
||||
period(b::Body) = 2π * √(b.a^3 / Sun.μ)
|
||||
|
||||
function state(p::Body, t::DateTime, add_v∞::Vector{T}=[0., 0., 0.], add_mass::Float64=1e10) where T <: Real
|
||||
time = utc2et(Dates.format(t,"yyyy-mm-ddTHH:MM:SS"))
|
||||
time = utc2et(format(t,"yyyy-mm-ddTHH:MM:SS"))
|
||||
[ spkssb(p.id, time, "ECLIPJ2000"); 0.0 ] + [ zeros(3); add_v∞; add_mass ]
|
||||
end
|
||||
|
||||
@@ -49,7 +49,7 @@ This is the opposite of the function below. It takes a vector and any other nece
|
||||
"""
|
||||
function Mission_Guess(x::Vector{Float64}, sc::Sc, mass::Float64, flybys::Vector{Body})
|
||||
# Variable mission params
|
||||
launch_date = Dates.unix2datetime(x[1])
|
||||
launch_date = unix2datetime(x[1])
|
||||
launch_v∞ = x[2:4]
|
||||
|
||||
# Try to intelligently determine n
|
||||
@@ -78,7 +78,7 @@ information from the guess though.
|
||||
"""
|
||||
function Base.Vector(g::Mission_Guess)
|
||||
result = Vector{Float64}()
|
||||
push!(result, Dates.datetime2unix(g.launch_date))
|
||||
push!(result, datetime2unix(g.launch_date))
|
||||
push!(result, g.launch_v∞...)
|
||||
for phase in g.phases
|
||||
push!(result,phase.v∞_in...)
|
||||
@@ -91,7 +91,7 @@ end
|
||||
|
||||
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, datetime2unix(launch_window[1]))
|
||||
push!(result, -10*ones(3)...)
|
||||
for i in 1:num_phases
|
||||
push!(result, -10*ones(3)...)
|
||||
@@ -104,7 +104,7 @@ end
|
||||
|
||||
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, datetime2unix(launch_window[2]))
|
||||
push!(result, 10*ones(3)...)
|
||||
for i in 1:num_phases
|
||||
push!(result, 10*ones(3)...)
|
||||
@@ -133,7 +133,7 @@ function Mission(sc::Sc, mass::Float64, date::DateTime, v∞::Vector{Float64}, p
|
||||
force=false)
|
||||
# First do some checks to make sure that it's valid
|
||||
if !force
|
||||
time = utc2et(Dates.format(date,"yyyy-mm-ddTHH:MM:SS"))
|
||||
time = utc2et(format(date,"yyyy-mm-ddTHH:MM:SS"))
|
||||
current_planet = Earth
|
||||
start = state(current_planet, time, v∞, mass)
|
||||
for phase in phases
|
||||
@@ -178,7 +178,7 @@ end
|
||||
|
||||
function Mission(x::Vector{Float64}, sc::Sc, mass::Float64, flybys::Vector{Body})
|
||||
# Variable mission params
|
||||
launch_date = Dates.unix2datetime(x[1])
|
||||
launch_date = unix2datetime(x[1])
|
||||
launch_v∞ = x[2:4]
|
||||
|
||||
# Try to intelligently determine n
|
||||
@@ -224,7 +224,7 @@ function Base.write(io::IO, m::Mission)
|
||||
time = m.launch_date
|
||||
i = 1
|
||||
for phase in m.phases
|
||||
time += Dates.Second(floor(phase.tof))
|
||||
time += Second(floor(phase.tof))
|
||||
write(io, "Phase $(i):\n")
|
||||
write(io, "\tPlanet: $(phase.planet.name)\n")
|
||||
write(io, "\tV∞_in: $(phase.v∞_in) km/s\n")
|
||||
@@ -6,8 +6,8 @@ Convenience function for solving lambert's problem
|
||||
"""
|
||||
function lamberts(planet1::Body,planet2::Body,leave::DateTime,arrive::DateTime)
|
||||
|
||||
time_leave = utc2et(Dates.format(leave,"yyyy-mm-ddTHH:MM:SS"))
|
||||
time_arrive = utc2et(Dates.format(arrive,"yyyy-mm-ddTHH:MM:SS"))
|
||||
time_leave = utc2et(format(leave,"yyyy-mm-ddTHH:MM:SS"))
|
||||
time_arrive = utc2et(format(arrive,"yyyy-mm-ddTHH:MM:SS"))
|
||||
tof_req = time_arrive - time_leave
|
||||
|
||||
state1 = [spkssb(planet1.id, time_leave, "ECLIPJ2000"); 0.0]
|
||||
@@ -265,7 +265,7 @@ function plot(m::Union{Mission, Mission_Guess}; title::String="Mision Plot")
|
||||
# First do the path
|
||||
path, final = prop(phase.thrust_profile, start, m.sc, phase.tof, interpolate=true)
|
||||
mass = final[7]
|
||||
time += Dates.Second(floor(phase.tof))
|
||||
time += Second(floor(phase.tof))
|
||||
current_planet = phase.planet
|
||||
start = state(current_planet, time, phase.v∞_out, mass)
|
||||
path_trace, new_limit = gen_plot(path, label="Phase "*string(i))
|
||||
@@ -70,6 +70,8 @@ function prop(ΔVs::Matrix{T},
|
||||
for j in 1:7 push!(states[j], state[j]) end
|
||||
end
|
||||
|
||||
state[7] > craft.dry_mass || throw(Mass_Error(state[7]))
|
||||
|
||||
return states, state
|
||||
|
||||
end
|
||||
@@ -82,7 +84,7 @@ prop(x::Vector{Float64}, t::Float64, p::Body=Sun) = prop(zeros(1000,3), [x;1.],
|
||||
"""
|
||||
This is solely for the purposes of getting the final state of a mission or guess
|
||||
"""
|
||||
function prop(m::Mission)
|
||||
function prop(m::Union{Mission, Mission_Guess})
|
||||
time = m.launch_date
|
||||
current_planet = Earth
|
||||
start = state(current_planet, time, m.launch_v∞, m.start_mass)
|
||||
@@ -92,7 +94,7 @@ function prop(m::Mission)
|
||||
final = prop(phase.thrust_profile, start, m.sc, phase.tof)[2]
|
||||
mass = final[7]
|
||||
current_planet = phase.planet
|
||||
time += Dates.Second(floor(phase.tof))
|
||||
time += Second(floor(phase.tof))
|
||||
start = state(current_planet, time, phase.v∞_out, mass)
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user