Newton’s Minimal Resistance Problem

For a description of Newton’s minimal resistance problem, see the JupyterLab notebook documentation for this problem.

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.newton

Functions

YAPSS solution of Newton’s minimal resistance problem.

main() None[source]

Demonstrate the solution to Newton’s minimal resistance problem.

plot_solution(solution: Solution) None[source]

Plot the solution to Newton’s minimal resistance problem.

Parameters:
solutionSolution

The solution to the Newton’s minimal resistance problem.

setup(y_max: float = 1.0) Problem[source]

Set up Newton’s minimal resistance problem as an optimal control problem.

Parameters:
y_maxfloat, optional

Maximum height of the nosecone, by default 1.0.

Returns:
Problem

Newton’s minimal resistance problem as an optimal control problem.

setup2(y_max: float = 1.0) Problem[source]

Set up alternate formulation of Newton’s minimal resistance problem.

In this alternate formulation, the radius of the flat portion of the nosecone is a parameter to be optimized, and the curve to be optimized starts at this radius. This formulation improves the solution considerably.

Parameters:
y_maxfloat, optional

Maximum height of the nosecone, by default 1.0.

Returns:
Problem

Newton’s minimal resistance problem as an optimal control problem.

Code

"""

YAPSS solution of Newton's minimal resistance problem.

"""

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

# third party imports
import matplotlib.pyplot as plt

# package imports
import numpy as np

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


def objective(arg: ObjectiveArg) -> None:
    """Objective function for Newton's minimal resistance problem."""
    arg.objective = arg.phase[0].integral[0]


def continuous(arg: ContinuousArg) -> None:
    """Newton's minimal resistance problem dynamics and cost integrand."""
    _, yp = arg.phase[0].state
    (u,) = arg.phase[0].control
    r = arg.phase[0].time
    arg.phase[0].dynamics[:] = yp, u
    arg.phase[0].integrand[:] = (8 * r / (1 + yp**2),)


# Gradient, Jacobian, and Hessian functions are not required except when
# derivatives.method = "user" option is selected.


def objective_gradient(arg: ObjectiveGradientArg) -> None:
    """Calculate gradient of the objective function for Newton's minimal resistance problem.

    Needed only if the ``derivatives.method = "user"`` option is selected.
    """
    arg.gradient[0, "q", 0] = 1


def objective_hessian(_: ObjectiveHessianArg) -> None:
    """Calculate Hessian of the objective function for Newton's minimal resistance problem.

    Needed only if the ``derivatives.method = "user"`` option is selected.
    """


def continuous_jacobian(arg: ContinuousJacobianArg) -> None:
    """Calculate Jacobian of the dynamics and cost integrand for minimal resistance problem.

    Needed only if the ``derivatives.method = "user"`` option is selected.
    """
    _, yp = arg.phase[0].state
    r = arg.phase[0].time
    jacobian = arg.phase[0].jacobian
    jacobian[("f", 0), ("x", 1)] = 1
    jacobian[("f", 1), ("u", 0)] = 1
    jacobian[("g", 0), ("t", 0)] = 8 / (1 + yp**2)
    jacobian[("g", 0), ("x", 1)] = -16 * yp * r / (1 + yp**2) ** 2


def continuous_hessian(arg: ContinuousHessianArg) -> None:
    """Calculate Hessian of the dynamics and cost integrand for Newton's minimal resistance problem.

    Needed only if the ``derivatives.method = "user"`` option is selected.
    """
    _, yp = arg.phase[0].state
    r = arg.phase[0].time
    hessian = arg.phase[0].hessian
    hessian[("g", 0), ("x", 1), ("x", 1)] = 16 * r * (3 * yp**2 - 1) / (1 + yp**2) ** 3
    hessian[("g", 0), ("x", 1), ("t", 0)] = -16 * yp / (1 + yp**2) ** 2


# end derivative functions


