Beispiel #1
0
    def compute_breguet(self, inputs, outputs):
        """
        Computes mission using simple Breguet formula at altitude==10000m

        Useful for initiating the computation.

        :param inputs: OpenMDAO input vector
        :param outputs: OpenMDAO output vector
        """
        propulsion_model = FuelEngineSet(
            self._engine_wrapper.get_model(inputs),
            inputs["data:geometry:propulsion:engine:count"])
        high_speed_polar = Polar(
            inputs["data:aerodynamics:aircraft:cruise:CL"],
            inputs["data:aerodynamics:aircraft:cruise:CD"],
        )

        breguet = Breguet(
            propulsion_model,
            max(
                10.0, high_speed_polar.optimal_cl /
                high_speed_polar.cd(high_speed_polar.optimal_cl)),
            inputs["data:TLAR:cruise_mach"],
            10000.0,
        )
        breguet.compute(
            inputs["data:weight:aircraft:MTOW"],
            inputs["data:TLAR:range"],
        )

        outputs["data:mission:sizing:ZFW"] = breguet.zfw
        outputs["data:mission:sizing:fuel"] = breguet.mission_fuel
Beispiel #2
0
def test_climb_fixed_altitude_at_constant_TAS(polar):
    propulsion = FuelEngineSet(DummyEngine(1.0e5, 1.0e-5), 2)

    # initialisation then change instance attributes
    segment = AltitudeChangeSegment(
        target={"altitude": 10000.0},
        propulsion=propulsion,
        reference_area=120.0,
        polar=polar,
    )  # not constant TAS order, as it is the default
    segment.thrust_rate = 1.0
    segment.time_step = 2.0
    flight_points = segment.compute_from({
        "altitude": 5000.0,
        "mass": 70000.0,
        "true_airspeed": 150.0
    })  # Test with dict

    last_point = flight_points.iloc[-1]
    # Note: reference values are obtained by running the process with 0.01s as time step
    assert_allclose(last_point.altitude, 10000.0)
    assert_allclose(last_point.true_airspeed, 150.0)
    assert_allclose(last_point.time, 143.5, rtol=1e-2)
    assert_allclose(last_point.mass, 69713.0, rtol=1e-4)
    assert_allclose(last_point.ground_distance, 20943.0, rtol=1e-3)
Beispiel #3
0
def test_ranged_flight(low_speed_polar, high_speed_polar, cleanup):

    engine = RubberEngine(5.0, 30.0, 1500.0, 1.0e5, 0.95, 10000.0)
    propulsion = FuelEngineSet(engine, 2)

    total_distance = 2.0e6
    flight_calculator = RangedRoute(
        StandardFlight(
            propulsion=propulsion,
            reference_area=120.0,
            low_speed_climb_polar=low_speed_polar,
            high_speed_polar=high_speed_polar,
            cruise_mach=0.78,
            thrust_rates={FlightPhase.CLIMB: 0.93, FlightPhase.DESCENT: 0.2},
        ),
        flight_distance=total_distance,
    )

    start = FlightPoint(
        true_airspeed=150.0 * knot, altitude=100.0 * foot, mass=70000.0, ground_distance=100000.0,
    )
    flight_points = flight_calculator.compute_from(start)

    plot_flight(flight_points, "test_ranged_flight.png")

    assert_allclose(
        flight_points.iloc[-1].ground_distance,
        total_distance + start.ground_distance,
        atol=flight_calculator.distance_accuracy,
    )
Beispiel #4
0
def test_optimal_cruise(polar):
    propulsion = FuelEngineSet(DummyEngine(0.5e5, 1.0e-5), 2)

    segment = OptimalCruiseSegment(
        target=FlightPoint(ground_distance=5.0e5),
        propulsion=propulsion,
        reference_area=120.0,
        polar=polar,
    )
    flight_points = segment.compute_from(
        FlightPoint(mass=70000.0, time=1000.0, ground_distance=1e5,
                    mach=0.78), )

    first_point = flight_points.iloc[0]
    last_point = FlightPoint(flight_points.iloc[-1])
    # Note: reference values are obtained by running the process with 0.05s as time step
    assert_allclose(first_point.altitude, 9156.0, atol=1.0)
    assert_allclose(first_point.true_airspeed, 236.4, atol=0.1)
    assert_allclose(first_point.CL, polar.optimal_cl)

    assert_allclose(last_point.ground_distance, 600000.0)
    assert_allclose(last_point.CL, polar.optimal_cl)
    assert_allclose(last_point.altitude, 9196.0, atol=1.0)
    assert_allclose(last_point.time, 3115.0, rtol=1e-2)
    assert_allclose(last_point.true_airspeed, 236.3, atol=0.1)
    assert_allclose(last_point.mass, 69577.0, rtol=1e-4)
Beispiel #5
0
    def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
        propulsion_model = FuelEngineSet(
            self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:engine:count"]
        )

        breguet = Breguet(
            propulsion_model,
            inputs["data:aerodynamics:aircraft:cruise:L_D_max"],
            inputs["data:TLAR:cruise_mach"],
            inputs["data:mission:sizing:main_route:cruise:altitude"],
            inputs["settings:mission:sizing:breguet:climb:mass_ratio"],
            inputs["settings:mission:sizing:breguet:descent:mass_ratio"],
            inputs["settings:mission:sizing:breguet:reserve:mass_ratio"],
            inputs["settings:mission:sizing:breguet:climb_descent_distance"],
        )
        breguet.compute(
            inputs["data:weight:aircraft:MTOW"], inputs["data:TLAR:range"],
        )

        outputs["data:propulsion:SFC"] = breguet.sfc
        outputs["data:propulsion:thrust_rate"] = breguet.thrust_rate
        outputs["data:propulsion:thrust"] = breguet.thrust
        outputs["data:mission:sizing:ZFW"] = breguet.zfw
        outputs["data:mission:sizing:fuel"] = breguet.mission_fuel
        outputs["data:mission:sizing:main_route:fuel"] = breguet.flight_fuel
        outputs["data:mission:sizing:main_route:climb:fuel"] = breguet.climb_fuel
        outputs["data:mission:sizing:main_route:cruise:fuel"] = breguet.cruise_fuel
        outputs["data:mission:sizing:main_route:descent:fuel"] = breguet.descent_fuel
        outputs["data:mission:sizing:main_route:climb:distance"] = breguet.climb_distance
        outputs["data:mission:sizing:main_route:cruise:distance"] = breguet.cruise_distance
        outputs["data:mission:sizing:main_route:descent:distance"] = breguet.descent_distance
        outputs["data:mission:sizing:fuel_reserve"] = breguet.reserve_fuel
