The Goddard Problem (One Phase)

For a description of the one-phase Goddard rocket problem, see the JupyterLab notebook documentation for this problem.

It turns out that this problem as a singular arc, and a better solution can be obtained by solving the problem in three phases, where the singular arc conditions are imposed as a path constraint in the middle phase. See the three phase solution as a Python script or as a JupyterLab notebook.

This example script has user-defined methods for computing the first and second derivatives of the objective and continuous functions. User-defined derivatives can be faster to compute than derivatives computed by automatic differentiation, but not by a large factor. Because for most problems as much time is spent in the Ipopt solver as in derivative functions evaluation, even a substantial speedup in derivative evaluation may not result in a significant speedup in the overall solution time, and so it’s almost never worth the effort to implement user-defined derivatives.

The python script in this example can be executed from the command line with:

$ python -m yapss.examples.goddard_problem_1_phase

Functions

YAPSS solution of the Goddard rocket problem with a single phase.

main() None[source]

Demonstrate the solution to the Goddard Rocket Problem (1 Phase).

plot_solution(solution: Solution) None[source]

Plot solution to the Goddard Rocket Problem.

Parameters:
solutionSolution

The solution to the Goddard Rocket Problem.

setup() Problem[source]

Set up the Goddard Rocket Problem as an optimal control problem.

Returns:
Problem

The Goddard Rocket Problem as an optimal control problem.

Code

"""

YAPSS solution of the Goddard rocket problem with a single phase.

"""

# Allow uppercase variables
# ruff: noqa: N806

__all__ = ["main", "plot_solution", "setup"]

# third party imports
import matplotlib.pyplot as plt

from yapss import (
    ContinuousArg,
    ContinuousHessianArg,
    ContinuousJacobianArg,
    ObjectiveArg,
    ObjectiveGradientArg,
    ObjectiveHessianArg,
    Problem,
    Solution,
)

# package imports
from yapss.math import exp


def setup() -> Problem:
    """Set up the Goddard Rocket Problem as an optimal control problem.

    Returns
    -------
    Problem
        The Goddard Rocket Problem as an optimal control problem.
    """
    h0, v0, m0 = 0, 0, 3
    mf = 1
    hmin, hmax = 0, 30000
    vmin, vmax = 0, 15000
    t0 = 0
    tfMin, tfMax = 20, 100

    ocp = Problem(name="One phase Goddard Rocket Problem", nx=[3], nu=[1])

    def objective(arg: ObjectiveArg) -> None:
        """Goddard Rocket Problem objective function."""
        arg.objective = arg.phase[0].final_state[0]

    def continuous(arg: ContinuousArg) -> None:
        """Goddard Rocket Problem dynamics."""
        auxdata = arg.auxdata
        h, v, m = arg.phase[0].state
        (T,) = arg.phase[0].control
        dynamics = arg.phase[0].dynamics
        v_dot = (T - auxdata.sigma * v**2 * exp(-h / auxdata.h0)) / m - auxdata.g
        m_dot = -T / auxdata.c
        dynamics[0] = v
        dynamics[1] = v_dot
        dynamics[2] = m_dot

    # Optional first derivative functions

    def objective_gradient(arg: ObjectiveGradientArg) -> None:
        """Objective gradient for the Goddard Rocket Problem."""
        arg.gradient[(0, "xf", 0)] = 1

    def continuous_jacobian(arg: ContinuousJacobianArg) -> None:
        """Jacobian of the dynamics for the Goddard Rocket Problem."""
        auxdata = arg.auxdata
        sigma = auxdata.sigma
        h0 = auxdata.h0
        c = auxdata.c

        h, v, m = arg.phase[0].state
        (T,) = arg.phase[0].control
        D_div_v2 = sigma * exp(-h / h0)
        D_div_v = D_div_v2 * v
        D = D_div_v * v

        jacobian = arg.phase[0].jacobian
        jacobian[("f", 0), ("x", 1)] = 1 + 0 * v
        jacobian[("f", 1), ("x", 0)] = D / (h0 * m)
        jacobian[("f", 1), ("x", 1)] = -2 * D_div_v / m
        jacobian[("f", 1), ("x", 2)] = -(T - D) / m**2
        jacobian[("f", 1), ("u", 0)] = 1 / m
        jacobian[("f", 2), ("u", 0)] = -1 / c + 0 * v

    # Optional second derivative functions

    def objective_hessian(_: ObjectiveHessianArg) -> None:
        """Hessian of the objective function for the Goddard Rocket Problem."""
        return

    def continuous_hessian(arg: ContinuousHessianArg) -> None:
        """Hessian of the dynamics for the Goddard Rocket Problem."""
        auxdata = arg.auxdata
        sigma = auxdata.sigma
        h0 = auxdata.h0

        for p in arg.phase_list:
            h, v, m = arg.phase[p].state
            (T,) = arg.phase[p].control
            D_div_v2 = sigma * exp(-h / h0)
            D_div_v = D_div_v2 * v
            D = D_div_v * v

            hessian = arg.phase[0].hessian
            hessian[("f", 1), ("x", 0), ("x", 0)] = -D / (h0**2 * m)
            hessian[("f", 1), ("x", 0), ("x", 1)] = 2 * D_div_v / (h0 * m)
            hessian[("f", 1), ("x", 0), ("x", 2)] = -D / (h0 * m**2)
            hessian[("f", 1), ("x", 1), ("x", 1)] = -2 * D_div_v2 / m
            hessian[("f", 1), ("x", 1), ("x", 2)] = 2 * D_div_v / m**2
            hessian[("f", 1), ("x", 2), ("x", 2)] = 2 * (T - D) / m**3
            hessian[("f", 1), ("x", 2), ("u", 0)] = -1 / m**2

    # physical constants
    auxdata = ocp.auxdata
    auxdata.Tm = Tm = 193.044
    auxdata.g = 32.174
    auxdata.sigma = 5.49153484923381010e-05
    auxdata.c = 1580.9425279876559
    auxdata.h0 = 23800

    functions = ocp.functions
    functions.objective = objective
    functions.objective_gradient = objective_gradient
    functions.objective_hessian = objective_hessian
    functions.continuous = continuous
    functions.continuous_jacobian = continuous_jacobian
    functions.continuous_hessian = continuous_hessian

    # bounds
    bounds = ocp.bounds.phase[0]
    bounds.initial_time.lower = bounds.initial_time.upper = t0
    bounds.final_time.lower = tfMin
    bounds.final_time.upper = tfMax
    bounds.initial_state.lower[:] = bounds.initial_state.upper[:] = [h0, v0, m0]
    bounds.state.lower[:] = [hmin, vmin, mf]
    bounds.state.upper[:] = [hmax, vmax, m0]
    bounds.final_state.lower[:] = [hmin, vmin, mf]
    bounds.final_state.upper[:] = [hmax, vmax, mf]
    bounds.control.lower[:] = (0,)
    bounds.control.upper[:] = (Tm,)

    # guess
    phase = ocp.guess.phase[0]
    phase.time = (t0, tfMax)
    phase.state = ((h0, h0), (v0, v0), (m0, mf))
    phase.control = ((0, Tm),)

    # solver settings
    ocp.derivatives.order = "second"
    ocp.derivatives.method = "auto"

    # ipopt options
    ocp.ipopt_options.tol = 1e-20
    ocp.ipopt_options.print_level = 3

    ocp.scale.objective = -1
    # TODO: Fails if all scales are integers
    ocp.scale.phase[0].state = ocp.scale.phase[0].dynamics = 18_000, 800, 3
    ocp.scale.phase[0].time = 30

    return ocp