def setup(y_max: float = 1.0) -> Problem:
    """Set up Newton's minimal resistance problem as an optimal control problem.

    Parameters
    ----------
    y_max : float, optional
        Maximum height of the nosecone, by default 1.0.

    Returns
    -------
    Problem
        Newton's minimal resistance problem as an optimal control problem.
    """
    ocp = Problem(
        name="Newton's minimal resistance problem",
        nx=[2],
        nu=[1],
        nq=[1],
    )

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

    # bounds
    bounds = ocp.bounds.phase[0]
    bounds.initial_time.lower = bounds.initial_time.upper = 0.0
    bounds.final_time.lower = bounds.final_time.upper = 1.0
    bounds.state.lower[0] = 0
    bounds.state.upper = y_max, 0
    bounds.control.upper = (0,)

    # guess
    phase = ocp.guess.phase[0]
    phase.time = [0.0, 1.0]
    phase.state = [[y_max, 0.0], [-y_max, -y_max]]
    phase.control = [[0.0, 0.0]]

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

    # we use the "user" option to demonstrate how to provide derivatives
    ocp.derivatives.method = "user"

    # ipopt options
    ocp.ipopt_options.print_level = 3

    return ocp


def objective2(arg: ObjectiveArg) -> None:
    """Improved objective function for Newton's minimal resistance problem."""
    arg.objective = arg.phase[0].integral[0] + 4 * arg.phase[0].initial_time ** 2


def objective_gradient2(arg: ObjectiveGradientArg) -> None:
    """Calculate gradient of improved objective function for Newton's minimal resistance problem.

    Needed only if the derivatives.method = "user" option is selected.
    """
    arg.gradient[0, "q", 0] = 1
    arg.gradient[0, "t0", 0] = 8 * arg.phase[0].initial_time


def objective_hessian2(arg: ObjectiveHessianArg) -> None:
    """Calculate Hessian of improved objective function for Newton's minimal resistance problem.

    Needed only if the derivatives.method = "user" option is selected.
    """
    arg.hessian[(0, "t0", 0), (0, "t0", 0)] = 8


def setup2(y_max: float = 1.0) -> Problem:
    """Set up alternate formulation of Newton's minimal resistance problem.

    In this alternate formulation, the radius of the flat portion of the nosecone is a
    parameter to be optimized, and the curve to be optimized starts at this radius. This
    formulation improves the solution considerably.

    Parameters
    ----------
    y_max : float, optional
        Maximum height of the nosecone, by default 1.0.

    Returns
    -------
    Problem
        Newton's minimal resistance problem as an optimal control problem.
    """
    ocp = setup(y_max)
    ocp.functions.objective = objective2
    ocp.functions.objective_gradient = objective_gradient2
    ocp.functions.objective_hessian = objective_hessian2
    ocp.bounds.phase[0].initial_time.upper = 0.7
    return ocp


def plot_solution(solution: Solution) -> None:
    """Plot the solution to Newton's minimal resistance problem.

    Parameters
    ----------
    solution : Solution
        The solution to the Newton's minimal resistance problem.
    """
    # plot style information
    linewidth = 2
    plt.rc("font", size=14)
    plt.rc("font", family="sans-serif")

    # extract information from solution
    r = solution.phase[0].time
    y, _ = solution.phase[0].state
    r = np.concatenate((-r[-1::-1], r))
    y = np.concatenate((y[-1::-1], y))

    # plot
    plt.plot(r, y, "r", linewidth=linewidth)
    plt.axis("equal")
    plt.xlim([-1, 1])
    plt.ylim([-0.1, 2.1])
    plt.xlabel("Radius, $r/R$")
    plt.ylabel("Height, $y/R$")
    plt.tight_layout()


def _truncate_float(number: float, decimals: int = 5) -> float:
    """Truncate a float to a specified number of decimal places."""
    factor = 10**decimals
    return float(int(number * factor) / factor)


