I am trying to solve a low thrust optimal control problem for Earth orbits, i.e. going from one orbit to another. The formulation of the problem includes six states (r_1, r_2, r_3, v_1, v_2, v_3) and 3 controls (u_1, u_2, u_3) with a simplified point model of gravity. When I specify the full initial state and half of the final state, the solver converges and yields a good solution. When I try the full final state, the problem is over constrained.
My thought on how to remedy this is to allow the trajectory to depart the initial orbit at any point along the orbital curve and join the final orbit an any point along the final orbital curve, giving it more degrees of freedom. Is there a way to constrain the initial and final values of all 6 states to a cspline
curve? This is what I have tried so far:
m = GEKKO(remote=True)
m.time = np.linspace(0, tof, t_steps)
theta_i = m.FV(lb = 0, ub = 360)
theta_f = m.FV(lb = 0, ub = 360)
rx_i = m.Param()
ry_i = m.Param()
rz_i = m.Param()
vx_i = m.Param()
vy_i = m.Param()
vz_i = m.Param()
rx_f = m.Param()
ry_f = m.Param()
rz_f = m.Param()
vx_f = m.Param()
vy_f = m.Param()
vz_f = m.Param()
m.cspline(theta_i, rx_i, initial.theta, initial.r[:, 0])
m.cspline(theta_i, ry_i, initial.theta, initial.r[:, 1])
m.cspline(theta_i, rz_i, initial.theta, initial.r[:, 2])
m.cspline(theta_i, vx_i, initial.theta, initial.v[:, 0])
m.cspline(theta_i, vy_i, initial.theta, initial.v[:, 1])
m.cspline(theta_i, vz_i, initial.theta, initial.v[:, 2])
m.cspline(theta_f, rx_f, final.theta, final.r[:, 0])
m.cspline(theta_f, ry_f, final.theta, final.r[:, 1])
m.cspline(theta_f, rz_f, final.theta, final.r[:, 2])
m.cspline(theta_f, vx_f, final.theta, final.v[:, 0])
m.cspline(theta_f, vy_f, final.theta, final.v[:, 1])
m.cspline(theta_f, vz_f, final.theta, final.v[:, 2])
r1 = m.Var(rx_i)
r2 = m.Var(ry_i)
r3 = m.Var(rz_i)
r1dot = m.Var(vx_i)
r2dot = m.Var(vy_i)
r3dot = m.Var(vz_i)
u1 = m.Var(lb = -max_u, ub = max_u)
u2 = m.Var(lb = -max_u, ub = max_u)
u3 = m.Var(lb = -max_u, ub = max_u)
m.Equation(r1.dt() == r1dot)
m.Equation(r2.dt() == r2dot)
m.Equation(r3.dt() == r3dot)
r = m.Intermediate(m.sqrt(r1**2 + r2**2 + r3**2))
v = m.Intermediate(m.sqrt(r1dot**2 + r2dot**2 + r3dot**3))
m.Equation(-mu*r1/r**3 == r1dot.dt() + u1)
m.Equation(-mu*r2/r**3 == r2dot.dt() + u2)
m.Equation(-mu*r3/r**3 == r3dot.dt() + u3)
m.fix_final(r1, rx_f)
m.fix_final(r2, ry_f)
m.fix_final(r3, rz_f)
m.fix_final(r1dot, vx_f)
m.fix_final(r2dot, vy_f)
m.fix_final(r3dot, vz_f)
m.Minimize(m.integral(u1**2 + u2**2 + u3**2))
m.options.IMODE = 6
m.options.solver = 3
#m.options.ATOL = 1e-3
m.options.MAX_ITER = 300
m.solve(disp=True) # solve
With cspline, I am trying to allow GEKKO to pick a fixed value of the true anomaly (the parameter that determines how far along the orbit you are) from data that I have generated about the states at sampled true anomalies that would have associated position and velocity states. Any help would be much appreciated!
SOLUTION:
I implemented the end constraints as "soft constraints", i.e. quantities to be minimized, instead of hard constraints which would use fix_final
. Specific implementation is as follows.
final = np.zeros(len(m.time))
final[-1] = 1
final = m.Param(value=final)
m.Obj(final*(r1-final_o.r[0, 0])**2)
m.Obj(final*(r2-final_o.r[0, 1])**2)
m.Obj(final*(r3-final_o.r[0, 2])**2)
m.Obj(final*(r1dot-final_o.v[0, 0])**2)
m.Obj(final*(r2dot-final_o.v[0, 1])**2)
m.Obj(final*(r3dot-final_o.v[0, 2])**2)
final
makes the solver only consider the last item (the end boundary) when multiplied.
It is generally much harder for an optimizer to exactly reach a fixed endpoint, especially when it depends on a complex sequence of moves. This often leads to infeasible solutions. An alternative is to create a soft constraint (objective minimization) to penalize deviations from the final trajectory. Here is an example that is similar:
import matplotlib.animation as animation
import numpy as np
from gekko import GEKKO
#Defining a model
m = GEKKO()
#################################
#Weight of item
m2 = 1
#################################
#Defining the time, we will go beyond the 6.2s
#to check if the objective was achieved
m.time = np.linspace(0,8,100)
end_loc = int(100.0*6.2/8.0)
#Parameters
m1a = m.Param(value=10)
m2a = m.Param(value=m2)
final = np.zeros(len(m.time))
for i in range(len(m.time)):
if m.time[i] < 6.2:
final[i] = 0
else:
final[i] = 1
final = m.Param(value=final)
#MV
ua = m.Var(value=0)
#State Variables
theta_a = m.Var(value=0)
qa = m.Var(value=0)
ya = m.Var(value=-1)
va = m.Var(value=0)
#Intermediates
epsilon = m.Intermediate(m2a/(m1a+m2a))
#Defining the State Space Model
m.Equation(ya.dt() == va)
m.Equation(va.dt() == -epsilon*theta_a + ua)
m.Equation(theta_a.dt() == qa)
m.Equation(qa.dt() == theta_a -ua)
#Define the Objectives
#Make all the state variables be zero at time >= 6.2
m.Obj(final*ya**2)
m.Obj(final*va**2)
m.Obj(final*theta_a**2)
m.Obj(final*qa**2)
m.fix(ya,pos=end_loc,val=0.0)
m.fix(va,pos=end_loc,val=0.0)
m.fix(theta_a,pos=end_loc,val=0.0)
m.fix(qa,pos=end_loc,val=0.0)
#Try to minimize change of MV over all horizon
m.Obj(0.001*ua**2)
m.options.IMODE = 6 #MPC
m.solve() #(disp=False)
This example uses a combination of soft and hard constraints to help it find the optimal solution.