.. DO NOT EDIT.
.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.
.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE:
.. "nesc_test_cases/nesc_case11.py"
.. LINE NUMBERS ARE GIVEN BELOW.

.. only:: html

    .. note::
        :class: sphx-glr-download-link-note

        :ref:`Go to the end <sphx_glr_download_nesc_test_cases_nesc_case11.py>`
        to download the full example code.

.. rst-class:: sphx-glr-example-title

.. _sphx_glr_nesc_test_cases_nesc_case11.py:


===================================================
Case 11: Subsonic F-16 trimmed flight across earth
===================================================

==============  ===============
Verifies        Atmosphere, air-data calculations
Gravitation     J2
Geodesy         WGS-84 rotating
Atmosphere      US 1976 STD
Winds           still air
Vehicle         F-16 (unaugmented)
Notes           Initial position is 10,000 ft above KFFA airport on a 45 degree true
                course. 335.15 KTAS. Stability augmentation off. Test of trim solution.
==============  ===============

In this example, we first find the trim conditions for the F16 in sub-sonic flight, then
perform a steady level sight. To find trim, we use scipy's optimize module to minimize a
trim residual.

.. GENERATED FROM PYTHON SOURCE LINES 21-31

.. code-block:: Python


    from simupy.block_diagram import BlockDiagram
    from simupy import systems
    import simupy_flight
    import numpy as np
    from scipy import optimize

    from nesc_testcase_helper import plot_nesc_comparisons, int_opts, benchmark
    from nesc_testcase_helper import ft_per_m, kg_per_slug








.. GENERATED FROM PYTHON SOURCE LINES 32-34

The F16_model.F16 class composes the aerodynamic, propulsion, and inertia  models
into a simupy_flight Vehicle

.. GENERATED FROM PYTHON SOURCE LINES 34-40

.. code-block:: Python


    import F16_model
    from F16_control import F16_control

    F16_vehicle = F16_model.F16()








.. GENERATED FROM PYTHON SOURCE LINES 41-42

Create a dictionary of keyword arguments for the initial condition

.. GENERATED FROM PYTHON SOURCE LINES 42-60

.. code-block:: Python


    spec_ic_args = dict(
        phi_D=36.01916667 * np.pi / 180,  # latitude
        lamda_D=-75.67444444 * np.pi / 180,  # longitude
        h=10_013 / ft_per_m,
        V_N=400.0 / ft_per_m,
        V_E=400.0 / ft_per_m,
        V_D=0.0 / ft_per_m,
        psi=45.0 * np.pi / 180,
        theta=2.653814 * np.pi / 180,
        phi=0.0 * np.pi / 180,
        p_B=0.0 * np.pi / 180,
        q_B=0.0 * np.pi / 180,
        r_B=0.0 * np.pi / 180,
    )

    knots_per_mps = 1.94384








.. GENERATED FROM PYTHON SOURCE LINES 61-62

Configure the earth model

.. GENERATED FROM PYTHON SOURCE LINES 62-74

.. code-block:: Python


    earth = simupy_flight.Planet(
        gravity=simupy_flight.earth_J2_gravity,
        winds=simupy_flight.get_constant_winds(),
        atmosphere=simupy_flight.atmosphere_1976,
        planetodetics=simupy_flight.Planetodetic(
            a=simupy_flight.earth_equitorial_radius,
            omega_p=simupy_flight.earth_rotation_rate,
            f=simupy_flight.earth_f,
        ),
    )








.. GENERATED FROM PYTHON SOURCE LINES 75-76

Air density at the surface is used for the equivalent air-speed calculation

.. GENERATED FROM PYTHON SOURCE LINES 76-79

.. code-block:: Python


    rho_0 = earth.atmosphere(0, 0, 0, 0)[0]








.. GENERATED FROM PYTHON SOURCE LINES 80-81

Select the feedback channels used for the controller

.. GENERATED FROM PYTHON SOURCE LINES 81-100

.. code-block:: Python


    controller_feedback_indices = np.array(
        [
            earth.h_D_idx,
            earth.V_T_idx,
            earth.alpha_idx,
            earth.beta_idx,
            earth.psi_idx,
            earth.theta_idx,
            earth.phi_idx,
            earth.p_B_idx,
            earth.q_B_idx,
            earth.r_B_idx,
            earth.rho_idx,
        ]
    )

    dim_feedback = len(controller_feedback_indices)








.. GENERATED FROM PYTHON SOURCE LINES 101-104

This is a function generator for the controller (auto-pilot). The generated function
uses the DaveML implementation of the controller, which has several configuration
options set by the generator.

.. GENERATED FROM PYTHON SOURCE LINES 104-188