Beispiel #6
0
def test_cruise_at_constant_altitude(polar):
    propulsion = FuelEngineSet(DummyEngine(0.5e5, 1.0e-5), 2)

    segment = CruiseSegment(
        target=FlightPoint(ground_distance=5.0e5),
        propulsion=propulsion,
        reference_area=120.0,
        polar=polar,
    )
    flight_points = segment.compute_from(
        FlightPoint(mass=70000.0, altitude=10000.0, mach=0.78))

    print_dataframe(flight_points)

    first_point = flight_points.iloc[0]
    last_point = FlightPoint(flight_points.iloc[-1])
    # Note: reference values are obtained by running the process with 0.05s as time step
    assert_allclose(first_point.altitude, 10000.0)
    assert_allclose(first_point.true_airspeed, 233.6, atol=0.1)

    assert_allclose(last_point.ground_distance, 500000.0)
    assert_allclose(last_point.altitude, 10000.0)
    assert_allclose(last_point.time, 2141.0, rtol=1e-2)
    assert_allclose(last_point.true_airspeed, 233.6, atol=0.1)
    assert_allclose(last_point.mass, 69568.0, rtol=1e-4)
Beispiel #7
0
def test_acceleration_not_enough_thrust(polar):
    propulsion = FuelEngineSet(DummyEngine(0.5e5, 1.0e-5), 2)

    segment = SpeedChangeSegment(
        target=FlightPoint(true_airspeed=250.0),
        propulsion=propulsion,
        reference_area=120.0,
        polar=polar,
        thrust_rate=0.1,
    )
    assert len(
        segment.compute_from(
            FlightPoint(altitude=5000.0, true_airspeed=150.0, mass=70000.0), ))
Beispiel #8
0
def test_climb_not_enough_thrust(polar):
    propulsion = FuelEngineSet(DummyEngine(1.0e5, 1.0e-5), 2)

    segment = AltitudeChangeSegment(
        target=FlightPoint(altitude=10000.0, true_airspeed="constant"),
        propulsion=propulsion,
        reference_area=120.0,
        polar=polar,
        thrust_rate=0.1,
    )
    assert (len(
        segment.compute_from(
            FlightPoint(altitude=5000.0, true_airspeed=150.0,
                        mass=70000.0), )) == 1)
Beispiel #9
0
def test_hold(polar):
    propulsion = FuelEngineSet(DummyEngine(0.5e5, 2.0e-5), 2)

    segment = HoldSegment(target=FlightPoint(time=3000.0),
                          propulsion=propulsion,
                          reference_area=120.0,
                          polar=polar)
    flight_points = segment.compute_from(
        FlightPoint(altitude=500.0, equivalent_airspeed=250.0, mass=60000.0), )

    last_point = flight_points.iloc[-1]
    assert_allclose(last_point.time, 3000.0)
    assert_allclose(last_point.altitude, 500.0)
    assert_allclose(last_point.equivalent_airspeed, 250.0, atol=0.1)
    assert_allclose(last_point.mass, 57776.0, rtol=1e-4)
    assert_allclose(last_point.ground_distance, 768323.0, rtol=1.0e-3)
Beispiel #10
0
    def compute_mission(self, inputs, outputs):
        """
        Computes mission using time-step integration.

        :param inputs: OpenMDAO input vector
        :param outputs: OpenMDAO output vector
        """
        propulsion_model = FuelEngineSet(
            self._engine_wrapper.get_model(inputs),
            inputs["data:geometry:propulsion:engine:count"])

        reference_area = inputs["data:geometry:wing:area"]

        self._mission.propulsion = propulsion_model
        self._mission.reference_area = reference_area

        end_of_takeoff = FlightPoint(
            time=0.0,
            # FIXME: legacy FAST was considering that aircraft was at MTOW before taxi out,
            #  though it supposed to be at MTOW at takeoff. We keep this logic for sake of
            #  non-regression, but it should be corrected later.
            mass=inputs["data:weight:aircraft:MTOW"] -
            inputs["data:mission:sizing:takeoff:fuel"] -
            inputs["data:mission:sizing:taxi_out:fuel"],
            true_airspeed=inputs["data:mission:sizing:takeoff:V2"],
            altitude=inputs["data:mission:sizing:takeoff:altitude"] +
            35 * foot,
            ground_distance=0.0,
        )

        self.flight_points = self._mission.compute(inputs, outputs,
                                                   end_of_takeoff)

        # Final ================================================================
        end_of_descent = FlightPoint.create(self.flight_points.loc[
            self.flight_points.name == "sizing:main_route:descent"].iloc[-1])
        end_of_taxi_in = FlightPoint.create(self.flight_points.iloc[-1])

        fuel_route = inputs["data:weight:aircraft:MTOW"] - end_of_descent.mass
        outputs[
            "data:mission:sizing:ZFW"] = end_of_taxi_in.mass - 0.03 * fuel_route
        outputs["data:mission:sizing:fuel"] = (
            inputs["data:weight:aircraft:MTOW"] -
            outputs["data:mission:sizing:ZFW"])

        if self.options["out_file"]:
            self.flight_points.to_csv(self.options["out_file"])
Beispiel #11
0
def test_standard_flight_fixed_altitude(low_speed_polar, high_speed_polar,
                                        cleanup):
    # Wild version of an additional flight phase.
    # Start altitude is high enough to skip initial climb and first segment of climb.
    # Cruise altitude is low so that first segment of descent will also be skipped.

    engine = RubberEngine(5.0, 30.0, 1500.0, 1.0e5, 0.95, 10000.0)
    propulsion = FuelEngineSet(engine, 2)

    flight_calculator = StandardFlight(
        propulsion=propulsion,
        reference_area=120.0,
        low_speed_climb_polar=low_speed_polar,
        high_speed_polar=high_speed_polar,
        cruise_mach=0.78,
        thrust_rates={
            FlightPhase.CLIMB: 0.93,
            FlightPhase.DESCENT: 0.2
        },
        cruise_distance=4.0e6,
        climb_target_altitude=20000.0 * foot,
        descent_target_altitude=1000.0 * foot,
        time_step=None,
    )

    flight_points = flight_calculator.compute_from(
        FlightPoint(equivalent_airspeed=260.0 * knot,
                    altitude=11000.0 * foot,
                    mass=60000.0), )
    plot_flight(flight_points, "test_standard_flight_fixed_altitude.png")

    assert not any(flight_points.name == FlightPhase.INITIAL_CLIMB.value)
    end_of_climb = FlightPoint(flight_points.loc[
        flight_points.name == FlightPhase.CLIMB.value].iloc[-1])
    end_of_cruise = FlightPoint(flight_points.loc[
        flight_points.name == FlightPhase.CRUISE.value].iloc[-1])
    end_of_descent = FlightPoint(flight_points.loc[
        flight_points.name == FlightPhase.DESCENT.value].iloc[-1])

    assert end_of_climb.mach < 0.78
    assert_allclose(end_of_climb.equivalent_airspeed, 300.0 * knot)
    assert_allclose(end_of_climb.altitude, 20000.0 * foot)
    assert_allclose(
        end_of_cruise.ground_distance - end_of_climb.ground_distance, 4.0e6)
    assert_allclose(end_of_descent.altitude, 1000.0 * foot)
    assert_allclose(end_of_descent.equivalent_airspeed, 250.0 * knot)