def plot_solution(solution: Solution) -> None:
    """Plot solution to the Goddard Rocket Problem.

    Parameters
    ----------
    solution : Solution
        The solution to the Goddard Rocket Problem.
    """
    # extract information from solution
    time = solution.phase[0].time
    time_c = solution.phase[0].time_c
    h, v, m = solution.phase[0].state
    (T,) = solution.phase[0].control
    hamiltonian = solution.phase[0].hamiltonian

    t0 = solution.phase[0].initial_time
    tf = solution.phase[0].final_time

    # thrust
    plt.figure(1)
    plt.plot(time_c, T)
    plt.ylabel("Thrust, $T$ (lbf)")

    # altitude
    plt.figure(2)
    plt.plot(time, h)
    plt.ylabel("Altitude, $h$ (ft)")

    # velocity
    plt.figure(3)
    plt.plot(time, v)
    plt.ylabel("Velocity, $v$ (ft/s)")

    # mass
    plt.figure(4)
    plt.plot(time, m)
    plt.ylabel("Mass, $m$ (slugs)")

    # hamiltonian
    plt.figure(5)
    plt.plot(time_c, hamiltonian)
    plt.ylabel(r"Hamiltonian, $\mathcal{H}")

    for i in range(1, 6):
        plt.figure(i)
        plt.xlabel("Time, $t$ (sec)")
        plt.xlim([t0, tf])
        plt.tight_layout()
        plt.grid()


def main() -> None:
    """Demonstrate the solution to the Goddard Rocket Problem (1 Phase)."""
    problem = setup()
    solution = problem.solve()
    plot_solution(solution)
    plt.show()


if __name__ == "__main__":
    main()

Text Output


******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit https://github.com/coin-or/Ipopt
******************************************************************************

Total number of variables............................:      391
                     variables with only lower bounds:        0
                variables with lower and upper bounds:      361
                     variables with only upper bounds:        0
Total number of equality constraints.................:      300
Total number of inequality constraints...............:        1
        inequality constraints with only lower bounds:        1
   inequality constraints with lower and upper bounds:        0
        inequality constraints with only upper bounds:        0


Number of Iterations....: 71

                                   (scaled)                 (unscaled)
Objective...............:  -1.8563367158669407e+04    1.8563367158669407e+04
Dual infeasibility......:   5.4569682106375694e-11    1.8189894035458565e-11
Constraint violation....:   9.4890613884975513e-14    1.7080310499295592e-09
Variable bound violation:   1.9304399927477789e-06    1.9304399927477789e-06
Complementarity.........:   5.0047179600681232e-21    5.0047179600681232e-21
Overall NLP error.......:   3.0230044705216128e-11    1.7080310499295592e-09


Number of objective function evaluations             = 77
Number of objective gradient evaluations             = 72
Number of equality constraint evaluations            = 77
Number of inequality constraint evaluations          = 77
Number of equality constraint Jacobian evaluations   = 72
Number of inequality constraint Jacobian evaluations = 72
Number of Lagrangian Hessian evaluations             = 71
Total seconds in IPOPT (w/o function evaluations)    =      0.665
Total seconds in NLP function evaluations            =      0.161

EXIT: Solved To Acceptable Level.

Plots

Thrust

../_images/goddard_problem_1_phase_plot_1.png

Altitude

../_images/goddard_problem_1_phase_plot_2.png

Velocity

../_images/goddard_problem_1_phase_plot_3.png

Mass

../_images/goddard_problem_1_phase_plot_4.png

Hamiltonian

The fact that the Hamiltonian is not constant even though the dynamics and cost integrand are time-invariant is a good indication that there is a singular arc. Indeed, this problem does have a singular arc in the middle of the trajectory.

../_images/goddard_problem_1_phase_plot_5.png