def main() -> None:
    """Demonstrate the solution to Newton's minimal resistance problem."""
    # solve problem using the first formulation
    problem = setup(y_max=1)
    solution1a = problem.solve()
    plt.figure(1, figsize=(6.4, 4))
    plot_solution(solution1a)
    plt.axis((-1.2, 1.2, -0.1, 1.1))
    plt.axis("equal")
    plt.title("Solution with $y_{max} = 1$, first formulation")
    plt.xticks(np.linspace(-1, 1, 5))
    plt.tight_layout()

    # solve problem using the second formulation
    problem = setup2(y_max=1)
    solution1b = problem.solve()
    plt.figure(2, figsize=(6.4, 4))
    plot_solution(solution1b)
    plt.axis((-1.2, 1.2, -0.1, 1.1))
    plt.axis("equal")
    plt.title("Solution with $y_{max} = 1$, second formulation")
    plt.xticks(np.linspace(-1, 1, 5))
    plt.tight_layout()

    # solve problem for different values of y_max
    plt.figure(3)
    objective_list: list[float] = []
    y_max_list = [0.5, 1, 2]
    for y_max in y_max_list:
        problem = setup2(y_max=y_max)
        solution = problem.solve()
        objective_list.append(solution.objective)
        plot_solution(solution)
    plt.title("Solution for different values of $y_{max}$")
    plt.tight_layout()

    # print objective values for different values of y_max, in a table
    print("\nObjective values for different values of y_max:\n")
    print("y_max | Objective (C_D)")
    print("------+----------------")
    for y_max, objective_value in zip(y_max_list, objective_list):
        print(f"{y_max:5.2f} |  {_truncate_float(objective_value):8.5f}...")

    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............................:      294
                     variables with only lower bounds:        0
                variables with lower and upper bounds:       91
                     variables with only upper bounds:      182
Total number of equality constraints.................:      201
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....: 59

                                   (scaled)                 (unscaled)
Objective...............:   1.5033522844316036e+00    1.5033522844316036e+00
Dual infeasibility......:   3.0935965659845183e-09    3.0935965659845183e-09
Constraint violation....:   2.2644555119910592e-09    2.2644555119910592e-09
Variable bound violation:   9.0877317203984134e-09    9.0877317203984134e-09
Complementarity.........:   5.2160175348069363e-09    5.2160175348069363e-09
Overall NLP error.......:   5.2160175348069363e-09    5.2160175348069363e-09


Number of objective function evaluations             = 60
Number of objective gradient evaluations             = 60
Number of equality constraint evaluations            = 60
Number of inequality constraint evaluations          = 60
Number of equality constraint Jacobian evaluations   = 60
Number of inequality constraint Jacobian evaluations = 60
Number of Lagrangian Hessian evaluations             = 59
Total seconds in IPOPT (w/o function evaluations)    =      0.298
Total seconds in NLP function evaluations            =      0.042

EXIT: Optimal Solution Found.
Total number of variables............................:      295
                     variables with only lower bounds:        0
                variables with lower and upper bounds:       92
                     variables with only upper bounds:      182
Total number of equality constraints.................:      201
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....: 16

                                   (scaled)                 (unscaled)
Objective...............:   1.4992639218536086e+00    1.4992639218536086e+00
Dual infeasibility......:   3.4050137239924398e-09    3.4050137239924398e-09
Constraint violation....:   1.2504534074864182e-09    1.2504534074864182e-09
Variable bound violation:   9.9783786942670577e-09    9.9783786942670577e-09
Complementarity.........:   9.8090854271441426e-10    9.8090854271441426e-10
Overall NLP error.......:   3.4050137239924398e-09    3.4050137239924398e-09


Number of objective function evaluations             = 18
Number of objective gradient evaluations             = 17
Number of equality constraint evaluations            = 18
Number of inequality constraint evaluations          = 18
Number of equality constraint Jacobian evaluations   = 17
Number of inequality constraint Jacobian evaluations = 17
Number of Lagrangian Hessian evaluations             = 16
Total seconds in IPOPT (w/o function evaluations)    =      0.046
Total seconds in NLP function evaluations            =      0.010

EXIT: Optimal Solution Found.
Total number of variables............................:      295
                     variables with only lower bounds:        0
                variables with lower and upper bounds:       92
                     variables with only upper bounds:      182