Beispiel #12
0
def test_standard_flight_optimal_altitude(low_speed_polar, high_speed_polar,
                                          cleanup):

    engine = RubberEngine(5.0, 30.0, 1500.0, 1.0e5, 0.95, 10000.0)
    propulsion = FuelEngineSet(engine, 2)

    flight_calculator = StandardFlight(
        propulsion=propulsion,
        reference_area=120.0,
        low_speed_climb_polar=low_speed_polar,
        high_speed_polar=high_speed_polar,
        cruise_mach=0.78,
        thrust_rates={
            FlightPhase.CLIMB: 0.93,
            FlightPhase.DESCENT: 0.2
        },
        cruise_distance=4.0e6,
        time_step=None,
    )

    flight_points = flight_calculator.compute_from(
        FlightPoint(true_airspeed=150.0 * knot,
                    altitude=100.0 * foot,
                    mass=70000.0), )
    print_dataframe(flight_points)
    plot_flight(flight_points, "test_standard_flight_max_finesse.png")

    end_of_initial_climb = FlightPoint(flight_points.loc[
        flight_points.name == FlightPhase.INITIAL_CLIMB.value].iloc[-1])
    end_of_climb = FlightPoint(flight_points.loc[
        flight_points.name == FlightPhase.CLIMB.value].iloc[-1])
    end_of_cruise = FlightPoint(flight_points.loc[
        flight_points.name == FlightPhase.CRUISE.value].iloc[-1])
    end_of_descent = FlightPoint(flight_points.loc[
        flight_points.name == FlightPhase.DESCENT.value].iloc[-1])

    assert_allclose(end_of_initial_climb.altitude, 1500.0 * foot)
    assert_allclose(end_of_initial_climb.equivalent_airspeed, 250.0 * knot)
    assert_allclose(end_of_climb.mach, 0.78)
    assert end_of_climb.equivalent_airspeed <= 300.0 * knot
    assert_allclose(
        end_of_cruise.ground_distance - end_of_climb.ground_distance, 4.0e6)
    assert_allclose(end_of_cruise.mach, 0.78)
    assert_allclose(end_of_descent.altitude, 1500.0 * foot)
    assert_allclose(end_of_descent.equivalent_airspeed, 250.0 * knot)
Beispiel #13
0
def test_taxi():
    propulsion = FuelEngineSet(DummyEngine(0.5e5, 1.0e-5), 2)

    segment = TaxiSegment(target=FlightPoint(time=500.0),
                          propulsion=propulsion,
                          thrust_rate=0.1)
    flight_points = segment.compute_from(
        FlightPoint(altitude=10.0,
                    true_airspeed=10.0,
                    mass=50000.0,
                    time=10000.0), )

    last_point = FlightPoint(flight_points.iloc[-1])
    assert_allclose(last_point.altitude, 10.0, atol=1.0)
    assert_allclose(last_point.time, 10500.0, rtol=1e-2)
    assert_allclose(last_point.true_airspeed, 10.0, atol=0.1)
    assert_allclose(last_point.mass, 49973.0, rtol=1e-4)
    assert_allclose(last_point.ground_distance, 5000.0)
Beispiel #14
0
def test_acceleration_to_EAS(polar):
    propulsion = FuelEngineSet(DummyEngine(0.5e5, 1.0e-5), 2)

    flight_points = SpeedChangeSegment(
        target=FlightPoint(equivalent_airspeed=250.0),
        propulsion=propulsion,
        reference_area=120.0,
        polar=polar,
        thrust_rate=1.0,
        time_step=0.2,
    ).compute_from(
        FlightPoint(altitude=1000.0, true_airspeed=150.0, mass=70000.0))

    last_point = flight_points.iloc[-1]
    # Note: reference values are obtained by running the process with 0.01s as time step
    assert_allclose(last_point.time, 128.2, rtol=1e-2)
    assert_allclose(last_point.altitude, 1000.0)
    assert_allclose(last_point.true_airspeed, 262.4, atol=1e-1)
    assert_allclose(last_point.mass, 69872.0, rtol=1e-4)
    assert_allclose(last_point.ground_distance, 26868.0, rtol=1e-3)
    def compute(self,
                inputs,
                outputs,
                discrete_inputs=None,
                discrete_outputs=None):

        mlw = inputs["data:weight:aircraft:MLW"]

        cl_max_landing = inputs["data:aerodynamics:aircraft:landing:CL_max"]
        wing_area = inputs["data:geometry:wing:area"]
        fa_length = inputs["data:geometry:wing:MAC:at25percent:x"]
        l0_wing = inputs["data:geometry:wing:MAC:length"]

        rho = Atmosphere(0.0).density

        v_s0 = math.sqrt(
            (mlw * 9.81) / (0.5 * rho * wing_area * cl_max_landing))
        v_ref = 1.3 * v_s0

        propulsion_model = FuelEngineSet(
            self._engine_wrapper.get_model(inputs),
            inputs["data:geometry:propulsion:count"])

        x_cg = float(fa_length)
        increment = l0_wing / 100.
        equilibrium_found = True
        climb_gradient_achieved = True

        while equilibrium_found and climb_gradient_achieved:
            climb_gradient, equilibrium_found = self.delta_climb_rate(
                x_cg, v_ref, mlw, propulsion_model, inputs)
            if climb_gradient < 0.033:
                climb_gradient_achieved = False
            x_cg -= increment

        outputs["data:handling_qualities:balked_landing_limit:x"] = x_cg

        x_cg_ratio = (x_cg - fa_length + 0.25 * l0_wing) / l0_wing

        outputs[
            "data:handling_qualities:balked_landing_limit:MAC_position"] = x_cg_ratio
Beispiel #16
0
def test_descent_to_fixed_altitude_at_constant_EAS(polar):
    propulsion = FuelEngineSet(DummyEngine(1.0e5, 1.0e-5), 2)

    flight_points = AltitudeChangeSegment(
        target=FlightPoint(altitude=5000.0, equivalent_airspeed="constant"),
        propulsion=propulsion,
        reference_area=100.0,
        polar=polar,
        thrust_rate=0.1,
        time_step=2.0,
    ).compute_from(
        FlightPoint(altitude=10000.0, equivalent_airspeed=200.0,
                    mass=70000.0), )

    last_point = flight_points.iloc[-1]
    # Note: reference values are obtained by running the process with 0.01s as time step
    assert_allclose(last_point.altitude, 5000.0)
    assert_allclose(last_point.equivalent_airspeed, 200.0)
    assert_allclose(last_point.time, 821.4, rtol=1e-2)
    assert_allclose(last_point.mass, 69910.0, rtol=1e-4)
    assert_allclose(last_point.ground_distance, 243155.0, rtol=1e-3)
