Skip to content

Commit

Permalink
Merge pull request #471 from fast-aircraft-design/payload-range-compo…
Browse files Browse the repository at this point in the history
…nent

Payload range component
  • Loading branch information
christophe-david committed Feb 3, 2023
2 parents 7fb9de6 + aa248ec commit 0e3cba3
Show file tree
Hide file tree
Showing 29 changed files with 2,060 additions and 334 deletions.
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ stdatm
Deprecated
click
importlib-metadata
pyDOE2
sphinx
sphinx-rtd-theme
sphinxcontrib-bibtex
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Deprecated = "^1.2.13"
click = "^8.0.3"
importlib-metadata = { version = "^4.2", python = "<3.10" }
mpi4py = {version = "^3", optional = true}
pyDOE2 = "^1.3.0"

[tool.poetry.extras]
mpi4py = ["mpi4py"]
Expand Down
28 changes: 16 additions & 12 deletions src/fastoad/models/performances/mission/mission.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def compute_from(self, start: FlightPoint) -> pd.DataFrame:

def get_reserve_fuel(self):
""":returns: the fuel quantity for reserve, obtained after mission computation."""
if not self.reserve_ratio:
if not self.reserve_ratio or not self.part_flight_points:
return 0.0

reserve_points = self.part_flight_points[-1]
Expand Down Expand Up @@ -130,17 +130,21 @@ def _solve_cruise_distance(self, start: FlightPoint) -> pd.DataFrame:
matches provided consumed fuel.
"""

self.first_route.solve_distance = False
input_cruise_distance = self.first_route.flight_distance

root_scalar(
self._compute_flight,
args=(start,),
x0=input_cruise_distance * 0.5,
x1=input_cruise_distance * 1.0,
xtol=self.fuel_accuracy,
method="secant",
)
if self.target_fuel_consumption == 0.0:
start.name = self.name
self._flight_points = pd.DataFrame([start, start])
else:
self.first_route.solve_distance = False
input_cruise_distance = self.first_route.flight_distance

root_scalar(
self._compute_flight,
args=(start,),
x0=input_cruise_distance * 0.5,
x1=input_cruise_distance * 1.0,
xtol=self.fuel_accuracy,
method="secant",
)

return self._flight_points

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ class InputDefinition:
#: Value of the "shape_by_conn" openMDAO flag for input declaration.
shape_by_conn: bool = False

#: Prefix used when replacement of "~" is needed.
prefix: str = ""

#: Used only for tests
variable_name: InitVar[Optional[str]] = None

Expand Down Expand Up @@ -118,7 +121,7 @@ def value(self):
return self.input_value

@classmethod
def from_dict(cls, parameter_name, definition_dict: dict, part_identifier=None):
def from_dict(cls, parameter_name, definition_dict: dict, part_identifier=None, prefix=None):
"""
Instantiates InputDefinition from definition_dict.
Expand All @@ -129,6 +132,7 @@ def from_dict(cls, parameter_name, definition_dict: dict, part_identifier=None):
:param definition_dict: dict with keys ("value", "unit", "default"). "unit" and "default"
are optional.
:param part_identifier: used if "~" is in definition_dict["value"]
:param prefix: used if "~" is in definition_dict["value"]
"""
if "value" not in definition_dict:
return None
Expand All @@ -140,6 +144,7 @@ def from_dict(cls, parameter_name, definition_dict: dict, part_identifier=None):
default_value=definition_dict.get("default", np.nan),
shape_by_conn=definition_dict.get("shape_by_conn", False),
part_identifier=part_identifier,
prefix=prefix,
)
return input_def

Expand Down Expand Up @@ -208,7 +213,7 @@ def variable_name(self, var_name: Optional[str]):

if not prefix:
# If nothing before "~", a default value is used
prefix = "data:mission"
prefix = self.prefix
if not suffix:
# If nothing after "~", the parameter name is used
suffix = self.parameter_name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,16 +60,31 @@ def __init__(
*,
propulsion: IPropulsion = None,
reference_area: float = None,
mission_name: Optional[str] = None,
variable_prefix: str = "data:mission",
):
"""
:param mission_definition: a file path or MissionDefinition instance
:param propulsion: if not provided, the property :attr:`propulsion` must be
set before calling :meth:`build`
:param reference_area: if not provided, the property :attr:`reference_area` must be
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] = {}
self.definition = mission_definition

#: The prefix for auto-generated variable names
self.variable_prefix: str = variable_prefix

#: The definition of missions as provided in input file.
self.definition: MissionDefinition = mission_definition

self._mission_name = mission_name

self._base_kwargs = {"reference_area": reference_area, "propulsion": propulsion}

@property
Expand All @@ -88,13 +103,7 @@ def definition(self, mission_definition: Union[str, MissionDefinition]):
else:
self._definition = mission_definition

for mission_name in self._definition[MISSION_DEFINITION_TAG]:
self._structure_builders[mission_name] = MissionStructureBuilder(
self._definition, mission_name
)

if self.get_input_weight_variable_name(mission_name) is None:
self._add_default_taxi_takeoff(mission_name)
self._update_structure_builders()

@property
def propulsion(self) -> IPropulsion:
Expand All @@ -114,35 +123,75 @@ def reference_area(self) -> float:
def reference_area(self, reference_area: float):
self._base_kwargs["reference_area"] = reference_area

@property
def mission_name(self):
"""The mission name, in case it has been specified, or if it is unique in the file."""
if self._mission_name is None:
self._mission_name = self.get_unique_mission_name()