Total number of equality constraints.................:      201
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....: 26

                                   (scaled)                 (unscaled)
Objective...............:   2.4300290388786467e+00    2.4300290388786467e+00
Dual infeasibility......:   3.1807670707070760e-09    3.1807670707070760e-09
Constraint violation....:   6.2818239499051742e-10    6.2818239499051742e-10
Variable bound violation:   9.9932551123060875e-09    9.9932551123060875e-09
Complementarity.........:   3.4046248623079831e-10    3.4046248623079831e-10
Overall NLP error.......:   3.1807670707070760e-09    3.1807670707070760e-09


Number of objective function evaluations             = 28
Number of objective gradient evaluations             = 27
Number of equality constraint evaluations            = 28
Number of inequality constraint evaluations          = 28
Number of equality constraint Jacobian evaluations   = 27
Number of inequality constraint Jacobian evaluations = 27
Number of Lagrangian Hessian evaluations             = 26
Total seconds in IPOPT (w/o function evaluations)    =      0.069
Total seconds in NLP function evaluations            =      0.017

EXIT: Optimal Solution Found.
Total number of variables............................:      295
                     variables with only lower bounds:        0
                variables with lower and upper bounds:       92
                     variables with only upper bounds:      182
Total number of equality constraints.................:      201
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....: 16

                                   (scaled)                 (unscaled)
Objective...............:   1.4992639218536086e+00    1.4992639218536086e+00
Dual infeasibility......:   3.4050137239924398e-09    3.4050137239924398e-09
Constraint violation....:   1.2504534074864182e-09    1.2504534074864182e-09
Variable bound violation:   9.9783786942670577e-09    9.9783786942670577e-09
Complementarity.........:   9.8090854271441426e-10    9.8090854271441426e-10
Overall NLP error.......:   3.4050137239924398e-09    3.4050137239924398e-09


Number of objective function evaluations             = 18
Number of objective gradient evaluations             = 17
Number of equality constraint evaluations            = 18
Number of inequality constraint evaluations          = 18
Number of equality constraint Jacobian evaluations   = 17
Number of inequality constraint Jacobian evaluations = 17
Number of Lagrangian Hessian evaluations             = 16
Total seconds in IPOPT (w/o function evaluations)    =      0.043
Total seconds in NLP function evaluations            =      0.010

EXIT: Optimal Solution Found.
Total number of variables............................:      295
                     variables with only lower bounds:        0
                variables with lower and upper bounds:       92
                     variables with only upper bounds:      182
Total number of equality constraints.................:      201
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....: 16

                                   (scaled)                 (unscaled)
Objective...............:   6.4170199377206250e-01    6.4170199377206250e-01
Dual infeasibility......:   5.2812526958960995e-09    5.2812526958960995e-09
Constraint violation....:   1.9560146835573278e-09    1.9560146835573278e-09
Variable bound violation:   1.9861985833813378e-08    1.9861985833813378e-08
Complementarity.........:   2.5091521318952312e-09    2.5091521318952312e-09
Overall NLP error.......:   5.2812526958960995e-09    5.2812526958960995e-09


Number of objective function evaluations             = 17
Number of objective gradient evaluations             = 17
Number of equality constraint evaluations            = 17
Number of inequality constraint evaluations          = 17
Number of equality constraint Jacobian evaluations   = 17
Number of inequality constraint Jacobian evaluations = 17
Number of Lagrangian Hessian evaluations             = 16
Total seconds in IPOPT (w/o function evaluations)    =      0.043
Total seconds in NLP function evaluations            =      0.010

EXIT: Optimal Solution Found.

Objective values for different values of y_max:

y_max | Objective (C_D)
------+----------------
 0.50 |   2.43002...
 1.00 |   1.49926...
 2.00 |   0.64170...

Plots

Nosecone Shape for First Formulation

../_images/newton_plot_1.png

Nosecone Shape for Second Formulation

../_images/newton_plot_2.png

Nosecone Shape for Various Heights

../_images/newton_plot_3.png