Skip to content

Commit

Permalink
Merge pull request #487 from fast-aircraft-design/takeoff-in-mission-…
Browse files Browse the repository at this point in the history
…file

Takeoff in mission file
  • Loading branch information
christophe-david committed Apr 20, 2023
2 parents a4a35cb + 2714904 commit 8c4f0b7
Show file tree
Hide file tree
Showing 21 changed files with 3,806 additions and 2,976 deletions.
4 changes: 2 additions & 2 deletions src/fastoad/model_base/flight_point.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Structure for managing flight point data."""
# This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
# Copyright (C) 2022 ONERA & ISAE-SUPAERO
# Copyright (C) 2023 ONERA & ISAE-SUPAERO
# FAST is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
Expand Down Expand Up @@ -102,7 +102,7 @@ class FlightPoint:
sfc: float = None #: Specific Fuel Consumption in kg/N/s.
slope_angle: float = None #: Slope angle in radians.
acceleration: float = None #: Acceleration value in m/s**2.
alpha: float = None #: angle of attack in radians
alpha: float = 0.0 #: angle of attack in radians
slope_angle_derivative: float = None #: slope angle derivative in rad/s
name: str = None #: Name of current phase.

Expand Down
16 changes: 14 additions & 2 deletions src/fastoad/models/performances/mission/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class IFlightPart(ABC, BaseDataClass):
"""

name: str = ""
target: FlightPoint = field(init=False, default=None)
target: FlightPoint = field(init=False, default_factory=FlightPoint)

@abstractmethod
def compute_from(self, start: FlightPoint) -> pd.DataFrame:
Expand Down Expand Up @@ -63,7 +63,12 @@ class FlightSequence(IFlightPart):

_sequence: List[IFlightPart] = field(default_factory=list, init=False)

_target: FlightPoint = None

def compute_from(self, start: FlightPoint) -> pd.DataFrame:
if self._target is not None:
self._sequence[-1].target = self._target

self.part_flight_points = []
part_start = deepcopy(start)
part_start.scalarize()
Expand Down Expand Up @@ -119,11 +124,18 @@ def compute_from(self, start: FlightPoint) -> pd.DataFrame:
@property
def target(self) -> Optional[FlightPoint]:
"""Target of the last element of current sequence."""
if len(self._sequence) > 0:
if hasattr(self, "_sequence") and len(self._sequence) > 0:
return self._sequence[-1].target

return None

@target.setter
def target(self, value: FlightPoint):
if hasattr(self, "_sequence") and len(self._sequence) > 0:
self._sequence[-1].target = value
else:
self._target = value

def append(self, flight_part: IFlightPart):
"""Append flight part to the end of the sequence."""
self._sequence.append(flight_part)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Constants for mission builder package.
"""
# This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
# Copyright (C) 2022 ONERA & ISAE-SUPAERO
# Copyright (C) 2023 ONERA & ISAE-SUPAERO
# FAST is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
Expand All @@ -18,8 +18,9 @@
TYPE_TAG = "type"
SEGMENT_TYPE_TAG = "segment_type"

# FIXME: should be set in Route class
# FIXME: should be set in classes that use these parameters.
BASE_UNITS = {
"range": "m",
"distance_accuracy": "m",
"alpha": "rad",
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from collections import ChainMap, OrderedDict
from collections import ChainMap
from copy import deepcopy
from dataclasses import fields
from typing import Dict, List, Mapping, Optional, Union

Expand All @@ -38,13 +39,15 @@
MissionDefinition,
PARTS_TAG,
PHASE_TAG,
POLAR_TAG,
RESERVE_TAG,
ROUTE_TAG,
SEGMENT_TAG,
)
from ...base import FlightSequence
from ...mission import Mission
from ...polar import Polar
from ...polar_modifier import RegisterPolarModifier, UnchangedPolar
from ...routes import RangedRoute
from ...segments.base import AbstractFlightSegment, RegisterSegment

Expand All @@ -71,9 +74,6 @@ def __init__(
set before calling :meth:`build`
:param mission_name: name of chosen mission, if already decided.
:param variable_prefix: prefix for auto-generated variable names.
:param force_all_block_fuel_usage: if True and if `mission_name` is provided, the mission
definition will be modified to set the target fuel
consumption to variable "~:block_fuel"
"""
self._structure_builders: Dict[str, AbstractStructureBuilder] = {}

