├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── adr ├── Components │ ├── Aerodynamic │ │ ├── AerodynamicSurface.py │ │ ├── Flap.py │ │ ├── RectangularAerodynamicSurface.py │ │ ├── RectangularHorizontalStabilizer.py │ │ ├── RectangularWing.py │ │ └── __init__.py │ ├── AttachedComponent.py │ ├── Auxiliary │ │ ├── LandingGear.py │ │ ├── Payload.py │ │ └── __init__.py │ ├── BaseComponent.py │ ├── FreeBody.py │ ├── Powertrain │ │ ├── Motor.py │ │ ├── SimpleMotor.py │ │ └── __init__.py │ └── __init__.py ├── Methods │ ├── Aerodynamic │ │ ├── __init__.py │ │ └── aerodynamic_fundamental_equations.py │ ├── Powertrain │ │ ├── __init__.py │ │ └── thrust_equations.py │ └── __init__.py ├── World │ ├── Aerodynamic │ │ ├── __init__.py │ │ └── coefficients_data.py │ ├── Ambient.py │ ├── __init__.py │ └── constants.py ├── __init__.py └── helper_functions │ ├── __init__.py │ └── algebric.py ├── docs ├── Components │ ├── AttachedComponent.md │ ├── Auxiliary │ │ └── LandingGear.md │ ├── BaseComponent.md │ └── FreeBody.md ├── World │ ├── Ambient.md │ └── constants.md ├── about.md ├── helper functions │ └── algebric.md └── index.md ├── example.py ├── mkdocs.yml ├── poetry.lock ├── pyproject.toml └── tests ├── Components ├── Auxiliary │ └── test_LandingGear.py ├── test_AttachedComponent.py ├── test_BaseComponent.py └── test_FreeBody.py ├── World ├── test_Ambient.py └── test_constants.py └── helper_functions └── test_algebric.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-python@v2 12 | with: 13 | python-version: 3.x 14 | - run: pip install mkdocs-material 15 | - run: mkdocs gh-deploy --force 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *adr.ca.egg-info 3 | .vscode -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.8" 4 | before_install: 5 | - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python 6 | - source $HOME/.poetry/env 7 | install: 8 | - poetry install 9 | before_script: 10 | - pip install coveralls 11 | script: 12 | - coverage run --source adr -m pytest 13 | after_script: 14 | - coveralls 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 The Python Packaging Authority 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ADR 2 | [![Build Status](https://travis-ci.com/CeuAzul/ADR.svg?branch=master)](https://travis-ci.com/CeuAzul/ADR) 3 | [![Coverage Status](https://coveralls.io/repos/github/CeuAzul/ADR/badge.svg)](https://coveralls.io/github/CeuAzul/ADR) 4 | 5 | 6 | Aircraft Design Resources aims to help engineers on conceptual design analysis, giving them the tools necessary to easily simulate different aircraft designs. 7 | 8 | ## Installation 9 | ``` 10 | pip install adr.ca 11 | ``` 12 | 13 | ## Usage 14 | For information on how to use ADR, [check our docs](https://CeuAzul.github.io/ADR). 15 | We try to make it as easy to follow as possible, but if you have any issue or 16 | question, or suggestions regarding the library or the docs, please 17 | [post an issue](https://github.com/CeuAzul/ADR/issues) so we can help you out. 18 | 19 | ## Development 20 | ADR uses [Poetry](https://python-poetry.org/) for dependency management. You can 21 | install poetry [with this single command](https://python-poetry.org/docs/#installation). 22 | Poetry ensures you are developing on a separated environment and with the same 23 | dependencies as the other developers. Once poetry is installed, follow those steps: 24 | ``` 25 | git clone https://github.com/CeuAzul/ADR.git 26 | cd ADR 27 | poetry install 28 | ``` 29 | After that you will need to use the Python virtualenv created by Poetry while developing for ADR. For instructions on how to set the virtualenv on VSCode [you can take a look at our wiki](https://github.com/CeuAzul/ADR/wiki/Use-Poetry-with-Visual-Studio-Code). 30 | ## Contributors 31 | This project exists thanks to all the people who contribute. 32 | 33 | [![Contributors](https://contributors-img.web.app/image?repo=CeuAzul/ADR)](https://github.com/CeuAzul/ADR/graphs/contributors) 34 | -------------------------------------------------------------------------------- /adr/Components/Aerodynamic/AerodynamicSurface.py: -------------------------------------------------------------------------------- 1 | import attr 2 | import math 3 | from vec import Vector2 4 | 5 | from adr.Components import AttachedComponent 6 | from adr.Methods.Aerodynamic.aerodynamic_fundamental_equations import get_lift, get_drag, get_moment 7 | from adr.World.Aerodynamic.coefficients_data import get_CL, get_CD, get_CM, get_CL_inv, get_CD_inv, get_CM_inv 8 | 9 | 10 | @attr.s(auto_attribs=True) 11 | class AerodynamicSurface(AttachedComponent): 12 | type: str = 'aerodynamic_surface' 13 | inverted: bool = False 14 | span: float = None 15 | chord: float = None 16 | 17 | def __attrs_post_init__(self): 18 | self.add_external_force_function('lift', self.get_lift) 19 | self.add_external_force_function('drag', self.get_drag) 20 | self.add_external_moment_function('moment', self.get_moment) 21 | 22 | @property 23 | def area(self): 24 | return -1 25 | 26 | @property 27 | def mean_aerodynamic_chord(self): 28 | return -1 29 | 30 | @property 31 | def aerodynamic_center(self): 32 | return -1 33 | 34 | def get_lift(self): 35 | if self.inverted: 36 | CL = get_CL_inv(self.angle_of_attack) 37 | else: 38 | CL = get_CL(self.angle_of_attack) 39 | lift_mag = get_lift( 40 | self.ambient.air_density, 41 | self.velocity.r, 42 | self.area, 43 | CL) 44 | lift_angle = math.radians(90) - self.angle_of_attack 45 | lift = Vector2(r=lift_mag, theta=lift_angle) 46 | return lift, self.aerodynamic_center 47 | 48 | def get_drag(self): 49 | if self.inverted: 50 | CD = get_CD_inv(self.angle_of_attack) 51 | else: 52 | CD = get_CD(self.angle_of_attack) 53 | drag_mag = get_drag( 54 | self.ambient.air_density, 55 | self.velocity.r, 56 | self.area, 57 | CD) 58 | drag_angle = math.radians(180) - self.angle_of_attack 59 | drag = Vector2(r=drag_mag, theta=drag_angle) 60 | return drag, self.aerodynamic_center 61 | 62 | def get_moment(self): 63 | if self.inverted: 64 | CM = get_CM_inv(self.angle_of_attack) 65 | else: 66 | CM = get_CM(self.angle_of_attack) 67 | moment_mag = get_moment(self.ambient.air_density, 68 | self.velocity.r, 69 | self.area, 70 | CM, 71 | self.mean_aerodynamic_chord) 72 | return moment_mag 73 | -------------------------------------------------------------------------------- /adr/Components/Aerodynamic/Flap.py: -------------------------------------------------------------------------------- 1 | import attr 2 | 3 | from adr.Components import AttachedComponent 4 | 5 | 6 | @attr.s(auto_attribs=True) 7 | class Flap(AttachedComponent): 8 | type: str = 'flap' 9 | width: float = None 10 | height: float = None 11 | -------------------------------------------------------------------------------- /adr/Components/Aerodynamic/RectangularAerodynamicSurface.py: -------------------------------------------------------------------------------- 1 | import attr 2 | from vec import Vector2 3 | 4 | from adr.Components.Aerodynamic import AerodynamicSurface 5 | 6 | 7 | @attr.s(auto_attribs=True) 8 | class RectangularAerodynamicSurface(AerodynamicSurface): 9 | type: str = 'rectangular_aerodynamic_surface' 10 | span: float = None 11 | chord: float = None 12 | 13 | @property 14 | def area(self): 15 | return self.span*self.chord 16 | 17 | @property 18 | def mean_aerodynamic_chord(self): 19 | return self.chord 20 | 21 | @property 22 | def aerodynamic_center(self): 23 | return Vector2(-0.25 * self.mean_aerodynamic_chord, 0) 24 | -------------------------------------------------------------------------------- /adr/Components/Aerodynamic/RectangularHorizontalStabilizer.py: -------------------------------------------------------------------------------- 1 | import attr 2 | import math 3 | from vec import Vector2 4 | 5 | from adr.Components.Aerodynamic import RectangularAerodynamicSurface 6 | 7 | 8 | @attr.s(auto_attribs=True) 9 | class RectangularHorizontalStabilizer(RectangularAerodynamicSurface): 10 | type: str = 'horizontal_stabilizer' 11 | inverted: bool = True 12 | -------------------------------------------------------------------------------- /adr/Components/Aerodynamic/RectangularWing.py: -------------------------------------------------------------------------------- 1 | import attr 2 | import math 3 | from vec import Vector2 4 | 5 | from adr.Components.Aerodynamic import RectangularAerodynamicSurface 6 | 7 | 8 | @attr.s(auto_attribs=True) 9 | class RectangularWing(RectangularAerodynamicSurface): 10 | type: str = 'wing' 11 | -------------------------------------------------------------------------------- /adr/Components/Aerodynamic/__init__.py: -------------------------------------------------------------------------------- 1 | from .AerodynamicSurface import AerodynamicSurface 2 | from .RectangularAerodynamicSurface import RectangularAerodynamicSurface 3 | from .RectangularHorizontalStabilizer import RectangularHorizontalStabilizer 4 | from .RectangularWing import RectangularWing 5 | from .Flap import Flap 6 | -------------------------------------------------------------------------------- /adr/Components/AttachedComponent.py: -------------------------------------------------------------------------------- 1 | import attr 2 | from typing import Dict, Callable 3 | from vec import Vector2 4 | import math 5 | 6 | from adr.helper_functions import transform 7 | from adr.Components import BaseComponent 8 | 9 | 10 | @attr.s(auto_attribs=True) 11 | class AttachedComponent(BaseComponent): 12 | parent: 'Component' = None 13 | relative_position: Vector2 = None 14 | relative_angle: float = None 15 | 16 | # State attributes 17 | actuation_angle: float = 0.0 18 | 19 | def reset_state(self): 20 | self.actuation_angle = 0.0 21 | super().reset_state() 22 | 23 | def set_parent(self, parent) -> None: 24 | if self.parent is not None: 25 | raise Exception(f'Component already has a parent: {self.parent}') 26 | self.parent = parent 27 | parent.append_child(self) 28 | 29 | @property 30 | def position(self) -> Vector2: 31 | return transform(self.relative_position, self.parent.angle, *self.parent.position) 32 | 33 | @property 34 | def angle(self) -> float: 35 | return self.parent.angle + self.relative_angle + self.actuation_angle 36 | 37 | @property 38 | def velocity(self) -> Vector2: 39 | return self.parent.velocity + None 40 | 41 | @property 42 | def ambient(self): 43 | return self.parent.ambient 44 | -------------------------------------------------------------------------------- /adr/Components/Auxiliary/LandingGear.py: -------------------------------------------------------------------------------- 1 | import attr 2 | import math 3 | from vec import Vector2 4 | 5 | from adr.World.constants import gravitational_acceleration 6 | from adr.Components import AttachedComponent 7 | from adr.Methods.Powertrain.thrust_equations import get_axial_thrust_from_linear_model 8 | 9 | 10 | @attr.s(auto_attribs=True) 11 | class LandingGear(AttachedComponent): 12 | type: str = 'landing_gear' 13 | height: float = None 14 | spring_coeff: float = None 15 | dump_coeff: float = None 16 | friction_coeff: float = None 17 | 18 | def __attrs_post_init__(self): 19 | self.add_external_force_function( 20 | 'gear_reaction', self.gear_reaction) 21 | self.add_external_force_function( 22 | 'gear_friction', self.gear_friction) 23 | 24 | @property 25 | def floor_contact_point(self): 26 | return Vector2(0, -self.height) 27 | 28 | def gear_reaction(self): 29 | displacement = self.height-self.position.y 30 | axial_velocity = self.velocity.y 31 | if displacement > 0: 32 | reaction_mag = self.spring_coeff*displacement - self.dump_coeff*axial_velocity 33 | else: 34 | reaction_mag = 0 35 | reaction = Vector2(0, reaction_mag) 36 | return reaction, self.floor_contact_point 37 | 38 | def gear_friction(self): 39 | normal_force, contact_point = self.gear_reaction() 40 | 41 | if self.velocity.x > 0: 42 | velocity_direction = 1 43 | else: 44 | velocity_direction = -1 45 | 46 | friction_mag = self.friction_coeff * normal_force.r * velocity_direction 47 | 48 | friction = Vector2(-friction_mag, 0) 49 | return friction, self.floor_contact_point 50 | -------------------------------------------------------------------------------- /adr/Components/Auxiliary/Payload.py: -------------------------------------------------------------------------------- 1 | import attr 2 | 3 | from adr.Components import AttachedComponent 4 | 5 | 6 | @attr.s(auto_attribs=True) 7 | class Payload(AttachedComponent): 8 | type: str = 'payload' 9 | -------------------------------------------------------------------------------- /adr/Components/Auxiliary/__init__.py: -------------------------------------------------------------------------------- 1 | from .LandingGear import LandingGear 2 | from .Payload import Payload 3 | -------------------------------------------------------------------------------- /adr/Components/BaseComponent.py: -------------------------------------------------------------------------------- 1 | import attr 2 | from typing import Dict, Callable 3 | from vec import Vector2 4 | import math 5 | 6 | from adr.helper_functions import transform 7 | from adr.World.constants import gravitational_acceleration 8 | 9 | 10 | @attr.s(auto_attribs=True) 11 | class BaseComponent: 12 | name: str = None 13 | type: str = None 14 | mass: float = None 15 | children: Dict['str', 'Component'] = attr.Factory(dict) 16 | external_forces: Dict['str', Callable] = attr.Factory(dict) 17 | external_moments: Dict['str', Callable] = attr.Factory(dict) 18 | 19 | def append_child(self, child) -> None: 20 | self.children[child.name] = child 21 | setattr(self, child.name, child) 22 | 23 | def reset_state(self) -> None: 24 | self.reset_children_state() 25 | 26 | def reset_children_state(self) -> None: 27 | for component in self.children.values(): 28 | component.reset_state() 29 | 30 | def add_external_force_function(self, name, function): 31 | self.external_forces[name] = function 32 | 33 | def add_external_moment_function(self, name, function): 34 | self.external_moments[name] = function 35 | 36 | def force_and_moment_at_component_origin(self): 37 | force = Vector2(0, 0) 38 | moment = 0.0 39 | 40 | _force, _moment = self.force_and_moment_from_external_forces() 41 | force += _force 42 | moment += _moment 43 | 44 | _moment = self.moment_from_external_moments() 45 | moment += _moment 46 | 47 | _force, _moment = self.force_and_moment_from_children() 48 | force += _force 49 | moment += _moment 50 | 51 | return force, moment 52 | 53 | def force_and_moment_from_external_forces(self): 54 | force = Vector2(0.00001, 0) 55 | moment = 0.0 56 | for force_function in self.external_forces.values(): 57 | _force, _application_point = force_function() 58 | force += _force 59 | moment += _application_point.cross(_force) 60 | return force, moment 61 | 62 | def moment_from_external_moments(self): 63 | moment = 0.0 64 | for moment_function in self.external_moments.values(): 65 | moment += moment_function() 66 | return moment 67 | 68 | def force_and_moment_from_children(self): 69 | force = Vector2(0, 0) 70 | moment = 0.0 71 | 72 | for component in self.children.values(): 73 | _force, _moment = component.force_and_moment_at_component_origin() 74 | force += _force.rotated(component.relative_angle) 75 | moment += _moment 76 | moment += component.relative_position.cross( 77 | _force.rotated(component.relative_angle)) 78 | return force, moment 79 | 80 | @property 81 | def nested_components(self): 82 | nested_components = {} 83 | nested_components[self.name] = self 84 | for component_name, component in self.children.items(): 85 | nested_components[component_name] = component 86 | nested_components = {**nested_components, 87 | **component.nested_components} 88 | return nested_components 89 | 90 | @property 91 | def angle_of_attack(self) -> float: 92 | return self.angle - self.velocity.theta 93 | 94 | @property 95 | def total_mass(self): 96 | mass = 0 97 | components = self.nested_components 98 | for component in components.values(): 99 | mass += component.mass 100 | return mass 101 | 102 | @property 103 | def empty_mass(self): 104 | mass = 0 105 | components = self.nested_components 106 | for component in components.values(): 107 | if component.type != 'payload': 108 | mass += component.mass 109 | return mass 110 | -------------------------------------------------------------------------------- /adr/Components/FreeBody.py: -------------------------------------------------------------------------------- 1 | import attr 2 | from typing import Dict, Callable 3 | from vec import Vector2 4 | import math 5 | 6 | from adr.Components import BaseComponent 7 | from adr.World import gravitational_acceleration, Ambient 8 | 9 | 10 | @attr.s(auto_attribs=True) 11 | class FreeBody(BaseComponent): 12 | position_cg: Vector2 = None 13 | pitch_rot_inertia: float = None 14 | ambient: Ambient = None 15 | 16 | # State attributes 17 | position: Vector2 = Vector2(0, 0) 18 | angle: float = 0 19 | velocity: Vector2 = Vector2(0.00001, 0.00001) 20 | rot_velocity: float = 0 21 | 22 | def __attrs_post_init__(self): 23 | self.add_external_force_function('weight', self.get_total_weight) 24 | 25 | def reset_state(self): 26 | self.position = Vector2(0, 0) 27 | self.angle = 0 28 | self.velocity = Vector2(0.00001, 0.00001) 29 | self.rot_velocity = 0 30 | super().reset_state() 31 | 32 | @property 33 | def gravitational_center(self) -> Vector2: 34 | return self.position_cg 35 | 36 | def force_and_moment_at_cg(self): 37 | force_at_cg = Vector2(0, 0) 38 | moment_at_cg = 0 39 | 40 | force_at_origin, moment_at_origin = self.force_and_moment_at_component_origin() 41 | 42 | force_at_cg += force_at_origin 43 | moment_at_cg += moment_at_origin 44 | moment_at_cg += (Vector2(0, 0) - 45 | self.gravitational_center).cross(force_at_origin) 46 | return force_at_cg, moment_at_cg 47 | 48 | def get_total_weight(self): 49 | weight_mag = gravitational_acceleration*self.total_mass 50 | weight_angle = math.radians(-90) - self.angle 51 | weight = Vector2(r=weight_mag, theta=weight_angle) 52 | return weight, self.gravitational_center 53 | 54 | def move(self, time_step): 55 | total_force, moment_z = self.force_and_moment_at_cg() 56 | 57 | acc = total_force/self.total_mass 58 | ang_acc_z = moment_z/self.pitch_rot_inertia 59 | 60 | self.rot_velocity = self.rot_velocity + ang_acc_z * time_step 61 | self.angle = self.angle + self.rot_velocity * time_step 62 | 63 | self.velocity = self.velocity + acc * time_step 64 | self.position = self.position + self.velocity * time_step 65 | 66 | return total_force, moment_z 67 | -------------------------------------------------------------------------------- /adr/Components/Powertrain/Motor.py: -------------------------------------------------------------------------------- 1 | import attr 2 | import math 3 | from vec import Vector2 4 | 5 | from adr.Components import AttachedComponent 6 | from adr.Methods.Powertrain.thrust_equations import get_axial_thrust_from_linear_model 7 | 8 | 9 | @attr.s(auto_attribs=True) 10 | class Motor(AttachedComponent): 11 | type: str = 'motor' 12 | 13 | def __attrs_post_init__(self): 14 | self.add_external_force_function('thrust', self.get_thrust) 15 | 16 | @property 17 | def thrust_center(self): 18 | return Vector2(0, 0) 19 | 20 | def get_thrust(self): 21 | thrust = Vector2(0, 0) 22 | return thrust, self.thrust_center 23 | -------------------------------------------------------------------------------- /adr/Components/Powertrain/SimpleMotor.py: -------------------------------------------------------------------------------- 1 | import attr 2 | from vec import Vector2 3 | 4 | from adr.Components.Powertrain import Motor 5 | from adr.Methods.Powertrain.thrust_equations import get_axial_thrust_from_linear_model 6 | 7 | 8 | @attr.s(auto_attribs=True) 9 | class SimpleMotor(Motor): 10 | static_thrust: float = None 11 | linear_coefficient: float = None 12 | distance_origin_to_propeller: float = None 13 | 14 | @property 15 | def thrust_center(self): 16 | return Vector2(self.distance_origin_to_propeller, 0) 17 | 18 | def get_thrust(self): 19 | angle_of_attack = self.angle_of_attack 20 | axial_velocity = self.velocity.rotated(angle_of_attack).x 21 | 22 | thrust_mag = get_axial_thrust_from_linear_model( 23 | self.ambient.air_density, 24 | axial_velocity, 25 | self.static_thrust, 26 | self.linear_coefficient 27 | ) 28 | 29 | thrust = Vector2(thrust_mag, 0) 30 | return thrust, self.thrust_center 31 | -------------------------------------------------------------------------------- /adr/Components/Powertrain/__init__.py: -------------------------------------------------------------------------------- 1 | from .Motor import Motor 2 | from .SimpleMotor import SimpleMotor 3 | -------------------------------------------------------------------------------- /adr/Components/__init__.py: -------------------------------------------------------------------------------- 1 | from .BaseComponent import BaseComponent 2 | from .FreeBody import FreeBody 3 | from .AttachedComponent import AttachedComponent 4 | from .Auxiliary import LandingGear, Payload 5 | from .Powertrain import Motor, SimpleMotor 6 | from .Aerodynamic import AerodynamicSurface, RectangularAerodynamicSurface, RectangularHorizontalStabilizer, RectangularWing, Flap 7 | -------------------------------------------------------------------------------- /adr/Methods/Aerodynamic/__init__.py: -------------------------------------------------------------------------------- 1 | from .aerodynamic_fundamental_equations import get_lift, get_drag, get_moment 2 | -------------------------------------------------------------------------------- /adr/Methods/Aerodynamic/aerodynamic_fundamental_equations.py: -------------------------------------------------------------------------------- 1 | def get_lift(air_density, velocity, area, lift_coefficient): 2 | cond1 = air_density > 0 3 | cond2 = area > 0 4 | cond3 = velocity >= 0 5 | 6 | if cond1 and cond2 and cond3: 7 | return 0.5 * air_density * velocity ** 2 * area * lift_coefficient 8 | else: 9 | raise ValueError( 10 | f'Air density and area must be positive. \ 11 | Velocity must be equal or greater than zero. \ 12 | Found density={air_density}, area={area} and velocity = {velocity}.') 13 | 14 | 15 | def get_drag(air_density, velocity, area, drag_coefficient): 16 | cond1 = air_density > 0 17 | cond2 = area > 0 18 | cond3 = velocity >= 0 19 | cond4 = drag_coefficient >= 0 20 | 21 | if cond1 and cond2 and cond3 and cond4: 22 | return 0.5 * air_density * velocity ** 2 * area * drag_coefficient 23 | else: 24 | raise ValueError( 25 | f'Air density and area must be positive. \ 26 | Velocity and drag coefficient must be equal or greater than zero. \ 27 | Found density={air_density}, area={area}, velocity = {velocity} and CD = {drag_coefficient}.') 28 | 29 | 30 | def get_moment(air_density, velocity, area, moment_coefficient, chord): 31 | cond1 = air_density > 0 32 | cond2 = area > 0 33 | cond3 = velocity >= 0 34 | cond4 = chord > 0 35 | 36 | if cond1 and cond2 and cond3 and cond4: 37 | return 0.5 * air_density * velocity ** 2 * area * moment_coefficient * chord 38 | else: 39 | raise ValueError( 40 | f'Air density, area and chord must be positive. \ 41 | Velocity must be equal or greater than zero. \ 42 | Found density={air_density}, area={area}, velocity = {velocity} and chord = {chord}.') 43 | -------------------------------------------------------------------------------- /adr/Methods/Powertrain/__init__.py: -------------------------------------------------------------------------------- 1 | from .thrust_equations import get_axial_thrust_from_linear_model 2 | -------------------------------------------------------------------------------- /adr/Methods/Powertrain/thrust_equations.py: -------------------------------------------------------------------------------- 1 | def get_axial_thrust_from_linear_model(air_density, velocity, static_thrust, linear_coefficient): 2 | cond1 = air_density > 0 3 | cond2 = velocity >= 0 4 | 5 | if cond1 and cond2: 6 | air_density_factor = air_density/1.225 7 | return (static_thrust + linear_coefficient * velocity) * air_density_factor 8 | else: 9 | raise ValueError( 10 | f'Air density must be positive. \ 11 | Velocity must be equal or greater than zero. \ 12 | Found density={air_density} and velocity = {velocity}.') 13 | -------------------------------------------------------------------------------- /adr/Methods/__init__.py: -------------------------------------------------------------------------------- 1 | from .Aerodynamic import aerodynamic_fundamental_equations 2 | from .Powertrain import thrust_equations 3 | -------------------------------------------------------------------------------- /adr/World/Aerodynamic/__init__.py: -------------------------------------------------------------------------------- 1 | from .coefficients_data import get_CL, get_CD, get_CM, get_CL_inv, get_CD_inv, get_CM_inv 2 | -------------------------------------------------------------------------------- /adr/World/Aerodynamic/coefficients_data.py: -------------------------------------------------------------------------------- 1 | def get_CL(angle_of_attack): 2 | if angle_of_attack > 15 or angle_of_attack < -5: 3 | return 0 4 | else: 5 | return 0.6 + 0.1 * angle_of_attack 6 | 7 | 8 | def get_CD(angle_of_attack): 9 | if angle_of_attack > 15 or angle_of_attack < -5: 10 | return 10 * (0.06 + 0.01 * 15) 11 | else: 12 | return 0.06 + 0.01 * angle_of_attack 13 | 14 | 15 | def get_CM(angle_of_attack): 16 | a = 0.0012365 17 | b = -0.016365 18 | c = -0.2327 19 | if angle_of_attack > 15 or angle_of_attack < -5: 20 | return 0 21 | else: 22 | return a * angle_of_attack**2 + b * angle_of_attack + c 23 | 24 | 25 | def get_CL_inv(angle_of_attack): 26 | angle_of_attack = -angle_of_attack 27 | if angle_of_attack > 15 or angle_of_attack < -5: 28 | return 0 29 | else: 30 | return -1 * (0.6 + 0.1 * angle_of_attack) 31 | 32 | 33 | def get_CD_inv(angle_of_attack): 34 | angle_of_attack = -angle_of_attack 35 | if angle_of_attack > 15 or angle_of_attack < -5: 36 | return 10 * (0.06 + 0.01 * 15) 37 | else: 38 | return 0.06 + 0.01 * angle_of_attack 39 | 40 | 41 | def get_CM_inv(angle_of_attack): 42 | angle_of_attack = -angle_of_attack 43 | a = 0.0012365 44 | b = -0.016365 45 | c = -0.2327 46 | if angle_of_attack > 15 or angle_of_attack < -5: 47 | return 0 48 | else: 49 | return -1 * (a * angle_of_attack**2 + b * angle_of_attack + c) 50 | -------------------------------------------------------------------------------- /adr/World/Ambient.py: -------------------------------------------------------------------------------- 1 | from adr.World import air_gas_constant 2 | 3 | 4 | class Ambient: 5 | def __init__(self, temperature=273.15, pressure=101325, humidity=0): 6 | self.temperature = temperature 7 | self.pressure = pressure 8 | self.humidity = humidity 9 | 10 | @property 11 | def air_density(self): 12 | cond1 = self.temperature > 0 13 | cond2 = self.pressure > 0 14 | cond3 = self.humidity >= 0 and self.humidity <= 100 15 | 16 | if cond1 and cond2 and cond3: 17 | return self.pressure / (air_gas_constant * self.temperature) 18 | else: 19 | raise ValueError( 20 | f'Absolute temperature and absolute pressure should be greater than zero. \ 21 | Humidity should be between 0 and 100. \ 22 | Found temp={self.temperature}, pressure={self.pressure} and humidity={self.humidity}') 23 | -------------------------------------------------------------------------------- /adr/World/__init__.py: -------------------------------------------------------------------------------- 1 | from .constants import gravitational_acceleration, air_gas_constant 2 | from .Ambient import Ambient 3 | from .Aerodynamic import coefficients_data 4 | -------------------------------------------------------------------------------- /adr/World/constants.py: -------------------------------------------------------------------------------- 1 | from scipy import constants as cnt 2 | 3 | air_molar_mass = 0.02896 4 | gravitational_acceleration = cnt.g 5 | air_gas_constant = cnt.R/air_molar_mass 6 | -------------------------------------------------------------------------------- /adr/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CeuAzul/ADR/09885062cfa79d19fa6370155d91466bda2a27d7/adr/__init__.py -------------------------------------------------------------------------------- /adr/helper_functions/__init__.py: -------------------------------------------------------------------------------- 1 | from .algebric import rotate, translate, transform, component_vector_in_absolute_frame, component_vector_coords_in_absolute_frame 2 | -------------------------------------------------------------------------------- /adr/helper_functions/algebric.py: -------------------------------------------------------------------------------- 1 | import math 2 | import vec 3 | 4 | 5 | def rotate(vector2d, angle_radians): 6 | x, y = vector2d.x, vector2d.y 7 | xx = x * math.cos(angle_radians) - y * math.sin(angle_radians) 8 | yy = x * math.sin(angle_radians) + y * math.cos(angle_radians) 9 | return vec.Vector2(xx, yy) 10 | 11 | 12 | def translate(vector2d, displacement_x, displacement_y): 13 | displacement_vector = vec.Vector2(displacement_x, displacement_y) 14 | return vector2d + displacement_vector 15 | 16 | 17 | def transform(vector2d, angle_radians, displacement_x, displacement_y): 18 | rotated_vector = rotate(vector2d, angle_radians) 19 | displaced_vector = translate( 20 | rotated_vector, displacement_x, displacement_y) 21 | return displaced_vector 22 | 23 | 24 | def component_vector_in_absolute_frame(vector, component): 25 | return transform(vector, component.angle, *component.position) 26 | 27 | 28 | def component_vector_coords_in_absolute_frame(vector_origin, vector, component): 29 | absolute_origin = component_vector_in_absolute_frame( 30 | vector_origin, component) 31 | absolute_end = component_vector_in_absolute_frame( 32 | vector_origin+vector, component) 33 | return (*absolute_origin, *absolute_end) 34 | -------------------------------------------------------------------------------- /docs/Components/AttachedComponent.md: -------------------------------------------------------------------------------- 1 | # AttachedComponent 2 | 3 | AttachedComponent is the class where all the other components on ADR inherit from. 4 | Together with the FreeBody class it allows one to create a complex network of components that interact to calculate their states. 5 | 6 | An AttachedComponent will have properties that identify its parent component and its position and angle relative to this parent. 7 | 8 | By definition an AttachedComponent should always have a parent component, as its states will be calculated using it (eg.: ```self.angle = self.parent.angle + self.relative_angle```). This parent component can be another AttachedComponent or a FreeBody. On the top of the component's network there should always be a FreeBody, where the main states (*position*, *angle*, *velocity* and *ambient*) will be set. 9 | 10 | The AttachedComponent class implements three properties and a state, being those *parent*, *relative_position*, *relative_angle*, *velocity* and *actuation_angle*. 11 | 12 | ## Instantiation 13 | To instantiate a AttacheComponent one needs to pass the arguments of its parent class (*name*, *type* and *mass*) and also its own (*relative_position* and *relative_angle*). The *parent* property is set with the special method *set_parent* afterwards. 14 | 15 | The *relative_position* property should be a 2D vector (using vec.Vector2 class) that represents the position of the component's origin relative to its parent origin (both origins being arbitrary and defined by the user). 16 | 17 | The *relative_angle* property should be a float representing the angle of the component relative to its parent, in radians and clockwise. 18 | 19 | ``` python 20 | from adr.Components import FreeBody, AttachedComponent 21 | from adr.World import Ambient 22 | from vec import Vector2 23 | import math 24 | 25 | env = Ambient() 26 | plane = FreeBody( 27 | name='plane', 28 | type='vehicle', 29 | mass=2.0, 30 | position_cg=Vector2(x=-0.05, y=0), 31 | pitch_rot_inertia=30.0, 32 | ambient=env 33 | ) 34 | 35 | wing = AttachedComponent( 36 | name='wing', 37 | type='wing', 38 | mass=0.3, 39 | relative_position=Vector2(x=-0.15, y=0.2), 40 | relative_angle=math.radians(6), 41 | ) 42 | 43 | aileron = AttachedComponent( 44 | name='left_aileron', 45 | type='aileron', 46 | mass=0.02, 47 | relative_position=Vector2(x=-0.3, y=0), 48 | relative_angle=math.radians(4), 49 | ) 50 | 51 | wing.set_parent(plane) 52 | aileron.set_parent(wing) 53 | 54 | print(math.degrees(wing.angle)) 55 | >>> 6.0 56 | print(math.degrees(aileron.angle)) 57 | >>> 10.0 58 | 59 | plane.angle = math.radians(5) 60 | print(math.degrees(aileron.angle)) 61 | >>> 14.99999999999 62 | ``` 63 | ## position property 64 | This property returns the absolute position vector of the component 65 | ```python 66 | wing.set_parent(plane) 67 | plane.angle = 8 68 | 69 | print(wing.position) 70 | >>>Vector2 (-0.17604664425338434, -0.17750374375522998) 71 | ``` 72 | ## ambient property 73 | This property returns the environment in which the component is located 74 | ```python 75 | print(wing.ambient) 76 | >>> adr.World.Ambient.Ambient object at 0x000001E4F70128E0 77 | ``` 78 | 79 | ## velocity property 80 | This property returns the speed of the component based on the speed of the parent component 81 | ```python 82 | wing.set_parent(plane) 83 | plane.velocity = Vector2(10, 20) 84 | 85 | print(wing.velocity) 86 | >>> Vector2 (10, 20) 87 | ``` 88 | 89 | ## *reset_state* method 90 | The *reset_state* method will reset all the component state variables (actuation_angle), and call BaseComponent's *reset_state* after, which will reset the state of all child components. 91 | ``` python 92 | plane.angle = math.radians(8) 93 | print(math.degrees(plane.angle)) 94 | >>> 8.0 95 | plane.reset_state() 96 | print(math.degrees(plane.angle)) 97 | >>> 0.0 98 | ```##Angle: 99 | The property angle will return to you the angle that the given component is set. 100 | 101 | Let's say we have a component 'Wing' attached to the body of the plane, that is set as a FreeBody Component called 'Plane'. The property angle will take in count the plane angle and the relative angle of the wing, just as shown in the example below: 102 | 103 | ``` python 104 | env = Ambient() 105 | plane = FreeBody( 106 | name='plane', 107 | type='vehicle', 108 | mass=2.0, 109 | angle = math.radians(3.2), 110 | position_cg=Vector2(x=-0.05, y=0), 111 | pitch_rot_inertia=30.0, 112 | ambient=env 113 | ) 114 | 115 | wing = AttachedComponent( 116 | name='wing', 117 | type='wing', 118 | mass=0.3, 119 | relative_position=Vector2(x=-0.15, y=0.2), 120 | relative_angle = math.radians(1) 121 | ) 122 | 123 | print(math.degrees(wing.angle)) 124 | >>> 4.2 125 | ``` 126 | 127 | If the given component has an actuation angle, the property will return the angle with the maximum actuation angle as well. 128 | 129 | Let's use another example to demonstrate that: an aileron is attached to the wing, with an actuation angle of 15 degrees and a relative angle of 0 degrees. 130 | 131 | ``` python 132 | aileron = AttachedComponent( 133 | name='left_aileron', 134 | type='aileron', 135 | mass=0.02, 136 | relative_position=Vector2(x=-0.3, y=0), 137 | relative_angle=math.radians(0), 138 | ) 139 | 140 | aileron.actuation_angle(math.radians(15)) 141 | aileron.set_parent(wing) 142 | print(math.degrees(aileron.angle)) 143 | >>> 19.2 144 | ``` 145 | 146 | ## Set_parent 147 | The *set_parent* has already been used in the example above, but to give a further explanation, what the function does is to set a parent to the current component. 148 | 149 | Note that if we try to set a parent to a component that has already a parent assigned it will return an error. In the example above, the aileron parent is the wing, see what happens if we try to set the plane as the aileron parent: 150 | 151 | ``` python 152 | 153 | aileron.set_parent(plane) 154 | >>> raise Exception('Component already has a parent: wing') 155 | ``` 156 | -------------------------------------------------------------------------------- /docs/Components/Auxiliary/LandingGear.md: -------------------------------------------------------------------------------- 1 | # LandingGear 2 | 3 | LandingGear is a component created to be used for ground simulation situations. It simulates a dumped oscillator, this is, a dumped string-mass system. 4 | Imagine you want to perform a simulation of a plane takeoff or landing. You need a component that interacts with the ground, generating reaction forces oposing the total weight of the body (normal reaction) and its velocity (friction force). LandingGear does that. 5 | 6 | ## Instantiation 7 | To instantiate a LandingGear one can pass the same arguments used to instantiate an AttachedComponent, plus the following: 8 | - height: used to calculate for the displacement of the spring 9 | - spring_coeff: used to calculate the spring reaction 10 | - dump_coeff: used to calculate the dumpener friction 11 | - friction_coeff: used to calculate the friction of the wheels to the ground 12 | 13 | 14 | ``` python 15 | import math 16 | from vec import Vector2 17 | 18 | from adr.Components.Auxiliary import LandingGear 19 | 20 | main_landing_gear = LandingGear( 21 | name='main_landing_gear', 22 | relative_position=Vector2(x=-0.2, y=0), 23 | relative_angle=math.radians(0), 24 | mass=0.3, 25 | height=0.1, 26 | spring_coeff=1000, 27 | dump_coeff=50, 28 | friction_coeff=0.05 29 | ) 30 | print(main_landing_gear.type) 31 | >>> landing_gear 32 | ``` 33 | 34 | To simulate the behaviour of the landing gear, one needs to attach it to a FreeBody instance. Let's to that: 35 | 36 | ``` python 37 | from adr.World import Ambient 38 | from adr.Components import FreeBody 39 | 40 | env = Ambient() 41 | plane = FreeBody( 42 | name='plane', 43 | type='plane', 44 | mass=23.4, 45 | position_cg=Vector2(-0.2, 0.02), 46 | pitch_rot_inertia=5.2, 47 | ambient=env, 48 | ) 49 | 50 | main_landing_gear.set_parent(plane) 51 | ``` 52 | 53 | And finally let's impose some state to the plane so the landing gear reactions can be calculated: 54 | 55 | ``` python 56 | plane.velocity = Vector2(6, 0.4) 57 | plane.position = Vector2(10, 0) 58 | 59 | reaction, contact_point = main_landing_gear.gear_reaction() 60 | print(reaction) 61 | print(contact_point) 62 | >>> 63 | >>> 64 | 65 | friction, contact_point = main_landing_gear.gear_friction() 66 | print(friction) 67 | print(contact_point) 68 | >>> 69 | >>> 70 | ``` 71 | 72 | So one can see that when the airplane is at 0 m to the ground (maximum spring displacement) and with a 10 m/s velocity on the forward direction, there's a 80 N normal force oposing the weight and a 4 N friction force oposing the movement. -------------------------------------------------------------------------------- /docs/Components/BaseComponent.md: -------------------------------------------------------------------------------- 1 | # Base Component 2 | 3 | The base component is the fountain where ADR drinks from. Its idea is to provide 4 | several properties and methods that are necessary for both AttachedComponent and 5 | FreeBody. Usually the final user of ADR won't use BaseComponent directly, using instead the other two. 6 | 7 | BaseComponent implements the idea of child components (with the children dictionary), methods for dealing with its state and the state of its children, methods for calculating the forces and moments on any component based on the loads in the component itself and its children, among other things. 8 | 9 | ## Instantiation 10 | One can instantiate a BaseComponent by passing three parameters: *name*, *type* and *mass*. Notice that all parameters should be passed as keyword arguments (eg.: name="plane"). 11 | ``` python 12 | from adr.Components import BaseComponent 13 | 14 | base_component = BaseComponent( 15 | name='component', 16 | type='generic_component', 17 | mass=3.4) 18 | 19 | print(base_component) 20 | >>> BaseComponent(name='component', type='generic_component', mass=3.4, children={}, external_forces={}, external_moments={}) 21 | ``` 22 | 23 | The other attributes (children, external_forces and external_moments) should not be passed during instantiation, but with specific methods. 24 | 25 | ## *reset_state* method 26 | The *reset_state* method will call the *reset_children_state* method. 27 | It exists so the classes inheriting from BaseComponent can always call it's superclass *reset_state* method, ultimately calling the *reset_children_state* method, which will reset all nested components. 28 | 29 | ## *reset_children_state* method 30 | The *reset_children_state* method will call the *reset_state* method of all the components listed on the children dictionary. 31 | 32 | ## append_child method 33 | This method is responsible for adding a child to a component 34 | ``` python 35 | child_component = BaseComponent("wing1", "wing", 1.1) 36 | base_component.append_child(child_component) 37 | 38 | print(base_component.children) 39 | >>> {'wing1': BaseComponent(name='wing1', type='wing', mass=1.1, children={}, external_forces={}, external_moments={})} 40 | 41 | print(base_component.wing1) 42 | >>> BaseComponent(name='wing1', type='wing', mass=1.1, children={}, external_forces={}, external_moments={}) 43 | ``` 44 | ## angle_of_attack property 45 | This property calculates the angle of attack of the plane 46 | ```python 47 | freebody_component = FreeBody( 48 | name='component', 49 | type='generic_component', 50 | mass=3.4, 51 | position_cg=Vector2(-0.7, 0.2), 52 | pitch_rot_inertia=30.0, 53 | ambient=Ambient() 54 | ) 55 | 56 | attached_component = AttachedComponent( 57 | name='attached_component', 58 | type='generic_attached_component', 59 | mass=1.4, 60 | relative_position=Vector2(-0.4, 0.1), 61 | relative_angle=math.radians(9) 62 | ) 63 | 64 | attached_component.set_parent(freebody_component) 65 | freebody_component.velocity = Vector2(r=12, theta=math.radians(5)) 66 | 67 | print(math.degrees(freebody_component.angle_of_attack)) 68 | >>> -5.0 69 | 70 | print(math.degrees(attached_component.angle_of_attack)) 71 | >>> 4.0 72 | ``` 73 | 74 | ## empty_mass property 75 | This property returns the total mass of the nested components, excluding payload components 76 | ```python 77 | print(base_component.empty_mass) 78 | >>> 4.5 79 | ``` 80 | ## nested_components property 81 | This property returns a dictionary with the hierarchical relationship of each component 82 | ```python 83 | print(base_component.nested_components) 84 | >>> {'component': BaseComponent(name='component', type='generic_component', mass=3.4, children={'wing1': BaseComponent(name='wing1', type='wing', mass=1.1, children={}, external_forces={}, external_moments={})}, external_forces={}, external_moments={}), 'wing1': BaseComponent(name='wing1', type='wing', mass=1.1, children={}, external_forces={}, external_moments={})} 85 | ``` 86 | ## add_external_force_function 87 | This method is responsible for appending an external force function to the component. The force function returns a force vector and an application point. 88 | ```python 89 | def force1(): 90 | mag = 10 91 | ang = math.radians(45) 92 | force_point = Vector2(-10, 0) 93 | force1 = Vector2(r=mag, theta=ang) 94 | return force1, force_point 95 | 96 | base_component.add_external_force_function('force1', force1) 97 | 98 | print(base_component.external_forces) 99 | >>> {'force1': } 100 | ``` 101 | ## *add_external_moment_function*: 102 | This method is responsible for appending an external moment function to the component. The appended function should return a float representing the magnitude of the moment. 103 | ```python 104 | def moment1(): 105 | moment1 = 13 106 | return moment1 107 | 108 | base_component.add_external_moment_function('moment1', moment1) 109 | 110 | print(base_component.external_moments) 111 | >>> {'moment1': } 112 | ``` 113 | 114 | ## *moment_from_external_moments* method: 115 | Returns the resultant pitch moment from the moment functions appended to the 116 | component. It does not include child's moments or moments from forces. 117 | ```python 118 | def moment_from_drag(): 119 | return 2 120 | 121 | def moment_from_lift(): 122 | return 5 123 | 124 | freebody_component.add_external_moment_function('drag_moment', moment_from_drag) 125 | freebody_component.add_external_moment_function('lift_moment', moment_from_lift) 126 | 127 | print(freebody_component.external_moments) 128 | >>> {'drag_moment': , 129 | 'lift_moment': } 130 | print(freebody_component.moment_from_external_moments()) 131 | >>> 7.0 132 | ``` 133 | 134 | ## *force_and_moment_from_external_forces* method: 135 | Similar to moment_from_external_moments, but it returns both the resultant force 136 | and the moment of the resultant force on a component (excluding children). 137 | ```python 138 | def drag_force(): 139 | return Vector2(-5, 0), Vector2(3, 2) 140 | 141 | def lift_force(): 142 | return Vector2(0, 50), Vector2(3, 2) 143 | 144 | freebody_component.external_forces.pop('weight') 145 | freebody_component.add_external_force_function('drag_force', drag_force) 146 | freebody_component.add_external_force_function('lift_force', lift_force) 147 | 148 | print(freebody_component.external_forces) 149 | >>> {'drag_force': , 150 | 'lift_force': } 151 | print(freebody_component.force_and_moment_from_external_forces()) 152 | >>> (, 160.0) 153 | ``` 154 | ## *force_and_moment_from_children* method: 155 | Returns the resultant force and moment from all the child components at its origin. 156 | 157 | ## *force_and_moment_at_component_origin* method: 158 | Returns the resultant force and moment from itself and all the child components, at its origin. 159 | -------------------------------------------------------------------------------- /docs/Components/FreeBody.md: -------------------------------------------------------------------------------- 1 | # Free Body 2 | 3 | Together with AttachedComponent, the FreeBody class is responsible for creating a network of nested components. 4 | 5 | Any network of components should have a FreeBody object as its main parent, this is, all the other components should be instances of the AttachedComponent class (or a class that inherits from it) and nested under a FreeBody. This is necessary because when you ask for any state of a AttachedComponent that is dependent of its parent (eg.: angle), it will look at the value of its parent to calculate its own (eg.: ```self.angle = self.parent.angle + self.relative_angle```). If the parent is also an AttachedComponent, it will also look at its own parent and this will continue until it reaches the top component, that should be able to calculate its own value, not asking any parent. This component is the FreeBody. 6 | 7 | The FreeBody class implements among other things the *position*, *angle*, *velocity* and *rot_velocity* states. The user can modify those directly, to represent a specific FreeBody state, or call the *move* method, which will calculate the forces and moments on the free body and modify the states accordingly. 8 | 9 | ## Instantiation 10 | To instantiate a FreeBody one needs to pass the arguments of its parent class (*name*, *type* and *mass*) and also its own (*position_cg*, *pitch_rot_inertia* and *ambient*). 11 | 12 | The *position_cg* property should be a 2D vector (using vec.Vector2 class) that represents the gravitational center position of the free body relative to its origin (arbitrary and defined by the user). 13 | 14 | The *pitch_rot_inertia* property should be a float representing the rotating inertia of the free body along its pitch axis and will be used to calculate for pitch states (*rot_velocity* and *angle*). 15 | 16 | The *ambient* property should be a instance of the Ambient class and which will be consulted for environment variables (eg.: air density and wind conditions). This ambient instance can be changed during analysis for variating environment conditions. 17 | 18 | ``` python 19 | from adr.Components import FreeBody 20 | from adr.World import Ambient 21 | from vec import Vector2 22 | import math 23 | 24 | env = Ambient() 25 | plane = FreeBody( 26 | name='plane', 27 | type='vehicle', 28 | mass=2.0, 29 | position_cg=Vector2(x=-0.05, y=0), 30 | pitch_rot_inertia=30.0, 31 | ambient=env 32 | ) 33 | 34 | print(plane.ambient.air_density) 35 | >>> 1.2920513674462337 36 | ``` 37 | 38 | ## *reset_state* method 39 | The *reset_state* method will reset all the component state variables (position, angle, velocity and rot_velocity), and call BaseComponent's *reset_state* after, which will reset the state of all child components. 40 | ``` python 41 | plane.angle = math.radians(8) 42 | print(math.degrees(plane.angle)) 43 | >>> 8.0 44 | plane.reset_state() 45 | print(math.degrees(plane.angle)) 46 | >>> 0.0 47 | ``` 48 | 49 | ## *gravitational_center* property 50 | This property returns the freebody gravitational center position 51 | ``` python 52 | print(plane.gravitational_center) 53 | >>> Vector2 (-0.05, 0) 54 | ``` 55 | ## *move* method 56 | This method moves the FreeBody instance according to free-body physical equations 57 | and the instance state, for a given time step. 58 | ``` python 59 | def thrust_force(): 60 | return Vector2(10, 0), Vector2(0.4, -0.1) 61 | 62 | def lift_force(): 63 | return Vector2(0, 50), Vector2(0.1, 0.2) 64 | 65 | plane.add_external_force_function('thrust', thrust_force) 66 | plane.add_external_force_function('lift', lift_force) 67 | 68 | plane.mass = 4.0 69 | plane.position = Vector2(10, 0) 70 | plane.velocity = Vector2(2.5, 0) 71 | plane.angle = math.radians(0) 72 | 73 | plane.move(1.0) 74 | print(plane.position) 75 | >>> 76 | print(plane.velocity) 77 | >>> 78 | print(math.degrees(plane.angle)) 79 | >>> 16.233804195373324 80 | 81 | plane.move(1.0) 82 | print(plane.position) 83 | >>> 84 | print(plane.velocity) 85 | >>> 86 | print(math.degrees(plane.angle)) 87 | >>> 48.70141258611997 88 | ``` 89 | 90 | Notice that the quality of the output depends on the quality of the input. If 91 | the force and moment functions are not representative, the moving method won't 92 | deliver good results. 93 | 94 | ## *force_and_moment_at_cg* method: 95 | Returns the resultant force and moment from itself and all the child components 96 | at its gravitational center. 97 | -------------------------------------------------------------------------------- /docs/World/Ambient.md: -------------------------------------------------------------------------------- 1 | # Ambient 2 | 3 | This class will give you the ambient parameters, such as Temperature, Pressure and humidity, it will also return to you the Air Density property. 4 | 5 | By default the instantiation values will be: Temperature = 273.15, Pressure = 101325 and Humidity = 0 6 | 7 | You can change these values in the instatiation of the class. 8 | 9 | ## Air density property 10 | 11 | This property returns the air density for the given ambient. 12 | 13 | ```python 14 | 15 | from adr.World import Ambient 16 | 17 | ambient = Ambient(temperature = 300, pressure = 101325, humidity = 10) 18 | 19 | print(ambient.air_density) 20 | >>> 1.1764 21 | ``` 22 | 23 | Note that if you enter unvalid ambient instantiaon values, it will return an error. 24 | 25 | ```python 26 | 27 | from adr.World import Ambient 28 | 29 | ambient = Ambient(temperature = 300, pressure = 101325, humidity = -30) 30 | 31 | print(ambient.air_density) 32 | >>> raise ValueError( 33 | f"Absolute temperature and absolute pressure should be greater than zero. \ 34 | Humidity should be between 0 and 100. \ 35 | Found temp = 300, pressure = 101325 and humidity = -30" 36 | ) 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/World/constants.md: -------------------------------------------------------------------------------- 1 | # Constants 2 | 3 | This file contains several constants available: 4 | 5 | | Constant | Value | 6 | | -------------------------- | -------- | 7 | | gravitacional acceleration | 9.80665 | 8 | | air molar mass | 0.02896 | 9 | | air gas constant | 287.1016 | 10 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | ## Why was ADR created? 4 | 5 | ADR was originally created to allow members of 6 | [Ceu Azul Aeronaves](http://www.aerodesign.ufsc.br/) to iterate 7 | through different aircraft conceptual designs in a programmatic manner. With 8 | time the team saw that sharing this knowledge by opening the project would 9 | stimulate the OpenSource roots to enter the 10 | [SAE Aerodesign](http://portal.saebrasil.org.br/programas-estudantis/sae-brasil-aerodesign) 11 | competition. 12 | -------------------------------------------------------------------------------- /docs/helper functions/algebric.md: -------------------------------------------------------------------------------- 1 | # Algebric helper functions 2 | Some algebric helper functions are available so one can easily deal with 3 | transformations of the vectors in 2D space. 4 | 5 | ## *rotate(vector2d, angle_radians)*: 6 | Given a vec.Vector2 and an angle (in radians), returns a new vec.Vector2 7 | instance equal to the original vector rotated by the angle. 8 | ``` python 9 | from adr.helper_functions import rotate 10 | from vec import Vector2 11 | import math 12 | v1 = Vector2(0.7, 0.2) 13 | v1_rot_90 = rotate(v1, math.radians(90)) 14 | print(v1_rot_90) 15 | >>> 16 | ``` 17 | Notice the vec library doesn't always provide rounded coordinates because of 18 | float point precision. 19 | 20 | ## *translate(vector2d, displacement_x, displacement_y)*: 21 | Given a vec.Vector2 and displacements on x and y coordinates, returns a new 22 | vec.Vector2 instance equal to the original vector translated by the displacements. 23 | ``` python 24 | from adr.helper_functions import translate 25 | from vec import Vector2 26 | import math 27 | v1 = Vector2(0.5, -0.1) 28 | v1_trans = translate(v1, 0.5, 0.1) 29 | print(v1_trans_90) 30 | >>> 31 | ``` 32 | 33 | ## *transform(vector2d, angle_radians, displacement_x, displacement_y)*: 34 | Given a vec.Vector2, an angle (in radians) and displacements on x and y 35 | coordinates, returns a new vec.Vector2 instance equal to the original vector 36 | rotated and translated by the given inputs. 37 | ``` python 38 | from adr.helper_functions import transform 39 | from vec import Vector2 40 | import math 41 | v1 = Vector2(0.5, -0.1) 42 | v1_new = transform(v1, math.radians(45) 0.5, 0.1) 43 | print(v1_new) 44 | >>> 45 | ``` 46 | Notice the rotating operation hapens before the translation, and they not commute. 47 | 48 | ## *component_vector_in_absolute_frame(vector, component)*: 49 | This function is an abstraction of the transform function for components. One 50 | can use it to get the absolute coordinates of a vector in the component 51 | reference frame. I 52 | ``` python 53 | from adr.helper_functions import component_vector_in_absolute_frame 54 | from adr.Components import FreeBody 55 | from adr.World import Ambient 56 | from vec import Vector2 57 | import math 58 | 59 | env = Ambient() 60 | plane = FreeBody( 61 | name='plane', 62 | type='vehicle', 63 | mass=2.0, 64 | position_cg=Vector2(x=-0.05, y=0), 65 | pitch_rot_inertia=30.0, 66 | ambient=env 67 | ) 68 | plane.position = Vector2(20, 5) 69 | plane.angle = math.radians(20) 70 | 71 | v1_in_plane = Vector2(2.0, 0) 72 | v1_in_absolute = component_vector_in_absolute_frame(v1_in_plane, plane) 73 | print(v1_in_absolute) 74 | >>> 75 | ``` 76 | 77 | ## *component_vector_coords_in_absolute_frame(vector_origin, vector, component)*: 78 | This function is similar to component_vector_in_absolute_frame, but it's goal is 79 | to return vectors in the component frame that have it's origin different from 80 | the component's origin. 81 | ``` python 82 | from adr.helper_functions import component_vector_coords_in_absolute_frame 83 | from adr.Components import FreeBody 84 | from adr.World import Ambient 85 | from vec import Vector2 86 | import math 87 | 88 | env = Ambient() 89 | plane = FreeBody( 90 | name='plane', 91 | type='vehicle', 92 | mass=2.0, 93 | position_cg=Vector2(x=-0.05, y=0), 94 | pitch_rot_inertia=30.0, 95 | ambient=env 96 | ) 97 | plane.position = Vector2(20, 5) 98 | plane.angle = math.radians(0) 99 | 100 | v1_origin_in_plane = Vector2(0, 1.0) 101 | v1 = Vector2(3, 0) 102 | x0, y0, x, y = component_vector_coords_in_absolute_frame(v1_origin_in_plane, v1, plane) 103 | print(x0, y0, x, y) 104 | >>> 20.0 6.0 23.0 6.0 105 | ``` 106 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Home 2 | 3 | ## What is **ADR**? 4 | ADR is a python library to analyse aircraft conceptual designs. ADR has 5 | several tools that allows one to create different aircraft designs and analyse 6 | those from different points os view. 7 | 8 | ## What can it do? 9 | From 2.0, users can specify a free-body with any number of attached (and nested) 10 | componentes, in any position. Because of its generalist object-oriented 11 | structure, ADR allows components to be attached in any way imaginable, by only 12 | specifing to which other component it is attached, its relative position and 13 | angle, like so: 14 | 15 | ``` python 16 | import math 17 | from vec import Vector2 18 | from adr.World import Ambient 19 | from adr.Components import FreeBody, AttachedComponent 20 | 21 | ambient = Ambient() 22 | 23 | plane = FreeBody( 24 | name='plane', 25 | mass=0.0, 26 | position_cg=Vector2(x=-0.05, y=0), 27 | pitch_rot_inertia=30.0, 28 | ambient=ambient 29 | ) 30 | 31 | wing = AttachedComponent( 32 | name='wing', 33 | relative_position=Vector2(x=-0.10, y=0), 34 | relative_angle=math.radians(+5), 35 | mass=0.370, 36 | ) 37 | 38 | wing.set_parent(plane) 39 | 40 | print(plane.wing.mass) 41 | >>> 0.37 42 | ``` -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import math 2 | from vec import Vector2 3 | from adr.World import Ambient 4 | from adr.Components import FreeBody, AttachedComponent 5 | 6 | ambient = Ambient() 7 | 8 | plane = FreeBody( 9 | name='plane', 10 | mass=0.0, 11 | position_cg=Vector2(x=-0.05, y=0), 12 | pitch_rot_inertia=30.0, 13 | ambient=ambient 14 | ) 15 | 16 | wing = AttachedComponent( 17 | name='wing', 18 | relative_position=Vector2(x=-0.10, y=0), 19 | relative_angle=math.radians(+5), 20 | mass=0.370, 21 | ) 22 | 23 | wing.set_parent(plane) 24 | 25 | print(plane.wing.mass) 26 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: ADR docs 2 | nav: 3 | - Home: index.md 4 | - About: about.md 5 | - Usage: 6 | Components: 7 | BaseComponent: Components/BaseComponent.md 8 | FreeBody: Components/FreeBody.md 9 | AttachedComponent: Components/AttachedComponent.md 10 | Auxiliary: 11 | LandingGear: Components/Auxiliary/LandingGear.md 12 | World: 13 | Ambient: World/Ambient.md 14 | constants: World/constants.md 15 | helper funtions: 16 | algebric: helper functions/algebric.md 17 | 18 | theme: 19 | name: material 20 | icon: 21 | repo: fontawesome/brands/github 22 | markdown_extensions: 23 | - admonition 24 | - codehilite: 25 | guess_lang: false 26 | - toc: 27 | permalink: true 28 | repo_name: CeuAzul/ADR 29 | repo_url: https://github.com/CeuAzul/ADR -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "Atomic file writes." 4 | marker = "sys_platform == \"win32\"" 5 | name = "atomicwrites" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | version = "1.4.0" 9 | 10 | [[package]] 11 | category = "main" 12 | description = "Classes Without Boilerplate" 13 | name = "attrs" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 16 | version = "19.3.0" 17 | 18 | [package.extras] 19 | azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] 20 | dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] 21 | docs = ["sphinx", "zope.interface"] 22 | tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] 23 | 24 | [[package]] 25 | category = "dev" 26 | description = "Cross-platform colored terminal text." 27 | marker = "sys_platform == \"win32\"" 28 | name = "colorama" 29 | optional = false 30 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 31 | version = "0.4.3" 32 | 33 | [[package]] 34 | category = "dev" 35 | description = "iniconfig: brain-dead simple config-ini parsing" 36 | name = "iniconfig" 37 | optional = false 38 | python-versions = "*" 39 | version = "1.0.1" 40 | 41 | [[package]] 42 | category = "dev" 43 | description = "More routines for operating on iterables, beyond itertools" 44 | name = "more-itertools" 45 | optional = false 46 | python-versions = ">=3.5" 47 | version = "8.4.0" 48 | 49 | [[package]] 50 | category = "main" 51 | description = "NumPy is the fundamental package for array computing with Python." 52 | name = "numpy" 53 | optional = false 54 | python-versions = ">=3.6" 55 | version = "1.19.1" 56 | 57 | [[package]] 58 | category = "dev" 59 | description = "Core utilities for Python packages" 60 | name = "packaging" 61 | optional = false 62 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 63 | version = "20.4" 64 | 65 | [package.dependencies] 66 | pyparsing = ">=2.0.2" 67 | six = "*" 68 | 69 | [[package]] 70 | category = "dev" 71 | description = "plugin and hook calling mechanisms for python" 72 | name = "pluggy" 73 | optional = false 74 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 75 | version = "0.13.1" 76 | 77 | [package.extras] 78 | dev = ["pre-commit", "tox"] 79 | 80 | [[package]] 81 | category = "dev" 82 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 83 | name = "py" 84 | optional = false 85 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 86 | version = "1.9.0" 87 | 88 | [[package]] 89 | category = "dev" 90 | description = "Python parsing module" 91 | name = "pyparsing" 92 | optional = false 93 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 94 | version = "2.4.7" 95 | 96 | [[package]] 97 | category = "dev" 98 | description = "pytest: simple powerful testing with Python" 99 | name = "pytest" 100 | optional = false 101 | python-versions = ">=3.5" 102 | version = "6.0.1" 103 | 104 | [package.dependencies] 105 | atomicwrites = ">=1.0" 106 | attrs = ">=17.4.0" 107 | colorama = "*" 108 | iniconfig = "*" 109 | more-itertools = ">=4.0.0" 110 | packaging = "*" 111 | pluggy = ">=0.12,<1.0" 112 | py = ">=1.8.2" 113 | toml = "*" 114 | 115 | [package.extras] 116 | checkqa_mypy = ["mypy (0.780)"] 117 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 118 | 119 | [[package]] 120 | category = "dev" 121 | description = "Thin-wrapper around the mock package for easier use with pytest" 122 | name = "pytest-mock" 123 | optional = false 124 | python-versions = ">=3.5" 125 | version = "3.2.0" 126 | 127 | [package.dependencies] 128 | pytest = ">=2.7" 129 | 130 | [package.extras] 131 | dev = ["pre-commit", "tox", "pytest-asyncio"] 132 | 133 | [[package]] 134 | category = "main" 135 | description = "SciPy: Scientific Library for Python" 136 | name = "scipy" 137 | optional = false 138 | python-versions = ">=3.6" 139 | version = "1.5.2" 140 | 141 | [package.dependencies] 142 | numpy = ">=1.14.5" 143 | 144 | [[package]] 145 | category = "dev" 146 | description = "Python 2 and 3 compatibility utilities" 147 | name = "six" 148 | optional = false 149 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 150 | version = "1.15.0" 151 | 152 | [[package]] 153 | category = "dev" 154 | description = "Python Library for Tom's Obvious, Minimal Language" 155 | name = "toml" 156 | optional = false 157 | python-versions = "*" 158 | version = "0.10.1" 159 | 160 | [[package]] 161 | category = "main" 162 | description = "A collection of classes for vectors and rects" 163 | name = "vec" 164 | optional = false 165 | python-versions = ">=3.6" 166 | version = "0.5" 167 | 168 | [metadata] 169 | content-hash = "91a7bc6c6f255d803bb3d69c03e8bf6866ef641b3d8ac37d53a75e84da3a0669" 170 | python-versions = "^3.8" 171 | 172 | [metadata.files] 173 | atomicwrites = [ 174 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 175 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 176 | ] 177 | attrs = [ 178 | {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, 179 | {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, 180 | ] 181 | colorama = [ 182 | {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, 183 | {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, 184 | ] 185 | iniconfig = [ 186 | {file = "iniconfig-1.0.1-py3-none-any.whl", hash = "sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437"}, 187 | {file = "iniconfig-1.0.1.tar.gz", hash = "sha256:e5f92f89355a67de0595932a6c6c02ab4afddc6fcdc0bfc5becd0d60884d3f69"}, 188 | ] 189 | more-itertools = [ 190 | {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, 191 | {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, 192 | ] 193 | numpy = [ 194 | {file = "numpy-1.19.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1cca51512299841bf69add3b75361779962f9cee7d9ee3bb446d5982e925b69"}, 195 | {file = "numpy-1.19.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c9591886fc9cbe5532d5df85cb8e0cc3b44ba8ce4367bd4cf1b93dc19713da72"}, 196 | {file = "numpy-1.19.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:cf1347450c0b7644ea142712619533553f02ef23f92f781312f6a3553d031fc7"}, 197 | {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:ed8a311493cf5480a2ebc597d1e177231984c818a86875126cfd004241a73c3e"}, 198 | {file = "numpy-1.19.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3673c8b2b29077f1b7b3a848794f8e11f401ba0b71c49fbd26fb40b71788b132"}, 199 | {file = "numpy-1.19.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:56ef7f56470c24bb67fb43dae442e946a6ce172f97c69f8d067ff8550cf782ff"}, 200 | {file = "numpy-1.19.1-cp36-cp36m-win32.whl", hash = "sha256:aaf42a04b472d12515debc621c31cf16c215e332242e7a9f56403d814c744624"}, 201 | {file = "numpy-1.19.1-cp36-cp36m-win_amd64.whl", hash = "sha256:082f8d4dd69b6b688f64f509b91d482362124986d98dc7dc5f5e9f9b9c3bb983"}, 202 | {file = "numpy-1.19.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e4f6d3c53911a9d103d8ec9518190e52a8b945bab021745af4939cfc7c0d4a9e"}, 203 | {file = "numpy-1.19.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b6885c12784a27e957294b60f97e8b5b4174c7504665333c5e94fbf41ae5d6a"}, 204 | {file = "numpy-1.19.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1bc0145999e8cb8aed9d4e65dd8b139adf1919e521177f198529687dbf613065"}, 205 | {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:5a936fd51049541d86ccdeef2833cc89a18e4d3808fe58a8abeb802665c5af93"}, 206 | {file = "numpy-1.19.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ef71a1d4fd4858596ae80ad1ec76404ad29701f8ca7cdcebc50300178db14dfc"}, 207 | {file = "numpy-1.19.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b9792b0ac0130b277536ab8944e7b754c69560dac0415dd4b2dbd16b902c8954"}, 208 | {file = "numpy-1.19.1-cp37-cp37m-win32.whl", hash = "sha256:b12e639378c741add21fbffd16ba5ad25c0a1a17cf2b6fe4288feeb65144f35b"}, 209 | {file = "numpy-1.19.1-cp37-cp37m-win_amd64.whl", hash = "sha256:8343bf67c72e09cfabfab55ad4a43ce3f6bf6e6ced7acf70f45ded9ebb425055"}, 210 | {file = "numpy-1.19.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e45f8e981a0ab47103181773cc0a54e650b2aef8c7b6cd07405d0fa8d869444a"}, 211 | {file = "numpy-1.19.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:667c07063940e934287993366ad5f56766bc009017b4a0fe91dbd07960d0aba7"}, 212 | {file = "numpy-1.19.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:480fdd4dbda4dd6b638d3863da3be82873bba6d32d1fc12ea1b8486ac7b8d129"}, 213 | {file = "numpy-1.19.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:935c27ae2760c21cd7354402546f6be21d3d0c806fffe967f745d5f2de5005a7"}, 214 | {file = "numpy-1.19.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:309cbcfaa103fc9a33ec16d2d62569d541b79f828c382556ff072442226d1968"}, 215 | {file = "numpy-1.19.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:7ed448ff4eaffeb01094959b19cbaf998ecdee9ef9932381420d514e446601cd"}, 216 | {file = "numpy-1.19.1-cp38-cp38-win32.whl", hash = "sha256:de8b4a9b56255797cbddb93281ed92acbc510fb7b15df3f01bd28f46ebc4edae"}, 217 | {file = "numpy-1.19.1-cp38-cp38-win_amd64.whl", hash = "sha256:92feb989b47f83ebef246adabc7ff3b9a59ac30601c3f6819f8913458610bdcc"}, 218 | {file = "numpy-1.19.1-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:e1b1dc0372f530f26a03578ac75d5e51b3868b9b76cd2facba4c9ee0eb252ab1"}, 219 | {file = "numpy-1.19.1.zip", hash = "sha256:b8456987b637232602ceb4d663cb34106f7eb780e247d51a260b84760fd8f491"}, 220 | ] 221 | packaging = [ 222 | {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, 223 | {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, 224 | ] 225 | pluggy = [ 226 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 227 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 228 | ] 229 | py = [ 230 | {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, 231 | {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, 232 | ] 233 | pyparsing = [ 234 | {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, 235 | {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, 236 | ] 237 | pytest = [ 238 | {file = "pytest-6.0.1-py3-none-any.whl", hash = "sha256:8b6007800c53fdacd5a5c192203f4e531eb2a1540ad9c752e052ec0f7143dbad"}, 239 | {file = "pytest-6.0.1.tar.gz", hash = "sha256:85228d75db9f45e06e57ef9bf4429267f81ac7c0d742cc9ed63d09886a9fe6f4"}, 240 | ] 241 | pytest-mock = [ 242 | {file = "pytest-mock-3.2.0.tar.gz", hash = "sha256:7122d55505d5ed5a6f3df940ad174b3f606ecae5e9bc379569cdcbd4cd9d2b83"}, 243 | {file = "pytest_mock-3.2.0-py3-none-any.whl", hash = "sha256:5564c7cd2569b603f8451ec77928083054d8896046830ca763ed68f4112d17c7"}, 244 | ] 245 | scipy = [ 246 | {file = "scipy-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cca9fce15109a36a0a9f9cfc64f870f1c140cb235ddf27fe0328e6afb44dfed0"}, 247 | {file = "scipy-1.5.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:1c7564a4810c1cd77fcdee7fa726d7d39d4e2695ad252d7c86c3ea9d85b7fb8f"}, 248 | {file = "scipy-1.5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:07e52b316b40a4f001667d1ad4eb5f2318738de34597bd91537851365b6c61f1"}, 249 | {file = "scipy-1.5.2-cp36-cp36m-win32.whl", hash = "sha256:d56b10d8ed72ec1be76bf10508446df60954f08a41c2d40778bc29a3a9ad9bce"}, 250 | {file = "scipy-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:8e28e74b97fc8d6aa0454989db3b5d36fc27e69cef39a7ee5eaf8174ca1123cb"}, 251 | {file = "scipy-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6e86c873fe1335d88b7a4bfa09d021f27a9e753758fd75f3f92d714aa4093768"}, 252 | {file = "scipy-1.5.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:a0afbb967fd2c98efad5f4c24439a640d39463282040a88e8e928db647d8ac3d"}, 253 | {file = "scipy-1.5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eecf40fa87eeda53e8e11d265ff2254729d04000cd40bae648e76ff268885d66"}, 254 | {file = "scipy-1.5.2-cp37-cp37m-win32.whl", hash = "sha256:315aa2165aca31375f4e26c230188db192ed901761390be908c9b21d8b07df62"}, 255 | {file = "scipy-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:ec5fe57e46828d034775b00cd625c4a7b5c7d2e354c3b258d820c6c72212a6ec"}, 256 | {file = "scipy-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fc98f3eac993b9bfdd392e675dfe19850cc8c7246a8fd2b42443e506344be7d9"}, 257 | {file = "scipy-1.5.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a785409c0fa51764766840185a34f96a0a93527a0ff0230484d33a8ed085c8f8"}, 258 | {file = "scipy-1.5.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0a0e9a4e58a4734c2eba917f834b25b7e3b6dc333901ce7784fd31aefbd37b2f"}, 259 | {file = "scipy-1.5.2-cp38-cp38-win32.whl", hash = "sha256:dac09281a0eacd59974e24525a3bc90fa39b4e95177e638a31b14db60d3fa806"}, 260 | {file = "scipy-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:92eb04041d371fea828858e4fff182453c25ae3eaa8782d9b6c32b25857d23bc"}, 261 | {file = "scipy-1.5.2.tar.gz", hash = "sha256:066c513d90eb3fd7567a9e150828d39111ebd88d3e924cdfc9f8ce19ab6f90c9"}, 262 | ] 263 | six = [ 264 | {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, 265 | {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, 266 | ] 267 | toml = [ 268 | {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, 269 | {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, 270 | ] 271 | vec = [ 272 | {file = "vec-0.5-py3-none-any.whl", hash = "sha256:814a89fd854df46ffe0515db928225fe580024d477f1a5c1aa7e6f4dcf6c15f2"}, 273 | {file = "vec-0.5.tar.gz", hash = "sha256:6bc6b6b1fb67fb7391683b53214d1238843ffa7865f5bcab8262abd89c231b48"}, 274 | ] 275 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "adr.ca" 3 | version = "2.0" 4 | description = "ADR is a python library to analyse aircraft conceptual designs. ADR has several tools that allows one to create different aircraft designs and analyse those from different points os view." 5 | 6 | packages = [ 7 | { include = "adr", from = "" } 8 | ] 9 | 10 | authors = ["Rafael Araujo Lehmkuhl "] 11 | maintainers = ["Rafael Araujo Lehmkuhl ", "João Gabriel Firta Foes ", "Cassiano Montibeller "] 12 | 13 | license = "MIT" 14 | 15 | readme = 'README.md' # Markdown files are supported 16 | 17 | repository = "https://github.com/CeuAzul/ADR" 18 | homepage = "https://CeuAzul.github.io/ADR" 19 | 20 | [tool.poetry.dependencies] 21 | python = "^3.8" 22 | vec = "^0.5" 23 | scipy = "^1.5.2" 24 | attrs = "^19.3.0" 25 | 26 | [tool.poetry.dev-dependencies] 27 | pytest = "^6.0.1" 28 | pytest-mock = "^3.2.0" 29 | 30 | [build-system] 31 | requires = ["poetry>=0.12"] 32 | build-backend = "poetry.masonry.api" 33 | -------------------------------------------------------------------------------- /tests/Components/Auxiliary/test_LandingGear.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import math 3 | from vec import Vector2 4 | import numpy.testing as npt 5 | 6 | from adr.World import Ambient 7 | from adr.Components import FreeBody 8 | from adr.Components.Auxiliary import LandingGear 9 | 10 | 11 | @pytest.fixture 12 | def plane(): 13 | env = Ambient() 14 | plane = FreeBody( 15 | name='plane', 16 | type='plane', 17 | mass=23.4, 18 | position_cg=Vector2(-0.2, 0.02), 19 | pitch_rot_inertia=5.2, 20 | ambient=env, 21 | ) 22 | return plane 23 | 24 | 25 | @pytest.fixture 26 | def main_landing_gear(): 27 | main_landing_gear = LandingGear( 28 | name='main_landing_gear', 29 | relative_position=Vector2(x=-0.2, y=0), 30 | relative_angle=math.radians(0), 31 | mass=0.3, 32 | height=0.1, 33 | spring_coeff=1000, 34 | dump_coeff=50, 35 | friction_coeff=0.05 36 | ) 37 | return main_landing_gear 38 | 39 | 40 | def test_instantiation(main_landing_gear): 41 | assert(main_landing_gear.type == 'landing_gear') 42 | assert(main_landing_gear.height == 0.1) 43 | assert(main_landing_gear.spring_coeff == 1000) 44 | assert(main_landing_gear.dump_coeff == 50) 45 | assert(main_landing_gear.friction_coeff == 0.05) 46 | 47 | 48 | def test_floor_contact_point(main_landing_gear): 49 | contact_point = Vector2(0, -0.1) 50 | npt.assert_almost_equal(contact_point.x, 0) 51 | npt.assert_almost_equal(contact_point.y, -0.1) 52 | 53 | 54 | def test_gear_reaction(plane, main_landing_gear): 55 | main_landing_gear.set_parent(plane) 56 | plane.velocity = Vector2(6, 0.4) 57 | 58 | # Plane on air (position.y = 2m), so no reaction on landing gear is expected 59 | plane.position = Vector2(10, 2) 60 | reaction, contact_point = main_landing_gear.gear_reaction() 61 | assert(type(contact_point) is Vector2) 62 | npt.assert_almost_equal(reaction.y, 0) 63 | 64 | # Plane on ground (position.y = 0m), so reaction on landing gear is expected 65 | plane.position = Vector2(10, 0) 66 | reaction, contact_point = main_landing_gear.gear_reaction() 67 | npt.assert_almost_equal(reaction.y, 80.0) 68 | 69 | 70 | def test_gear_friction(plane, main_landing_gear): 71 | main_landing_gear.set_parent(plane) 72 | plane.velocity = Vector2(6, 0.4) 73 | 74 | # Plane on air (position.y = 2m), so no friction on landing gear is expected 75 | plane.position = Vector2(10, 2) 76 | friction, contact_point = main_landing_gear.gear_friction() 77 | assert(type(contact_point) is Vector2) 78 | npt.assert_almost_equal(friction.x, 0) 79 | 80 | # Plane on ground (position.y = 0m), going forward, expected friction on negative x direction 81 | plane.position = Vector2(10, 0) 82 | friction, contact_point = main_landing_gear.gear_friction() 83 | npt.assert_almost_equal(friction.x, -4.0) 84 | 85 | # Plane on ground (position.y = 0m), going backwards, expected friction on positive x direction 86 | plane.velocity = Vector2(-6, 0.4) 87 | plane.position = Vector2(10, 0) 88 | friction, contact_point = main_landing_gear.gear_friction() 89 | npt.assert_almost_equal(friction.x, 4.0) 90 | -------------------------------------------------------------------------------- /tests/Components/test_AttachedComponent.py: -------------------------------------------------------------------------------- 1 | from adr.Components import AttachedComponent, FreeBody, BaseComponent 2 | from adr.World import Ambient 3 | from vec import Vector2 4 | import math 5 | import numpy.testing as npt 6 | import pytest 7 | 8 | 9 | @pytest.fixture 10 | def attached_component(): 11 | attached_component = AttachedComponent( 12 | name='attached_component', 13 | type='generic_attached_component', 14 | mass=1.4, 15 | relative_position=Vector2(-0.4, 0.1), 16 | relative_angle=math.radians(9) 17 | ) 18 | return attached_component 19 | 20 | @pytest.fixture 21 | def freebody_component(): 22 | freebody_component = FreeBody( 23 | name='component', 24 | type='generic_component', 25 | mass=3.4, 26 | angle=math.radians(5), 27 | position_cg=Vector2(-0.7, 0.2), 28 | pitch_rot_inertia=30.0, 29 | ambient=Ambient() 30 | ) 31 | return freebody_component 32 | 33 | @pytest.fixture 34 | def base_component(): 35 | base_component = BaseComponent( 36 | name='component', 37 | type='generic_component', 38 | mass=3.4, 39 | ) 40 | return base_component 41 | 42 | @pytest.fixture 43 | def extra_base_component(): 44 | base_component = BaseComponent( 45 | name='component', 46 | type='generic_component', 47 | mass=3.4, 48 | ) 49 | return extra_base_component 50 | 51 | def test_instantiation(attached_component): 52 | assert(attached_component.relative_position.x == -0.4) 53 | assert(attached_component.relative_position.y == 0.1) 54 | assert(attached_component.relative_angle == math.radians(9)) 55 | 56 | 57 | def test_reset_state(attached_component): 58 | attached_component.actuation_angle = math.radians(-7) 59 | attached_component.reset_state() 60 | assert (attached_component.actuation_angle == 0) 61 | 62 | 63 | def test_states(attached_component): 64 | attached_component.actuation_angle = math.radians(3) 65 | assert(attached_component.actuation_angle == math.radians(3)) 66 | 67 | def test_angle(attached_component, freebody_component): 68 | attached_component.set_parent(freebody_component) 69 | angle = math.degrees(attached_component.angle) 70 | assert(angle == 14) 71 | attached_component.actuation_angle = math.radians(10) 72 | angle = math.degrees(attached_component.angle) 73 | npt.assert_almost_equal(angle, 24, decimal = 0) 74 | 75 | def test_set_parent(attached_component, base_component, extra_base_component): 76 | attached_component.set_parent(base_component) 77 | assert(attached_component.parent == base_component) 78 | with pytest.raises(Exception): 79 | assert attached_component.set_parent(extra_base_component) 80 | 81 | 82 | def test_ambient(attached_component, freebody_component): 83 | attached_component.set_parent(freebody_component) 84 | assert(attached_component.ambient == freebody_component.ambient) 85 | 86 | 87 | def test_velocity(attached_component, freebody_component): 88 | attached_component.set_parent(freebody_component) 89 | freebody_component.velocity = Vector2(20, 10) 90 | assert(attached_component.velocity == Vector2(20, 10)) 91 | freebody_component.velocity = Vector2(-5, 0) 92 | assert(attached_component.velocity == Vector2(-5, 0)) 93 | freebody_component.velocity = Vector2(-15, -20) 94 | assert(attached_component.velocity == Vector2(-15, -20)) 95 | 96 | 97 | def test_position(freebody_component, attached_component): 98 | attached_component.set_parent(freebody_component) 99 | freebody_component.angle = math.radians(0) 100 | freebody_component.position = Vector2(0, 0) 101 | assert(attached_component.position == Vector2(-0.4, 0.1)) 102 | freebody_component.angle = math.radians(2) 103 | freebody_component.position = Vector2(-0.5, 0.2) 104 | assert(attached_component.position == 105 | Vector2(-0.9032462804778885, 0.2859792840209092)) 106 | freebody_component.angle = math.radians(-2) 107 | freebody_component.position = Vector2(0.3, -0.5) 108 | assert(attached_component.position == 109 | Vector2(-0.09626638113738828, -0.38610111861709)) 110 | -------------------------------------------------------------------------------- /tests/Components/test_BaseComponent.py: -------------------------------------------------------------------------------- 1 | import numpy as npt 2 | from vec import Vector2 3 | from adr.World import gravitational_acceleration, Ambient 4 | from adr.Components import BaseComponent, FreeBody, AttachedComponent 5 | import pytest 6 | import math 7 | 8 | 9 | @pytest.fixture 10 | def base_component(): 11 | base_component = BaseComponent( 12 | name='component', 13 | type='generic_component', 14 | mass=3.4, 15 | ) 16 | return base_component 17 | 18 | 19 | @pytest.fixture 20 | def freebody_component(): 21 | freebody_component = FreeBody( 22 | name='component', 23 | type='generic_component', 24 | mass=3.4, 25 | position_cg=Vector2(-0.7, 0.2), 26 | pitch_rot_inertia=30.0, 27 | ambient=Ambient() 28 | ) 29 | return freebody_component 30 | 31 | 32 | @pytest.fixture 33 | def attached_component(): 34 | attached_component = AttachedComponent( 35 | name='attached_component', 36 | type='generic_attached_component', 37 | mass=1.4, 38 | relative_position=Vector2(-0.4, 0.1), 39 | relative_angle=math.radians(9) 40 | ) 41 | return attached_component 42 | 43 | 44 | @pytest.fixture 45 | def payload(): 46 | payload = AttachedComponent( 47 | name='payload_generic', 48 | type='payload', 49 | mass=3.0, 50 | relative_position=Vector2(-0.6, 0.08), 51 | relative_angle=math.radians(9) 52 | ) 53 | return payload 54 | 55 | @pytest.fixture 56 | def moment1(): 57 | moment1 = 25 58 | return moment1 59 | 60 | 61 | @pytest.fixture 62 | def force1(): 63 | mag = 10 64 | ang = math.radians(45) 65 | force_point = Vector2(-10, 0) 66 | force1 = Vector2(r=mag, theta=ang) 67 | return force1, force_point 68 | 69 | 70 | def test_instantiation(base_component): 71 | assert(base_component.name == 'component') 72 | assert(base_component.type == 'generic_component') 73 | assert(base_component.mass == 3.4) 74 | 75 | 76 | def test_reset_state(mocker, base_component): 77 | spy = mocker.spy(base_component, 'reset_children_state') 78 | base_component.reset_state() 79 | spy.assert_called_once() 80 | 81 | 82 | def test_reset_children_state(mocker, base_component): 83 | child_component_mock = BaseComponent 84 | child_component_mock.reset_state = mocker.Mock() 85 | base_component.children = {'mock_component': child_component_mock} 86 | spy = mocker.spy(child_component_mock, 'reset_state') 87 | 88 | base_component.reset_children_state() 89 | spy.assert_called_once() 90 | 91 | 92 | def test_append_child(base_component): 93 | child_component = BaseComponent("wing1", "wing", 1.1) 94 | base_component.append_child(child_component) 95 | assert(base_component.children["wing1"] == child_component) 96 | assert(base_component.wing1 == child_component) 97 | 98 | 99 | def test_angle_of_attack(freebody_component, attached_component): 100 | freebody_component.velocity = Vector2(r=12, theta=math.radians(5)) 101 | attached_component.set_parent(freebody_component) 102 | npt.testing.assert_almost_equal( 103 | math.degrees(freebody_component.angle_of_attack), -5.0, decimal=1) 104 | npt.testing.assert_almost_equal( 105 | math.degrees(attached_component.angle_of_attack), 4.0, decimal=1) 106 | 107 | 108 | def test_empty_mass(freebody_component, base_component, attached_component, payload): 109 | freebody_component.append_child(attached_component) 110 | base_component.append_child(attached_component) 111 | base_component.append_child(payload) 112 | assert(base_component.empty_mass == 4.8) 113 | assert(freebody_component.empty_mass == 4.8) 114 | assert(attached_component.empty_mass == 1.4) 115 | 116 | def test_total_mass(freebody_component, base_component, attached_component, payload): 117 | freebody_component.append_child(attached_component) 118 | base_component.append_child(attached_component) 119 | base_component.append_child(payload) 120 | assert base_component.total_mass == 7.8 121 | assert freebody_component.total_mass == 4.8 122 | assert attached_component.total_mass == 1.4 123 | 124 | 125 | def test_moment_from_external_moments(freebody_component, attached_component): 126 | attached_component.set_parent(freebody_component) 127 | freebody_component.external_moments['moment1'] = lambda: 10.0 128 | attached_component.external_moments['moment2'] = lambda: 20.0 129 | assert(freebody_component.moment_from_external_moments() == 10.0) 130 | 131 | 132 | def test_force_and_moment_from_external_forces(freebody_component, attached_component): 133 | def ext_force_function1(): 134 | return Vector2(-0.9, 0.5), Vector2(0.6, 2.0) 135 | 136 | def ext_force_function2(): 137 | return Vector2(1, 1.4), Vector2(-0.5, -0.1) 138 | 139 | freebody_component.angle = math.radians(15) 140 | attached_component.set_parent(freebody_component) 141 | freebody_component.external_forces['force1'] = ext_force_function1 142 | attached_component.external_forces['force2'] = ext_force_function2 143 | freebody_component.external_forces.pop('weight') 144 | force, moment = freebody_component.force_and_moment_from_external_forces() 145 | npt.testing.assert_almost_equal(force.x, -0.9, decimal=3) 146 | npt.testing.assert_almost_equal(force.y, 0.5, decimal=3) 147 | npt.testing.assert_almost_equal(moment, 2.1, decimal=3) 148 | 149 | 150 | def test_force_and_moment_from_children(freebody_component, attached_component): 151 | def ext_force_function1(): 152 | return Vector2(-0.9, 0.5), Vector2(0.6, 2.0) 153 | 154 | def ext_force_function2(): 155 | return Vector2(1, 1.4), Vector2(-0.5, -0.1) 156 | 157 | attached_component.set_parent(freebody_component) 158 | freebody_component.external_forces['force1'] = ext_force_function1 159 | attached_component.external_forces['force2'] = ext_force_function2 160 | force, moment = freebody_component.force_and_moment_from_children() 161 | npt.testing.assert_almost_equal(force.x, 0.7686, decimal=3) 162 | npt.testing.assert_almost_equal(force.y, 1.5391, decimal=3) 163 | npt.testing.assert_almost_equal(moment, -1.292, decimal=3) 164 | 165 | 166 | def test_force_and_moment_at_component_origin(freebody_component, attached_component): 167 | def ext_force_function1(): 168 | return Vector2(-0.9, 0.5), Vector2(0.6, 2.0) 169 | 170 | def ext_force_function2(): 171 | return Vector2(1, 1.4), Vector2(-0.5, -0.1) 172 | attached_component.set_parent(freebody_component) 173 | freebody_component.external_forces.pop('weight') 174 | freebody_component.external_forces['force1'] = ext_force_function1 175 | freebody_component.external_moments['moment1'] = lambda: 10.0 176 | attached_component.external_forces['force2'] = ext_force_function2 177 | attached_component.external_moments['moment2'] = lambda: 20.0 178 | force, moment = freebody_component.force_and_moment_at_component_origin() 179 | npt.testing.assert_almost_equal(force.x, -0.131, decimal=3) 180 | npt.testing.assert_almost_equal(force.y, 2.04, decimal=3) 181 | npt.testing.assert_almost_equal(moment, 30.807, decimal=3) 182 | 183 | 184 | def test_nested_components(base_component, attached_component): 185 | base_component.append_child(attached_component) 186 | assert(base_component.nested_components["component"] == base_component) 187 | assert( 188 | base_component.nested_components["attached_component"] == attached_component) 189 | assert( 190 | attached_component.nested_components["attached_component"] == attached_component) 191 | assert(base_component.nested_components == { 192 | 'component': base_component, 'attached_component': attached_component}) 193 | assert(attached_component.nested_components == { 194 | 'attached_component': attached_component}) 195 | 196 | def test_add_external_force_function(force1, base_component): 197 | base_component.add_external_force_function('force1', force1) 198 | assert(base_component.external_forces['force1'] == force1) 199 | 200 | def test_add_external_moment_function(moment1, base_component): 201 | base_component.add_external_moment_function('moment1', moment1) 202 | assert(base_component.external_moments['moment1'] == moment1) 203 | -------------------------------------------------------------------------------- /tests/Components/test_FreeBody.py: -------------------------------------------------------------------------------- 1 | from adr.Components import FreeBody, AttachedComponent 2 | from adr.World import Ambient 3 | from vec import Vector2 4 | import math 5 | import pytest 6 | import numpy.testing as npt 7 | 8 | 9 | @pytest.fixture 10 | def freebody(): 11 | env = Ambient() 12 | freebody = FreeBody( 13 | name='freebody', 14 | type='generic_freebody', 15 | mass=23.4, 16 | position_cg=Vector2(-0.2, 0.02), 17 | pitch_rot_inertia=5.2, 18 | ambient=env, 19 | ) 20 | return freebody 21 | 22 | 23 | @pytest.fixture 24 | def attached_component(): 25 | attached_component = AttachedComponent( 26 | name='attached_component', 27 | type='generic_attached_component', 28 | mass=1.4, 29 | relative_position=Vector2(-0.4, 0.1), 30 | relative_angle=math.radians(9) 31 | ) 32 | return attached_component 33 | 34 | 35 | def test_instantiation(freebody): 36 | assert(freebody.position_cg.x == -0.2) 37 | assert(freebody.position_cg.y == 0.02) 38 | assert(freebody.pitch_rot_inertia == 5.2) 39 | assert(freebody.ambient.temperature == 273.15) 40 | 41 | 42 | def test_reset_state(freebody): 43 | freebody.position = Vector2(2, 4) 44 | freebody.angle = 3 45 | freebody.velocity = Vector2(7, 2) 46 | freebody.rot_velocity = 6 47 | 48 | freebody.reset_state() 49 | 50 | assert (freebody.position == Vector2(0, 0)) 51 | assert (freebody.angle == 0) 52 | assert (freebody.velocity == Vector2(0.00001, 0.00001)) 53 | assert (freebody.rot_velocity == 0) 54 | 55 | 56 | def test_states(freebody): 57 | freebody.position = Vector2(15.1, 1.2) 58 | freebody.angle = math.radians(15) 59 | freebody.velocity = Vector2(12.1, 0.7) 60 | freebody.rot_velocity = 14 61 | assert(freebody.position == Vector2(15.1, 1.2)) 62 | assert(freebody.angle == math.radians(15)) 63 | assert(freebody.velocity == Vector2(12.1, 0.7)) 64 | assert(freebody.rot_velocity == 14) 65 | 66 | 67 | def test_gravitational_center(freebody): 68 | assert(freebody.gravitational_center == Vector2(-0.2, 0.02)) 69 | 70 | 71 | def test_get_total_weight(freebody): 72 | freebody.angle = math.radians(15) 73 | weight, weight_point = freebody.get_total_weight() 74 | npt.assert_almost_equal(weight.r, 229.47, decimal=2) 75 | npt.assert_almost_equal(weight.theta, math.radians(-105), decimal=2) 76 | assert weight_point == Vector2(-0.2, 0.02) 77 | 78 | 79 | def test_force_and_moment_at_cg(freebody, attached_component): 80 | def ext_force_function1(): 81 | return Vector2(-0.9, 0.5), Vector2(0.6, 2.0) 82 | 83 | def ext_force_function2(): 84 | return Vector2(1, 1.4), Vector2(-0.5, -0.1) 85 | 86 | attached_component.set_parent(freebody) 87 | freebody.external_forces.pop('weight') 88 | freebody.external_forces['force1'] = ext_force_function1 89 | freebody.external_moments['moment1'] = lambda: 10.0 90 | attached_component.external_forces['force2'] = ext_force_function2 91 | attached_component.external_moments['moment2'] = lambda: 20.0 92 | force, moment = freebody.force_and_moment_at_cg() 93 | npt.assert_almost_equal(force.x, -0.131, decimal=3) 94 | npt.assert_almost_equal(force.y, 2.04, decimal=3) 95 | npt.assert_almost_equal(moment, 31.212, decimal=3) 96 | 97 | 98 | def test_move(monkeypatch, freebody): 99 | freebody.mass = 2 100 | freebody.pitch_rot_inertia = 3 101 | freebody.rot_velocity = 2 102 | freebody.angle = 1 103 | freebody.velocity = Vector2(12, 3) 104 | freebody.position = Vector2(5, 1) 105 | 106 | def force_and_moment_at_cg_mock(): 107 | return Vector2(50, 10), 1.5 108 | 109 | freebody.force_and_moment_at_cg = force_and_moment_at_cg_mock 110 | 111 | total_force, moment_z = freebody.move(0.1) 112 | 113 | npt.assert_almost_equal(freebody.rot_velocity, 2.05) 114 | npt.assert_almost_equal(freebody.angle, 1.205) 115 | npt.assert_almost_equal(freebody.velocity.x, 14.5) 116 | npt.assert_almost_equal(freebody.velocity.y, 3.5) 117 | npt.assert_almost_equal(freebody.position.x, 6.45) 118 | npt.assert_almost_equal(freebody.position.y, 1.35) 119 | 120 | npt.assert_almost_equal(total_force.x, 50) 121 | npt.assert_almost_equal(total_force.y, 10) 122 | npt.assert_almost_equal(moment_z, 1.5) 123 | -------------------------------------------------------------------------------- /tests/World/test_Ambient.py: -------------------------------------------------------------------------------- 1 | from adr.World import Ambient 2 | import numpy.testing as npt 3 | import pytest 4 | 5 | 6 | @pytest.fixture 7 | def base_ambient(): 8 | base_ambient = Ambient(temperature=288.15, pressure=101325, humidity=30) 9 | return base_ambient 10 | 11 | 12 | def test_instantiation(base_ambient): 13 | assert base_ambient.temperature == 288.15 14 | assert base_ambient.pressure == 101325 15 | assert base_ambient.humidity == 30 16 | 17 | 18 | def test_air_density(base_ambient): 19 | npt.assert_almost_equal(base_ambient.air_density, 1.224, decimal=3) 20 | 21 | 22 | def test_instantiation_error(): 23 | ambient = Ambient(temperature=0) 24 | with pytest.raises(ValueError) as e_info: 25 | air_density = ambient.air_density 26 | 27 | ambient = Ambient(humidity=104) 28 | with pytest.raises(ValueError) as e_info: 29 | air_density = ambient.air_density 30 | 31 | ambient = Ambient(pressure=-3) 32 | with pytest.raises(ValueError) as e_info: 33 | air_density = ambient.air_density 34 | 35 | -------------------------------------------------------------------------------- /tests/World/test_constants.py: -------------------------------------------------------------------------------- 1 | from adr.World import constants 2 | import numpy.testing as npt 3 | 4 | 5 | def test_fixed_constants(): 6 | assert constants.air_molar_mass == 0.02896 7 | assert constants.gravitational_acceleration == 9.80665 8 | 9 | 10 | def test_air_gas_constant(): 11 | npt.assert_almost_equal(constants.air_gas_constant, 287.101, decimal=3) 12 | -------------------------------------------------------------------------------- /tests/helper_functions/test_algebric.py: -------------------------------------------------------------------------------- 1 | from adr.helper_functions import transform, rotate, translate, \ 2 | component_vector_in_absolute_frame, component_vector_coords_in_absolute_frame 3 | from vec import Vector2 4 | import math 5 | import numpy.testing as npt 6 | from adr.Components import BaseComponent 7 | 8 | 9 | def test_rotate(): 10 | v1 = Vector2(0.7, 0.1) 11 | v1_rot_90 = rotate(v1, math.radians(90)) 12 | v1_rot_n90 = rotate(v1, math.radians(-90)) 13 | v1_rot_n135 = rotate(v1, math.radians(-135)) 14 | npt.assert_almost_equal(v1_rot_90.x, -0.1) 15 | npt.assert_almost_equal(v1_rot_90.y, 0.7) 16 | npt.assert_almost_equal(v1_rot_n90.x, 0.1) 17 | npt.assert_almost_equal(v1_rot_n90.y, -0.7) 18 | npt.assert_almost_equal(v1_rot_n135.x, -0.424, decimal=3) 19 | npt.assert_almost_equal(v1_rot_n135.y, -0.565, decimal=3) 20 | 21 | v2 = Vector2(-1.4, 0.6) 22 | v2_rot_90 = rotate(v2, math.radians(90)) 23 | v2_rot_n90 = rotate(v2, math.radians(-90)) 24 | v2_rot_n135 = rotate(v2, math.radians(-135)) 25 | npt.assert_almost_equal(v2_rot_90.x, -0.6) 26 | npt.assert_almost_equal(v2_rot_90.y, -1.4) 27 | npt.assert_almost_equal(v2_rot_n90.x, 0.6) 28 | npt.assert_almost_equal(v2_rot_n90.y, 1.4) 29 | npt.assert_almost_equal(v2_rot_n135.x, 1.414, decimal=3) 30 | npt.assert_almost_equal(v2_rot_n135.y, 0.565, decimal=3) 31 | 32 | 33 | def test_translate(): 34 | v1 = Vector2(-1.4, 0.6) 35 | v1_trans1 = translate(v1, 0.3, 0.6) 36 | v1_trans2 = translate(v1, -0.3, -0.6) 37 | npt.assert_almost_equal(v1_trans1.x, -1.1) 38 | npt.assert_almost_equal(v1_trans1.y, 1.2) 39 | npt.assert_almost_equal(v1_trans2.x, -1.7) 40 | npt.assert_almost_equal(v1_trans2.y, 0.0) 41 | 42 | 43 | def test_transform(): 44 | v1 = Vector2(0.7, 0.1) 45 | v1_trans1 = transform(v1, math.radians(90), -0.2, 0.5) 46 | v1_trans2 = transform(v1, math.radians(-90), -0.2, 0.5) 47 | npt.assert_almost_equal(v1_trans1.x, -0.3) 48 | npt.assert_almost_equal(v1_trans1.y, 1.2) 49 | npt.assert_almost_equal(v1_trans2.x, -0.1) 50 | npt.assert_almost_equal(v1_trans2.y, -0.2) 51 | 52 | 53 | def test_component_vector_in_absolute_frame(mocker): 54 | component = mocker.Mock() 55 | component.position = Vector2(1.4, 0.6) 56 | component.angle = math.radians(24) 57 | vector = Vector2(-0.4, 0.1) 58 | new_vector = component_vector_in_absolute_frame(vector, component) 59 | npt.assert_almost_equal(new_vector.x, 0.994, decimal=3) 60 | npt.assert_almost_equal(new_vector.y, 0.53, decimal=3) 61 | 62 | 63 | def test_component_vector_coords_in_absolute_frame(mocker): 64 | component = mocker.Mock() 65 | component.position = Vector2(1.4, 0.6) 66 | component.angle = math.radians(25) 67 | vector_origin = Vector2(-0.4, 0.1) 68 | vector = Vector2(0, 0.5) 69 | x0, y0, x, y = component_vector_coords_in_absolute_frame( 70 | vector_origin, vector, component) 71 | npt.assert_almost_equal(x0, 0.994, decimal=3) 72 | npt.assert_almost_equal(y0, 0.521, decimal=3) 73 | npt.assert_almost_equal(x, 0.784, decimal=3) 74 | npt.assert_almost_equal(y, 0.975, decimal=3) 75 | --------------------------------------------------------------------------------