Beispiel #17
0
def test_descent_to_fixed_EAS_at_constant_mach(polar):
    propulsion = FuelEngineSet(DummyEngine(1.0e5, 1.0e-5), 2)

    flight_points = AltitudeChangeSegment(
        target=FlightPoint(equivalent_airspeed=150.0, mach="constant"),
        propulsion=propulsion,
        reference_area=100.0,
        polar=polar,
        thrust_rate=0.1,
        # time_step=5.0, # we use default time step
    ).compute_from(FlightPoint(altitude=10000.0, mass=70000.0, mach=0.78), )

    last_point = flight_points.iloc[-1]
    # Note: reference values are obtained by running the process with 0.01s as time step
    assert_allclose(last_point.equivalent_airspeed, 150.0)
    assert_allclose(last_point.mach, 0.78)
    assert_allclose(last_point.time, 343.6, rtol=1e-2)
    assert_allclose(last_point.altitude, 8654.0, atol=1.0)
    assert_allclose(last_point.true_airspeed, 238.1, atol=0.1)
    assert_allclose(last_point.mass, 69962.0, rtol=1e-4)
    assert_allclose(last_point.ground_distance, 81042.0, rtol=1e-3)
Beispiel #18
0
def test_climb_optimal_altitude_at_fixed_TAS(polar):
    propulsion = FuelEngineSet(DummyEngine(1.0e5, 1.0e-5), 2)

    flight_points = AltitudeChangeSegment(
        target=FlightPoint(altitude=AltitudeChangeSegment.OPTIMAL_ALTITUDE,
                           true_airspeed="constant"),
        propulsion=propulsion,
        reference_area=120.0,
        polar=polar,
        thrust_rate=1.0,
        time_step=2.0,
    ).compute_from(
        FlightPoint(altitude=5000.0, true_airspeed=250.0, mass=70000.0), )

    last_point = flight_points.iloc[-1]
    # Note: reference values are obtained by running the process with 0.01s as time step
    assert_allclose(last_point.altitude, 10085.0, atol=0.1)
    assert_allclose(last_point.true_airspeed, 250.0)
    assert_allclose(last_point.time, 84.1, rtol=1e-2)
    assert_allclose(last_point.mach, 0.8359, rtol=1e-4)
    assert_allclose(last_point.mass, 69832.0, rtol=1e-4)
    assert_allclose(last_point.ground_distance, 20401.0, rtol=1e-3)
Beispiel #19
0
def test_climb_and_cruise_at_optimal_flight_level(polar):
    propulsion = FuelEngineSet(DummyEngine(0.5e5, 3.0e-5), 2)
    reference_area = 120.0

    segment = ClimbAndCruiseSegment(
        target=FlightPoint(
            ground_distance=10.0e6,
            altitude=AltitudeChangeSegment.OPTIMAL_FLIGHT_LEVEL),
        propulsion=propulsion,
        reference_area=reference_area,
        polar=polar,
        climb_segment=AltitudeChangeSegment(
            target=FlightPoint(),
            propulsion=propulsion,
            reference_area=reference_area,
            polar=polar,
            thrust_rate=0.9,
        ),
    )

    flight_points = segment.compute_from(
        FlightPoint(mass=70000.0,
                    altitude=8000.0,
                    mach=0.78,
                    ground_distance=1.0e6))

    first_point = flight_points.iloc[0]
    last_point = flight_points.iloc[-1]
    # Note: reference values are obtained by running the process with 1.0s as time step

    assert_allclose(first_point.altitude, 8000.0)
    assert_allclose(first_point.thrust_rate, 0.9)
    assert_allclose(first_point.true_airspeed, 240.3, atol=0.1)

    assert_allclose(last_point.altitude, 9753.6)
    assert_allclose(last_point.ground_distance, 11.0e6)
    assert_allclose(last_point.time, 42658.7, rtol=1e-2)
    assert_allclose(last_point.true_airspeed, 234.4, atol=0.1)
    assert_allclose(last_point.mass, 48874.0, rtol=1e-4)
Beispiel #20
0
def test_deceleration_not_enough_thrust(polar):
    propulsion = FuelEngineSet(DummyEngine(0.5e5, 1.0e-5), 2)

    segment = SpeedChangeSegment(
        target=FlightPoint(true_airspeed=150.0),
        propulsion=propulsion,
        reference_area=120.0,
        polar=polar,
        thrust_rate=0.1,
        time_step=1.0,
    )
    segment.time_step = 1.0
    flight_points = segment.compute_from(
        FlightPoint(altitude=5000.0, true_airspeed=250.0, mass=70000.0), )

    last_point = flight_points.iloc[-1]
    # Note: reference values are obtained by running the process with 0.01s as time step
    assert_allclose(last_point.time, 315.8, rtol=1e-2)
    assert_allclose(last_point.altitude, 5000.0)
    assert_allclose(last_point.true_airspeed, 150.0)
    assert_allclose(last_point.mass, 69982.0, rtol=1e-4)
    assert_allclose(last_point.ground_distance, 62804.0, rtol=1e-3)
Beispiel #21
0
def test_climb_optimal_flight_level_at_fixed_TAS(polar):
    propulsion = FuelEngineSet(DummyEngine(1.0e5, 1.0e-5), 2)

    flight_points = AltitudeChangeSegment(
        target=FlightPoint(altitude=AltitudeChangeSegment.OPTIMAL_FLIGHT_LEVEL,
                           true_airspeed="constant"),
        propulsion=propulsion,
        reference_area=120.0,
        polar=polar,
        thrust_rate=1.0,
        time_step=2.0,
    ).compute_from(
        FlightPoint(altitude=5000.0, true_airspeed=250.0, mass=70000.0), )
    print_dataframe(flight_points)

    last_point = flight_points.iloc[-1]
    # Note: reference values are obtained by running the process with 0.01s as time step
    assert_allclose(last_point.altitude / foot, 32000.0, atol=0.1)
    assert_allclose(last_point.true_airspeed, 250.0)
    assert_allclose(last_point.time, 78.7, rtol=1e-2)
    assert_allclose(last_point.mach, 0.8318, rtol=1e-4)
    assert_allclose(last_point.mass, 69843.0, rtol=1e-4)
    assert_allclose(last_point.ground_distance, 19091.0, rtol=1e-3)