Expand Down Expand Up @@ -287,7 +287,7 @@ def _update_structure_builders(self):
if self.get_input_weight_variable_name(mission_name) is None:
self._add_default_taxi_takeoff(mission_name)

def _build_mission(self, mission_structure: OrderedDict) -> Mission:
def _build_mission(self, mission_structure: dict) -> Mission:
"""
Builds mission instance from provided structure.
Expand Down Expand Up @@ -327,7 +327,7 @@ def _build_mission(self, mission_structure: OrderedDict) -> Mission:

return mission

def _build_route(self, route_structure: OrderedDict, kwargs: Mapping = None):
def _build_route(self, route_structure: dict, kwargs: Mapping = None):
"""
Builds route instance.
Expand Down Expand Up @@ -399,22 +399,34 @@ def _build_phase(self, phase_structure: Mapping, kwargs: Mapping = None):

return phase

def _build_segment(self, segment_definition: Mapping, kwargs: Mapping) -> AbstractFlightSegment:
def _build_segment(self, segment_definition: Mapping, kwargs: dict) -> AbstractFlightSegment:
"""
Builds a flight segment according to provided definition.
:param segment_definition: the segment definition from mission file
:param kwargs: a preset of keyword arguments for AbstractFlightSegment instantiation
:param tag: the expected tag for specifying the segment type
:return: the FlightSegment instance
"""
segment_class = RegisterSegment.get_class(segment_definition[SEGMENT_TYPE_TAG])
part_kwargs = kwargs.copy()
part_kwargs.update(segment_definition)
part_kwargs.update(self._base_kwargs)
part_kwargs["polar_modifier"] = UnchangedPolar()
for key, value in part_kwargs.items():
if key == "polar":
value = Polar(value["CL"].value, value["CD"].value)
if key == POLAR_TAG:
modifier_kwargs = deepcopy(value.get("modifier"))
value = Polar(
cl=value["CL"].value,
cd=value["CD"].value,
alpha=value["alpha"].value if "alpha" in value else None,
)
if modifier_kwargs:
if isinstance(modifier_kwargs, InputDefinition):
modifier_kwargs = {NAME_TAG: modifier_kwargs.value}
modifier_class = RegisterPolarModifier.get_class(modifier_kwargs[NAME_TAG])
del modifier_kwargs[NAME_TAG]
self._replace_input_definitions_by_values(modifier_kwargs)
part_kwargs["polar_modifier"] = modifier_class(**modifier_kwargs)
elif key == "target":
if not isinstance(value, FlightPoint):
target_parameters = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The mission file provides a "human" definition of the mission.
Structures are the translation of this human definition, that is ready to
be transformed in a Python implementation.
be transformed into a Python implementation.
"""
# This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
# Copyright (C) 2023 ONERA & ISAE-SUPAERO
Expand All @@ -19,7 +19,6 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from abc import ABC, abstractmethod
from collections import OrderedDict
from copy import deepcopy
from dataclasses import InitVar, dataclass, field, fields
from itertools import chain
Expand All @@ -37,6 +36,7 @@
PARTS_TAG,
PHASE_DEFINITIONS_TAG,
PHASE_TAG,
POLAR_TAG,
ROUTE_DEFINITIONS_TAG,
ROUTE_TAG,
SEGMENT_TAG,
Expand Down Expand Up @@ -67,7 +67,7 @@ class AbstractStructureBuilder(ABC):
parent_name: str = None
variable_prefix: str = ""

_structure: OrderedDict = field(default=None, init=False)
_structure: dict = field(default=None, init=False)

_input_definitions: List[InputDefinition] = field(default_factory=list, init=False)
_builders: List[Tuple["AbstractStructureBuilder", dict]] = field(
Expand All @@ -82,13 +82,14 @@ def __init_subclass__(cls, *, structure_type=None):

def __post_init__(self, definition):
self._structure = self._build(definition)
self._structure[NAME_TAG] = self.qualified_name
if self.__class__.type:
self._structure[TYPE_TAG] = self.__class__.type
self._structure[NAME_TAG] = self.qualified_name
self._process_polar(self._structure)
self._parse_inputs(self._structure, self._input_definitions)

@property
def structure(self) -> OrderedDict:
def structure(self) -> dict:
"""A dictionary that is ready to be translated to the matching implementation."""
for builder, placeholder in self._builders:
placeholder.update(builder.structure)
Expand Down Expand Up @@ -124,7 +125,7 @@ def process_builder(self, builder: "AbstractStructureBuilder") -> dict:
return placeholder

@abstractmethod
def _build(self, definition: dict) -> OrderedDict:
def _build(self, definition: dict) -> dict:
"""
This method creates the needed structure dict.
Expand Down Expand Up @@ -171,9 +172,6 @@ def _parse_inputs(
"""

if isinstance(structure, dict):
if "polar" in structure:
structure["polar"] = self._process_polar(structure["polar"])

if "value" in structure:
input_definition = InputDefinition.from_dict(
parent, structure, part_identifier=part_identifier, prefix=self.variable_prefix
Expand Down Expand Up @@ -216,6 +214,8 @@ def _parse_inputs(

# Here structure is not a dict, hence a directly given value.
key, value = parent, structure
if key == POLAR_TAG:
return None
input_definition = InputDefinition(
key, value, part_identifier=part_identifier, prefix=self.variable_prefix
)
Expand All @@ -233,10 +233,9 @@ def _is_shape_by_conn(key, segment_class) -> bool:
segment_fields = [fld for fld in fields(segment_class) if fld.name == key]
return len(segment_fields) == 1 and issubclass(segment_fields[0].type, (list, np.ndarray))

@staticmethod
def _process_polar(polar_structure):
def _process_polar(self, structure):
"""
Polar data are handled specifically, as it a particular parameter of segments (
Polar data are handled specifically, as it is a particular parameter of segments (
a Polar class).
If "foo:bar:baz" is provided as `polar_structure`, it is replaced by the dict
Expand All @@ -246,21 +245,11 @@ def _process_polar(polar_structure):
Also, even if `polar_structure` is a dict, it is ensured that it has the structure above.
"""

if isinstance(polar_structure, str):
polar_structure = OrderedDict(
{
"CL": {"value": polar_structure + ":CL", "shape_by_conn": True},
"CD": {"value": polar_structure + ":CD", "shape_by_conn": True},
}
if POLAR_TAG in structure:
builder = PolarStructureBuilder(
structure[POLAR_TAG], "", self.qualified_name, self.variable_prefix
)
elif isinstance(polar_structure, dict):
for key in ["CL", "CD"]:
if isinstance(polar_structure[key], str):
polar_structure[key] = {"value": polar_structure[key], "shape_by_conn": True}
elif isinstance(polar_structure[key], dict):
polar_structure[key]["shape_by_conn"] = True

return polar_structure
structure[POLAR_TAG] = self.process_builder(builder)


class DefaultStructureBuilder(AbstractStructureBuilder):
Expand All @@ -270,8 +259,38 @@ class DefaultStructureBuilder(AbstractStructureBuilder):
:param definition: the definition for the part only
"""

def _build(self, definition: dict) -> OrderedDict:
return OrderedDict(deepcopy(definition))
def _build(self, definition: dict) -> dict:
return deepcopy(definition)


class PolarStructureBuilder(AbstractStructureBuilder, structure_type=POLAR_TAG):
"""
Structure builder for polar definition.
:param definition: the definition for the polar only
"""

def _build(self, definition: dict) -> dict:
polar_structure = {}
if isinstance(definition, str):
polar_structure = {
"CL": {"value": definition + ":CL", "shape_by_conn": True},
"CD": {"value": definition + ":CD", "shape_by_conn": True},
}

elif isinstance(definition, dict):
polar_structure = deepcopy(definition)
for key in ["CL", "CD", "alpha"]:
if key in polar_structure:
if isinstance(polar_structure[key], str):
polar_structure[key] = {
"value": polar_structure[key],
"shape_by_conn": True,
}
elif isinstance(polar_structure[key], dict):
polar_structure[key]["shape_by_conn"] = True

return polar_structure


class SegmentStructureBuilder(AbstractStructureBuilder, structure_type=SEGMENT_TAG):
Expand All @@ -281,8 +300,8 @@ class SegmentStructureBuilder(AbstractStructureBuilder, structure_type=SEGMENT_T
:param definition: the definition for the segment only
"""

def _build(self, definition: dict) -> OrderedDict:
segment_structure = OrderedDict(deepcopy(definition))
def _build(self, definition: dict) -> dict:
segment_structure = deepcopy(definition)
del segment_structure[SEGMENT_TAG]
segment_structure[SEGMENT_TYPE_TAG] = definition[SEGMENT_TAG]

Expand All @@ -296,9 +315,9 @@ class PhaseStructureBuilder(AbstractStructureBuilder, structure_type=PHASE_TAG):
:param definition: the whole content of definition file
"""

def _build(self, definition: dict) -> OrderedDict:
def _build(self, definition: dict) -> dict:
phase_definition = definition[PHASE_DEFINITIONS_TAG][self.name]
phase_structure = OrderedDict(deepcopy(phase_definition))
phase_structure = deepcopy(phase_definition)

for i, part in enumerate(phase_definition[PARTS_TAG]):
if PHASE_TAG in part:
Expand All @@ -312,7 +331,6 @@ def _build(self, definition: dict) -> OrderedDict:
else:
raise RuntimeError(f"Unexpected structure in definition of phase {self.name}")

phase_structure[PARTS_TAG][i] = {}
phase_structure[PARTS_TAG][i] = self.process_builder(builder)

return phase_structure
Expand All @@ -325,9 +343,9 @@ class RouteStructureBuilder(AbstractStructureBuilder, structure_type=ROUTE_TAG):
:param definition: the whole content of definition file
"""

def _build(self, definition: dict) -> OrderedDict:
def _build(self, definition: dict) -> dict:
route_definition = definition[ROUTE_DEFINITIONS_TAG][self.name]
route_structure = OrderedDict(deepcopy(route_definition))
route_structure = deepcopy(route_definition)

route_structure[CLIMB_PARTS_TAG] = self._get_route_climb_or_descent_structure(
definition, route_definition[CLIMB_PARTS_TAG]
Expand All @@ -342,6 +360,11 @@ def _build(self, definition: dict) -> OrderedDict:
definition, route_definition[DESCENT_PARTS_TAG]
)

if POLAR_TAG in route_structure:
builder = PolarStructureBuilder(
route_structure[POLAR_TAG], "", self.qualified_name, self.variable_prefix
)
route_structure[POLAR_TAG] = self.process_builder(builder)
return route_structure

def _get_route_climb_or_descent_structure(self, global_definition, parts_definition):
Expand All @@ -363,9 +386,9 @@ class MissionStructureBuilder(AbstractStructureBuilder, structure_type="mission"
:param definition: the whole content of definition file
"""

def _build(self, definition: dict) -> OrderedDict:
def _build(self, definition: dict) -> dict:
mission_definition = definition[MISSION_DEFINITION_TAG][self.name]
mission_structure = OrderedDict(deepcopy(mission_definition))
mission_structure = deepcopy(mission_definition)

mission_parts = []
for part_definition in mission_definition[PARTS_TAG]:
Expand Down

0 comments on commit 8c4f0b7

Please sign in to comment.