Skip to content

Commit

Permalink
Merge pull request #485 from fast-aircraft-design/mission-registering…
Browse files Browse the repository at this point in the history
…-mechanism

New mission registering mechanism
  • Loading branch information
christophe-david committed Mar 31, 2023
2 parents f95d25c + 27ca43e commit 16c4c95
Show file tree
Hide file tree
Showing 21 changed files with 238 additions and 122 deletions.
119 changes: 113 additions & 6 deletions src/fastoad/models/performances/mission/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Base classes for mission computation."""
# 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 @@ -15,12 +15,13 @@
from abc import ABC, abstractmethod
from copy import deepcopy
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Dict, List, Optional

import pandas as pd

from fastoad.model_base import FlightPoint
from fastoad.model_base.datacls import BaseDataClass
from .exceptions import FastUnknownMissionElementError


@dataclass
Expand All @@ -46,7 +47,7 @@ def compute_from(self, start: FlightPoint) -> pd.DataFrame:


@dataclass
class FlightSequence(IFlightPart, list):
class FlightSequence(IFlightPart):
"""
Defines and computes a flight sequence.
Expand All @@ -60,14 +61,16 @@ class FlightSequence(IFlightPart, list):
# running :meth:`compute_from`
part_flight_points: List[pd.DataFrame] = field(default_factory=list, init=False)

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

def compute_from(self, start: FlightPoint) -> pd.DataFrame:
self.part_flight_points = []
part_start = deepcopy(start)
part_start.scalarize()

self.consumed_mass_before_input_weight = 0.0
consumed_mass = 0.0
for part in self:
for part in self._sequence:
# This check has to be done first because relative target parameters
# will be made absolute during compute_from()
part_has_target_mass = not (part.target.mass is None or part.target.is_relative("mass"))
Expand Down Expand Up @@ -116,12 +119,116 @@ 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) > 0:
return self[-1].target
if len(self._sequence) > 0:
return self._sequence[-1].target

return None

def append(self, flight_part: IFlightPart):
"""Append flight part to the end of the sequence."""
self._sequence.append(flight_part)

def clear(self):
"""Remove all parts from flight sequence."""
self._sequence.clear()
self.part_flight_points.clear()
self.consumed_mass_before_input_weight = 0.0

def extend(self, seq):
"""Extend flight sequence by appending elements from the iterable."""
self._sequence.extend(seq)

def index(self, *args, **kwargs):
"""Return first index of value (see list.index())."""
return self._sequence.index(*args, **kwargs)

def __len__(self):
return len(self._sequence)

def __getitem__(self, item):
return self._sequence[item]

def __add__(self, other):
result = self.__class__()
result.extend(other)
return result

def __iter__(self):
return iter(self._sequence)


class RegisterElement:
"""
Base class for decorators that can associate a class with a keyword.
When subclassing, the argument 'base_class' allow to specify a class that should be
a parent of all registered classes. A specific check will be done at register time.
>>> class RegisterFeature(RegisterElement, base_class=AbstractFeature)
>>> ...
Then the newly created class may be used as decorator like:
>>> @RegisterFeature("identifier_foo")
>>> class FooFeature(AbstractFeature):
>>> ...
Then the registered class can be obtained by:
>>> my_class = RegisterFeature.get_class("identifier_foo")
"""

_base_class = object
_keyword_vs_implementation: Dict[str, type] = {}

@classmethod
def __init_subclass__(cls, *, base_class=object):
cls._base_class = base_class

def __init__(self, keyword=""):
self._keyword = keyword

def __call__(self, class_to_register):
cls = type(self)
cls._add_element(self._keyword, class_to_register)
return class_to_register

@classmethod
def _add_element(cls, keyword: str, element_class: type):
"""
Adds an element definition.
:param keyword: element name (mission file keyword)
:param element_class: element implementation
"""
if issubclass(element_class, cls._base_class):
cls._keyword_vs_implementation[keyword] = element_class
else:
raise RuntimeWarning(
f'"{element_class}" is not registered because it'
f"does not derive from {cls._base_class}."
)

@classmethod
def get_class(cls, keyword) -> Optional[type]:
"""
Provides the element implementation for provided name.
:param keyword:
:return: the element implementation
:raise FastUnknownMissionElementError: if element has not been declared.
"""
element_class = cls._keyword_vs_implementation.get(keyword)

if element_class is None:
raise FastUnknownMissionElementError(keyword)

return element_class