Beispiel #22
0
    def compute_breguet(self, inputs, outputs):
        propulsion_model = FuelEngineSet(
            self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:engine:count"]
        )
        high_speed_polar = Polar(
            inputs["data:aerodynamics:aircraft:cruise:CL"],
            inputs["data:aerodynamics:aircraft:cruise:CD"],
        )

        breguet = Breguet(
            propulsion_model,
            max(
                10.0, high_speed_polar.optimal_cl / high_speed_polar.cd(high_speed_polar.optimal_cl)
            ),
            inputs["data:TLAR:cruise_mach"],
            10000.0,
        )
        breguet.compute(
            inputs["data:weight:aircraft:MTOW"], inputs["data:TLAR:range"],
        )

        outputs["data:mission:sizing:ZFW"] = breguet.zfw
        outputs["data:mission:sizing:fuel"] = breguet.mission_fuel
Beispiel #23
0
def test_climb_fixed_altitude_at_constant_EAS(polar):
    propulsion = FuelEngineSet(DummyEngine(1.0e5, 1.0e-5), 2)

    flight_points = AltitudeChangeSegment(
        target=FlightPoint(altitude=10000.0, equivalent_airspeed="constant"),
        propulsion=propulsion,
        reference_area=120.0,
        polar=polar,
        thrust_rate=1.0,
        time_step=2.0,
    ).compute_from(
        FlightPoint(altitude=5000.0, mass=70000.0, equivalent_airspeed=100.0))

    first_point = flight_points.iloc[0]
    last_point = flight_points.iloc[-1]
    # Note: reference values are obtained by running the process with 0.01s as time step
    assert_allclose(last_point.altitude, 10000.0)
    assert_allclose(last_point.equivalent_airspeed, 100.0)
    assert_allclose(last_point.time, 145.2, rtol=1e-2)
    assert_allclose(first_point.true_airspeed, 129.0, atol=0.1)
    assert_allclose(last_point.true_airspeed, 172.3, atol=0.1)
    assert_allclose(last_point.mass, 69710.0, rtol=1e-4)
    assert_allclose(last_point.ground_distance, 20915.0, rtol=1e-3)
Beispiel #24
0
def test_acceleration_to_TAS(polar):
    propulsion = FuelEngineSet(DummyEngine(0.5e5, 1.0e-5), 2)

    segment = SpeedChangeSegment(
        target={"true_airspeed": 250.0},
        propulsion=propulsion,
        reference_area=120.0,
        polar=polar,
        thrust_rate=1.0,
        time_step=0.2,
    )
    flight_points = segment.compute_from({
        "altitude": 5000.0,
        "true_airspeed": 150.0,
        "mass": 70000.0
    })  # Test with dict

    last_point = flight_points.iloc[-1]
    # Note: reference values are obtained by running the process with 0.01s as time step
    assert_allclose(last_point.time, 103.3, rtol=1e-2)
    assert_allclose(last_point.altitude, 5000.0)
    assert_allclose(last_point.true_airspeed, 250.0)
    assert_allclose(last_point.mass, 69896.0, rtol=1e-4)
    assert_allclose(last_point.ground_distance, 20697.0, rtol=1e-3)
    def compute(self,
                inputs,
                outputs,
                discrete_inputs=None,
                discrete_outputs=None):
        # Sizing constraints for the horizontal tail (methods from Torenbeek).
        # Limiting cases: Rotating power at takeoff/landing, with the most
        # forward CG position. Returns maximum area.

        propulsion_model = FuelEngineSet(
            self._engine_wrapper.get_model(inputs),
            inputs["data:geometry:propulsion:count"])
        n_engines = inputs["data:geometry:propulsion:count"]
        cg_range = inputs["settings:weight:aircraft:CG:range"]
        takeoff_t_rate = inputs["data:mission:sizing:takeoff:thrust_rate"]
        wing_area = inputs["data:geometry:wing:area"]
        x_wing_aero_center = inputs["data:geometry:wing:MAC:at25percent:x"]
        lp_ht = inputs[
            "data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"]
        wing_mac = inputs["data:geometry:wing:MAC:length"]
        mtow = inputs["data:weight:aircraft:MTOW"]
        mlw = inputs["data:weight:aircraft:MLW"]
        x_cg_aft = inputs["data:weight:aircraft:CG:aft:x"]
        z_cg_aircraft = inputs["data:weight:aircraft_empty:CG:z"]
        z_cg_engine = inputs["data:weight:propulsion:engine:CG:z"]
        x_lg = inputs["data:weight:airframe:landing_gear:main:CG:x"]
        cl0_clean = inputs["data:aerodynamics:wing:low_speed:CL0_clean"]
        cl_max_clean = inputs["data:aerodynamics:wing:low_speed:CL_max_clean"]
        cl_max_landing = inputs["data:aerodynamics:aircraft:landing:CL_max"]
        cl_max_takeoff = inputs["data:aerodynamics:aircraft:takeoff:CL_max"]
        cl_flaps_landing = inputs["data:aerodynamics:flaps:landing:CL"]
        cl_flaps_takeoff = inputs["data:aerodynamics:flaps:takeoff:CL"]
        tail_efficiency_factor = inputs[
            "data:aerodynamics:horizontal_tail:efficiency"]
        cl_htp_landing = inputs["landing:cl_htp"]
        cl_htp_takeoff = inputs["takeoff:cl_htp"]
        cm_landing = inputs[
            "data:aerodynamics:wing:low_speed:CM0_clean"] + inputs[
                "data:aerodynamics:flaps:landing:CM"]
        cm_takeoff = inputs[
            "data:aerodynamics:wing:low_speed:CM0_clean"] + inputs[
                "data:aerodynamics:flaps:takeoff:CM"]
        cl_alpha_htp_isolated = inputs["low_speed:cl_alpha_htp_isolated"]

        z_eng = z_cg_aircraft - z_cg_engine

        # Conditions for calculation
        atm = Atmosphere(0.0)
        rho = atm.density

        # CASE1: TAKE-OFF ##############################################################################################
        # method extracted from Torenbeek 1982 p325

        # Calculation of take-off minimum speed
        weight = mtow * g
        vs0 = math.sqrt(weight / (0.5 * rho * wing_area * cl_max_takeoff))
        vs1 = math.sqrt(weight / (0.5 * rho * wing_area * cl_max_clean))
        # Rotation speed requirement from FAR 23.51 (depends on number of engines)
        if n_engines == 1:
            v_r = vs1 * 1.0
        else:
            v_r = vs1 * 1.1
        # Definition of max forward gravity center position
        x_cg = x_cg_aft - cg_range * wing_mac
        # Definition of horizontal tail global position
        x_ht = x_wing_aero_center + lp_ht
        # Calculation of wheel factor
        flight_point = FlightPoint(mach=v_r / atm.speed_of_sound,
                                   altitude=0.0,
                                   engine_setting=EngineSetting.TAKEOFF,
                                   thrust_rate=takeoff_t_rate)
        propulsion_model.compute_flight_points(flight_point)
        thrust = float(flight_point.thrust)
        fact_wheel = (
            (x_lg - x_cg - z_eng * thrust / weight) / wing_mac * (vs0 / v_r)**2
        )  # FIXME: not clear if vs0 or vs1 should be used in formula
        # Compute aerodynamic coefficients for takeoff @ 0° aircraft angle
        cl0_takeoff = cl0_clean + cl_flaps_takeoff
        # Calculation of correction coefficient n_h and n_q
        n_h = (
            x_ht - x_lg
        ) / lp_ht * tail_efficiency_factor  # tail_efficiency_factor: dynamic pressure reduction at
        # tail (typical value)
        n_q = 1 + cl_alpha_htp_isolated / cl_htp_takeoff * _ANG_VEL * (
            x_ht - x_lg) / v_r
        # Calculation of volume coefficient based on Torenbeek formula
        coef_vol = (cl_max_takeoff / (n_h * n_q * cl_htp_takeoff) *
                    (cm_takeoff / cl_max_takeoff - fact_wheel) +
                    cl0_takeoff / cl_htp_takeoff *
                    (x_lg - x_wing_aero_center) / wing_mac)
        # Calculation of equivalent area
        area_1 = coef_vol * wing_area * wing_mac / lp_ht

        # CASE2: LANDING ###############################################################################################
        # method extracted from Torenbeek 1982 p325

        # Calculation of take-off minimum speed
        weight = mlw * g
        vs0 = math.sqrt(weight / (0.5 * rho * wing_area * cl_max_landing))
        # Rotation speed requirement from FAR 23.73
        v_r = vs0 * 1.3
        # Calculation of wheel factor
        flight_point = FlightPoint(
            mach=v_r / atm.speed_of_sound,
            altitude=0.0,
            engine_setting=EngineSetting.IDLE,
            thrust_rate=0.1
        )  # FIXME: fixed thrust rate (should depend on wished descent rate)
        propulsion_model.compute_flight_points(flight_point)
        thrust = float(flight_point.thrust)
        fact_wheel = (
            (x_lg - x_cg - z_eng * thrust / weight) / wing_mac * (vs0 / v_r)**2
        )  # FIXME: not clear if vs0 or vs1 should be used in formula
        # Evaluate aircraft overall angle (aoa)
        cl0_landing = cl0_clean + cl_flaps_landing
        # Calculation of correction coefficient n_h and n_q
        n_h = (
            x_ht - x_lg
        ) / lp_ht * tail_efficiency_factor  # tail_efficiency_factor: dynamic pressure reduction at
        # tail (typical value)
        n_q = 1 + cl_alpha_htp_isolated / cl_htp_landing * _ANG_VEL * (
            x_ht - x_lg) / v_r
        # Calculation of volume coefficient based on Torenbeek formula
        coef_vol = (cl_max_landing / (n_h * n_q * cl_htp_landing) *
                    (cm_landing / cl_max_landing - fact_wheel) +
                    cl0_landing / cl_htp_landing *
                    (x_lg - x_wing_aero_center) / wing_mac)
        # Calculation of equivalent area
        area_2 = coef_vol * wing_area * wing_mac / lp_ht

        if max(area_1, area_2) < 0.0:
            print(
                "Warning: HTP area estimated negative (in ComputeHTArea) forced to 1m²!"
            )
            outputs["data:geometry:horizontal_tail:area"] = 1.0
        else:
            outputs["data:geometry:horizontal_tail:area"] = max(area_1, area_2)
