def test_EngineSet(): """Tests behaviour of FuelEngineSet""" engine = DummyEngine(1.2e5, 1.0e-5) # input = thrust rate thrust_rate = np.linspace(0.0, 1.0, 20) for engine_count in [1, 2, 3, 4]: engine_set = FuelEngineSet(engine, engine_count) flight_point = FlightPoint(mach=0.0, altitude=0.0, engine_setting=1, thrust_rate=thrust_rate) engine_set.compute_flight_points(flight_point) assert_allclose(flight_point.sfc, engine.max_sfc * thrust_rate) assert_allclose(flight_point.thrust, engine.max_thrust * engine_count * thrust_rate) # input = thrust thrust = np.linspace(0.0, engine.max_thrust, 30) for engine_count in [1, 2, 3, 4]: engine_set = FuelEngineSet(engine, engine_count) flight_point = FlightPoint(mach=0.0, altitude=0.0, engine_setting=1, thrust=thrust) engine_set.compute_flight_points(flight_point) assert_allclose(flight_point.thrust_rate, thrust / engine_count / engine.max_thrust) assert_allclose(flight_point.sfc, engine.max_sfc * flight_point.thrust_rate)
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), ) print_dataframe(flight_points) first_point = flight_points.iloc[0] last_point = 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 complete_flight_point(self, flight_point: FlightPoint): """ Computes data for provided flight point. Assumes that it is already defined for time, altitude, mass, ground distance and speed (TAS, EAS, or Mach). :param flight_point: the flight point that will be completed in-place """ flight_point.engine_setting = self.engine_setting self._complete_speed_values(flight_point) atm = AtmosphereSI(flight_point.altitude) reference_force = 0.5 * atm.density * flight_point.true_airspeed ** 2 * self.reference_area if self.polar: flight_point.CL = flight_point.mass * g / reference_force flight_point.CD = self.polar.cd(flight_point.CL) else: flight_point.CL = flight_point.CD = 0.0 flight_point.drag = flight_point.CD * reference_force self._compute_propulsion(flight_point) flight_point.slope_angle, flight_point.acceleration = self._get_gamma_and_acceleration( flight_point.mass, flight_point.drag, flight_point.thrust )
def compute_from(self, start: FlightPoint) -> pd.DataFrame: self.complete_flight_point( start) # needed to ensure all speed values are computed. if self.target.altitude: if isinstance(self.target.altitude, str): # Target altitude will be modified along the process, so we keep track # of the original order in target CL, that is not used otherwise. self.target.CL = self.target.altitude # pylint: disable=invalid-name # let's put a numerical, negative value in self.target.altitude to # ensure there will be no problem in self._get_distance_to_target() self.target.altitude = -1000.0 self.interrupt_if_getting_further_from_target = False else: # Target altitude is fixed, back to original settings (in case # this instance is used more than once) self.target.CL = None self.interrupt_if_getting_further_from_target = True atm = AtmosphereSI(start.altitude) if self.target.equivalent_airspeed == self.CONSTANT_VALUE: start.true_airspeed = atm.get_true_airspeed( start.equivalent_airspeed) elif self.target.mach == self.CONSTANT_VALUE: start.true_airspeed = start.mach * atm.speed_of_sound return super().compute_from(start)
def compute_next_flight_point( self, flight_points: List[FlightPoint], time_step: float ) -> FlightPoint: """ Computes time, altitude, speed, mass and ground distance of next flight point. :param flight_points: previous flight points :param time_step: time step for computing next point :return: the computed next flight point """ start = flight_points[0] previous = flight_points[-1] next_point = FlightPoint() next_point.mass = previous.mass - self.propulsion.get_consumed_mass(previous, time_step) next_point.time = previous.time + time_step next_point.ground_distance = ( previous.ground_distance + previous.true_airspeed * time_step * np.cos(previous.slope_angle) ) self._compute_next_altitude(next_point, previous) if self.target.true_airspeed == "constant": next_point.true_airspeed = previous.true_airspeed elif self.target.equivalent_airspeed == "constant": next_point.equivalent_airspeed = start.equivalent_airspeed elif self.target.mach == "constant": next_point.mach = start.mach else: next_point.true_airspeed = previous.true_airspeed + time_step * previous.acceleration # The naming is not done in complete_flight_point for not naming the start point next_point.name = self.name return next_point
def _climb_to_altitude_and_cruise( self, start: FlightPoint, cruise_altitude: float, climb_segment: AltitudeChangeSegment, cruise_segment: CruiseSegment, ): """ Climbs up to cruise_altitude and cruise, while ensuring final ground_distance is equal to self.target.ground_distance. :param start: :param cruise_altitude: :param climb_segment: :param cruise_segment: :return: """ climb_segment.target = FlightPoint( altitude=cruise_altitude, mach=cruise_segment.target.mach, true_airspeed=cruise_segment.target.true_airspeed, equivalent_airspeed=cruise_segment.target.equivalent_airspeed, ) climb_points = climb_segment.compute_from(start) cruise_start = FlightPoint.create(climb_points.iloc[-1]) cruise_segment.target.ground_distance = (self.target.ground_distance - cruise_start.ground_distance) cruise_points = cruise_segment.compute_from(cruise_start) return pd.concat([climb_points, cruise_points]).reset_index(drop=True)
def test_breguet_cruise(polar): propulsion = FuelEngineSet(DummyEngine(0.5e5, 1.0e-5), 2) segment = BreguetCruiseSegment( 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 = flight_points.iloc[-1] # Note: reference values are obtained by running the process with CruiseSegment and 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 compute(self, inputs: Vector, outputs: Vector, start_flight_point: FlightPoint) -> pd.DataFrame: """ To be used during compute() of an OpenMDAO component. Builds the mission from input file, and computes it. `outputs` vector is filled with duration, burned fuel and covered ground distance for each part of the flight. :param inputs: the input vector of the OpenMDAO component :param outputs: the output vector of the OpenMDAO component :param start_flight_point: the starting flight point just after takeoff :return: a pandas DataFrame where columns names match :meth:`fastoad.base.flight_point.FlightPoint.get_attribute_keys` """ mission = self.build(inputs, self.mission_name) def _compute_vars(name_root, start: FlightPoint, end: FlightPoint): """Computes duration, burned fuel and covered distance.""" if name_root + ":duration" in outputs: outputs[name_root + ":duration"] = end.time - start.time if name_root + ":fuel" in outputs: outputs[name_root + ":fuel"] = start.mass - end.mass if name_root + ":distance" in outputs: outputs[ name_root + ":distance"] = end.ground_distance - start.ground_distance if not start_flight_point.name: start_flight_point.name = mission.flight_sequence[0].name current_flight_point = start_flight_point flight_points = mission.compute_from(start_flight_point) for part in mission.flight_sequence: var_name_root = "data:mission:%s" % part.name part_end = FlightPoint.create( flight_points.loc[flight_points.name.str.startswith( part.name)].iloc[-1]) _compute_vars(var_name_root, current_flight_point, part_end) if isinstance(part, FlightSequence): # In case of a route, outputs are computed for each phase in the route phase_start = current_flight_point for phase in part.flight_sequence: phase_points = flight_points.loc[flight_points.name == phase.name] if len(phase_points) > 0: phase_end = FlightPoint.create(phase_points.iloc[-1]) var_name_root = "data:mission:%s" % phase.name _compute_vars(var_name_root, phase_start, phase_end) phase_start = phase_end current_flight_point = part_end # Outputs for the whole mission var_name_root = "data:mission:%s" % mission.name _compute_vars(var_name_root, start_flight_point, current_flight_point) return flight_points
def compute_flight_points(self, flight_point: FlightPoint): if flight_point.thrust_is_regulated or flight_point.thrust_rate is None: flight_point.thrust_rate = flight_point.thrust / self.max_thrust else: flight_point.thrust = self.max_thrust * flight_point.thrust_rate flight_point.sfc = self.max_sfc * (1.0 + flight_point.thrust_rate) / 2.0
def test_compute_flight_points(): engine = RubberEngine(5, 30, 1500, 1, 0.95, 10000) # f0=1 so that output is simply fc/f0 # Test with scalars flight_point = FlightPoint( mach=0, altitude=0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=0.8) # with engine_setting as EngineSetting engine.compute_flight_points(flight_point) np.testing.assert_allclose(flight_point.thrust, 0.9553 * 0.8, rtol=1e-4) np.testing.assert_allclose(flight_point.sfc, 0.99210e-5, rtol=1e-4) flight_point = FlightPoint(mach=0.3, altitude=0, engine_setting=EngineSetting.CLIMB.value, thrust=0.35677) # with engine_setting as int engine.compute_flight_points(flight_point) np.testing.assert_allclose(flight_point.thrust_rate, 0.5, rtol=1e-4) np.testing.assert_allclose(flight_point.sfc, 1.3496e-5, rtol=1e-4) # Test full arrays # 2D arrays are used, where first line is for thrust rates, and second line # is for thrust values. # As thrust rates and thrust values match, thrust rate results are 2 equal # lines and so are thrust value results. machs = [0, 0.3, 0.3, 0.8, 0.8] altitudes = [0, 0, 0, 10000, 13000] thrust_rates = [0.8, 0.5, 0.5, 0.4, 0.7] thrusts = [0.9553 * 0.8, 0.38851, 0.35677, 0.09680, 0.11273] engine_settings = [ EngineSetting.TAKEOFF, EngineSetting.TAKEOFF, EngineSetting.CLIMB, EngineSetting.IDLE, EngineSetting.CRUISE.value, ] # mix EngineSetting with integers expected_sfc = [0.99210e-5, 1.3496e-5, 1.3496e-5, 1.8386e-5, 1.5957e-5] flight_points = pd.DataFrame() flight_points["mach"] = machs + machs flight_points["altitude"] = altitudes + altitudes flight_points["engine_setting"] = engine_settings + engine_settings flight_points["thrust_is_regulated"] = [False] * 5 + [True] * 5 flight_points["thrust_rate"] = thrust_rates + [0.0] * 5 flight_points["thrust"] = [0.0] * 5 + thrusts engine.compute_flight_points(flight_points) np.testing.assert_allclose(flight_points.sfc, expected_sfc + expected_sfc, rtol=1e-4) np.testing.assert_allclose(flight_points.thrust_rate, thrust_rates + thrust_rates, rtol=1e-4) np.testing.assert_allclose(flight_points.thrust, thrusts + thrusts, rtol=1e-4)
def test_dummy_reserve(): dummy_reserve = DummyTransitionSegment( target=FlightPoint(altitude=0.0, mach=0.0), reserve_mass_ratio=0.1, ) flight_points = dummy_reserve.compute_from( FlightPoint(altitude=0.0, mach=0.0, mass=55.0e3)) assert_allclose(flight_points.mass, [55.0e3, 55.0e3, 50.0e3]) assert_allclose(flight_points.altitude, [0.0, 0.0, 0.0]) assert_allclose(flight_points.ground_distance, [0.0, 0.0, 0.0]) assert_allclose(flight_points.mach, [0.0, 0.0, 0.0]) assert_allclose(flight_points.true_airspeed, [0.0, 0.0, 0.0], rtol=1.0e-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_dummy_descent_with_reserve(): dummy_descent_reserve = DummyTransitionSegment( target=FlightPoint(altitude=0.0, mach=0.0, ground_distance=500.0e3), mass_ratio=0.9, reserve_mass_ratio=0.08, ) flight_points = dummy_descent_reserve.compute_from( FlightPoint(altitude=9.0e3, mass=60.0e3, mach=0.8)) assert_allclose(flight_points.mass, [60.0e3, 54.0e3, 50.0e3]) assert_allclose(flight_points.altitude, [9.0e3, 0.0, 0.0]) assert_allclose(flight_points.ground_distance, [0.0, 500.0e3, 500.0e3]) assert_allclose(flight_points.mach, [0.8, 0.0, 0.0]) assert_allclose(flight_points.true_airspeed, [243.04, 0.0, 0.0], rtol=1.0e-4)
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 compute_flight_points(self, flight_points: Union[FlightPoint, pd.DataFrame]): if isinstance(flight_points, FlightPoint): flight_points_per_engine = FlightPoint(flight_points) else: flight_points_per_engine = flight_points.copy() if flight_points.thrust is not None: flight_points_per_engine.thrust = flight_points.thrust / self.engine_count self.engine.compute_flight_points(flight_points_per_engine) flight_points.sfc = flight_points_per_engine.sfc flight_points.thrust = flight_points_per_engine.thrust * self.engine_count flight_points.thrust_rate = flight_points_per_engine.thrust_rate
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_wrapper.propulsion = propulsion_model self._mission_wrapper.reference_area = reference_area end_of_takeoff = FlightPoint( time=0.0, mass=inputs[self._mission_vars.TOW.value], true_airspeed=inputs[self._mission_vars.TAKEOFF_V2.value], altitude=inputs[self._mission_vars.TAKEOFF_ALTITUDE.value] + 35 * foot, ground_distance=0.0, ) self.flight_points = self._mission_wrapper.compute( inputs, outputs, end_of_takeoff) # Final ================================================================ end_of_mission = FlightPoint.create(self.flight_points.iloc[-1]) zfw = end_of_mission.mass - self._mission_wrapper.get_reserve( self.flight_points, self.options["mission_name"]) outputs[self._mission_vars.BLOCK_FUEL.value] = ( inputs[self._mission_vars.TOW.value] + inputs[self._mission_vars.TAKEOFF_FUEL.value] + inputs[self._mission_vars.TAXI_OUT_FUEL.value] - zfw) if self.options["is_sizing"]: outputs["data:weight:aircraft:sizing_block_fuel"] = outputs[ self._mission_vars.BLOCK_FUEL.value] def as_scalar(value): if isinstance(value, np.ndarray): return np.asscalar(value) return value self.flight_points = self.flight_points.applymap(as_scalar) if self.options["out_file"]: self.flight_points.to_csv(self.options["out_file"])
def __init__( self, *, target: FlightPoint, propulsion: IPropulsion, reference_area: float, polar: Polar, **kwargs ): """ Only keyword arguments are accepted. :param target: the target flight point, defined for any relevant parameter :param propulsion: the propulsion model :param reference_area: the reference area for aerodynamic forces :param polar: the aerodynamic polar """ self.propulsion = propulsion self.reference_area = reference_area self.polar = polar self.target = FlightPoint(target) # If true, computation will stop if a flight point is further from target # than previous one. self.interrupt_if_getting_further_from_target = True super().__init__(**kwargs)
def test_dummy_climb(): dummy_climb = DummyTransitionSegment(target=FlightPoint( altitude=9.0e3, mach=0.8, ground_distance=400.0e3), mass_ratio=0.8) flight_points = dummy_climb.compute_from( FlightPoint(altitude=0.0, mass=100.0e3, mach=0.0, ground_distance=100.0e3)) assert_allclose(flight_points.mass, [100.0e3, 80.0e3]) assert_allclose(flight_points.altitude, [0.0, 9.0e3]) assert_allclose(flight_points.ground_distance, [100.0e3, 500.0e3]) assert_allclose(flight_points.mach, [0.0, 0.8]) assert_allclose(flight_points.true_airspeed, [0.0, 243.04], rtol=1.0e-4)
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): n_eng = inputs["data:geometry:propulsion:count"] propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), n_eng) flight_point = FlightPoint( mach=0.0, altitude=0.0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=1.0) # with engine_setting as EngineSetting propulsion_model.compute_flight_points(flight_point) # This should give the UNINSTALLED weight sl_thrust_newton = float(flight_point.thrust) sl_thrust_lbs = sl_thrust_newton / lbf b1_2 = 0.082 * n_eng * sl_thrust_lbs**0.65 outputs["data:weight:propulsion:engine_oil:mass"] = b1_2
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]) if self.options["taxi_out"]: thrust_rate = inputs["data:mission:sizing:taxi_out:thrust_rate"] duration = inputs["data:mission:sizing:taxi_out:duration"] mach = inputs["data:mission:sizing:taxi_out:speed"] / Atmosphere( 0.0).speed_of_sound else: thrust_rate = inputs["data:mission:sizing:taxi_in:thrust_rate"] duration = inputs["data:mission:sizing:taxi_in:duration"] mach = inputs["data:mission:sizing:taxi_in:speed"] / Atmosphere( 0.0).speed_of_sound # FIXME: no specific settings for taxi (to be changed in fastoad\constants.py) flight_point = FlightPoint(mach=mach, altitude=0.0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=thrust_rate) propulsion_model.compute_flight_points(flight_point) fuel_mass = propulsion_model.get_consumed_mass(flight_point, duration) if self.options["taxi_out"]: outputs["data:mission:sizing:taxi_out:fuel"] = fuel_mass else: outputs["data:mission:sizing:taxi_in:fuel"] = fuel_mass
def compute(self, inputs, outputs, discrete_inputs=None, discrete_outputs=None): n_eng = inputs["data:geometry:propulsion:count"] wing_area = inputs["data:geometry:wing:area"] mfw = inputs["data:weight:aircraft:MFW"] n_tank = 2.0 propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), n_eng) flight_point = FlightPoint( mach=0.0, altitude=0.0, engine_setting=EngineSetting.TAKEOFF, thrust_rate=1.0) # with engine_setting as EngineSetting propulsion_model.compute_flight_points(flight_point) sl_thrust_newton = float(flight_point.thrust) sl_thrust_lbs = sl_thrust_newton / lbf sl_thrust_lbs_per_engine = sl_thrust_lbs / n_eng b3 = 11.5 * n_eng * sl_thrust_lbs_per_engine ** 0.2 + \ 0.07 * wing_area + \ 1.6 * n_tank * mfw ** 0.28 outputs["data:weight:propulsion:unusable_fuel:mass"] = b3
def delta_axial_load(self, air_speed, inputs, altitude, mass): propulsion_model = FuelEngineSet( self._engine_wrapper.get_model(inputs), inputs["data:geometry:propulsion:count"]) wing_area = inputs["data:geometry:wing:area"] cd0 = inputs["data:aerodynamics:aircraft:cruise:CD0"] coef_k = inputs[ "data:aerodynamics:wing:cruise:induced_drag_coefficient"] # Get the available thrust from propulsion system atm = Atmosphere(altitude, altitude_in_feet=False) flight_point = FlightPoint(mach=air_speed / atm.speed_of_sound, altitude=altitude, engine_setting=EngineSetting.TAKEOFF, thrust_rate=1.0) propulsion_model.compute_flight_points(flight_point) thrust = float(flight_point.thrust) # Get the necessary thrust to overcome cl = (mass * g) / (0.5 * atm.density * wing_area * air_speed**2.0) cd = cd0 + coef_k * cl**2.0 drag = 0.5 * atm.density * wing_area * cd * air_speed**2.0 return thrust - drag
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_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 compute_from(self, start: FlightPoint) -> pd.DataFrame: start = FlightPoint(start) self.complete_flight_point(start) # needed to ensure all speed values are computed. if self.target.altitude and self.target.altitude < 0.0: # Target altitude will be modified along the process, so we keep track # of the original order in target CL, that is not used otherwise. self.target.CL = self.target.altitude self.interrupt_if_getting_further_from_target = False atm = AtmosphereSI(start.altitude) if self.target.equivalent_airspeed == "constant": start.true_airspeed = atm.get_true_airspeed(start.equivalent_airspeed) elif self.target.mach == "constant": start.true_airspeed = start.mach * atm.speed_of_sound return super().compute_from(start)
def test_breguet_with_rubber_engine(): ivc = om.IndepVarComp() ivc.add_output("data:mission:sizing:main_route:cruise:altitude", 35000, units="ft") ivc.add_output("data:TLAR:cruise_mach", 0.78) ivc.add_output("data:TLAR:range", 500, units="NM") ivc.add_output("data:TLAR:NPAX", 150) ivc.add_output("data:aerodynamics:aircraft:cruise:L_D_max", 16.0) ivc.add_output("data:weight:aircraft:MTOW", 74000, units="kg") ivc.add_output("data:propulsion:rubber_engine:bypass_ratio", 5) ivc.add_output("data:propulsion:rubber_engine:maximum_mach", 0.95) ivc.add_output("data:propulsion:rubber_engine:design_altitude", 35000, units="ft") ivc.add_output("data:propulsion:MTO_thrust", 100000, units="N") ivc.add_output("data:propulsion:rubber_engine:overall_pressure_ratio", 30) ivc.add_output("data:propulsion:rubber_engine:turbine_inlet_temperature", 1500, units="K") # With rubber engine OM component group = om.Group() group.add_subsystem("breguet", OMBreguet(), promotes=["*"]) group.add_subsystem("engine", OMRubberEngineComponent(), promotes=["*"]) group.nonlinear_solver = om.NonlinearBlockGS() problem = run_system(group, ivc) assert_allclose(problem["data:mission:sizing:ZFW"], 65076.0, atol=1) assert_allclose(problem["data:mission:sizing:fuel"], 8924.0, atol=1) assert_allclose(problem["data:mission:sizing:fuel:unitary"], 0.0642, rtol=1e-3) # With direct call to rubber engine problem2 = run_system( OMBreguet(propulsion_id="fastoad.wrapper.propulsion.rubber_engine"), ivc) assert_allclose(problem2["data:mission:sizing:ZFW"], 65076.0, atol=1) assert_allclose(problem2["data:mission:sizing:fuel"], 8924.0, atol=1) assert_allclose(problem2["data:mission:sizing:fuel:unitary"], 0.0642, rtol=1e-3) engine = RubberEngine(5, 30, 1500, 100000, 0.95, 35000 * foot, -50) directly_computed_flight_point = FlightPoint( mach=0.78, altitude=35000 * foot, engine_setting=EngineSetting.CRUISE, thrust=problem["data:propulsion:required_thrust"], ) engine.compute_flight_points(directly_computed_flight_point) assert_allclose( directly_computed_flight_point.sfc, problem["data:propulsion:SFC"], )
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 = 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)