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
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)
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, )
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)
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
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)
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), ))
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)
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)
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"])
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)
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)
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)
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
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)
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)
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)
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)
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)
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)
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
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)
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)
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)
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"])
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