Beispiel #26
0
    def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):
        # Sizing constraints for the vertical tail.
        # Limiting cases: rotating torque objective (cn_beta_goal) during cruise, and  
        # compensation of engine failure induced torque at approach speed/altitude. 
        # Returns maximum area.

        propulsion_model = FuelEngineSet(
            self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]
        )
        engine_number = inputs["data:geometry:propulsion:count"]
        wing_area = inputs["data:geometry:wing:area"]
        span = inputs["data:geometry:wing:span"]
        l0_wing = inputs["data:geometry:wing:MAC:length"]
        cg_mac_position = inputs["data:weight:aircraft:CG:aft:MAC_position"]
        cn_beta_fuselage = inputs["data:aerodynamics:fuselage:cruise:CnBeta"]
        cl_alpha_vt = inputs["data:aerodynamics:vertical_tail:cruise:CL_alpha"]
        cruise_speed = inputs["data:TLAR:v_cruise"]
        approach_speed = inputs["data:TLAR:v_approach"]
        cruise_altitude = inputs["data:mission:sizing:main_route:cruise:altitude"]
        wing_htp_distance = inputs["data:geometry:vertical_tail:MAC:at25percent:x:from_wingMAC25"]
        nac_wet_area = inputs["data:geometry:propulsion:nacelle:wet_area"]
        y_nacelle = inputs["data:geometry:propulsion:nacelle:y"]

        # CASE1: OBJECTIVE TORQUE @ CRUISE #############################################################################

        atm = Atmosphere(cruise_altitude)
        speed_of_sound = atm.speed_of_sound
        cruise_mach = cruise_speed / speed_of_sound
        # Matches suggested goal by Raymer, Fig 16.20
        cn_beta_goal = 0.0569 - 0.01694 * cruise_mach + 0.15904 * cruise_mach ** 2

        required_cnbeta_vtp = cn_beta_goal - cn_beta_fuselage
        distance_to_cg = wing_htp_distance + 0.25 * l0_wing - cg_mac_position * l0_wing
        area_1 = required_cnbeta_vtp / (distance_to_cg / wing_area / span * cl_alpha_vt)

        # CASE2: ENGINE FAILURE COMPENSATION DURING CLIMB ##############################################################

        failure_altitude = 5000.0  # CS23 for Twin engine - at 5000ft
        atm = Atmosphere(failure_altitude)
        speed_of_sound = atm.speed_of_sound
        pressure = atm.pressure
        if engine_number == 2.0:
            stall_speed = approach_speed / 1.3
            mc_speed = 1.2 * stall_speed  # Flights mechanics from GA - Serge Bonnet CS23
            mc_mach = mc_speed / speed_of_sound
            # Calculation of engine power for given conditions
            flight_point = FlightPoint(
                mach=mc_mach, altitude=failure_altitude, engine_setting=EngineSetting.CLIMB,
                thrust_rate=1.0
            )  # forced to maximum thrust
            propulsion_model.compute_flight_points(flight_point)
            thrust = float(flight_point.thrust)
            # Calculation of engine thrust and nacelle drag (failed one)
            nac_drag = 0.07 * nac_wet_area  # FIXME: the form factor should not be fixed outside propulsion module!
            # Torque compensation
            area_2 = (
                    2 * (y_nacelle / wing_htp_distance) * (thrust + nac_drag)
                    / (pressure * mc_mach ** 2 * 0.9 * 0.42 * 10)
            )
        else:
            area_2 = 0.0

        outputs["data:geometry:vertical_tail:area"] = max(area_1, area_2)