.. code-block:: Python



    class F16ControllerBlock:
        num_events = 1
        dim_state = 0
        dim_output = 4
        dim_input = 15

        initial_condition = np.array([])

        def __init__(self, throttleTrim, longStkTrim, sasOn=False, apOn=False, event_t=0.):
            self.throttleTrim = throttleTrim
            self.longStkTrim = longStkTrim
            self.sasOn = sasOn
            self.apOn = apOn
            self.event_t = event_t

        def update_equation_function(self, t, u, event_channels):
            return self.__call__(t, u)

        def prepare_to_integrate(self, t, u):
            return self.__call__(t, u)

        def event_equation_function(self, t, u):
            return np.array([t - self.event_t])

        def output_equation_function(self, t, u):
            return self.__call__(t, u)

        def __call__(self, t, u):
            throttle, longStk, latStk, pedal = 0.0, 0.0, 0.0, 0.0  # pilot command

            (
                # feedback
                alt,
                V_T,
                alpha,
                beta,
                psi,
                theta,
                phi,
                pb,
                qb,
                rb,
                # rho to calculate equivalent airspeed
                rho,
                # commands
                keasCmd,
                altCmd,
                latOffset,
                baseChiCmd,
            ) = u

            Vequiv = V_T * np.sqrt(rho / rho_0)
            angles = np.array([alpha, beta, phi, theta, psi])
            alpha, beta, phi, theta, psi = angles * 180 / np.pi

            control_eart = F16_control(
                throttle,
                longStk,
                latStk,
                pedal,
                self.sasOn,
                self.apOn,
                keasCmd,
                altCmd,
                latOffset,
                baseChiCmd,
                alt * ft_per_m,
                Vequiv * knots_per_mps,
                alpha,
                beta,
                phi,
                theta,
                psi,
                pb,
                qb,
                rb,
                self.throttleTrim,
                self.longStkTrim,
            )
            return control_eart









.. GENERATED FROM PYTHON SOURCE LINES 189-193

This function computes the trim residual using the
``local_translational_trim_residual`` helper function, which calculates the
accelerations experienced by the vehicle for a given flight condition, longitudinal
stick position, and throttle value.

.. GENERATED FROM PYTHON SOURCE LINES 193-213

.. code-block:: Python



    def eval_trim(flight_condition, longStk, throttle):
        kin_out = earth.output_equation_function(0, flight_condition)
        controller_func = F16ControllerBlock(
            throttleTrim=throttle, longStkTrim=longStk
        )
        aero_plus_prop_acceleration = simupy_flight.dynamics.dynamics_output_function(
            F16_vehicle, 0, *kin_out, *controller_func(0, np.zeros(dim_feedback + 4))
        )

        gen_accel = aero_plus_prop_acceleration

        gen_accel[:3] = earth.local_translational_trim_residual(
            *flight_condition[:-3], *aero_plus_prop_acceleration[:-3]
        ).squeeze()

        return gen_accel









.. GENERATED FROM PYTHON SOURCE LINES 214-218

This function uses optimize.minimize() to minimize the trim redisual. It uses the
throttle, longitudinal stick position, pitch angle, and optionally the roll
angle, as free variables to achieve trim. The euler angles are passed through the
flight condition variable.

.. GENERATED FROM PYTHON SOURCE LINES 218-301

.. code-block:: Python



    def run_trimmer(flight_ic_args, throttle_ic=0.0, longStk_ic=0.0, allow_roll=False):
        len_vars = 3 + allow_roll

        psi, theta_ic, phi_ic = (
            flight_ic_args["psi"],
            flight_ic_args["theta"],
            flight_ic_args["phi"],
        )

        initial_guess = np.zeros(len_vars)
        initial_guess[0] = theta_ic
        initial_guess[1] = throttle_ic
        initial_guess[2] = longStk_ic

        extra_index = 3

        if allow_roll:
            initial_guess[extra_index] = phi_ic
            extra_index += 1

        def parse_x(x):
            theta, throttle, longStk = x[:3]
            extra_index = 3

            if allow_roll:
                phi = x[extra_index]
                extra_index += 1
            else:
                phi = phi_ic

            return theta, phi, longStk, throttle

        weighting_matrix = np.eye(6)

        aileron, rudder = 0.0, 0.0

        def trim_opt_func(x):
            eval_args = flight_ic_args.copy()
            theta, phi, longStk, throttle = parse_x(x)
            eval_args["theta"] = theta
            eval_args["phi"] = phi
            flight_condition = earth.ic_from_planetodetic(**eval_args)
            return np.linalg.norm(
                weighting_matrix @ eval_trim(flight_condition, longStk, throttle), ord=2
            )

        opt_res = optimize.minimize(
            trim_opt_func,
            initial_guess,
            tol=1e-12,
            options={
                "disp": True,
                "adaptive": True,
                "fatol": 1e-12,
                "maxiter": 20_000,
                "xatol": 1e-12,
            },
            method="Nelder-Mead",
        )

        opt_theta, opt_phi, opt_longStk, opt_throttle = opt_result = parse_x(opt_res.x)
        opt_args = flight_ic_args.copy()
        opt_args["theta"] = opt_theta
        opt_args["phi"] = opt_phi
        opt_flight_condition = earth.ic_from_planetodetic(**opt_args)
        print(
            "pitch: %.4e  roll: %.4e  longStk: %.4f  throttle: %.4f"
            % (
                opt_theta * 180 / np.pi,
                opt_phi * 180 / np.pi,
                opt_longStk * 100,
                opt_throttle * 100,
            )
        )
        print(
            "accelerations:\n",
            eval_trim(opt_flight_condition, opt_longStk, opt_throttle).reshape((2, 3)),
        )
        return opt_args, np.array([opt_throttle, opt_longStk])