@classmethod
def get_classes(cls) -> Dict[str, type]:
"""
:return: dict that associates keywords to their registered class.
"""
return cls._keyword_vs_implementation.copy()
13 changes: 12 additions & 1 deletion src/fastoad/models/performances/mission/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Exceptions for mission package."""
# This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
# Copyright (C) 2020 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 @@ -31,3 +31,14 @@ class FastFlightSegmentIncompleteFlightPoint(FastError):
"""
Raised when a segment computation encounters a FlightPoint instance without needed parameters.
"""


class FastUnknownMissionElementError(FastError):
"""Raised when an undeclared element type is requested."""

def __init__(self, element_type: str):
self.segment_type = element_type

msg = f'Element type "{element_type}" has not been declared.'

super().__init__(self, msg)
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
from ...mission import Mission
from ...polar import Polar
from ...routes import RangedRoute
from ...segments.base import AbstractFlightSegment, SegmentDefinitions
from ...segments.base import AbstractFlightSegment, RegisterSegment


class MissionBuilder:
Expand Down Expand Up @@ -408,7 +408,7 @@ def _build_segment(self, segment_definition: Mapping, kwargs: Mapping) -> Abstra
:param tag: the expected tag for specifying the segment type
:return: the FlightSegment instance
"""
segment_class = SegmentDefinitions.get_segment_class(segment_definition[SEGMENT_TYPE_TAG])
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
be transformed in a Python implementation.
"""
# 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 @@ -41,7 +41,7 @@
ROUTE_TAG,
SEGMENT_TAG,
)
from ...segments.base import SegmentDefinitions
from ...segments.base import RegisterSegment


@dataclass
Expand Down Expand Up @@ -183,7 +183,7 @@ def _parse_inputs(

part_identifier = structure.get(NAME_TAG, part_identifier)
if SEGMENT_TYPE_TAG in structure:
segment_class = SegmentDefinitions.get_segment_class(structure[SEGMENT_TYPE_TAG])
segment_class = RegisterSegment.get_class(structure[SEGMENT_TYPE_TAG])
else:
segment_class = None
for key, value in structure.items():
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# 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 @@ -26,7 +26,10 @@
from fastoad.model_base.propulsion import IPropulsion
from fastoad.models.performances.mission.base import FlightSequence
from fastoad.models.performances.mission.segments.altitude_change import AltitudeChangeSegment
from fastoad.models.performances.mission.segments.base import AbstractFlightSegment
from fastoad.models.performances.mission.segments.base import (
AbstractFlightSegment,
RegisteredSegment,
)
from fastoad.models.performances.mission.segments.hold import HoldSegment
from fastoad.models.performances.mission.segments.mass_input import MassTargetSegment
from fastoad.models.performances.mission.segments.speed_change import SpeedChangeSegment
Expand All @@ -42,8 +45,9 @@
DATA_FOLDER_PATH = pth.join(pth.dirname(__file__), "data")


# For this class, we purposely use the deprecated inheritance mechanism
@dataclass
class TestSegment(AbstractFlightSegment, mission_file_keyword="test_segment_B"):
class TestSegment(AbstractFlightSegment, RegisteredSegment, mission_file_keyword="test_segment_B"):
scalar_parameter: float = MANDATORY_FIELD
vector_parameter_1: np.ndarray = MANDATORY_FIELD
vector_parameter_2: np.ndarray = MANDATORY_FIELD
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from fastoad.model_base.datacls import MANDATORY_FIELD
from ..mission_run import AdvancedMissionComp
from ..mission_wrapper import MissionWrapper
from ...segments.base import AbstractFlightSegment
from ...segments.base import AbstractFlightSegment, RegisterSegment

DATA_FOLDER_PATH = pth.join(pth.dirname(__file__), "data")
RESULTS_FOLDER_PATH = pth.join(pth.dirname(__file__), "results")
Expand All @@ -35,8 +35,9 @@ def cleanup():
rmtree(RESULTS_FOLDER_PATH, ignore_errors=True)


@RegisterSegment("test_segment_A")
@dataclass
class TestSegment(AbstractFlightSegment, mission_file_keyword="test_segment_A"):
class TestSegment(AbstractFlightSegment):
scalar_parameter: float = MANDATORY_FIELD
vector_parameter_1: np.ndarray = MANDATORY_FIELD
vector_parameter_2: np.ndarray = MANDATORY_FIELD
Expand Down
18 changes: 17 additions & 1 deletion src/fastoad/models/performances/mission/polar_modifier.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
""" Aerodynamics polar modifier."""
# This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
# Copyright (C) 2021 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 @@ -16,6 +16,7 @@
from dataclasses import dataclass

from fastoad.model_base.flight_point import FlightPoint
from .base import RegisterElement
from .polar import Polar


Expand All @@ -36,6 +37,20 @@ def modify_polar(self, polar: Polar, flight_point: FlightPoint) -> Polar:
"""


class RegisterPolarModifier(RegisterElement, base_class=AbstractPolarModifier):
"""
Decorator for registering AbstractPolarModifier classes.
>>> @RegisterPolarModifier("polar_modifier_foo")
>>> class FooPolarModifier(IFlightPart):
>>> ...
Then the registered class can be obtained by:
>>> my_class = RegisterPolarModifier.get_class("polar_modifier_foo")
"""


@dataclass
class UnchangedPolar(AbstractPolarModifier):
"""
Expand All @@ -51,6 +66,7 @@ def modify_polar(self, polar: Polar, flight_point: FlightPoint) -> Polar:
return polar


@RegisterPolarModifier("ground_effect_raymer")
@dataclass
class GroundEffectRaymer(AbstractPolarModifier):

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Classes for climb/descent segments."""
# 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 @@ -21,18 +21,16 @@
from scipy.constants import foot, g

from fastoad.model_base import FlightPoint
from .base import AbstractManualThrustSegment
from .base import AbstractManualThrustSegment, RegisterSegment
from ..exceptions import FastFlightSegmentIncompleteFlightPoint
from ..util import get_closest_flight_level

_LOGGER = logging.getLogger(__name__) # Logger for this module


@RegisterSegment("altitude_change")
@dataclass
class AltitudeChangeSegment(
AbstractManualThrustSegment,
mission_file_keyword="altitude_change",
):
class AltitudeChangeSegment(AbstractManualThrustSegment):
"""
Computes a flight path segment where altitude is modified with constant speed.
Expand Down

0 comments on commit 16c4c95

Please sign in to comment.