Beispiel #27
0
    def compute_mission(self, inputs, outputs):
        propulsion_model = FuelEngineSet(
            self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:engine:count"]
        )

        reference_area = inputs["data:geometry:wing:area"]
        cruise_mach = inputs["data:TLAR:cruise_mach"]
        flight_distance = inputs["data:TLAR:range"]
        thrust_rates = {
            FlightPhase.CLIMB: inputs["data:mission:sizing:climb:thrust_rate"],
            FlightPhase.DESCENT: inputs["data:mission:sizing:descent:thrust_rate"],
        }
        high_speed_polar = Polar(
            inputs["data:aerodynamics:aircraft:cruise:CL"],
            inputs["data:aerodynamics:aircraft:cruise:CD"],
        )
        low_speed_climb_polar = Polar(
            inputs["data:aerodynamics:aircraft:takeoff:CL"],
            inputs["data:aerodynamics:aircraft:takeoff:CD"],
        )

        base_flight_calculator = RangedFlight(
            StandardFlight(
                propulsion=propulsion_model,
                reference_area=reference_area,
                low_speed_climb_polar=low_speed_climb_polar,
                high_speed_polar=high_speed_polar,
                cruise_mach=cruise_mach,
                thrust_rates=thrust_rates,
            ),
            flight_distance,
        )

        end_of_takeoff = FlightPoint(
            mass=inputs["data:weight:aircraft:MTOW"] - inputs["data:mission:sizing:takeoff:fuel"],
            true_airspeed=inputs["data:mission:sizing:takeoff:V2"],
            altitude=inputs["data:mission:sizing:takeoff:altitude"] + 35 * foot,
            ground_distance=0.0,
        )

        flight_points = base_flight_calculator.compute_from(end_of_takeoff)

        # Update start flight point with computed (non initialized) parameters
        end_of_takeoff = FlightPoint(flight_points.iloc[0])

        # Get flight points for each end of phase
        end_of_initial_climb = FlightPoint(
            flight_points.loc[flight_points.name == FlightPhase.INITIAL_CLIMB.value].iloc[-1]
        )
        end_of_climb = FlightPoint(
            flight_points.loc[flight_points.name == FlightPhase.CLIMB.value].iloc[-1]
        )
        end_of_cruise = FlightPoint(
            flight_points.loc[flight_points.name == FlightPhase.CRUISE.value].iloc[-1]
        )
        end_of_descent = FlightPoint(
            flight_points.loc[flight_points.name == FlightPhase.DESCENT.value].iloc[-1]
        )

        # Set OpenMDAO outputs
        outputs["data:mission:sizing:initial_climb:fuel"] = (
            end_of_takeoff.mass - end_of_initial_climb.mass
        )
        outputs["data:mission:sizing:main_route:climb:fuel"] = (
            end_of_initial_climb.mass - end_of_climb.mass
        )
        outputs["data:mission:sizing:main_route:cruise:fuel"] = (
            end_of_climb.mass - end_of_cruise.mass
        )
        outputs["data:mission:sizing:main_route:descent:fuel"] = (
            end_of_cruise.mass - end_of_descent.mass
        )
        outputs["data:mission:sizing:initial_climb:distance"] = (
            end_of_initial_climb.ground_distance - end_of_takeoff.ground_distance
        )
        outputs["data:mission:sizing:main_route:climb:distance"] = (
            end_of_climb.ground_distance - end_of_initial_climb.ground_distance
        )
        outputs["data:mission:sizing:main_route:cruise:distance"] = (
            end_of_cruise.ground_distance - end_of_climb.ground_distance
        )
        outputs["data:mission:sizing:main_route:descent:distance"] = (
            end_of_descent.ground_distance - end_of_cruise.ground_distance
        )
        outputs["data:mission:sizing:initial_climb:duration"] = (
            end_of_initial_climb.time - end_of_takeoff.time
        )
        outputs["data:mission:sizing:main_route:climb:duration"] = (
            end_of_climb.time - end_of_initial_climb.time
        )
        outputs["data:mission:sizing:main_route:cruise:duration"] = (
            end_of_cruise.time - end_of_climb.time
        )
        outputs["data:mission:sizing:main_route:descent:duration"] = (
            end_of_descent.time - end_of_cruise.time
        )

        # Diversion flight =====================================================
        diversion_distance = inputs["data:mission:sizing:diversion:distance"]
        if diversion_distance <= 200 * nautical_mile:
            diversion_cruise_altitude = 22000 * foot

        else:
            diversion_cruise_altitude = 31000 * foot

        diversion_flight_calculator = RangedFlight(
            StandardFlight(
                propulsion=propulsion_model,
                reference_area=reference_area,
                low_speed_climb_polar=low_speed_climb_polar,
                high_speed_polar=high_speed_polar,
                cruise_mach=cruise_mach,
                thrust_rates=thrust_rates,
                climb_target_altitude=diversion_cruise_altitude,
            ),
            diversion_distance,
        )
        diversion_flight_points = diversion_flight_calculator.compute_from(end_of_descent)

        # Get flight points for each end of phase
        end_of_diversion_climb = FlightPoint(
            diversion_flight_points.loc[
                diversion_flight_points.name == FlightPhase.CLIMB.value
            ].iloc[-1]
        )
        end_of_diversion_cruise = FlightPoint(
            diversion_flight_points.loc[
                diversion_flight_points.name == FlightPhase.CRUISE.value
            ].iloc[-1]
        )
        end_of_diversion_descent = FlightPoint(
            diversion_flight_points.loc[
                diversion_flight_points.name == FlightPhase.DESCENT.value
            ].iloc[-1]
        )

        # rename phases because all flight points will be concatenated later.
        diversion_flight_points.name = "diversion_" + diversion_flight_points.name

        # Set OpenMDAO outputs
        outputs["data:mission:sizing:diversion:climb:fuel"] = (
            end_of_descent.mass - end_of_diversion_climb.mass
        )
        outputs["data:mission:sizing:diversion:cruise:fuel"] = (
            end_of_diversion_climb.mass - end_of_diversion_cruise.mass
        )
        outputs["data:mission:sizing:diversion:descent:fuel"] = (
            end_of_diversion_cruise.mass - end_of_diversion_descent.mass
        )
        outputs["data:mission:sizing:diversion:climb:distance"] = (
            end_of_diversion_climb.ground_distance - end_of_descent.ground_distance
        )
        outputs["data:mission:sizing:diversion:cruise:distance"] = (
            end_of_diversion_cruise.ground_distance - end_of_diversion_climb.ground_distance
        )
        outputs["data:mission:sizing:diversion:descent:distance"] = (
            end_of_diversion_descent.ground_distance - end_of_diversion_cruise.ground_distance
        )
        outputs["data:mission:sizing:diversion:climb:duration"] = (
            end_of_diversion_climb.time - end_of_descent.time
        )
        outputs["data:mission:sizing:diversion:cruise:duration"] = (
            end_of_diversion_cruise.time - end_of_diversion_climb.time
        )
        outputs["data:mission:sizing:diversion:descent:duration"] = (
            end_of_diversion_descent.time - end_of_diversion_cruise.time
        )

        # Holding ==============================================================

        holding_duration = inputs["data:mission:sizing:holding:duration"]

        holding_calculator = HoldSegment(
            target=FlightPoint(time=holding_duration),
            propulsion=propulsion_model,
            reference_area=reference_area,
            polar=high_speed_polar,
            name="holding",
        )

        holding_flight_points = holding_calculator.compute_from(end_of_diversion_descent)

        end_of_holding = FlightPoint(holding_flight_points.iloc[-1])
        outputs["data:mission:sizing:holding:fuel"] = (
            end_of_diversion_descent.mass - end_of_holding.mass
        )

        # Taxi-in ==============================================================
        taxi_in_duration = inputs["data:mission:sizing:taxi_in:duration"]
        taxi_in_thrust_rate = inputs["data:mission:sizing:taxi_in:thrust_rate"]

        taxi_in_calculator = TaxiSegment(
            target=FlightPoint(time=taxi_in_duration),
            propulsion=propulsion_model,
            thrust_rate=taxi_in_thrust_rate,
            name=FlightPhase.TAXI_IN.value,
        )
        start_of_taxi_in = FlightPoint(end_of_holding)
        start_of_taxi_in.true_airspeed = inputs["data:mission:sizing:taxi_in:speed"]
        taxi_in_flight_points = taxi_in_calculator.compute_from(end_of_holding)

        end_of_taxi_in = FlightPoint(taxi_in_flight_points.iloc[-1])
        outputs["data:mission:sizing:taxi_in:fuel"] = end_of_holding.mass - end_of_taxi_in.mass

        # Final ================================================================
        fuel_route = inputs["data:weight:aircraft:MTOW"] - end_of_descent.mass
        outputs["data:mission:sizing:ZFW"] = end_of_taxi_in.mass - 0.03 * fuel_route
        outputs["data:mission:sizing:fuel"] = (
            inputs["data:weight:aircraft:MTOW"] - outputs["data:mission:sizing:ZFW"]
        )

        self.flight_points = (
            pd.concat(
                [
                    flight_points,
                    diversion_flight_points,
                    holding_flight_points,
                    taxi_in_flight_points,
                ]
            )
            .reset_index(drop=True)
            .applymap(lambda x: np.asscalar(np.asarray(x)))
        )

        if self.options["out_file"]:
            self.flight_points.to_csv(self.options["out_file"])