.. GENERATED FROM PYTHON SOURCE LINES 302-303

Run the trimmer to determine the initial condition for the simulation

.. GENERATED FROM PYTHON SOURCE LINES 303-321

.. code-block:: Python


    opt_args, opt_ctrl = run_trimmer(
        spec_ic_args, throttle_ic=0.0, longStk_ic=0.0, allow_roll=False
    )

    trimmed_flight_condition = earth.ic_from_planetodetic(**opt_args)

    trimmed_KEAS = (
        earth.output_equation_function(0, trimmed_flight_condition)[earth.V_T_idx]
        * np.sqrt(
            earth.output_equation_function(0, trimmed_flight_condition)[earth.rho_idx]
            / rho_0
        )
        * knots_per_mps
    )

    earth.initial_condition = trimmed_flight_condition





.. rst-class:: sphx-glr-script-out

 .. code-block:: none

    Optimization terminated successfully.
             Current function value: 0.006500
             Iterations: 291
             Function evaluations: 554
    pitch: 2.6351e+00  roll: 0.0000e+00  longStk: 12.9236  throttle: 13.7561
    accelerations:
     [[-4.59609832e-03  4.59610301e-03  2.09229079e-09]
     [-2.98437663e-09 -1.70643291e-09  2.12618714e-09]]




.. GENERATED FROM PYTHON SOURCE LINES 322-324

Configure the controller using the trim settings. This requires additional blocks to
generate command signals at the trimmed value

.. GENERATED FROM PYTHON SOURCE LINES 324-336

.. code-block:: Python


    controller_block = F16ControllerBlock(*opt_ctrl)

    keasCmdOutput = np.array([trimmed_KEAS])
    keasCmdBlock = systems.SystemFromCallable(lambda *args: keasCmdOutput, 0, 1)

    altCmdOutput = np.array([spec_ic_args["h"] * ft_per_m])
    altCmdBlock = systems.SystemFromCallable(lambda *args: altCmdOutput, 0, 1)

    baseChiCmdOutput = np.array([spec_ic_args["psi"] * 180 / np.pi])
    baseChiCmdBlock = systems.SystemFromCallable(lambda *args: baseChiCmdOutput, 0, 1)








.. GENERATED FROM PYTHON SOURCE LINES 337-339

This block  computes the rate of lateral offset error, which is integrated and used
by the auto-pilot to compute the heading to drive the offset to zero.

.. GENERATED FROM PYTHON SOURCE LINES 339-360

.. code-block:: Python



    def latOffsetStateEquation(t, x, u):
        chi_cmd, V_N, V_E = u
        V_ground_magnitude = np.sqrt(V_N**2 + V_E**2)
        V_ground_heading = np.arctan2(V_E, V_N)
        return V_ground_magnitude * np.sin(V_ground_heading - chi_cmd * np.pi / 180)


    def latOffsetOutputEquation(t, x):
        return x * ft_per_m


    latOffsetBlock = systems.DynamicalSystem(
        state_equation_function=latOffsetStateEquation,
        output_equation_function=latOffsetOutputEquation,
        dim_state=1,
        dim_input=3,
        dim_output=1,
    )








.. GENERATED FROM PYTHON SOURCE LINES 361-364

Build a block diagram of the planet kinematics block, the F16 vehicle block, the base
controller block, and the various command generating blocks and connect them
appropriately.

.. GENERATED FROM PYTHON SOURCE LINES 364-393