return self._mission_name

@mission_name.setter
def mission_name(self, value):
if value is None:
self._mission_name = None
elif value in self.definition[MISSION_DEFINITION_TAG]:
self._mission_name = value
else:
raise FastMissionFileMissingMissionNameError(
f'No Mission named "{value}" in provided mission file.'
)

def build(self, inputs: Optional[Mapping] = None, mission_name: str = None) -> Mission:
"""
Builds the flight sequence from definition file.
:param inputs: if provided, any input parameter that is a string which matches
a key of `inputs` will be replaced by the corresponding value
:param mission_name: mission name (can be omitted if only one mission is defined)
:param mission_name: mission name (can be omitted if only one mission is defined or if
:attr:`mission` has been defined)
:return:
"""
if mission_name is None:
mission_name = self.mission_name

if self.get_input_weight_variable_name(mission_name) is None:
self._add_default_taxi_takeoff(mission_name)

for input_def in self._structure_builders[mission_name].get_input_definitions():
input_def.set_variable_value(inputs)
if mission_name is None:
mission_name = self.get_unique_mission_name()

mission = self._build_mission(self._structure_builders[mission_name].structure)
return mission

def get_route_names(self, mission_name: str = None) -> List[str]:
"""
:param mission_name:
:return: a list with names of all routes in the mission, in order.
"""
if mission_name is None:
mission_name = self.mission_name

mission_parts = self.definition[MISSION_DEFINITION_TAG][mission_name][PARTS_TAG]
route_names = [part[ROUTE_TAG] for part in mission_parts if ROUTE_TAG in part]

return route_names

def get_route_ranges(
self, inputs: Optional[Mapping] = None, mission_name: str = None
) -> List[float]:
"""
:param inputs: if provided, any input parameter that is a string which matches
a key of `inputs` will be replaced by the corresponding value
:param mission_name: mission name (can be omitted if only one mission is defined)
:param mission_name: mission name (can be omitted if only one mission is defined or if
:attr:`mission` has been defined)
:return: list of flight ranges for each element of the flight sequence that is a route
"""
if mission_name is None:
mission_name = self.mission_name

routes = self.build(inputs, mission_name)
return [route.flight_distance for route in routes if isinstance(route, RangedRoute)]

Expand All @@ -152,12 +201,13 @@ def get_reserve(self, flight_points: pd.DataFrame, mission_name: str = None) ->
:param flight_points: the dataframe returned by compute_from() method of the
instance returned by :meth:`build`
:param mission_name: mission name (can be omitted if only one mission is defined)
:param mission_name: mission name (can be omitted if only one mission is defined or if
:attr:`mission` has been defined)
:return: the reserve fuel mass in kg, or 0.0 if no reserve is defined.
"""

if mission_name is None:
mission_name = self.get_unique_mission_name()
mission_name = self.mission_name

last_part_spec = self._get_mission_part_structures(mission_name)[-1]
if RESERVE_TAG in last_part_spec:
Expand All @@ -176,11 +226,12 @@ def get_input_variables(self, mission_name=None) -> VariableList:
"""
Identify variables for a defined mission.
:param mission_name: mission name (can be omitted if only one mission is defined)
:param mission_name: mission name (can be omitted if only one mission is defined or if
:attr:`mission` has been defined)
:return: a VariableList instance.
"""
if mission_name is None:
mission_name = self.get_unique_mission_name()
mission_name = self.mission_name

input_definition = VariableList()
for input_def in self._structure_builders[mission_name].get_input_definitions():
Expand All @@ -204,18 +255,29 @@ def get_unique_mission_name(self) -> str:
"Mission name must be specified if several missions are defined in mission file."
)

def get_input_weight_variable_name(self, mission_name: str) -> Optional[str]:
def get_input_weight_variable_name(self, mission_name: str = None) -> Optional[str]:
"""
Search the mission structure for a segment that has a target absolute mass defined and
returns the associated variable name.
:param mission_name:
:param mission_name: mission name (can be omitted if only one mission is defined or if
:attr:`mission` has been defined)
:return: The variable name, or None if no target mass found.
"""

return self._get_input_weight_variable_name_in_structure(
self._get_mission_part_structures(mission_name)
)

def _update_structure_builders(self):
for mission_name in self._definition[MISSION_DEFINITION_TAG]:
self._structure_builders[mission_name] = MissionStructureBuilder(
self._definition, mission_name, variable_prefix=self.variable_prefix
)

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:
"""
Builds mission instance from provided structure.
Expand Down Expand Up @@ -464,10 +526,10 @@ def _add_default_taxi_takeoff(self, mission_name):
}
}

taxi_out = PhaseStructureBuilder(definition, "taxi_out", mission_name)
taxi_out = PhaseStructureBuilder(definition, "taxi_out", mission_name, self.variable_prefix)
taxi_out_structure = self._structure_builders[mission_name].process_builder(taxi_out)
self._structure_builders[mission_name].structure[PARTS_TAG].insert(0, taxi_out_structure)

takeoff = PhaseStructureBuilder(definition, "takeoff", mission_name)
takeoff = PhaseStructureBuilder(definition, "takeoff", mission_name, self.variable_prefix)
takeoff_structure = self._structure_builders[mission_name].process_builder(takeoff)
self._structure_builders[mission_name].structure[PARTS_TAG].insert(1, takeoff_structure)

0 comments on commit 0e3cba3

Please sign in to comment.