Beispiel #28
0
    def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None):

        cl_max_takeoff = inputs["data:aerodynamics:wing:low_speed:CL_max_clean"]
        cl0_clean = inputs["data:aerodynamics:wing:low_speed:CL0_clean"]
        cl_flaps_takeoff = inputs["data:aerodynamics:flaps:takeoff:CL"]
        cm_takeoff = inputs["takeoff:cm_wing"]
        cl_alpha_htp_isolated = inputs["data:aerodynamics:horizontal_tail:low_speed:CL_alpha_isolated"]
        cl_htp = inputs["takeoff:cl_htp"]
        tail_efficiency_factor = inputs["data:aerodynamics:horizontal_tail:efficiency"]

        n_engines = inputs["data:geometry:propulsion:count"]
        x_wing_aero_center = inputs["data:geometry:wing:MAC:at25percent:x"]
        wing_area = inputs["data:geometry:wing:area"]
        wing_mac = inputs["data:geometry:wing:MAC:length"]
        ht_area = inputs["data:geometry:horizontal_tail:area"]
        lp_ht = inputs["data:geometry:horizontal_tail:MAC:at25percent:x:from_wingMAC25"]

        mtow = inputs["data:weight:aircraft:MTOW"]

        x_lg = inputs["data:weight:airframe:landing_gear:main:CG:x"]
        z_cg_aircraft = inputs["data:weight:aircraft_empty:CG:z"]
        z_cg_engine = inputs["data:weight:propulsion:engine:CG:z"]

        propulsion_model = FuelEngineSet(
            self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]
        )

        # Conditions for calculation
        atm = Atmosphere(0.0)
        rho = atm.density
        sos = atm.speed_of_sound

        # Calculation of take-off minimum speed
        weight = mtow * g
        vs1 = math.sqrt(weight / (0.5 * rho * wing_area * cl_max_takeoff))

        if n_engines == 1.0:
            vr = 1.10 * vs1
        else:
            vr = 1.0 * vs1

        mach_r = vr / sos

        flight_point = FlightPoint(
            mach=mach_r, altitude=0.0, engine_setting=EngineSetting.TAKEOFF,
            thrust_rate=1.0
        )
        propulsion_model.compute_flight_points(flight_point)
        thrust = float(flight_point.thrust)

        x_ht = x_wing_aero_center + lp_ht

        # Compute aerodynamic coefficients for takeoff @ 0° aircraft angle
        cl0_takeoff = cl0_clean + cl_flaps_takeoff

        eta_q = 1. + cl_alpha_htp_isolated / cl_htp * _ANG_VEL * (x_ht - x_lg) / vr
        eta_h = (x_ht - x_lg) / lp_ht * tail_efficiency_factor

        k_cl = cl_max_takeoff / (eta_q * eta_h * cl_htp)

        tail_volume_coefficient = ht_area * lp_ht / (wing_area * wing_mac)

        zt = z_cg_aircraft - z_cg_engine
        engine_contribution = zt * thrust / weight

        x_cg = (
                       1. / k_cl * (
                       tail_volume_coefficient - cl0_takeoff / cl_htp * (x_lg / wing_mac - 0.25)
               ) - cm_takeoff / cl_max_takeoff
               ) * (vr / vs1) ** 2.0 + x_lg - engine_contribution

        outputs["data:handling_qualities:to_rotation_limit:x"] = x_cg

        x_cg_ratio = (x_cg - x_wing_aero_center + 0.25 * wing_mac) / wing_mac

        outputs["data:handling_qualities:to_rotation_limit:MAC_position"] = x_cg_ratio