.. code-block:: Python


    BD = BlockDiagram(
        earth,
        F16_vehicle,
        controller_block,
        keasCmdBlock,
        altCmdBlock,
        latOffsetBlock,
        baseChiCmdBlock,
    )
    BD.connect(earth, F16_vehicle, inputs=np.arange(earth.dim_output))
    BD.connect(F16_vehicle, earth, inputs=np.arange(F16_vehicle.dim_output))
    BD.connect(
        controller_block,
        F16_vehicle,
        inputs=np.arange(earth.dim_output, earth.dim_output + 4),
    )
    BD.connect(
        earth,
        controller_block,
        outputs=controller_feedback_indices,
        inputs=np.arange(dim_feedback),
    )

    BD.connect(keasCmdBlock, controller_block, inputs=[dim_feedback + 0])
    BD.connect(altCmdBlock, controller_block, inputs=[dim_feedback + 1])
    BD.connect(latOffsetBlock, controller_block, inputs=[dim_feedback + 2])
    BD.connect(baseChiCmdBlock, controller_block, inputs=[dim_feedback + 3])








.. GENERATED FROM PYTHON SOURCE LINES 394-395

Simulate and assess the results

.. GENERATED FROM PYTHON SOURCE LINES 395-403

.. code-block:: Python


    int_opts["nsteps"] = 5_000
    if __name__ == "__main__":

        with benchmark() as b:
            res = BD.simulate(180, integrator_options=int_opts)

        plot_nesc_comparisons(res, "11")



.. rst-class:: sphx-glr-horizontal


    *

      .. image-sg:: /nesc_test_cases/images/sphx_glr_nesc_case11_001.png
         :alt: nesc case11
         :srcset: /nesc_test_cases/images/sphx_glr_nesc_case11_001.png
         :class: sphx-glr-multi-img

    *

      .. image-sg:: /nesc_test_cases/images/sphx_glr_nesc_case11_002.png
         :alt: nesc case11
         :srcset: /nesc_test_cases/images/sphx_glr_nesc_case11_002.png
         :class: sphx-glr-multi-img

    *

      .. image-sg:: /nesc_test_cases/images/sphx_glr_nesc_case11_003.png
         :alt: nesc case11
         :srcset: /nesc_test_cases/images/sphx_glr_nesc_case11_003.png
         :class: sphx-glr-multi-img

    *

      .. image-sg:: /nesc_test_cases/images/sphx_glr_nesc_case11_004.png
         :alt: nesc case11
         :srcset: /nesc_test_cases/images/sphx_glr_nesc_case11_004.png
         :class: sphx-glr-multi-img

    *

      .. image-sg:: /nesc_test_cases/images/sphx_glr_nesc_case11_005.png
         :alt: nesc case11
         :srcset: /nesc_test_cases/images/sphx_glr_nesc_case11_005.png
         :class: sphx-glr-multi-img

    *

      .. image-sg:: /nesc_test_cases/images/sphx_glr_nesc_case11_006.png
         :alt: nesc case11
         :srcset: /nesc_test_cases/images/sphx_glr_nesc_case11_006.png
         :class: sphx-glr-multi-img

    *

      .. image-sg:: /nesc_test_cases/images/sphx_glr_nesc_case11_007.png
         :alt: nesc case11
         :srcset: /nesc_test_cases/images/sphx_glr_nesc_case11_007.png
         :class: sphx-glr-multi-img

    *

      .. image-sg:: /nesc_test_cases/images/sphx_glr_nesc_case11_008.png
         :alt: nesc case11
         :srcset: /nesc_test_cases/images/sphx_glr_nesc_case11_008.png
         :class: sphx-glr-multi-img

    *

      .. image-sg:: /nesc_test_cases/images/sphx_glr_nesc_case11_009.png
         :alt: nesc case11
         :srcset: /nesc_test_cases/images/sphx_glr_nesc_case11_009.png
         :class: sphx-glr-multi-img

    *

      .. image-sg:: /nesc_test_cases/images/sphx_glr_nesc_case11_010.png
         :alt: nesc case11
         :srcset: /nesc_test_cases/images/sphx_glr_nesc_case11_010.png
         :class: sphx-glr-multi-img


.. rst-class:: sphx-glr-script-out

 .. code-block:: none

    time to simulate: 49.374 s





.. rst-class:: sphx-glr-timing

   **Total running time of the script:** (0 minutes 54.860 seconds)


.. _sphx_glr_download_nesc_test_cases_nesc_case11.py:

.. only:: html

  .. container:: sphx-glr-footer sphx-glr-footer-example

    .. container:: sphx-glr-download sphx-glr-download-jupyter

      :download:`Download Jupyter notebook: nesc_case11.ipynb <nesc_case11.ipynb>`

    .. container:: sphx-glr-download sphx-glr-download-python

      :download:`Download Python source code: nesc_case11.py <nesc_case11.py>`

    .. container:: sphx-glr-download sphx-glr-download-zip

      :download:`Download zipped: nesc_case11.zip <nesc_case11.zip>`


.. only:: html

 .. rst-class:: sphx-glr-signature

    `Gallery generated by Sphinx-Gallery <https://sphinx-gallery.github.io>`_