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 fields of :class:`~fastoad.model_base.flight_point.FlightPoint` """ 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 _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 compute_from(self, start: FlightPoint) -> pd.DataFrame: parts = [] part_start = start for part in self.flight_sequence: flight_points = part.compute_from(part_start) if len(parts) > 0 and len(flight_points) > 1: # First point of the segment is omitted, as it is the last of previous segment. # # But sometimes (especially in the case of simplistic segments), the new first # point may contain more information than the previous last one. In such case, # it is interesting to complete the previous last one. last_flight_points = parts[-1] last_index = last_flight_points.index[-1] for name in flight_points.columns: value = last_flight_points.loc[last_index, name] if not value: last_flight_points.loc[last_index, name] = flight_points.loc[0, name] parts.append(flight_points.iloc[1:]) else: # But it is kept if the computed segment is the first one. parts.append(flight_points) part_start = FlightPoint.create(flight_points.iloc[-1]) if parts: return pd.concat(parts).reset_index(drop=True)
def _compute_breguet(self, inputs, outputs): """ Computes mission using simple Breguet formula at altitude==100m and Mach 0.1 (but max L/D ratio is assumed anyway) Useful only 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 = self._get_initial_polar(inputs) distance = np.asscalar( np.sum(self._mission_wrapper.get_route_ranges(inputs, self.options["mission_name"])) ) altitude = 100.0 cruise_mach = 0.1 breguet = BreguetCruiseSegment( FlightPoint(ground_distance=distance), propulsion=propulsion_model, polar=high_speed_polar, use_max_lift_drag_ratio=True, ) start_point = FlightPoint( mass=inputs[self._mission_vars.TOW.value], altitude=altitude, mach=cruise_mach ) flight_points = breguet.compute_from(start_point) end_point = FlightPoint.create(flight_points.iloc[-1]) outputs[self._mission_vars.NEEDED_BLOCK_FUEL.value] = start_point.mass - end_point.mass
def compute_from(self, start: FlightPoint) -> pd.DataFrame: parts = [] part_start = start for part in self.flight_sequence: flight_points = part.compute_from(part_start) if len(parts) > 0: # First point of the segment is omitted, as it is the # last of previous segment. if len(flight_points) > 1: parts.append(flight_points.iloc[1:]) else: # But it is kept if the computed segment is the first one. parts.append(flight_points) part_start = FlightPoint.create(flight_points.iloc[-1]) if parts: return pd.concat(parts).reset_index(drop=True)
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 self._compute_taxi_out(inputs, outputs, propulsion_model) end_of_takeoff = FlightPoint( time=0.0, mass=inputs[self._mission_vars.TOW], true_airspeed=inputs[self._mission_vars.TAKEOFF_V2], altitude=inputs[self._mission_vars.TAKEOFF_ALTITUDE] + 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]) reserve = self._mission_wrapper.get_reserve( self.flight_points, self.options["mission_name"]) zfw = end_of_mission.mass - reserve reserve_name = self._mission_wrapper.get_reserve_variable_name() if reserve_name in outputs: outputs[reserve_name] = reserve outputs[self._mission_vars.NEEDED_BLOCK_FUEL] = ( inputs[self._mission_vars.TOW] + inputs[self._mission_vars.TAKEOFF_FUEL] + outputs[self._mission_vars.TAXI_OUT_FUEL] - zfw) outputs[self._mission_vars.NEEDED_FUEL_AT_TAKEOFF] = ( outputs[self._mission_vars.NEEDED_BLOCK_FUEL] - inputs[self._mission_vars.TAKEOFF_FUEL] - outputs[self._mission_vars.TAXI_OUT_FUEL]) if self.options["is_sizing"]: outputs["data:weight:aircraft:sizing_block_fuel"] = outputs[ self._mission_vars.NEEDED_BLOCK_FUEL] outputs[ "data:weight:aircraft:sizing_onboard_fuel_at_takeoff"] = outputs[ self._mission_vars.NEEDED_FUEL_AT_TAKEOFF] def as_scalar(value): if isinstance(value, np.ndarray): return np.asscalar(value) return value self.flight_points = self.flight_points.applymap(as_scalar) rename_dict = { field_name: "%s [%s]" % (field_name, unit) for field_name, unit in FlightPoint.get_units().items() } self.flight_points.rename(columns=rename_dict, inplace=True) if self.options["out_file"]: self.flight_points.to_csv(self.options["out_file"])