aerosandbox
===========
.. py:module:: aerosandbox
Submodules
----------
.. toctree::
:maxdepth: 1
/autoapi/aerosandbox/aerodynamics/index
/autoapi/aerosandbox/atmosphere/index
/autoapi/aerosandbox/common/index
/autoapi/aerosandbox/dynamics/index
/autoapi/aerosandbox/geometry/index
/autoapi/aerosandbox/library/index
/autoapi/aerosandbox/modeling/index
/autoapi/aerosandbox/numpy/index
/autoapi/aerosandbox/optimization/index
/autoapi/aerosandbox/performance/index
/autoapi/aerosandbox/propulsion/index
/autoapi/aerosandbox/structures/index
/autoapi/aerosandbox/tools/index
/autoapi/aerosandbox/visualization/index
/autoapi/aerosandbox/weights/index
Attributes
----------
.. autoapisummary::
aerosandbox._asb_root
aerosandbox.mp1
aerosandbox.__version__
Classes
-------
.. autoapisummary::
aerosandbox.Opti
aerosandbox.AeroSandboxObject
aerosandbox.ExplicitAnalysis
aerosandbox.ImplicitAnalysis
aerosandbox.Opti
aerosandbox.OptiSol
aerosandbox.FittedModel
aerosandbox.InterpolatedModel
aerosandbox.UnstructuredInterpolatedModel
aerosandbox.Airfoil
aerosandbox.KulfanAirfoil
aerosandbox.Wing
aerosandbox.WingXSec
aerosandbox.ControlSurface
aerosandbox.Fuselage
aerosandbox.FuselageXSec
aerosandbox.Airplane
aerosandbox.Propulsor
aerosandbox.Atmosphere
aerosandbox.AeroSandboxObject
aerosandbox.MassProperties
aerosandbox.OperatingPoint
aerosandbox.DynamicsPointMass1DHorizontal
aerosandbox.DynamicsPointMass1DVertical
aerosandbox.DynamicsPointMass2DCartesian
aerosandbox.DynamicsPointMass2DSpeedGamma
aerosandbox.DynamicsPointMass3DCartesian
aerosandbox.DynamicsPointMass3DSpeedGammaTrack
aerosandbox.DynamicsRigidBody2DBody
aerosandbox.DynamicsRigidBody3DBodyEuler
aerosandbox.AirfoilInviscid
aerosandbox.XFoil
aerosandbox.MSES
aerosandbox.VortexLatticeMethod
aerosandbox.LiftingLine
aerosandbox.NonlinearLiftingLine
aerosandbox.AeroBuildup
aerosandbox.AVL
Functions
---------
.. autoapisummary::
aerosandbox.load
aerosandbox.black_box
aerosandbox.is_casadi_type
aerosandbox.reflect_over_XZ_plane
aerosandbox.mass_properties_from_radius_of_gyration
aerosandbox.trim_string
aerosandbox.docs
aerosandbox.run_tests
Package Contents
----------------
.. py:data:: _asb_root
.. py:class:: Opti(variable_categories_to_freeze = None, cache_filename = None, load_frozen_variables_from_cache = False, save_to_cache_on_solve = False, ignore_violated_parametric_constraints = False, freeze_style = 'parameter')
Bases: :py:obj:`casadi.Opti`
The base class for mathematical optimization. For detailed usage, see the docstrings in its key methods:
* Opti.variable()
* Opti.subject_to()
* Opti.parameter()
* Opti.solve()
Example usage is as follows:
>>> opti = asb.Opti() # Initializes an optimization environment
>>> x = opti.variable(init_guess=5) # Initializes a new variable in that environment
>>> f = x ** 2 # Evaluates a (in this case, nonlinear) function based on a variable
>>> opti.subject_to(x > 3) # Adds a constraint to be enforced
>>> opti.minimize(f) # Sets the objective function as f
>>> sol = opti.solve() # Solves the problem using CasADi and IPOPT backend
>>> print(sol(x)) # Prints the value of x at the optimum.
.. py:attribute:: variable_categories_to_freeze
:value: None
.. py:attribute:: cache_filename
:value: None
.. py:attribute:: load_frozen_variables_from_cache
:value: False
.. py:attribute:: save_to_cache_on_solve
:value: False
.. py:attribute:: ignore_violated_parametric_constraints
:value: False
.. py:attribute:: freeze_style
:value: 'parameter'
.. py:attribute:: variables_categorized
.. py:attribute:: _variable_declarations
.. py:attribute:: _constraint_declarations
.. py:attribute:: _variable_index_counter
:value: 0
.. py:attribute:: _constraint_index_counter
:value: 0
.. py:method:: variable(init_guess = None, n_vars = None, scale = None, freeze = False, log_transform = False, category = 'Uncategorized', lower_bound = None, upper_bound = None, _stacklevel = 1)
Initializes a new decision variable (or vector of decision variables). You should pass an initial guess (
`init_guess`) upon defining a new variable. Dimensionality is inferred from this initial guess, but it can be
overridden; see below for syntax.
It is highly, highly recommended that you provide a scale (`scale`) for each variable, especially for
nonconvex problems, although this is not strictly required.
Usage notes:
When using vector variables, individual components of this vector of variables can be accessed via normal
indexing. Example:
>>> opti = asb.Opti()
>>> my_var = opti.variable(n_vars = 5)
>>> opti.subject_to(my_var[3] >= my_var[2]) # This is a valid way of indexing
>>> my_sum = asb.sum(my_var) # This will sum up all elements of `my_var`
:param init_guess: Initial guess for the optimal value of the variable being initialized. This is where in the
design space the optimizer will start looking.
This can be either a float or a NumPy ndarray; the dimension of the variable (i.e. scalar,
vector) that is created will be automatically inferred from the shape of the initial guess you
provide here. (Although it can also be overridden using the `n_vars` parameter; see below.)
For scalar variables, your initial guess should be a float:
>>> opti = asb.Opti()
>>> scalar_var = opti.variable(init_guess=5) # Initializes a scalar variable at a value of 5
For vector variables, your initial guess should be either:
* a float, in which case you must pass the length of the vector as `n_vars`, otherwise a scalar
variable will be created:
>>> opti = asb.Opti()
>>> vector_var = opti.variable(init_guess=5, n_vars=10) # Initializes a vector variable of length
>>> # 10, with all 10 elements set to an initial guess of 5.
* a NumPy ndarray, in which case each element will be initialized to the corresponding value in
the given array:
>>> opti = asb.Opti()
>>> vector_var = opti.variable(init_guess=np.linspace(0, 5, 10)) # Initializes a vector variable of
>>> # length 10, with all 10 elements initialized to linearly vary between 0 and 5.
In the case where the variable is to be log-transformed (see `log_transform`), the initial guess
should not be log-transformed as well - just supply the initial guess as usual. (Log-transform of the
initial guess happens under the hood.) The initial guess must, of course, be a positive number in
this case.
:param n_vars: [Optional] Used to manually override the dimensionality of the variable to create; if not
provided, the dimensionality of the variable is inferred from the initial guess `init_guess`.
The only real case where you need to use this argument would be if you are initializing a vector
variable to a scalar value, but you don't feel like using `init_guess=value * np.ones(n_vars)`.
For example:
>>> opti = asb.Opti()
>>> vector_var = opti.variable(init_guess=5, n_vars=10) # Initializes a vector variable of length
>>> # 10, with all 10 elements set to an initial guess of 5.
:param scale: [Optional] Approximate scale of the variable.
For example, if you're optimizing the design of a automobile and setting the tire diameter as an
optimization variable, you might choose `scale=0.5`, corresponding to 0.5 meters.
Properly scaling your variables can have a huge impact on solution speed (or even if the optimizer
converges at all). Although most modern second-order optimizers (such as IPOPT, used here) are
theoretically scale-invariant, numerical precision issues due to floating-point arithmetic can make
solving poorly-scaled problems really difficult or impossible. See here for more info:
https://web.casadi.org/blog/nlp-scaling/
If not specified, the code will try to pick a sensible value by defaulting to the `init_guess`.
:param freeze: [Optional] This boolean tells the optimizer to "freeze" the variable at a specific value. In
order to select the determine to freeze the variable at, the optimizer will use the following logic:
* If you initialize a new variable with the parameter `freeze=True`: the optimizer will freeze
the variable at the value of initial guess.
>>> opti = Opti()
>>> my_var = opti.variable(init_guess=5, freeze=True) # This will freeze my_var at a value of 5.
* If the Opti instance is associated with a cache file, and you told it to freeze a specific
category(s) of variables that your variable is a member of, and you didn't manually specify to
freeze the variable: the variable will be frozen based on the value in the cache file (and ignore
the `init_guess`). Example:
>>> opti = Opti(cache_filename="my_file.json", variable_categories_to_freeze=["Wheel Sizing"])
>>> # Assume, for example, that `my_file.json` was from a previous run where my_var=10.
>>> my_var = opti.variable(init_guess=5, category="Wheel Sizing")
>>> # This will freeze my_var at a value of 10 (from the cache file, not the init_guess)
* If the Opti instance is associated with a cache file, and you told it to freeze a specific
category(s) of variables that your variable is a member of, but you then manually specified that
the variable should be frozen: the variable will once again be frozen at the value of `init_guess`:
>>> opti = Opti(cache_filename="my_file.json", variable_categories_to_freeze=["Wheel Sizing"])
>>> # Assume, for example, that `my_file.json` was from a previous run where my_var=10.
>>> my_var = opti.variable(init_guess=5, category="Wheel Sizing", freeze=True)
>>> # This will freeze my_var at a value of 5 (`freeze` overrides category loading.)
Motivation for freezing variables:
The ability to freeze variables is exceptionally useful when designing engineering systems. Let's say
we're designing an airplane. In the beginning of the design process, we're doing "clean-sheet" design
- any variable is up for grabs for us to optimize on, because the airplane doesn't exist yet!
However, the farther we get into the design process, the more things get "locked in" - we may have
ordered jigs, settled on a wingspan, chosen an engine, et cetera. So, if something changes later (
let's say that we discover that one of our assumptions was too optimistic halfway through the design
process), we have to make up for that lost margin using only the variables that are still free. To do
this, we would freeze the variables that are already decided on.
By categorizing variables, you can also freeze entire categories of variables. For example,
you can freeze all of the wing design variables for an airplane but leave all of the fuselage
variables free.
This idea of freezing variables can also be used to look at off-design performance - freeze a
design, but change the operating conditions.
:param log_transform: [Optional] Advanced use only. A flag of whether to internally-log-transform this variable
before passing it to the optimizer. Good for known positive engineering quantities that become nonsensical
if negative (e.g. mass). Log-transforming these variables can also help maintain convexity.
:param category: [Optional] What category of variables does this belong to? # TODO expand docs
:param lower_bound: [Optional] If provided, defines a bounds constraint on the new variable that keeps the
variable above a given value.
:param upper_bound: [Optional] If provided, defines a bounds constraint on the new variable that keeps the
variable below a given value.
:param _stacklevel: Optional and advanced, purely used for debugging. Allows users to correctly track where
variables are declared in the event that they are subclassing `aerosandbox.Opti`. Modifies the
stacklevel of the declaration tracked, which is then presented using
`aerosandbox.Opti.variable_declaration()`.
:returns: The variable itself as a symbolic CasADi variable (MX type).
.. py:method:: subject_to(constraint, _stacklevel = 1)
Initialize a new equality or inequality constraint(s).
:param constraint: A constraint that you want to hold true at the optimum.
Inequality example:
>>> x = opti.variable()
>>> opti.subject_to(x >= 5)
Equality example; also showing that you can directly constrain functions of variables:
>>> x = opti.variable()
>>> f = np.sin(x)
>>> opti.subject_to(f == 0.5)
You can also pass in a list of multiple constraints using list syntax. For example:
>>> x = opti.variable()
>>> opti.subject_to([
>>> x >= 5,
>>> x <= 10
>>> ])
:param _stacklevel: Optional and advanced, purely used for debugging. Allows users to correctly track where
:param constraints are declared in the event that they are subclassing `aerosandbox.Opti`. Modifies the:
:param stacklevel of the declaration tracked:
:param which is then presented using:
:param `aerosandbox.Opti.constraint_declaration()`.:
:returns: The dual variable associated with the new constraint. If the `constraint` input is a list, returns
a list of dual variables.
.. py:method:: minimize(f)
[INTERNAL]
::
minimize(self, MX f)
Set objective.
Objective must be a scalar. Default objective: 0 When method is called
multiple times, the last call takes effect
Extra doc: https://github.com/casadi/casadi/wiki/L_1a
Doc source:
https://github.com/casadi/casadi/blob/develop/casadi/core/optistack.hpp#L133
Implementation:
https://github.com/casadi/casadi/blob/develop/casadi/core/optistack.cpp#L82-L88
.. py:method:: maximize(f)
.. py:method:: parameter(value = 0.0, n_params = None)
Initializes a new parameter (or vector of parameters). You must pass a value (`value`) upon defining a new
parameter. Dimensionality is inferred from this value, but it can be overridden; see below for syntax.
:param value: Value to set the new parameter to.
This can either be a float or a NumPy ndarray; the dimension of the parameter (i.e. scalar,
vector) that is created will be automatically inferred from the shape of the value you provide here.
(Although it can be overridden using the `n_params` parameter; see below.)
For scalar parameters, your value should be a float:
>>> opti = asb.Opti()
>>> scalar_param = opti.parameter(value=5) # Initializes a scalar parameter and sets its value to 5.
For vector variables, your value should be either:
* a float, in which case you must pass the length of the vector as `n_params`, otherwise a scalar
parameter will be created:
>>> opti = asb.Opti()
>>> vector_param = opti.parameter(value=5, n_params=10) # Initializes a vector parameter of length
>>> # 10, with all 10 elements set to value of 5.
* a NumPy ndarray, in which case each element will be set to the corresponding value in the given
array:
>>> opti = asb.Opti()
>>> vector_param = opti.parameter(value=np.linspace(0, 5, 10)) # Initializes a vector parameter of
>>> # length 10, with all 10 elements set to a value varying from 0 to 5.
:param n_params: [Optional] Used to manually override the dimensionality of the parameter to create; if not
provided, the dimensionality of the parameter is inferred from `value`.
The only real case where you need to use this argument would be if you are initializing a vector
parameter to a scalar value, but you don't feel like using `value=my_value * np.ones(n_vars)`.
For example:
>>> opti = asb.Opti()
>>> vector_param = opti.parameter(value=5, n_params=10) # Initializes a vector parameter of length
>>> # 10, with all 10 elements set to a value of 5.
:returns: The parameter itself as a symbolic CasADi variable (MX type).
.. py:method:: solve(parameter_mapping = None, max_iter = 1000, max_runtime = 1e+20, callback = None, verbose = True, jit = False, detect_simple_bounds = False, expand = False, options = None, behavior_on_failure = 'raise')
Solve the optimization problem using CasADi with IPOPT backend.
:param parameter_mapping: [Optional] Allows you to specify values for parameters.
Dictionary where the key is the parameter and the value is the value to be set to.
Example: # TODO update syntax for required init_guess
>>> opti = asb.Opti()
>>> x = opti.variable()
>>> p = opti.parameter()
>>> opti.minimize(x ** 2)
>>> opti.subject_to(x >= p)
>>> sol = opti.solve(
>>> {
>>> p: 5 # Sets the value of parameter p to 5, then solves.
>>> }
>>> )
:param max_iter: [Optional] The maximum number of iterations allowed before giving up.
:param max_runtime: [Optional] Gives the maximum allowable runtime before giving up.
:param callback: [Optional] A function to be called at each iteration of the optimization algorithm.
Useful for printing progress or displaying intermediate results.
The callback function `func` should have the syntax `func(iteration_number)`, where iteration_number
is an integer corresponding to the current iteration number. In order to access intermediate
quantities of optimization variables (e.g. for plotting), use the `Opti.debug.value(x)` syntax for
each variable `x`.
:param verbose: Controls the verbosity of the solver. If True, IPOPT will print its progress to the console.
:param jit: Experimental. If True, the optimization problem will be compiled to C++ and then JIT-compiled
using the CasADi JIT compiler. This can lead to significant speedups, but may also lead to
unexpected behavior, and may not work on all platforms.
:param options: [Optional] A dictionary of options to pass to IPOPT. See the IPOPT documentation for a list of
available options.
:param behavior_on_failure: [Optional] What should we do if the optimization fails? Options are:
* "raise": Raise an exception. This is the default behavior.
* "return_last": Returns the solution from the last iteration, and raise a warning.
NOTE: The returned solution may not be feasible! (It also may not be optimal.)
Returns: An OptiSol object that contains the solved optimization problem. To extract values, use
my_optisol(variable).
Example:
>>> sol = opti.solve()
>>> x_opt = sol(x) # Get the value of variable x at the optimum.
.. py:method:: solve_sweep(parameter_mapping, update_initial_guesses_between_solves=False, verbose=True, solve_kwargs = None, return_callable = False, garbage_collect_between_runs = False)
.. py:method:: find_variable_declaration(index, use_full_filename = False, return_string = False)
.. py:method:: find_constraint_declaration(index, use_full_filename = False, return_string = False)
.. py:method:: set_initial_from_sol(sol, initialize_primals=True, initialize_duals=True)
Sets the initial value of all variables in the Opti object to the solution of another Opti instance. Useful
for warm-starting an Opti instance based on the result of another instance.
Args: sol: Takes in the solution object. Assumes that sol corresponds to exactly the same optimization
problem as this Opti instance, perhaps with different parameter values.
Returns: None (in-place)
.. py:method:: save_solution()
.. py:method:: get_solution_dict_from_cache()
.. py:method:: derivative_of(variable, with_respect_to, derivative_init_guess, derivative_scale = None, method = 'trapezoidal', explicit = False, _stacklevel = 1)
Returns a quantity that is either defined or constrained to be a derivative of an existing variable.
For example:
>>> opti = Opti()
>>> position = opti.variable(init_guess=0, n_vars=100)
>>> time = np.linspace(0, 1, 100)
>>> velocity = opti.derivative_of(position, with_respect_to=time)
>>> acceleration = opti.derivative_of(velocity, with_respect_to=time)
:param variable: The variable or quantity that you are taking the derivative of. The "numerator" of the
:param derivative:
:param in colloquial parlance.:
:param with_respect_to: The variable or quantity that you are taking the derivative with respect to. The
:param "denominator" of the derivative: In a typical example case, this `with_respect_to` parameter would be time. Please make sure that the
value of this parameter is monotonically increasing, otherwise you may get nonsensical answers.
:param in colloquial parlance.: In a typical example case, this `with_respect_to` parameter would be time. Please make sure that the
value of this parameter is monotonically increasing, otherwise you may get nonsensical answers.
:param derivative_init_guess: Initial guess for the value of the derivative. Should be either a float (in which
:param case the initial guess will be a vector equal to this value) or a vector of initial guesses with the same:
:param length as `variable`. For more info:
:param look at the docstring of opti.variable()'s `init_guess` parameter.:
:param derivative_scale: Scale factor for the value of the derivative. For more info, look at the docstring of
:param opti.variable()'s `scale` parameter.:
:param method: The type of integrator to use to define this derivative. Options are:
* "forward euler" - a first-order-accurate forward Euler method
Citation: https://en.wikipedia.org/wiki/Euler_method
* "backwards euler" - a first-order-accurate backwards Euler method
Citation: https://en.wikipedia.org/wiki/Backward_Euler_method
* "midpoint" or "trapezoid" - a second-order-accurate midpoint method
Citation: https://en.wikipedia.org/wiki/Midpoint_method
* "simpson" - Simpson's rule for integration
Citation: https://en.wikipedia.org/wiki/Simpson%27s_rule
* "runge-kutta" or "rk4" - a fourth-order-accurate Runge-Kutta method. I suppose that technically,
"forward euler", "backward euler", and "midpoint" are all (lower-order) Runge-Kutta methods...
Citation: https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#The_Runge%E2%80%93Kutta_method
* "runge-kutta-3/8" - A modified version of the Runge-Kutta 4 proposed by Kutta in 1901. Also
fourth-order-accurate, but all of the error coefficients are smaller than they are in the standard
Runge-Kutta 4 method. The downside is that more floating point operations are required per timestep,
as the Butcher tableau is more dense (i.e. not banded).
Citation: Kutta, Martin (1901), "Beitrag zur näherungsweisen Integration totaler
Differentialgleichungen", Zeitschrift für Mathematik und Physik, 46: 435–453
:param explicit: If true, returns an explicit derivative rather than an implicit one. In other words,
:param this *defines* the output to be a derivative of the input rather than *constraining* the output to the a:
:param derivative of the input.: Explicit derivatives result in smaller, denser systems of equations that are more akin to
shooting-type methods. Implicit derivatives result in larger, sparser systems of equations that are
more akin to collocation methods. Explicit derivatives are better for simple, stable systems with few
states, while implicit derivatives are better for complex, potentially-unstable systems with many
states.
# TODO implement explicit
:param _stacklevel: Optional and advanced, purely used for debugging. Allows users to correctly track where
:param constraints are declared in the event that they are subclassing `aerosandbox.Opti`. Modifies the:
:param stacklevel of the declaration tracked:
:param which is then presented using:
:param `aerosandbox.Opti.variable_declaration:
:type `aerosandbox.Opti.variable_declaration: )` and `aerosandbox.Opti.constraint_declaration(
Returns: A vector consisting of the derivative of the parameter `variable` with respect to `with_respect_to`.
.. py:method:: constrain_derivative(derivative, variable, with_respect_to, method = 'trapezoidal', _stacklevel = 1)
Adds a constraint to the optimization problem such that:
d(variable) / d(with_respect_to) == derivative
Can be used directly; also called indirectly by opti.derivative_of() for implicit derivative creation.
:param derivative: The derivative that is to be constrained here.
:param variable: The variable or quantity that you are taking the derivative of. The "numerator" of the
:param derivative:
:param in colloquial parlance.:
:param with_respect_to: The variable or quantity that you are taking the derivative with respect to. The
:param "denominator" of the derivative: In a typical example case, this `with_respect_to` parameter would be time. Please make sure that the
value of this parameter is monotonically increasing, otherwise you may get nonsensical answers.
:param in colloquial parlance.: In a typical example case, this `with_respect_to` parameter would be time. Please make sure that the
value of this parameter is monotonically increasing, otherwise you may get nonsensical answers.
:param method: The type of integrator to use to define this derivative. Options are:
* "forward euler" - a first-order-accurate forward Euler method
Citation: https://en.wikipedia.org/wiki/Euler_method
* "backwards euler" - a first-order-accurate backwards Euler method
Citation: https://en.wikipedia.org/wiki/Backward_Euler_method
* "midpoint" or "trapezoid" - a second-order-accurate midpoint method
Citation: https://en.wikipedia.org/wiki/Midpoint_method
* "simpson" - Simpson's rule for integration
Citation: https://en.wikipedia.org/wiki/Simpson%27s_rule
* "runge-kutta" or "rk4" - a fourth-order-accurate Runge-Kutta method. I suppose that technically,
"forward euler", "backward euler", and "midpoint" are all (lower-order) Runge-Kutta methods...
Citation: https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#The_Runge%E2%80%93Kutta_method
* "runge-kutta-3/8" - A modified version of the Runge-Kutta 4 proposed by Kutta in 1901. Also
fourth-order-accurate, but all of the error coefficients are smaller than they are in the standard
Runge-Kutta 4 method. The downside is that more floating point operations are required per timestep,
as the Butcher tableau is more dense (i.e. not banded).
Citation: Kutta, Martin (1901), "Beitrag zur näherungsweisen Integration totaler
Differentialgleichungen", Zeitschrift für Mathematik und Physik, 46: 435–453
:param Note that all methods are expressed as integrators rather than differentiators; this prevents:
:param singularities from forming in the limit of timestep approaching zero. (For those coming from the PDE:
:param world:
:param this is analogous to using finite volume methods rather than finite difference methods to allow:
:param shock capturing.):
:param _stacklevel: Optional and advanced, purely used for debugging. Allows users to correctly track where
:param constraints are declared in the event that they are subclassing `aerosandbox.Opti`. Modifies the:
:param stacklevel of the declaration tracked:
:param which is then presented using:
:param `aerosandbox.Opti.variable_declaration:
:type `aerosandbox.Opti.variable_declaration: )` and `aerosandbox.Opti.constraint_declaration(
Returns: None (adds constraint in-place).
.. py:class:: AeroSandboxObject
Bases: :py:obj:`abc.ABC`
Helper class that provides a standard way to create an ABC using
inheritance.
.. py:attribute:: _asb_metadata
:type: Dict[str, str]
:value: None
.. py:method:: __eq__(other)
Checks if two AeroSandbox objects are value-equivalent. A more sensible default for classes that represent
physical objects than checking for memory equivalence.
This is done by checking if the two objects are of the same type and have the same __dict__.
:param other: Another object.
:returns: True if the objects are equal, False otherwise.
:rtype: bool
.. py:method:: save(filename = None, verbose = True, automatically_add_extension = True)
Saves the object to a binary file, using the `dill` library.
Creates a .asb file, which is a binary file that can be loaded with `aerosandbox.load()`. This can be loaded
into memory in a different Python session or a different computer, and it will be exactly the same as when it
was saved.
:param filename: The filename to save this object to. Should be a .asb file.
:param verbose: If True, prints messages to console on successful save.
:param automatically_add_extension: If True, automatically adds the .asb extension to the filename if it doesn't
already have it. If False, does not add the extension.
Returns: None (writes to file)
.. py:method:: copy()
Returns a shallow copy of the object.
.. py:method:: deepcopy()
Returns a deep copy of the object.
.. py:method:: substitute_solution(sol, inplace = None)
Substitutes a solution from CasADi's solver recursively as an in-place operation.
In-place operation. To make it not in-place, do `y = copy.deepcopy(x)` or similar first.
:param sol: OptiSol object.
:return:
.. py:function:: load(filename, verbose = True)
Loads an AeroSandboxObject from a file.
Upon load, will compare metadata from the file to the current Python version and AeroSandbox version. If there are
any discrepancies, will raise a warning.
:param filename: The filename to load from. Should be a .asb file.
:param verbose: If True, prints messages to console on successful load.
Returns: An AeroSandboxObject.
.. py:class:: ExplicitAnalysis
Bases: :py:obj:`AeroSandboxObject`
Helper class that provides a standard way to create an ABC using
inheritance.
.. py:attribute:: default_analysis_specific_options
:type: Dict[type, Dict[str, Any]]
This is part of AeroSandbox's "analysis-specific options" feature, which lets you "tag" geometry objects with
flags that change how different analyses act on them.
This variable, `default_analysis_specific_options`, allows you to specify default values for options that can be used for
specific problems.
This should be a dictionary, where: * keys are the geometry-like types that you might be interested in defining
parameters for. * values are dictionaries, where: * keys are strings that label a given option * values are
anything. These are used as the default values, in the event that the associated geometry doesn't override those.
An example of what this variable might look like, for a vortex-lattice method aerodynamic analysis:
>>> default_analysis_specific_options = {
>>> Airplane: dict(
>>> profile_drag_coefficient=0
>>> ),
>>> Wing : dict(
>>> wing_level_spanwise_spacing=True,
>>> spanwise_resolution=12,
>>> spanwise_spacing="cosine",
>>> chordwise_resolution=12,
>>> chordwise_spacing="cosine",
>>> component=None, # type: int
>>> no_wake=False,
>>> no_alpha_beta=False,
>>> no_load=False,
>>> drag_polar=dict(
>>> CL1=0,
>>> CD1=0,
>>> CL2=0,
>>> CD2=0,
>>> CL3=0,
>>> CD3=0,
>>> ),
>>> )
>>> }
.. py:method:: get_options(geometry_object)
Retrieves the analysis-specific options that correspond to both:
* An analysis type (which is this object, "self"), and
* A specific geometry object, such as an Airplane or Wing.
:param geometry_object: An instance of an AeroSandbox geometry object, such as an Airplane, Wing, etc.
* In order for this function to do something useful, you probably want this option to have
`analysis_specific_options` defined. See the asb.Airplane constructor for an example of this.
Returns: A dictionary that combines:
* This analysis's default options for this geometry, if any exist.
* The geometry's declared analysis-specific-options for this analysis, if it exists. These geometry
options will override the defaults from the analysis.
This dictionary has the format:
* keys are strings, listing specific options
* values can be any type, and simply state the value of the analysis-specific option following the
logic above.
Note: if this analysis defines a set of default options for the geometry type in question (by using
`self.default_analysis_specific_options`), all keys from the geometry object's `analysis_specific_options`
will be validated against those in the default options set. A warning will be raised if keys do not
correspond to those in the defaults, as this (potentially) indicates a typo, which would otherwise be
difficult to debug.
.. py:class:: ImplicitAnalysis
Bases: :py:obj:`AeroSandboxObject`
Helper class that provides a standard way to create an ABC using
inheritance.
.. py:method:: initialize(init_method)
:staticmethod:
A decorator that should be applied to the __init__ method of ImplicitAnalysis or any subclass of it.
Usage example:
>>> class MyAnalysis(ImplicitAnalysis):
>>>
>>> @ImplicitAnalysis.initialize
>>> def __init__(self):
>>> self.a = self.opti.variable(init_guess = 1)
>>> self.b = self.opti.variable(init_guess = 2)
>>>
>>> self.opti.subject_to(
>>> self.a == self.b ** 2
>>> ) # Add a nonlinear governing equation
Functionality:
The basic purpose of this wrapper is to ensure that every ImplicitAnalysis has an `opti` property that points to
an optimization environment (asb.Opti type) that it can work in.
How do we obtain an asb.Opti environment to work in? Well, this decorator adds an optional `opti` parameter to
the __init__ method that it is applied to.
1. If this `opti` parameter is not provided, then a new empty `asb.Opti` environment is created and stored as
`ImplicitAnalysis.opti`.
2. If the `opti` parameter is provided, then we simply assign the given `asb.Opti` environment (which may
already contain other variables/constraints/objective) to `ImplicitAnalysis.opti`.
In addition, a property called `ImplicitAnalysis.opti_provided` is stored, which records whether the user
provided an Opti environment or if one was instead created for them.
If the user did not provide an Opti environment (Option 1 from our list above), we assume that the user basically
just wants to perform a normal, single-disciplinary analysis. So, in this case, we proceed to solve the analysis as-is
and do an in-place substitution of the solution.
If the user did provide an Opti environment (Option 2 from our list above), we assume that the user might potentially want
to add other implicit analyses to the problem. So, in this case, we don't solve the analysis, and the user must later
solve the analysis by calling `sol = opti.solve()` or similar.
.. py:exception:: ImplicitAnalysisInitError(message="\n Your ImplicitAnalysis object doesn't have an `opti` property!\n This is almost certainly because you didn't decorate your object's __init__ method with \n `@ImplicitAnalysis.initialize`, which you should go do.\n ")
Bases: :py:obj:`Exception`
Common base class for all non-exit exceptions.
.. py:attribute:: message
:value: Multiline-String
.. raw:: html
Show Value
.. code-block:: python
"""
Your ImplicitAnalysis object doesn't have an `opti` property!
This is almost certainly because you didn't decorate your object's __init__ method with
`@ImplicitAnalysis.initialize`, which you should go do.
"""
.. raw:: html
.. py:property:: opti
.. py:property:: opti_provided
.. py:class:: Opti(variable_categories_to_freeze = None, cache_filename = None, load_frozen_variables_from_cache = False, save_to_cache_on_solve = False, ignore_violated_parametric_constraints = False, freeze_style = 'parameter')
Bases: :py:obj:`casadi.Opti`
The base class for mathematical optimization. For detailed usage, see the docstrings in its key methods:
* Opti.variable()
* Opti.subject_to()
* Opti.parameter()
* Opti.solve()
Example usage is as follows:
>>> opti = asb.Opti() # Initializes an optimization environment
>>> x = opti.variable(init_guess=5) # Initializes a new variable in that environment
>>> f = x ** 2 # Evaluates a (in this case, nonlinear) function based on a variable
>>> opti.subject_to(x > 3) # Adds a constraint to be enforced
>>> opti.minimize(f) # Sets the objective function as f
>>> sol = opti.solve() # Solves the problem using CasADi and IPOPT backend
>>> print(sol(x)) # Prints the value of x at the optimum.
.. py:attribute:: variable_categories_to_freeze
:value: None
.. py:attribute:: cache_filename
:value: None
.. py:attribute:: load_frozen_variables_from_cache
:value: False
.. py:attribute:: save_to_cache_on_solve
:value: False
.. py:attribute:: ignore_violated_parametric_constraints
:value: False
.. py:attribute:: freeze_style
:value: 'parameter'
.. py:attribute:: variables_categorized
.. py:attribute:: _variable_declarations
.. py:attribute:: _constraint_declarations
.. py:attribute:: _variable_index_counter
:value: 0
.. py:attribute:: _constraint_index_counter
:value: 0
.. py:method:: variable(init_guess = None, n_vars = None, scale = None, freeze = False, log_transform = False, category = 'Uncategorized', lower_bound = None, upper_bound = None, _stacklevel = 1)
Initializes a new decision variable (or vector of decision variables). You should pass an initial guess (
`init_guess`) upon defining a new variable. Dimensionality is inferred from this initial guess, but it can be
overridden; see below for syntax.
It is highly, highly recommended that you provide a scale (`scale`) for each variable, especially for
nonconvex problems, although this is not strictly required.
Usage notes:
When using vector variables, individual components of this vector of variables can be accessed via normal
indexing. Example:
>>> opti = asb.Opti()
>>> my_var = opti.variable(n_vars = 5)
>>> opti.subject_to(my_var[3] >= my_var[2]) # This is a valid way of indexing
>>> my_sum = asb.sum(my_var) # This will sum up all elements of `my_var`
:param init_guess: Initial guess for the optimal value of the variable being initialized. This is where in the
design space the optimizer will start looking.
This can be either a float or a NumPy ndarray; the dimension of the variable (i.e. scalar,
vector) that is created will be automatically inferred from the shape of the initial guess you
provide here. (Although it can also be overridden using the `n_vars` parameter; see below.)
For scalar variables, your initial guess should be a float:
>>> opti = asb.Opti()
>>> scalar_var = opti.variable(init_guess=5) # Initializes a scalar variable at a value of 5
For vector variables, your initial guess should be either:
* a float, in which case you must pass the length of the vector as `n_vars`, otherwise a scalar
variable will be created:
>>> opti = asb.Opti()
>>> vector_var = opti.variable(init_guess=5, n_vars=10) # Initializes a vector variable of length
>>> # 10, with all 10 elements set to an initial guess of 5.
* a NumPy ndarray, in which case each element will be initialized to the corresponding value in
the given array:
>>> opti = asb.Opti()
>>> vector_var = opti.variable(init_guess=np.linspace(0, 5, 10)) # Initializes a vector variable of
>>> # length 10, with all 10 elements initialized to linearly vary between 0 and 5.
In the case where the variable is to be log-transformed (see `log_transform`), the initial guess
should not be log-transformed as well - just supply the initial guess as usual. (Log-transform of the
initial guess happens under the hood.) The initial guess must, of course, be a positive number in
this case.
:param n_vars: [Optional] Used to manually override the dimensionality of the variable to create; if not
provided, the dimensionality of the variable is inferred from the initial guess `init_guess`.
The only real case where you need to use this argument would be if you are initializing a vector
variable to a scalar value, but you don't feel like using `init_guess=value * np.ones(n_vars)`.
For example:
>>> opti = asb.Opti()
>>> vector_var = opti.variable(init_guess=5, n_vars=10) # Initializes a vector variable of length
>>> # 10, with all 10 elements set to an initial guess of 5.
:param scale: [Optional] Approximate scale of the variable.
For example, if you're optimizing the design of a automobile and setting the tire diameter as an
optimization variable, you might choose `scale=0.5`, corresponding to 0.5 meters.
Properly scaling your variables can have a huge impact on solution speed (or even if the optimizer
converges at all). Although most modern second-order optimizers (such as IPOPT, used here) are
theoretically scale-invariant, numerical precision issues due to floating-point arithmetic can make
solving poorly-scaled problems really difficult or impossible. See here for more info:
https://web.casadi.org/blog/nlp-scaling/
If not specified, the code will try to pick a sensible value by defaulting to the `init_guess`.
:param freeze: [Optional] This boolean tells the optimizer to "freeze" the variable at a specific value. In
order to select the determine to freeze the variable at, the optimizer will use the following logic:
* If you initialize a new variable with the parameter `freeze=True`: the optimizer will freeze
the variable at the value of initial guess.
>>> opti = Opti()
>>> my_var = opti.variable(init_guess=5, freeze=True) # This will freeze my_var at a value of 5.
* If the Opti instance is associated with a cache file, and you told it to freeze a specific
category(s) of variables that your variable is a member of, and you didn't manually specify to
freeze the variable: the variable will be frozen based on the value in the cache file (and ignore
the `init_guess`). Example:
>>> opti = Opti(cache_filename="my_file.json", variable_categories_to_freeze=["Wheel Sizing"])
>>> # Assume, for example, that `my_file.json` was from a previous run where my_var=10.
>>> my_var = opti.variable(init_guess=5, category="Wheel Sizing")
>>> # This will freeze my_var at a value of 10 (from the cache file, not the init_guess)
* If the Opti instance is associated with a cache file, and you told it to freeze a specific
category(s) of variables that your variable is a member of, but you then manually specified that
the variable should be frozen: the variable will once again be frozen at the value of `init_guess`:
>>> opti = Opti(cache_filename="my_file.json", variable_categories_to_freeze=["Wheel Sizing"])
>>> # Assume, for example, that `my_file.json` was from a previous run where my_var=10.
>>> my_var = opti.variable(init_guess=5, category="Wheel Sizing", freeze=True)
>>> # This will freeze my_var at a value of 5 (`freeze` overrides category loading.)
Motivation for freezing variables:
The ability to freeze variables is exceptionally useful when designing engineering systems. Let's say
we're designing an airplane. In the beginning of the design process, we're doing "clean-sheet" design
- any variable is up for grabs for us to optimize on, because the airplane doesn't exist yet!
However, the farther we get into the design process, the more things get "locked in" - we may have
ordered jigs, settled on a wingspan, chosen an engine, et cetera. So, if something changes later (
let's say that we discover that one of our assumptions was too optimistic halfway through the design
process), we have to make up for that lost margin using only the variables that are still free. To do
this, we would freeze the variables that are already decided on.
By categorizing variables, you can also freeze entire categories of variables. For example,
you can freeze all of the wing design variables for an airplane but leave all of the fuselage
variables free.
This idea of freezing variables can also be used to look at off-design performance - freeze a
design, but change the operating conditions.
:param log_transform: [Optional] Advanced use only. A flag of whether to internally-log-transform this variable
before passing it to the optimizer. Good for known positive engineering quantities that become nonsensical
if negative (e.g. mass). Log-transforming these variables can also help maintain convexity.
:param category: [Optional] What category of variables does this belong to? # TODO expand docs
:param lower_bound: [Optional] If provided, defines a bounds constraint on the new variable that keeps the
variable above a given value.
:param upper_bound: [Optional] If provided, defines a bounds constraint on the new variable that keeps the
variable below a given value.
:param _stacklevel: Optional and advanced, purely used for debugging. Allows users to correctly track where
variables are declared in the event that they are subclassing `aerosandbox.Opti`. Modifies the
stacklevel of the declaration tracked, which is then presented using
`aerosandbox.Opti.variable_declaration()`.
:returns: The variable itself as a symbolic CasADi variable (MX type).
.. py:method:: subject_to(constraint, _stacklevel = 1)
Initialize a new equality or inequality constraint(s).
:param constraint: A constraint that you want to hold true at the optimum.
Inequality example:
>>> x = opti.variable()
>>> opti.subject_to(x >= 5)
Equality example; also showing that you can directly constrain functions of variables:
>>> x = opti.variable()
>>> f = np.sin(x)
>>> opti.subject_to(f == 0.5)
You can also pass in a list of multiple constraints using list syntax. For example:
>>> x = opti.variable()
>>> opti.subject_to([
>>> x >= 5,
>>> x <= 10
>>> ])
:param _stacklevel: Optional and advanced, purely used for debugging. Allows users to correctly track where
:param constraints are declared in the event that they are subclassing `aerosandbox.Opti`. Modifies the:
:param stacklevel of the declaration tracked:
:param which is then presented using:
:param `aerosandbox.Opti.constraint_declaration()`.:
:returns: The dual variable associated with the new constraint. If the `constraint` input is a list, returns
a list of dual variables.
.. py:method:: minimize(f)
[INTERNAL]
::
minimize(self, MX f)
Set objective.
Objective must be a scalar. Default objective: 0 When method is called
multiple times, the last call takes effect
Extra doc: https://github.com/casadi/casadi/wiki/L_1a
Doc source:
https://github.com/casadi/casadi/blob/develop/casadi/core/optistack.hpp#L133
Implementation:
https://github.com/casadi/casadi/blob/develop/casadi/core/optistack.cpp#L82-L88
.. py:method:: maximize(f)
.. py:method:: parameter(value = 0.0, n_params = None)
Initializes a new parameter (or vector of parameters). You must pass a value (`value`) upon defining a new
parameter. Dimensionality is inferred from this value, but it can be overridden; see below for syntax.
:param value: Value to set the new parameter to.
This can either be a float or a NumPy ndarray; the dimension of the parameter (i.e. scalar,
vector) that is created will be automatically inferred from the shape of the value you provide here.
(Although it can be overridden using the `n_params` parameter; see below.)
For scalar parameters, your value should be a float:
>>> opti = asb.Opti()
>>> scalar_param = opti.parameter(value=5) # Initializes a scalar parameter and sets its value to 5.
For vector variables, your value should be either:
* a float, in which case you must pass the length of the vector as `n_params`, otherwise a scalar
parameter will be created:
>>> opti = asb.Opti()
>>> vector_param = opti.parameter(value=5, n_params=10) # Initializes a vector parameter of length
>>> # 10, with all 10 elements set to value of 5.
* a NumPy ndarray, in which case each element will be set to the corresponding value in the given
array:
>>> opti = asb.Opti()
>>> vector_param = opti.parameter(value=np.linspace(0, 5, 10)) # Initializes a vector parameter of
>>> # length 10, with all 10 elements set to a value varying from 0 to 5.
:param n_params: [Optional] Used to manually override the dimensionality of the parameter to create; if not
provided, the dimensionality of the parameter is inferred from `value`.
The only real case where you need to use this argument would be if you are initializing a vector
parameter to a scalar value, but you don't feel like using `value=my_value * np.ones(n_vars)`.
For example:
>>> opti = asb.Opti()
>>> vector_param = opti.parameter(value=5, n_params=10) # Initializes a vector parameter of length
>>> # 10, with all 10 elements set to a value of 5.
:returns: The parameter itself as a symbolic CasADi variable (MX type).
.. py:method:: solve(parameter_mapping = None, max_iter = 1000, max_runtime = 1e+20, callback = None, verbose = True, jit = False, detect_simple_bounds = False, expand = False, options = None, behavior_on_failure = 'raise')
Solve the optimization problem using CasADi with IPOPT backend.
:param parameter_mapping: [Optional] Allows you to specify values for parameters.
Dictionary where the key is the parameter and the value is the value to be set to.
Example: # TODO update syntax for required init_guess
>>> opti = asb.Opti()
>>> x = opti.variable()
>>> p = opti.parameter()
>>> opti.minimize(x ** 2)
>>> opti.subject_to(x >= p)
>>> sol = opti.solve(
>>> {
>>> p: 5 # Sets the value of parameter p to 5, then solves.
>>> }
>>> )
:param max_iter: [Optional] The maximum number of iterations allowed before giving up.
:param max_runtime: [Optional] Gives the maximum allowable runtime before giving up.
:param callback: [Optional] A function to be called at each iteration of the optimization algorithm.
Useful for printing progress or displaying intermediate results.
The callback function `func` should have the syntax `func(iteration_number)`, where iteration_number
is an integer corresponding to the current iteration number. In order to access intermediate
quantities of optimization variables (e.g. for plotting), use the `Opti.debug.value(x)` syntax for
each variable `x`.
:param verbose: Controls the verbosity of the solver. If True, IPOPT will print its progress to the console.
:param jit: Experimental. If True, the optimization problem will be compiled to C++ and then JIT-compiled
using the CasADi JIT compiler. This can lead to significant speedups, but may also lead to
unexpected behavior, and may not work on all platforms.
:param options: [Optional] A dictionary of options to pass to IPOPT. See the IPOPT documentation for a list of
available options.
:param behavior_on_failure: [Optional] What should we do if the optimization fails? Options are:
* "raise": Raise an exception. This is the default behavior.
* "return_last": Returns the solution from the last iteration, and raise a warning.
NOTE: The returned solution may not be feasible! (It also may not be optimal.)
Returns: An OptiSol object that contains the solved optimization problem. To extract values, use
my_optisol(variable).
Example:
>>> sol = opti.solve()
>>> x_opt = sol(x) # Get the value of variable x at the optimum.
.. py:method:: solve_sweep(parameter_mapping, update_initial_guesses_between_solves=False, verbose=True, solve_kwargs = None, return_callable = False, garbage_collect_between_runs = False)
.. py:method:: find_variable_declaration(index, use_full_filename = False, return_string = False)
.. py:method:: find_constraint_declaration(index, use_full_filename = False, return_string = False)
.. py:method:: set_initial_from_sol(sol, initialize_primals=True, initialize_duals=True)
Sets the initial value of all variables in the Opti object to the solution of another Opti instance. Useful
for warm-starting an Opti instance based on the result of another instance.
Args: sol: Takes in the solution object. Assumes that sol corresponds to exactly the same optimization
problem as this Opti instance, perhaps with different parameter values.
Returns: None (in-place)
.. py:method:: save_solution()
.. py:method:: get_solution_dict_from_cache()
.. py:method:: derivative_of(variable, with_respect_to, derivative_init_guess, derivative_scale = None, method = 'trapezoidal', explicit = False, _stacklevel = 1)
Returns a quantity that is either defined or constrained to be a derivative of an existing variable.
For example:
>>> opti = Opti()
>>> position = opti.variable(init_guess=0, n_vars=100)
>>> time = np.linspace(0, 1, 100)
>>> velocity = opti.derivative_of(position, with_respect_to=time)
>>> acceleration = opti.derivative_of(velocity, with_respect_to=time)
:param variable: The variable or quantity that you are taking the derivative of. The "numerator" of the
:param derivative:
:param in colloquial parlance.:
:param with_respect_to: The variable or quantity that you are taking the derivative with respect to. The
:param "denominator" of the derivative: In a typical example case, this `with_respect_to` parameter would be time. Please make sure that the
value of this parameter is monotonically increasing, otherwise you may get nonsensical answers.
:param in colloquial parlance.: In a typical example case, this `with_respect_to` parameter would be time. Please make sure that the
value of this parameter is monotonically increasing, otherwise you may get nonsensical answers.
:param derivative_init_guess: Initial guess for the value of the derivative. Should be either a float (in which
:param case the initial guess will be a vector equal to this value) or a vector of initial guesses with the same:
:param length as `variable`. For more info:
:param look at the docstring of opti.variable()'s `init_guess` parameter.:
:param derivative_scale: Scale factor for the value of the derivative. For more info, look at the docstring of
:param opti.variable()'s `scale` parameter.:
:param method: The type of integrator to use to define this derivative. Options are:
* "forward euler" - a first-order-accurate forward Euler method
Citation: https://en.wikipedia.org/wiki/Euler_method
* "backwards euler" - a first-order-accurate backwards Euler method
Citation: https://en.wikipedia.org/wiki/Backward_Euler_method
* "midpoint" or "trapezoid" - a second-order-accurate midpoint method
Citation: https://en.wikipedia.org/wiki/Midpoint_method
* "simpson" - Simpson's rule for integration
Citation: https://en.wikipedia.org/wiki/Simpson%27s_rule
* "runge-kutta" or "rk4" - a fourth-order-accurate Runge-Kutta method. I suppose that technically,
"forward euler", "backward euler", and "midpoint" are all (lower-order) Runge-Kutta methods...
Citation: https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#The_Runge%E2%80%93Kutta_method
* "runge-kutta-3/8" - A modified version of the Runge-Kutta 4 proposed by Kutta in 1901. Also
fourth-order-accurate, but all of the error coefficients are smaller than they are in the standard
Runge-Kutta 4 method. The downside is that more floating point operations are required per timestep,
as the Butcher tableau is more dense (i.e. not banded).
Citation: Kutta, Martin (1901), "Beitrag zur näherungsweisen Integration totaler
Differentialgleichungen", Zeitschrift für Mathematik und Physik, 46: 435–453
:param explicit: If true, returns an explicit derivative rather than an implicit one. In other words,
:param this *defines* the output to be a derivative of the input rather than *constraining* the output to the a:
:param derivative of the input.: Explicit derivatives result in smaller, denser systems of equations that are more akin to
shooting-type methods. Implicit derivatives result in larger, sparser systems of equations that are
more akin to collocation methods. Explicit derivatives are better for simple, stable systems with few
states, while implicit derivatives are better for complex, potentially-unstable systems with many
states.
# TODO implement explicit
:param _stacklevel: Optional and advanced, purely used for debugging. Allows users to correctly track where
:param constraints are declared in the event that they are subclassing `aerosandbox.Opti`. Modifies the:
:param stacklevel of the declaration tracked:
:param which is then presented using:
:param `aerosandbox.Opti.variable_declaration:
:type `aerosandbox.Opti.variable_declaration: )` and `aerosandbox.Opti.constraint_declaration(
Returns: A vector consisting of the derivative of the parameter `variable` with respect to `with_respect_to`.
.. py:method:: constrain_derivative(derivative, variable, with_respect_to, method = 'trapezoidal', _stacklevel = 1)
Adds a constraint to the optimization problem such that:
d(variable) / d(with_respect_to) == derivative
Can be used directly; also called indirectly by opti.derivative_of() for implicit derivative creation.
:param derivative: The derivative that is to be constrained here.
:param variable: The variable or quantity that you are taking the derivative of. The "numerator" of the
:param derivative:
:param in colloquial parlance.:
:param with_respect_to: The variable or quantity that you are taking the derivative with respect to. The
:param "denominator" of the derivative: In a typical example case, this `with_respect_to` parameter would be time. Please make sure that the
value of this parameter is monotonically increasing, otherwise you may get nonsensical answers.
:param in colloquial parlance.: In a typical example case, this `with_respect_to` parameter would be time. Please make sure that the
value of this parameter is monotonically increasing, otherwise you may get nonsensical answers.
:param method: The type of integrator to use to define this derivative. Options are:
* "forward euler" - a first-order-accurate forward Euler method
Citation: https://en.wikipedia.org/wiki/Euler_method
* "backwards euler" - a first-order-accurate backwards Euler method
Citation: https://en.wikipedia.org/wiki/Backward_Euler_method
* "midpoint" or "trapezoid" - a second-order-accurate midpoint method
Citation: https://en.wikipedia.org/wiki/Midpoint_method
* "simpson" - Simpson's rule for integration
Citation: https://en.wikipedia.org/wiki/Simpson%27s_rule
* "runge-kutta" or "rk4" - a fourth-order-accurate Runge-Kutta method. I suppose that technically,
"forward euler", "backward euler", and "midpoint" are all (lower-order) Runge-Kutta methods...
Citation: https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#The_Runge%E2%80%93Kutta_method
* "runge-kutta-3/8" - A modified version of the Runge-Kutta 4 proposed by Kutta in 1901. Also
fourth-order-accurate, but all of the error coefficients are smaller than they are in the standard
Runge-Kutta 4 method. The downside is that more floating point operations are required per timestep,
as the Butcher tableau is more dense (i.e. not banded).
Citation: Kutta, Martin (1901), "Beitrag zur näherungsweisen Integration totaler
Differentialgleichungen", Zeitschrift für Mathematik und Physik, 46: 435–453
:param Note that all methods are expressed as integrators rather than differentiators; this prevents:
:param singularities from forming in the limit of timestep approaching zero. (For those coming from the PDE:
:param world:
:param this is analogous to using finite volume methods rather than finite difference methods to allow:
:param shock capturing.):
:param _stacklevel: Optional and advanced, purely used for debugging. Allows users to correctly track where
:param constraints are declared in the event that they are subclassing `aerosandbox.Opti`. Modifies the:
:param stacklevel of the declaration tracked:
:param which is then presented using:
:param `aerosandbox.Opti.variable_declaration:
:type `aerosandbox.Opti.variable_declaration: )` and `aerosandbox.Opti.constraint_declaration(
Returns: None (adds constraint in-place).
.. py:class:: OptiSol(opti, cas_optisol)
.. py:attribute:: opti
.. py:attribute:: _sol
.. py:method:: __call__(x)
A shorthand alias for `sol.value(x)`. See `OptiSol.value()` documentation for details.
:param x: A Python data structure to substitute values into, using the solution in this OptiSol object.
:returns: A copy of `x`, where all symbolic optimization variables (recursively substituted at unlimited depth)
have been converted to float or array values.
.. py:method:: _value_scalar(x)
Gets the value of a variable at the solution point. For developer use - see following paragraph.
This method is basically a less-powerful version of calling `sol(x)` - if you're a
user and not a developer, you almost-certainly want to use that method instead, as those are less
fragile with respect to various input data types. This method exists only as an abstraction to make it easier
for other developers to subclass OptiSol, if they wish to intercept the variable substitution process.
:param x:
Returns:
.. py:method:: value(x, recursive = True, warn_on_unknown_types = False)
Gets the value of a variable (or a data structure) at the solution point. This solution point is the optimum,
if the optimization process solved successfully.
On a computer science level, this method converts a symbolic optimization variable to a concrete float or
array value. More generally, it converts any Python data structure (along with any of its contents,
recursively, at unlimited depth), replacing any symbolic optimization variables it finds with concrete float
or array values.
Note that, for convenience, you can simply call:
>>> sol(x)
if you prefer. This is equivalent to calling this method with the syntax:
>>> sol.value(x)
(these are aliases of each other)
:param x: A Python data structure to substitute values into, using the solution in this OptiSol object.
:param recursive: If True, the substitution will be performed recursively. Otherwise, only the top-level data
structure will be converted.
:param warn_on_unknown_types: If True, a warning will be issued if a data type that cannot be converted or
parsed as definitively un-convertable is encountered.
:returns:
A copy of `x`, where all symbolic optimization variables (recursively substituted at unlimited depth)
have been converted to float or array values.
Usage:
.. py:method:: stats()
.. py:method:: value_variables()
.. py:method:: value_parameters()
.. py:method:: show_infeasibilities(tol = 0.001)
Prints a summary of any violated constraints in the solution.
:param tol: The tolerance for violation. If the constraint is violated by less than this amount, it will not be
printed.
Returns: None (prints to console)
.. py:class:: FittedModel(model, x_data, y_data, parameter_guesses, parameter_bounds = None, residual_norm_type = 'L2', fit_type = 'best', weights = None, put_residuals_in_logspace = False, verbose=True)
Bases: :py:obj:`aerosandbox.modeling.surrogate_model.SurrogateModel`
A model that is fitted to data. Maps from R^N -> R^1.
You can evaluate this model at a given point by calling it just like a function, e.g.:
>>> my_fitted_model = FittedModel(...) # See FittedModel.__init__ docstring for syntax
>>> y = my_fitted_model(x)
The input to the model (`x` in the example above) is of the type:
* in the general N-dimensional case, a dictionary where: keys are variable names and values are float/array
* in the case of a 1-dimensional input (R^1 -> R^1), a float/array.
If you're not sure what the input type of `my_fitted_model` should be, just do:
>>> print(my_fitted_model) # Displays the valid input type to the model
The output of the model (`y` in the example above) is always a float or array.
See the docstring __init__ method of FittedModel for more details of how to instantiate and use FittedModel.
One might have expected a fitted model to be a literal Python function rather than a Python class - the
benefit of having FittedModel as a class rather than a function is that you can easily save (pickle) classes
including data (e.g. parameters, x_data, y_data), but you can't do that with functions. And, because the
FittedModel class has a __call__ method, you can basically still just think of it like a function.
.. py:attribute:: model
.. py:attribute:: x_data
.. py:attribute:: y_data
.. py:attribute:: parameters
.. py:attribute:: parameter_guesses
.. py:attribute:: parameter_bounds
:value: None
.. py:attribute:: residual_norm_type
:value: 'L2'
.. py:attribute:: fit_type
:value: 'best'
.. py:attribute:: weights
.. py:attribute:: put_residuals_in_logspace
:value: False
.. py:method:: __call__(x)
Evaluates the surrogate model at some given input x.
The input `x` is of the type:
* in the general N-dimensional case, a dictionary where keys are variable names and values are float/array.
* in the case of a 1-dimensional input (R^1 -> R^2), a float/array.
.. py:method:: goodness_of_fit(type='R^2')
Returns a metric of the goodness of the fit.
:param type: Type of metric to use for goodness of fit. One of:
* "R^2": The coefficient of determination. Strictly speaking only mathematically rigorous to use this
for linear fits.
https://en.wikipedia.org/wiki/Coefficient_of_determination
* "mean_absolute_error" or "mae" or "L1": The mean absolute error of the fit.
* "root_mean_squared_error" or "rms" or "L2": The root mean squared error of the fit.
* "max_absolute_error" or "Linf": The maximum deviation of the fit from any of the data points.
Returns: The metric of the goodness of the fit.
.. py:class:: InterpolatedModel(x_data_coordinates, y_data_structured, method = 'bspline', fill_value=np.nan)
Bases: :py:obj:`aerosandbox.modeling.surrogate_model.SurrogateModel`
A model that is interpolated to structured (i.e., gridded) N-dimensional data. Maps from R^N -> R^1.
You can evaluate this model at a given point by calling it just like a function, e.g.:
>>> y = my_interpolated_model(x)
The input to the model (`x` in the example above) is of the type:
* in the general N-dimensional case, a dictionary where: keys are variable names and values are float/array
* in the case of a 1-dimensional input (R^1 -> R^1), it can optionally just be a float/array.
If you're not sure what the input type of `my_interpolated_model` should be, just do:
>>> print(my_interpolated_model) # Displays the valid input type to the model
The output of the model (`y` in the example above) is always a float or array.
See the docstring __init__ method of InterpolatedModel for more details of how to instantiate and use InterpolatedModel.
One might have expected a interpolated model to be a literal Python function rather than a Python class - the
benefit of having InterpolatedModel as a class rather than a function is that you can easily save (pickle) classes
including data (e.g. parameters, x_data, y_data), but you can't do that with functions. And, because the
InterpolatedModel class has a __call__ method, you can basically still just think of it like a function.
.. py:attribute:: x_data_coordinates
.. py:attribute:: x_data_coordinates_values
.. py:attribute:: y_data_structured
.. py:attribute:: method
:value: 'bspline'
.. py:attribute:: fill_value
.. py:attribute:: x_data
.. py:attribute:: y_data
.. py:method:: __call__(x)
Evaluates the surrogate model at some given input x.
The input `x` is of the type:
* in the general N-dimensional case, a dictionary where keys are variable names and values are float/array.
* in the case of a 1-dimensional input (R^1 -> R^2), a float/array.
.. py:class:: UnstructuredInterpolatedModel(x_data, y_data, x_data_resample = 10, resampling_interpolator = interpolate.RBFInterpolator, resampling_interpolator_kwargs = None, fill_value=np.nan, interpolated_model_kwargs = None)
Bases: :py:obj:`aerosandbox.modeling.interpolation.InterpolatedModel`
A model that is interpolated to unstructured (i.e., point cloud) N-dimensional data. Maps from R^N -> R^1.
You can evaluate this model at a given point by calling it just like a function, e.g.:
>>> y = my_interpolated_model(x)
The input to the model (`x` in the example above) is of the type:
* in the general N-dimensional case, a dictionary where: keys are variable names and values are float/array
* in the case of a 1-dimensional input (R^1 -> R^1), it can optionally just be a float/array.
If you're not sure what the input type of `my_interpolated_model` should be, just do:
>>> print(my_interpolated_model) # Displays the valid input type to the model
The output of the model (`y` in the example above) is always a float or array.
See the docstring __init__ method of InterpolatedModel for more details of how to instantiate and use UnstructuredInterpolatedModel.
.. py:attribute:: x_data_raw_unstructured
.. py:attribute:: y_data_raw
.. py:function:: black_box(function, n_in = None, n_out = 1, fd_method = 'central', fd_step = None, fd_step_iter = None)
Wraps a function as a black box, allowing it to be used in AeroSandbox / CasADi optimization problems.
Obtains gradients via finite differences. Assumes that the function's Jacobian is fully dense, always.
:param function:
:param n_in:
:param n_out:
:param fd_method: One of:
- 'forward'
- 'backward'
- 'central'
- 'smoothed'
Returns:
.. py:class:: Airfoil(name = 'Untitled', coordinates = None, **deprecated_keyword_arguments)
Bases: :py:obj:`aerosandbox.geometry.polygon.Polygon`
An airfoil. See constructor docstring for usage details.
.. py:attribute:: name
:value: 'Untitled'
.. py:attribute:: coordinates
:value: None
.. py:method:: __repr__()
.. py:method:: to_kulfan_airfoil(n_weights_per_side = 8, N1 = 0.5, N2 = 1.0, normalize_coordinates = True, use_leading_edge_modification = True)
.. py:method:: generate_polars(alphas=np.linspace(-13, 13, 27), Res=np.geomspace(1000.0, 100000000.0, 12), cache_filename = None, xfoil_kwargs = None, unstructured_interpolated_model_kwargs = None, include_compressibility_effects = True, transonic_buffet_lift_knockdown = 0.3, make_symmetric_polars = False)
Generates airfoil polar surrogate models (CL, CD, CM functions) from XFoil data and assigns them in-place to
this Airfoil's polar functions.
In other words, when this function is run, the following functions will be added (or overwritten) to the instance:
* Airfoil.CL_function(alpha, Re, mach)
* Airfoil.CD_function(alpha, Re, mach)
* Airfoil.CM_function(alpha, Re, mach)
Where alpha is in degrees.
Warning: In-place operation! Modifies this Airfoil object by setting Airfoil.CL_function, etc. to the new
polars.
:param alphas: The range of alphas to sample from XFoil at. Given in degrees.
:param Res: The range of Reynolds numbers to sample from XFoil at. Dimensionless.
:param cache_filename: A path-like filename (ideally a "*.json" file) that can be used to cache the XFoil
results, making it much faster to regenerate the results.
* If the file does not exist, XFoil will be run, and a cache file will be created.
* If the file does exist, XFoil will not be run, and the cache file will be read instead.
:param xfoil_kwargs: Keyword arguments to pass into the AeroSandbox XFoil module. See the aerosandbox.XFoil
constructor for options.
:param unstructured_interpolated_model_kwargs: Keyword arguments to pass into the UnstructuredInterpolatedModels
that contain the polars themselves. See the aerosandbox.UnstructuredInterpolatedModel constructor for
options.
:param include_compressibility_effects: Includes compressibility effects in the polars, such as wave drag,
mach tuck, CL effects across normal shocks. Note that accuracy here is dubious in the transonic regime
and above - you should really specify your own CL/CD/CM models
Returns: None (in-place), adds the following functions to the instance:
* Airfoil.CL_function(alpha, Re, mach)
* Airfoil.CD_function(alpha, Re, mach)
* Airfoil.CM_function(alpha, Re, mach)
.. py:method:: get_aero_from_neuralfoil(alpha, Re, mach = 0.0, n_crit = 9.0, xtr_upper = 1.0, xtr_lower = 1.0, model_size = 'large', control_surfaces = None, include_360_deg_effects = True)
.. py:method:: plot_polars(alphas = np.linspace(-20, 20, 500), Res = 10**np.arange(3, 9), mach = 0.0, show = True, Re_colors=None)
.. py:method:: local_camber(x_over_c = np.linspace(0, 1, 101))
Returns the local camber of the airfoil at a given point or points.
:param x_over_c: The x/c locations to calculate the camber at [1D array, more generally, an iterable of floats]
:returns: Local camber of the airfoil (y/c) [1D array].
.. py:method:: local_thickness(x_over_c = np.linspace(0, 1, 101))
Returns the local thickness of the airfoil at a given point or points.
:param x_over_c: The x/c locations to calculate the thickness at [1D array, more generally, an iterable of floats]
:returns: Local thickness of the airfoil (y/c) [1D array].
.. py:method:: max_camber(x_over_c_sample = np.linspace(0, 1, 101))
Returns the maximum camber of the airfoil.
:param x_over_c_sample: Where should the airfoil be sampled to determine the max camber?
Returns: The maximum thickness, as a fraction of chord.
.. py:method:: max_thickness(x_over_c_sample = np.linspace(0, 1, 101))
Returns the maximum thickness of the airfoil.
:param x_over_c_sample: Where should the airfoil be sampled to determine the max thickness?
Returns: The maximum thickness, as a fraction of chord.
.. py:method:: draw(draw_mcl=False, draw_markers=True, backend='matplotlib', show=True)
Draw the airfoil object.
:param draw_mcl: Should we draw the mean camber line (MCL)? [boolean]
:param backend: Which backend should we use? "plotly" or "matplotlib"
:param show: Should we show the plot? [boolean]
Returns: None
.. py:method:: LE_index()
Returns the index of the leading edge point in the airfoil coordinates.
.. py:method:: lower_coordinates()
Returns an Nx2 ndarray of [x, y] coordinates that describe the lower surface of the airfoil.
Order is from the leading edge to the trailing edge.
Includes the leading edge point; be careful about duplicates if using this method in conjunction with
Airfoil.upper_coordinates().
.. py:method:: upper_coordinates()
Returns an Nx2 ndarray of [x, y] coordinates that describe the upper surface of the airfoil.
Order is from the trailing edge to the leading edge.
Includes the leading edge point; be careful about duplicates if using this method in conjunction with
Airfoil.lower_coordinates().
.. py:method:: LE_radius(softness = 1e-06)
.. py:method:: TE_thickness()
Returns the thickness of the trailing edge of the airfoil.
.. py:method:: TE_angle()
Returns the trailing edge angle of the airfoil, in degrees.
.. py:method:: repanel(n_points_per_side = 100, spacing_function_per_side=np.cosspace)
Returns a repaneled copy of the airfoil with cosine-spaced coordinates on the upper and lower surfaces.
:param n_points_per_side: Number of points per side (upper and lower) of the airfoil [int]
Notes: The number of points defining the final airfoil will be `n_points_per_side * 2 - 1`,
since one point (the leading edge point) is shared by both the upper and lower surfaces.
:param spacing_function_per_side: Determines how to space the points on each side of the airfoil. Can be
`np.linspace` or `np.cosspace`, or any other function of the call signature `f(a, b, n)` that returns
a spaced array of `n` points between `a` and `b`. [function]
Returns: A copy of the airfoil with the new coordinates.
.. py:method:: normalize(return_dict = False)
Returns a copy of the Airfoil with a new set of `coordinates`, such that:
- The leading edge (LE) is at (0, 0)
- The trailing edge (TE) is at (1, 0)
- The chord length is equal to 1
The trailing-edge (TE) point is defined as the midpoint of the line segment connecting the first and last coordinate points (upper and lower surface TE points, respectively). The TE point is not necessarily one of the original points in the airfoil coordinates (`Airfoil.coordinates`); in general, it will not be one of the points if the TE thickness is nonzero.
The leading-edge (LE) point is defined as the coordinate point with the largest Euclidian distance from the trailing edge. (In other words, if you were to center a circle on the trailing edge and progressively grow it, what's the last coordinate point that it would intersect?) The LE point is always one of the original points in the airfoil coordinates.
The chord is defined as the Euclidian distance between the LE and TE points.
Coordinate modifications to achieve the constraints described above (LE @ origin, TE at (1, 0), and chord of 1) are done by means of a translation and rotation.
:param return_dict: Determines the output type of the function.
- If `False` (default), returns a copy of the Airfoil with the new coordinates.
- If `True`, returns a dictionary with keys:
- "airfoil": a copy of the Airfoil with the new coordinates
- "x_translation": the amount by which the airfoil's LE was translated in the x-direction
- "y_translation": the amount by which the airfoil's LE was translated in the y-direction
- "scale_factor": the amount by which the airfoil was scaled (if >1, the airfoil had to get
bigger)
- "rotation_angle": the angle (in degrees) by which the airfoil was rotated about the LE.
Sign convention is that positive angles rotate the airfoil counter-clockwise.
All of thes values represent the "required change", e.g.:
- "x_translation" is the amount by which the airfoil's LE had to be translated in the
x-direction to get it to the origin.
- "rotation_angle" is the angle (in degrees) by which the airfoil had to be rotated (CCW).
Returns: Depending on the value of `return_dict`, either:
- A copy of the airfoil with the new coordinates (default), or
- A dictionary with keys "airfoil", "x_translation", "y_translation", "scale_factor", and "rotation_angle".
documentation for `return_tuple` for more information.
.. py:method:: add_control_surface(deflection = 0.0, hinge_point_x = 0.75, modify_coordinates = True, modify_polars = True)
Returns a version of the airfoil with a trailing-edge control surface added at a given point. Implicitly
repanels the airfoil as part of this operation.
:param deflection: Deflection angle [degrees]. Downwards-positive.
:param hinge_point_x: Chordwise location of the hinge, as a fraction of chord (x/c) [float]
Returns: an Airfoil object with the new control deflection.
.. py:method:: set_TE_thickness(thickness = 0.0)
Creates a modified copy of the Airfoil that has a specified trailing-edge thickness.
Note that the trailing-edge thickness is given nondimensionally (e.g., as a fraction of chord).
:param thickness: The target trailing-edge thickness, given nondimensionally (e.g., as a fraction of chord).
Returns: The modified airfoil.
.. py:method:: scale(scale_x = 1.0, scale_y = 1.0)
Scales an Airfoil about the origin.
:param scale_x: Amount to scale in the x-direction.
:param scale_y: Amount to scale in the y-direction. Scaling by a negative y-value will result in coordinates
being re-ordered such that the order of the coordinates is still correct (i.e., starts from the
upper-surface trailing edge, continues along the upper surface to the nose, then continues along the
lower surface to the trailing edge).
Returns: A copy of the Airfoil with appropriate scaling applied.
.. py:method:: translate(translate_x = 0.0, translate_y = 0.0)
Translates an Airfoil by a given amount.
:param translate_x: Amount to translate in the x-direction
:param translate_y: Amount to translate in the y-direction
Returns: The translated Airfoil.
.. py:method:: rotate(angle, x_center = 0.0, y_center = 0.0)
Rotates the airfoil clockwise by the specified amount, in radians.
Rotates about the point (x_center, y_center), which is (0, 0) by default.
:param angle: Angle to rotate, counterclockwise, in radians.
:param x_center: The x-coordinate of the center of rotation.
:param y_center: The y-coordinate of the center of rotation.
Returns: The rotated Airfoil.
.. py:method:: blend_with_another_airfoil(airfoil, blend_fraction = 0.5, n_points_per_side = 100)
Blends this airfoil with another airfoil. Merges both the coordinates and the aerodynamic functions.
:param airfoil: The other airfoil to blend with.
:param blend_fraction: The fraction of the other airfoil to use when blending. Defaults to 0.5 (50%).
* A blend fraction of 0 will return an identical airfoil to this one (self).
* A blend fraction of 1 will return an identical airfoil to the other one (`airfoil` parameter).
:param n_points_per_side: The number of points per side to use when blending the coordinates of the two airfoils.
Returns: A new airfoil that is a blend of this airfoil and another one.
.. py:method:: write_dat(filepath = None, include_name = True)
Writes a .dat file corresponding to this airfoil to a filepath.
:param filepath: filepath (including the filename and .dat extension) [string]
If None, this function returns the .dat file as a string.
:param include_name: Should the name be included in the .dat file? (In a standard *.dat file, it usually is.)
Returns: None
.. py:class:: KulfanAirfoil(name = 'Untitled', lower_weights = None, upper_weights = None, leading_edge_weight = 0.0, TE_thickness = 0.0, N1 = 0.5, N2 = 1.0)
Bases: :py:obj:`aerosandbox.geometry.airfoil.airfoil.Airfoil`
An airfoil. See constructor docstring for usage details.
.. py:attribute:: name
:value: 'Untitled'
.. py:attribute:: lower_weights
:value: None
.. py:attribute:: upper_weights
:value: None
.. py:attribute:: leading_edge_weight
:value: 0.0
.. py:attribute:: TE_thickness
:value: 0.0
Returns the thickness of the trailing edge of the airfoil.
.. py:attribute:: N1
:value: 0.5
.. py:attribute:: N2
:value: 1.0
.. py:method:: __repr__()
.. py:property:: kulfan_parameters
.. py:property:: coordinates
:type: aerosandbox.numpy.ndarray
.. py:method:: to_airfoil(n_coordinates_per_side=200, spacing_function_per_side=np.cosspace)
.. py:method:: repanel(n_points_per_side = 100, spacing_function_per_side=np.cosspace)
Returns a repaneled copy of the airfoil with cosine-spaced coordinates on the upper and lower surfaces.
:param n_points_per_side: Number of points per side (upper and lower) of the airfoil [int]
Notes: The number of points defining the final airfoil will be `n_points_per_side * 2 - 1`,
since one point (the leading edge point) is shared by both the upper and lower surfaces.
:param spacing_function_per_side: Determines how to space the points on each side of the airfoil. Can be
`np.linspace` or `np.cosspace`, or any other function of the call signature `f(a, b, n)` that returns
a spaced array of `n` points between `a` and `b`. [function]
Returns: A copy of the airfoil with the new coordinates.
.. py:method:: normalize(return_dict = False)
Returns a copy of the Airfoil with a new set of `coordinates`, such that:
- The leading edge (LE) is at (0, 0)
- The trailing edge (TE) is at (1, 0)
- The chord length is equal to 1
The trailing-edge (TE) point is defined as the midpoint of the line segment connecting the first and last coordinate points (upper and lower surface TE points, respectively). The TE point is not necessarily one of the original points in the airfoil coordinates (`Airfoil.coordinates`); in general, it will not be one of the points if the TE thickness is nonzero.
The leading-edge (LE) point is defined as the coordinate point with the largest Euclidian distance from the trailing edge. (In other words, if you were to center a circle on the trailing edge and progressively grow it, what's the last coordinate point that it would intersect?) The LE point is always one of the original points in the airfoil coordinates.
The chord is defined as the Euclidian distance between the LE and TE points.
Coordinate modifications to achieve the constraints described above (LE @ origin, TE at (1, 0), and chord of 1) are done by means of a translation and rotation.
:param return_dict: Determines the output type of the function.
- If `False` (default), returns a copy of the Airfoil with the new coordinates.
- If `True`, returns a dictionary with keys:
- "airfoil": a copy of the Airfoil with the new coordinates
- "x_translation": the amount by which the airfoil's LE was translated in the x-direction
- "y_translation": the amount by which the airfoil's LE was translated in the y-direction
- "scale_factor": the amount by which the airfoil was scaled (if >1, the airfoil had to get
bigger)
- "rotation_angle": the angle (in degrees) by which the airfoil was rotated about the LE.
Sign convention is that positive angles rotate the airfoil counter-clockwise.
All of thes values represent the "required change", e.g.:
- "x_translation" is the amount by which the airfoil's LE had to be translated in the
x-direction to get it to the origin.
- "rotation_angle" is the angle (in degrees) by which the airfoil had to be rotated (CCW).
Returns: Depending on the value of `return_dict`, either:
- A copy of the airfoil with the new coordinates (default), or
- A dictionary with keys "airfoil", "x_translation", "y_translation", "scale_factor", and "rotation_angle".
documentation for `return_tuple` for more information.
.. py:method:: draw(*args, draw_markers=False, **kwargs)
Draw the airfoil object.
:param draw_mcl: Should we draw the mean camber line (MCL)? [boolean]
:param backend: Which backend should we use? "plotly" or "matplotlib"
:param show: Should we show the plot? [boolean]
Returns: None
.. py:method:: get_aero_from_neuralfoil(alpha, Re, mach = 0.0, n_crit = 9.0, xtr_upper = 1.0, xtr_lower = 1.0, model_size = 'large', control_surfaces = None, include_360_deg_effects = True)
.. py:method:: upper_coordinates(x_over_c = np.linspace(1, 0, 101))
Returns an Nx2 ndarray of [x, y] coordinates that describe the upper surface of the airfoil.
Order is from the trailing edge to the leading edge.
Includes the leading edge point; be careful about duplicates if using this method in conjunction with
Airfoil.lower_coordinates().
.. py:method:: lower_coordinates(x_over_c = np.linspace(0, 1, 101))
Returns an Nx2 ndarray of [x, y] coordinates that describe the lower surface of the airfoil.
Order is from the leading edge to the trailing edge.
Includes the leading edge point; be careful about duplicates if using this method in conjunction with
Airfoil.upper_coordinates().
.. py:method:: local_camber(x_over_c = np.linspace(0, 1, 101))
Returns the local camber of the airfoil at a given point or points.
:param x_over_c: The x/c locations to calculate the camber at [1D array, more generally, an iterable of floats]
:returns: Local camber of the airfoil (y/c) [1D array].
.. py:method:: local_thickness(x_over_c = np.linspace(0, 1, 101))
Returns the local thickness of the airfoil at a given point or points.
:param x_over_c: The x/c locations to calculate the thickness at [1D array, more generally, an iterable of floats]
:returns: Local thickness of the airfoil (y/c) [1D array].
.. py:method:: LE_radius(relative_softness = 0.03)
.. py:method:: TE_angle()
Returns the trailing edge angle of the airfoil, in degrees.
.. py:method:: area()
Returns the area of the polygon.
.. py:method:: set_TE_thickness(thickness = 0.0)
Creates a modified copy of the KulfanAirfoil that has a specified trailing-edge thickness.
Note that the trailing-edge thickness is given nondimensionally (e.g., as a fraction of chord).
:param thickness: The target trailing-edge thickness, given nondimensionally (e.g., as a fraction of chord).
Returns: The modified KulfanAirfoil.
.. py:method:: scale(scale_x = 1.0, scale_y = 1.0)
Scales a KulfanAirfoil about the origin.
:param scale_x: Amount to scale in the x-direction. Note: not supported by KulfanAirfoil due to inherent
limitations of parameterization; only given here so that argument symmetry to Airfoil.scale() is
retained. Raises a ValueError if modified, along with instructions to use `Airfoil` if needed.
:param scale_y: Amount to scale in the y-direction. Scaling by a negative y-value will result in `lower_weights`
and `upper_weights` being flipped as appropriate.
Returns: A copy of the KulfanAirfoil with appropriate scaling applied.
.. py:method:: blend_with_another_airfoil(airfoil, blend_fraction = 0.5)
Blends this airfoil with another airfoil. Merges both the coordinates and the aerodynamic functions.
:param airfoil: The other airfoil to blend with.
:param blend_fraction: The fraction of the other airfoil to use when blending. Defaults to 0.5 (50%).
* A blend fraction of 0 will return an identical airfoil to this one (self).
* A blend fraction of 1 will return an identical airfoil to the other one (`airfoil` parameter).
:param n_points_per_side: The number of points per side to use when blending the coordinates of the two airfoils.
Returns: A new airfoil that is a blend of this airfoil and another one.
.. py:class:: Wing(name = None, xsecs = None, symmetric = False, color = None, analysis_specific_options = None, **kwargs)
Bases: :py:obj:`aerosandbox.common.AeroSandboxObject`
Definition for a Wing.
Anatomy of a Wing:
A wing consists chiefly of a collection of cross-sections, or "xsecs". A cross-section is a 2D "slice" of a
wing. These can be accessed with `Wing.xsecs`, which gives a list of xsecs in the Wing. Each xsec is a
WingXSec object, a class that is defined separately.
You may also see references to wing "sections", which are different than cross-sections (xsecs)! Sections are
the portions of the wing that are in between xsecs. In other words, a wing with N cross-sections (xsecs,
WingXSec objects) will always have N-1 sections. Sections are never explicitly defined, since you can get all
needed information by lofting from the adjacent cross-sections. For example, section 0 (the first one) is a
loft between cross-sections 0 and 1.
Wings are lofted linearly between cross-sections.
If the wing is symmetric across the XZ plane, just define the right half and supply `symmetric=True` in the
constructor.
If the wing is not symmetric across the XZ plane (e.g., a single vertical stabilizer), just define the wing.
.. py:attribute:: name
:value: None
.. py:attribute:: xsecs
:value: None
.. py:attribute:: symmetric
:value: False
.. py:attribute:: color
:value: None
.. py:attribute:: analysis_specific_options
:value: None
.. py:method:: __repr__()
.. py:method:: translate(xyz)
Translates the entire Wing by a certain amount.
:param xyz:
Returns: The new wing object.
.. py:method:: span(type = 'yz', include_centerline_distance=False, _sectional = False)
Computes the span, with options for various ways of measuring this (see `type` argument).
If the wing is symmetric, both left/right sides are included in order to obtain the full span. In the case
where the root cross-section is not coincident with the center plane (e.g., XZ plane), this function's
behavior depends on the `include_centerline_distance` argument.
:param type: One of the following options, as a string:
* "xyz": First, computes the quarter-chord point of each WingXSec. Then, connects these with
straight lines. Then, adds up the lengths of these lines.
* "xy" or "top": Same as "xyz", except it projects each line segment onto the XY plane before adding up the
lengths.
* "yz" (default) or "front": Same as "xyz", except it projects each line segment onto the YZ plane (i.e., front view)
before adding up the lengths.
* "xz" or "side": Same as "xyz", except it projects each line segment onto the XZ plane before adding up the
lengths. Rarely needed.
* "x": Same as "xyz", except it only counts the x-components of each line segment when adding up the
lengths.
* "y": Same as "xyz", except it only counts the y-components of each line segment when adding up the
lengths.
* "z": Same as "xyz", except it only counts the z-components of each line segment when adding up the
lengths.
:param include_centerline_distance: A boolean flag that tells the function what to do if a wing's root is not
:param coincident with the centerline plane:
* If True, we first figure out which WingXSec has its quarter-chord point closest to the centerline
plane (i.e., XZ plane). Then, we compute the distance from that quarter-chord point directly to the
centerline plane (along Y). We then add that distance to the span calculation. In other words,
the fictitious span connecting the left and right root cross-sections is included.
* If False, this distance is ignored. In other words, the fictitious span connecting the left and
right root cross-sections is not included. This is the default behavior.
Note: For computation, either the root WingXSec (i.e., index=0) or the tip WingXSec (i.e., index=-1)
is used, whichever is closer to the centerline plane. This will almost-always be the root WingXSec,
but some weird edge cases (e.g., a half-wing defined on the left-hand-side of the airplane,
rather than the conventional right-hand side) will result in the tip WingXSec being used.
:type coincident with the centerline plane: i.e., XZ plane
:param _sectional: A boolean. If False, returns the total span. If True, returns a list of spans for each of the
`n-1` lofted sections (between the `n` wing cross-sections in wing.xsec).
.. py:method:: area(type = 'planform', include_centerline_distance=False, _sectional = False)
Computes the wing area, with options for various ways of measuring this (see `type` argument):
If the wing is symmetric, both left/right sides are included in order to obtain the full area. In the case
where the root cross-section is not coincident with the center plane (e.g., XZ plane), this function's
behavior depends on the `include_centerline_distance` argument.
:param type: One of the following options, as a string:
* "planform" (default): First, lofts a quadrilateral mean camber surface between each WingXSec. Then,
computes the area of each of these sectional surfaces. Then, sums up all the areas and returns it.
When airplane designers refer to "wing area" (in the absence of any other qualifiers),
this is typically what they mean.
* "wetted": Computes the actual surface area of the wing that is in contact with the air. Will
typically be a little more than double the "planform" area above; intuitively, this is because it
adds both the "top" and "bottom" surface areas. Accounts for airfoil thickness/shape effects.
* "xy" or "projected" or "top": Same as "planform", but each sectional surface is projected onto the XY plane
(i.e., top-down view) before computing the areas. Note that if you try to use this method with a
vertically-oriented wing, like most vertical stabilizers, you will get an area near zero.
* "xz" or "side": Same as "planform", but each sectional surface is projected onto the XZ plane before
computing the areas.
:param include_centerline_distance: A boolean flag that tells the function what to do if a wing's root chord is
:param not coincident with the centerline plane:
* If True, we first figure out which WingXSec is closest to the centerline plane (i.e., XZ plane).
Then, we imagine that this WingXSec is extruded along the Y axis to the centerline plane (assuming a
straight extrusion to produce a rectangular mid-camber surface). In doing so, we use the wing
geometric chord as the extrusion width. We then add the area of this fictitious surface to the area
calculation.
* If False, this function will simply ignore this fictitious wing area. This is the default behavior.
:type not coincident with the centerline plane: i.e., XZ plane
:param _sectional: A boolean. If False, returns the total area. If True, returns a list of areas for each of the
`n-1` lofted sections (between the `n` wing cross-sections in wing.xsec).
.. py:method:: aspect_ratio(type = 'geometric')
Computes the aspect ratio of the wing, with options for various ways of measuring this.
* geometric: geometric aspect ratio, computed in the typical fashion (b^2 / S).
* effective: Differs from the geometric aspect ratio only in the case of symmetric wings whose root
cross-section is not on the centerline. In these cases, it includes the span and area of the fictitious wing
center when computing aspect ratio.
:param type: One of the above options, as a string.
.. py:method:: is_entirely_symmetric()
.. py:method:: mean_geometric_chord()
Returns the mean geometric chord of the wing (S/b).
:return:
.. py:method:: mean_aerodynamic_chord()
Computes the length of the mean aerodynamic chord of the wing.
Uses the generalized methodology described here:
https://core.ac.uk/download/pdf/79175663.pdf
Returns: The length of the mean aerodynamic chord.
.. py:method:: mean_twist_angle()
Returns the mean twist angle (in degrees) of the wing, weighted by area.
:return: mean twist angle (in degrees)
.. py:method:: mean_sweep_angle(x_nondim=0.25)
Returns the mean sweep angle (in degrees) of the wing, relative to the x-axis.
Positive sweep is backwards, negative sweep is forward.
This is purely measured from root to tip, with no consideration for the sweep of the individual
cross-sections in between.
:param x_nondim: The nondimensional x-coordinate of the cross-section to use for sweep angle computation.
* If you provide 0, it will use the leading edge of the cross-section.
* If you provide 0.25, it will use the quarter-chord point of the cross-section.
* If you provide 1, it will use the trailing edge of the cross-section.
:returns: The mean sweep angle, in degrees.
.. py:method:: mean_dihedral_angle(x_nondim=0.25)
Returns the mean dihedral angle (in degrees) of the wing, relative to the XY plane.
Positive dihedral is bending up, negative dihedral is bending down.
This is purely measured from root to tip, with no consideration for the dihedral of the individual
cross-sections in between.
:param x_nondim: The nondimensional x-coordinate of the cross-section to use for sweep angle computation.
* If you provide 0, it will use the leading edge of the cross-section.
* If you provide 0.25, it will use the quarter-chord point of the cross-section.
* If you provide 1, it will use the trailing edge of the cross-section.
:returns: The mean dihedral angle, in degrees
.. py:method:: aerodynamic_center(chord_fraction = 0.25, _sectional=False)
Computes the location of the aerodynamic center of the wing.
Uses the generalized methodology described here:
https://core.ac.uk/downloattttd/pdf/79175663.pdf
Args: chord_fraction: The position of the aerodynamic center along the MAC, as a fraction of MAC length.
Typically, this value (denoted `h_0` in the literature) is 0.25 for a subsonic wing. However,
wing-fuselage interactions can cause a forward shift to a value more like 0.1 or less. Citing Cook,
Michael V., "Flight Dynamics Principles", 3rd Ed., Sect. 3.5.3 "Controls-fixed static stability". PDF:
https://www.sciencedirect.com/science/article/pii/B9780080982427000031
Returns: The (x, y, z) coordinates of the aerodynamic center of the wing.
.. py:method:: taper_ratio()
Gives the taper ratio of the Wing. Strictly speaking, only valid for trapezoidal wings.
:returns: Taper ratio of the Wing.
.. py:method:: volume(_sectional = False)
Computes the volume of the Wing.
:param _sectional: A boolean. If False, returns the total volume. If True, returns a list of volumes for each of
:param the `n-1` lofted sections:
:type the `n-1` lofted sections: between the `n` wing cross-sections in wing.xsec
:returns: The computed volume.
.. py:method:: get_control_surface_names()
Gets the names of all control surfaces on this wing.
:returns: A list of control surface names.
.. py:method:: set_control_surface_deflections(control_surface_mappings)
Sets the deflection of all control surfaces on this wing, based on the provided mapping.
:param control_surface_mappings: A dictionary mapping control surface names to their deflection angles, in degrees.
Note: control surface names are set in the asb.ControlSurface constructor.
:returns: None. (in-place)
.. py:method:: control_surface_area(by_name = None, type = 'planform')
Computes the total area of all control surfaces on this wing, optionally filtered by their name.
Control surfaces are defined on a section-by-section basis, and are defined in the WingXSec constructor using
its `control_surfaces` argument.
Note: If redundant control surfaces are defined (e.g., elevons, as defined by separate ailerons + elevator),
the area will be duplicated.
If the wing is symmetric, control surfaces on both left/right sides are included in order to obtain the full area.
:param by_name: If not None, only control surfaces with this name will be included in the area calculation.
Note: control surface names are set in the asb.ControlSurface constructor.
:param type: One of the following options, as a string:
* "planform" (default): First, lofts a quadrilateral mean camber surface between each WingXSec. Then,
computes the area of each of these sectional surfaces. Then, computes what fraction of this area is
control surface. Then, sums up all the areas and returns it. When airplane designers refer to
"control surface area" (in the absence of any other qualifiers), this is typically what they mean.
* "wetted": Computes the actual surface area of the control surface that is in contact with the air.
Will typically be a little more than double the "planform" area above; intuitively, this is because
it adds both the "top" and "bottom" surface areas. Accounts for airfoil thickness/shape effects.
* "xy" or "projected" or "top": Same as "planform", but each sectional surface is projected onto the XY plane
(i.e., top-down view) before computing the areas. Note that if you try to use this method with a
vertically-oriented wing, like most vertical stabilizers, you will get an area near zero.
* "xz" or "side": Same as "planform", but each sectional surface is projected onto the XZ plane before
computing the areas.
.. py:method:: mesh_body(method='quad', chordwise_resolution = 36, chordwise_spacing_function_per_side = np.cosspace, mesh_surface = True, mesh_tips = True, mesh_trailing_edge = True, mesh_symmetric = True)
Meshes the outer mold line surface of the wing.
Uses the `(points, faces)` standard mesh format. For reference on this format, see the documentation in
`aerosandbox.geometry.mesh_utilities`.
Order of faces:
* On the right wing (or, if `Wing.symmetric` is `False`, just the wing itself):
* If `mesh_surface` is `True`:
* First face is nearest the top-side trailing edge of the wing root.
* Proceeds chordwise, along the upper surface of the wing from back to front. Upon reaching the
leading edge, continues along the lower surface of the wing from front to back.
* Then, repeats this process for the next spanwise slice of the wing, and so on.
* If `mesh_trailing_edge` is `True`:
* Continues by meshing the trailing edge of the wing. Meshes the inboard trailing edge first, then
proceeds spanwise to the outboard trailing edge.
* If `mesh_tips` is `True`:
* Continues by meshing the wing tips. Meshes the inboard tip first, then meshes the outboard tip.
* Within each tip, meshes from the
:param method: One of the following options, as a string:
* "tri": Triangular mesh.
* "quad": Quadrilateral mesh.
:param chordwise_resolution: Number of points to use per wing chord, per wing section.
:param chordwise_spacing_function_per_side: A function that determines how to space points in the chordwise
:param direction along the top and bottom surfaces. Common values would be `np.linspace` or `np.cosspace`:
:param :
:param but it can be any function with the call signature `f:
:type but it can be any function with the call signature `f: a, b, n
:param between `a` and `b`. [function]:
:param mesh_surface: If True, includes the actual wing surface in the mesh.
:param mesh_tips: If True, includes the wing tips (both on the inboard-most section and on the outboard-most
:param section) in the mesh.:
:param mesh_trailing_edge: If True, includes the wing trailing edge in the mesh, if the trailing-edge thickness
:param is nonzero.:
:param mesh_symmetric: Has no effect if the wing is not symmetric. If the wing is symmetric this determines whether
:param the generated mesh is also symmetric:
:type the generated mesh is also symmetric: right side
:param or if if only one side of the wing:
:type or if if only one side of the wing: right side
Returns: Standard unstructured mesh format: A tuple of `points` and `faces`, where:
* `points` is a `n x 3` array of points, where `n` is the number of points in the mesh.
* `faces` is a `m x 3` array of faces if `method` is "tri", or a `m x 4` array of faces if `method` is "quad".
* Each row of `faces` is a list of indices into `points`, which specifies a face.
.. py:method:: mesh_thin_surface(method='tri', chordwise_resolution = 36, chordwise_spacing_function = np.cosspace, add_camber = True)
Meshes the mean camber line of the wing as a thin-sheet body.
Uses the `(points, faces)` standard mesh format. For reference on this format, see the documentation in
`aerosandbox.geometry.mesh_utilities`.
Order of faces:
* On the right wing (or, if `Wing.symmetric` is `False`, just the wing itself):
* First face is the face nearest the leading edge of the wing root.
* Proceeds along a chordwise strip to the trailing edge.
* Then, goes to the subsequent spanwise location and does another chordwise strip, et cetera until
we get to the wing tip.
* On the left wing (applicable only if `Wing.symmetric` is `True`):
* Same order: Starts at the root leading edge, goes in chordwise strips.
Order of vertices within each face:
* On the right wing (or, if `Wing.symmetric` is `False`, just the wing itself):
* Front-left
* Back-left
* Back-right
* Front-right
* On the left wing (applicable only if `Wing.symmetric` is `True`):
* Front-left
* Back-left
* Back-right
* Front-right
:param method: A string, which determines whether to mesh the fuselage as a series of quadrilaterals or triangles.
* "quad" meshes the fuselage as a series of quadrilaterals.
* "tri" meshes the fuselage as a series of triangles.
:param chordwise_resolution: Determines the number of chordwise panels to use in the meshing. [int]
:param chordwise_spacing_function: Determines how to space the chordwise panels. Can be `np.linspace` or
:param `np.cosspace`:
:type `np.cosspace`: a, b, n
:param or any other function of the call signature `f:
:type or any other function of the call signature `f: a, b, n
:param `n` points between `a` and `b`. [function]:
:param add_camber: Controls whether to mesh the thin surface with camber (i.e., mean camber line), or to just
:param mesh the flat planform. [bool]:
Returns: Standard unstructured mesh format: A tuple of `points` and `faces`, where:
* `points` is a `n x 3` array of points, where `n` is the number of points in the mesh.
* `faces` is a `m x 3` array of faces if `method` is "tri", or a `m x 4` array of faces if `method` is "quad".
* Each row of `faces` is a list of indices into `points`, which specifies a face.
.. py:method:: mesh_line(x_nondim = 0.25, z_nondim = 0, add_camber = True)
Meshes a line that goes through each of the WingXSec objects in this wing.
:param x_nondim: The nondimensional (chord-normalized) x-coordinate that the line should go through. Can either
:param be a single value used at all cross-sections:
:param or can be an iterable of values to be used at the:
:param respective cross-sections.:
:param z_nondim: The nondimensional (chord-normalized) y-coordinate that the line should go through. Here,
:param y-coordinate means the "vertical" component:
:type y-coordinate means the "vertical" component: think standard 2D airfoil axes
:param value used at all cross-sections:
:param or can be an iterable of values to be used at the respective cross:
:param sections.:
:param add_camber: Controls whether the camber of each cross-section's airfoil should be added to the line or
:param not. Essentially modifies `z_nondim` to be `z_nondim + camber`.:
Returns: A list of points, where each point is a 3-element array of the form `[x, y, z]`. Goes from the root
to the tip. Ignores any wing symmetry (e.g., only gives one side).
.. py:method:: draw(*args, **kwargs)
An alias to the more general Airplane.draw() method. See there for documentation.
:param \*args: Arguments to pass through to Airplane.draw()
:param \*\*kwargs: Keyword arguments to pass through to Airplane.draw()
Returns: Same return as Airplane.draw()
.. py:method:: draw_wireframe(*args, **kwargs)
An alias to the more general Airplane.draw_wireframe() method. See there for documentation.
:param \*args: Arguments to pass through to Airplane.draw_wireframe()
:param \*\*kwargs: Keyword arguments to pass through to Airplane.draw_wireframe()
Returns: Same return as Airplane.draw_wireframe()
.. py:method:: draw_three_view(*args, **kwargs)
An alias to the more general Airplane.draw_three_view() method. See there for documentation.
:param \*args: Arguments to pass through to Airplane.draw_three_view()
:param \*\*kwargs: Keyword arguments to pass through to Airplane.draw_three_view()
Returns: Same return as Airplane.draw_three_view()
.. py:method:: subdivide_sections(ratio, spacing_function = np.linspace)
Generates a new Wing that subdivides the existing sections of this Wing into several smaller ones. Splits
each section into N=`ratio` smaller sub-sections by inserting new cross-sections (xsecs) as needed.
This can allow for finer aerodynamic resolution of sectional properties in certain analyses.
:param ratio: The number of new sections to split each old section into.
:param spacing_function: A function that takes in three arguments: the start, end, and number of points to generate.
The default is `np.linspace`, which generates a linearly-spaced array of points.
Other options include `np.cosspace`, which generates a cosine-spaced array of points.
Returns: A new Wing object with subdivided sections.
.. py:method:: _compute_xyz_le_of_WingXSec(index)
.. py:method:: _compute_xyz_te_of_WingXSec(index)
.. py:method:: _compute_xyz_of_WingXSec(index, x_nondim, z_nondim)
.. py:method:: _compute_frame_of_WingXSec(index)
Computes the local reference frame associated with a particular cross-section (XSec) of this wing.
:param index: Which cross-section (as indexed in Wing.xsecs) should we get the frame of?
:returns: A tuple of (xg_local, yg_local, zg_local), where each entry refers to the respective (normalized) axis of
the local reference frame of the WingXSec. Given in geometry axes.
.. py:method:: _compute_frame_of_section(index)
Computes the local reference frame associated with a particular section. (Note that sections and cross
sections are different! cross-sections, or xsecs, are the vertices, and sections are the parts in between. In
other words, a wing with N cross-sections (xsecs) will always have N-1 sections.
:param index: Which section should we get the frame of? If given `i`, this retrieves the frame of the section
:param between xsecs `i` and `i+1`.:
:returns: A tuple of (xg_local, yg_local, zg_local), where each entry refers to the respective (normalized) axis
of the local reference frame of the section. Given in geometry axes.
.. py:class:: WingXSec(xyz_le = None, chord = 1.0, twist = 0.0, airfoil = None, control_surfaces = None, analysis_specific_options = None, **deprecated_kwargs)
Bases: :py:obj:`aerosandbox.common.AeroSandboxObject`
Definition for a wing cross-section ("X-section").
.. py:attribute:: xyz_le
.. py:attribute:: chord
:value: 1.0
.. py:attribute:: twist
:value: 0.0
.. py:attribute:: airfoil
:value: None
.. py:attribute:: control_surfaces
:value: None
.. py:attribute:: analysis_specific_options
:value: None
.. py:method:: __repr__()
.. py:method:: translate(xyz)
Returns a copy of this WingXSec that has been translated by `xyz`.
:param xyz: The amount to translate the WingXSec. Given as a 3-element NumPy vector.
Returns: A new WingXSec object.
.. py:method:: xsec_area()
Computes the WingXSec's cross-sectional (xsec) area.
Returns: The (dimensional) cross-sectional area of the WingXSec.
.. py:class:: ControlSurface(name = 'Untitled', symmetric = True, deflection = 0.0, hinge_point = 0.75, trailing_edge = True, analysis_specific_options = None)
Bases: :py:obj:`aerosandbox.common.AeroSandboxObject`
Definition for a control surface, which is attached to a particular WingXSec via WingXSec's `control_surfaces=[]` parameter.
.. py:attribute:: name
:value: 'Untitled'
.. py:attribute:: symmetric
:value: True
.. py:attribute:: deflection
:value: 0.0
.. py:attribute:: hinge_point
:value: 0.75
.. py:attribute:: trailing_edge
:value: True
.. py:attribute:: analysis_specific_options
:value: None
.. py:method:: __repr__()
.. py:class:: Fuselage(name = 'Untitled', xsecs = None, color = None, analysis_specific_options = None, **kwargs)
Bases: :py:obj:`aerosandbox.AeroSandboxObject`
Definition for a Fuselage or other slender body (pod, fuel tank, etc.).
Anatomy of a Fuselage:
A fuselage consists chiefly of a collection of cross-sections, or "xsecs". A cross-section is a 2D "slice" of
a fuselage. These can be accessed with `Fuselage.xsecs`, which gives a list of xsecs in the Fuselage. Each
xsec is a FuselageXSec object, a class that is defined separately.
You may also see references to fuselage "sections", which are different from cross-sections (xsecs)! Sections
are the portions of the fuselage that are in between xsecs. In other words, a fuselage with N cross-sections
(xsecs, FuselageXSec objects) will always have N-1 sections. Sections are never explicitly defined,
since you can get all needed information by lofting from the adjacent cross-sections. For example,
section 0 (the first one) is a loft between cross-sections 0 and 1.
Fuselages are lofted linearly between cross-sections.
.. py:attribute:: name
:value: 'Untitled'
.. py:attribute:: xsecs
:value: None
.. py:attribute:: color
:value: None
.. py:attribute:: analysis_specific_options
:value: None
.. py:method:: __repr__()
.. py:method:: add_loft(kind, to_xsec, from_xsec = None, n_points = 5, spacing = np.cosspace)
:abstractmethod:
.. py:method:: translate(xyz)
Translates the entire Fuselage by a certain amount.
:param xyz:
Returns: self
.. py:method:: area_wetted()
Returns the wetted area of the fuselage.
:return:
.. py:method:: area_projected(type = 'XY')
Returns the area of the fuselage as projected onto one of the principal planes.
:param type: A string, which determines which principal plane to use for projection. One of:
* "XY", in which case the projected area is onto the XY plane (i.e., top-down)
* "XZ", in which case the projected area is onto the XZ plane (i.e., side-view)
Returns: The projected area.
.. py:method:: area_base()
Returns the area of the base (i.e. "trailing edge") of the fuselage. Useful for certain types of drag
calculation.
Returns:
.. py:method:: fineness_ratio(assumed_shape='cylinder')
Approximates the fineness ratio using the volume and length. The fineness ratio of a fuselage is defined as:
FR = length / max_diameter
:param assumed_shape: A string, which determines the assumed shape of the fuselage for the approximation. One of:
* "cylinder", in which case the fuselage is assumed to have a cylindrical shape.
* "sears-haack", in which case the fuselage is assumed to have Sears-Haack fuselage shape.
Returns: An approximate value of the fuselage's fineness ratio.
.. py:method:: length()
Returns the total front-to-back length of the fuselage. Measured as the difference between the x-coordinates
of the leading and trailing cross-sections.
:return:
.. py:method:: volume(_sectional = False)
Computes the volume of the Fuselage.
:param _sectional: A boolean. If False, returns the total volume. If True, returns a list of volumes for each of
:param the `n-1` lofted sections:
:type the `n-1` lofted sections: between the `n` fuselage cross-sections in fuselage.xsec
:returns: The computed volume.
.. py:method:: x_centroid_projected(type = 'XY')
Returns the x_g coordinate of the centroid of the planform area.
:param type: A string, which determines which principal plane to use for projection. One of:
* "XY", in which case the projected area is onto the XY plane (i.e., top-down)
* "XZ", in which case the projected area is onto the XZ plane (i.e., side-view)
Returns: The x_g coordinate of the centroid.
.. py:method:: mesh_body(method='quad', tangential_resolution = 36)
Meshes the fuselage as a solid (thickened) body.
Uses the `(points, faces)` standard mesh format. For reference on this format, see the documentation in
`aerosandbox.geometry.mesh_utilities`.
:param method: A string, which determines whether to mesh the fuselage as a series of quadrilaterals or triangles.
* "quad" meshes the fuselage as a series of quadrilaterals.
* "tri" meshes the fuselage as a series of triangles.
:param tangential_resolution: An integer, which determines the number of points to use to mesh each cross-section.
Returns: Standard unstructured mesh format: A tuple of`points` and `faces`, where:
* `points` is a `n x 3` array of points, where `n` is the number of points in the mesh.
* `faces` is a `m x 3` array of faces if `method` is "tri", or a `m x 4` array of faces if `method` is "quad".
* Each row of `faces` is a list of indices into `points`, which specifies a face.
.. py:method:: mesh_line(y_nondim = 0.0, z_nondim = 0.0)
Returns points along a line that goes through each of the FuselageXSec objects in this Fuselage.
:param y_nondim: The nondimensional (width-normalized) y-coordinate that the line should go through. Can either
:param be a single value used at all cross-sections:
:param or can be an iterable of values to be used at the:
:param respective cross-sections.:
:param z_nondim: The nondimensional (height-normalized) z-coordinate that the line should go through. Can either
:param be a single value used at all cross-sections:
:param or can be an iterable of values to be used at the:
:param respective cross-sections.:
Returns: A list of points, where each point is a 3-element array of the form `[x, y, z]`. Goes from the nose
to the tail.
.. py:method:: draw(*args, **kwargs)
An alias to the more general Airplane.draw() method. See there for documentation.
:param \*args: Arguments to pass through to Airplane.draw()
:param \*\*kwargs: Keyword arguments to pass through to Airplane.draw()
Returns: Same return as Airplane.draw()
.. py:method:: draw_wireframe(*args, **kwargs)
An alias to the more general Airplane.draw_wireframe() method. See there for documentation.
:param \*args: Arguments to pass through to Airplane.draw_wireframe()
:param \*\*kwargs: Keyword arguments to pass through to Airplane.draw_wireframe()
Returns: Same return as Airplane.draw_wireframe()
.. py:method:: draw_three_view(*args, **kwargs)
An alias to the more general Airplane.draw_three_view() method. See there for documentation.
:param \*args: Arguments to pass through to Airplane.draw_three_view()
:param \*\*kwargs: Keyword arguments to pass through to Airplane.draw_three_view()
Returns: Same return as Airplane.draw_three_view()
.. py:method:: subdivide_sections(ratio, spacing_function = np.linspace)
Generates a new Fuselage that subdivides the existing sections of this Fuselage into several smaller ones. Splits
each section into N=`ratio` smaller subsections by inserting new cross-sections (xsecs) as needed.
This can allow for finer aerodynamic resolution of sectional properties in certain analyses.
:param ratio: The number of new sections to split each old section into.
:param spacing_function: A function that takes in three arguments: the start, end, and number of points to generate.
The default is `np.linspace`, which generates a linearly-spaced array of points.
Other options include `np.cosspace`, which generates a cosine-spaced array of points.
Returns: A new Fuselage object with subdivided sections.
.. py:method:: _compute_frame_of_FuselageXSec(index)
Computes the local frame of a FuselageXSec, given the index of the FuselageXSec in the Fuselage.xsecs list.
:param index: The index of the FuselageXSec in the Fuselage.xsecs list.
Returns: A tuple:
xg_local: The x-axis of the local coordinate frame, in aircraft geometry axes.
yg_local: The y-axis of the local coordinate frame, in aircraft geometry axes.
zg_local: The z-axis of the local coordinate frame, in aircraft geometry axes.
.. py:class:: FuselageXSec(xyz_c = None, xyz_normal = None, radius = None, width = None, height = None, shape = 2.0, analysis_specific_options = None)
Bases: :py:obj:`aerosandbox.AeroSandboxObject`
Definition for a fuselage cross-section ("X-section").
.. py:attribute:: xyz_c
.. py:attribute:: xyz_normal
.. py:attribute:: shape
:value: 2.0
.. py:attribute:: analysis_specific_options
:value: None
.. py:method:: __repr__()
.. py:method:: xsec_area()
Computes the FuselageXSec's cross-sectional (xsec) area.
The computation method is a closed-form approximation for the area of a superellipse. The exact equation for
the area of a superellipse with shape parameter `s` is:
area = width * height * (gamma(1 + 1/n))^2 / gamma(1 + 2/n)
where gamma() is the gamma function. The gamma function is (relatively) computationally expensive to evaluate
and differentiate, so we replace this area calculation with a closed-form approximation (with essentially no
loss in accuracy):
area = width * height / (s^-1.8717618013591173 + 1)
This approximation has the following properties:
* It is numerically exact for the case of s = 1 (a diamond)
* It is numerically exact for the case of s = 2 (a circle)
* It is correct in the asymptotic limit where s -> infinity (a square)
* In the range of sensible s values (1 < s < infinity), its error is less than 0.6%.
* It always produces a positive area for any physically-meaningful value of s (s > 0). In the range of s
values where s is physically-meaningful but not in a sensible range (0 < s < 1), this equation will
over-predict area.
The value of the constant seen in this expression (1.872...) is given by log(4/pi - 1) / log(2), and it is
chosen as such so that the expression is exactly correct in the s=2 (circle) case.
Returns:
.. py:method:: xsec_perimeter()
Computes the FuselageXSec's perimeter. ("Circumference" in the case of a circular cross-section.)
The computation method is a closed-form approximation for the perimeter of a superellipse. The exact equation
for the perimeter of a superellipse is quite long and is not repeated here for brevity; a Google search will
bring it up. More importantly, this exact equation can only be represented as an infinite sum - not
particularly useful for fast computation.
We replace this exact equation with the following closed-form approximation obtained from symbolic regression:
Imagine a superellipse centered on the origin of a 2D plane. Now, imagine that the superellipse is
stretched such that the first quadrant (e.g., x>0, y>0) goes from (1, 0) to (0, h). Assume it has shape
parameter s (where, as a reminder, s=1 is a diamond, s=2 is a circle, s=Inf is a square).
Then, the perimeter of that single quadrant is:
h + (((((s-0.88487077) * h + 0.2588574 / h) ^ exp(s / -0.90069205)) + h) + 0.09919785) ^ (-1.4812293 / s)
See `AeroSandbox/studies/SuperellipseProperties` for details about how this was obtained.
We can extrapolate from here to the general case of a superellipse, as shown in the code below.
This approximation has the following properties:
* For the s=1 case (diamond), the error is +0.2%.
* For the s=2 case (circle), the error is -0.1%.
* In the s -> infinity limit (square), the error is +0.1%.
Returns:
.. py:method:: compute_frame()
Computes the local coordinate frame of the FuselageXSec, in aircraft geometry axes.
xg_local is aligned with the FuselageXSec's normal vector.
zg_local is roughly aligned with the z-axis of the aircraft geometry axes, but projected onto the FuselageXSec's plane.
yg_local is the cross product of zg_local and xg_local.
Returns: A tuple:
xg_local: The x-axis of the local coordinate frame, in aircraft geometry axes.
yg_local: The y-axis of the local coordinate frame, in aircraft geometry axes.
zg_local: The z-axis of the local coordinate frame, in aircraft geometry axes.
.. py:method:: get_3D_coordinates(theta = None)
Samples points from the perimeter of this FuselageXSec.
:param theta: Coordinate in the tangential-ish direction to sample points at. Given in the 2D FuselageXSec
:param coordinate system:
* y_2D points along the (global) y_g
* z_2D points along the (global) z_g
In other words, a value of:
* theta=0 -> samples points from the right side of the FuselageXSec
* theta=pi/2 -> samples points from the top of the FuselageXSec
* theta=pi -> samples points from the left side of the FuselageXSec
* theta=3pi/2 -> samples points from the bottom of the FuselageXSec
:param where:
* y_2D points along the (global) y_g
* z_2D points along the (global) z_g
In other words, a value of:
* theta=0 -> samples points from the right side of the FuselageXSec
* theta=pi/2 -> samples points from the top of the FuselageXSec
* theta=pi -> samples points from the left side of the FuselageXSec
* theta=3pi/2 -> samples points from the bottom of the FuselageXSec
Returns: Points sampled from the perimeter of the FuselageXSec, as a [x, y, z] tuple.
If theta is a float, then each of x, y, and z will be floats.
If theta is an array, then x, y, and z will also be arrays of the same size.
.. py:method:: equivalent_radius(preserve='area')
Computes an equivalent radius for non-circular cross-sections. This may be necessary when doing analysis that
uses axisymmetric assumptions.
Can either hold area or perimeter fixed, depending on whether cross-sectional area or wetted area is more
important.
:param preserve: One of:
* "area": holds the cross-sectional area constant
* "perimeter": holds the cross-sectional perimeter (i.e., the wetted area of the Fuselage) constant
Returns: An equivalent radius value.
.. py:method:: translate(xyz)
Returns a copy of this FuselageXSec that has been translated by `xyz`.
:param xyz: The amount to translate the FuselageXSec. Given as a 3-element NumPy vector.
Returns: A copy of this FuselageXSec, translated by `xyz`.
.. py:class:: Airplane(name = None, xyz_ref = None, wings = None, fuselages = None, propulsors = None, s_ref = None, c_ref = None, b_ref = None, analysis_specific_options = None)
Bases: :py:obj:`aerosandbox.AeroSandboxObject`
Definition for an airplane.
Anatomy of an Airplane:
An Airplane consists chiefly of a collection of wings and fuselages. These can be accessed with
`Airplane.wings` and `Airplane.fuselages`, which gives a list of those respective components. Each wing is a
Wing object, and each fuselage is a Fuselage object.
.. py:attribute:: name
:value: None
.. py:attribute:: xyz_ref
.. py:attribute:: wings
:value: None
.. py:attribute:: fuselages
:value: None
.. py:attribute:: propulsors
:value: None
.. py:attribute:: analysis_specific_options
:value: None
.. py:method:: __repr__()
.. py:method:: mesh_body(method='quad', thin_wings=False, stack_meshes=True)
Returns a surface mesh of the Airplane, in (points, faces) format. For reference on this format,
see the documentation in `aerosandbox.geometry.mesh_utilities`.
:param method:
:param thin_wings: Controls whether wings should be meshed as thin surfaces, rather than full 3D bodies.
:param stack_meshes: Controls whether the meshes should be merged into a single mesh or not.
* If True, returns a (points, faces) tuple in standard mesh format.
* If False, returns a list of (points, faces) tuples in standard mesh format.
Returns:
.. py:method:: draw(backend = 'pyvista', thin_wings = False, ax=None, use_preset_view_angle = None, set_background_pane_color = None, set_background_pane_alpha = None, set_lims = True, set_equal = True, set_axis_visibility = None, show = True, show_kwargs = None)
Produces an interactive 3D visualization of the airplane.
:param backend: The visualization backend to use. Options are:
* "matplotlib" for a Matplotlib backend
* "pyvista" for a PyVista backend
* "plotly" for a Plot.ly backend
* "trimesh" for a trimesh backend
:param thin_wings: A boolean that determines whether to draw the full airplane (i.e. thickened, 3D bodies), or to use a
:param thin-surface representation for any Wing objects.:
:param show: A boolean that determines whether to display the object after plotting it. If False, the object is
:param returned but not displayed. If True:
:param the object is displayed and returned.:
Returns: The plotted object, in its associated backend format. Also displays the object if `show` is True.
.. py:method:: draw_wireframe(ax=None, color='k', thin_linewidth=0.2, thick_linewidth=0.5, fuselage_longeron_theta=None, use_preset_view_angle = None, set_background_pane_color = None, set_background_pane_alpha = None, set_lims = True, set_equal = True, set_axis_visibility = None, show = True)
Draws a wireframe of the airplane on a Matplotlib 3D axis.
:param ax: The axis to draw on. Must be a 3D axis. If None, creates a new axis.
:param color: The color of the wireframe.
:param thin_linewidth: The linewidth of the thin lines.
.. py:method:: draw_three_view(axs=None, style = 'shaded', show = True)
Draws a standard 4-panel three-view diagram of the airplane using Matplotlib backend. Creates a new figure.
:param axs: A 2D numpy array of Matplotlib axes objects, with shape at least (2, 2). Each subplot must be 3D. If
:param None:
:param creates a new figure with the required axes.:
:param style: Determines what drawing style to use for the three-view. A string, one of:
* "shaded"
* "wireframe"
:param show: A Boolean of whether to show the figure after creating it, or to hold it so that the user can
:param modify the figure further before showing.:
Returns: A 2D NumPy array of Matplotlib axes objects, with shape (2, 2). The axes are arranged as follows:
* axs[0, 0]: Top view
* axs[0, 1]: Front view
* axs[1, 0]: Side view
* axs[1, 1]: Isometric view
.. py:method:: is_entirely_symmetric()
Returns a boolean describing whether the airplane is geometrically entirely symmetric across the XZ-plane.
:return: [boolean]
.. py:method:: aerodynamic_center(chord_fraction = 0.25)
Computes the approximate location of the aerodynamic center of the wing.
Uses the generalized methodology described here:
https://core.ac.uk/download/pdf/79175663.pdf
:param chord_fraction: The position of the aerodynamic center along the MAC, as a fraction of MAC length.
:param Typically:
:type Typically: denoted `h_0` in the literature
:param this value:
:type this value: denoted `h_0` in the literature
:param wing-fuselage interactions can cause a forward shift to a value more like 0.1 or less. Citing Cook:
:param :
:param Michael V.:
:param "Flight Dynamics Principles":
:param 3rd Ed.:
:param Sect. 3.5.3 "Controls-fixed static stability". PDF:
:param https: //www.sciencedirect.com/science/article/pii/B9780080982427000031
Returns: The (x, y, z) coordinates of the aerodynamic center of the airplane.
.. py:method:: with_control_deflections(control_surface_deflection_mappings)
Returns a copy of the airplane with the specified control surface deflections applied.
:param control_surface_deflection_mappings: A dictionary mapping control surface names to deflections.
* Keys: Control surface names.
* Values: Deflections, in degrees. Downwards-positive, following typical convention.
Returns: A copy of the airplane with the specified control surface deflections applied.
.. py:method:: generate_cadquery_geometry(minimum_airfoil_TE_thickness = 0.001, fuselage_tol = 0.0001)
Uses the CADQuery library (OpenCASCADE backend) to generate a 3D CAD model of the airplane.
:param minimum_airfoil_TE_thickness: The minimum thickness of the trailing edge of the airfoils, as a fraction
:param of each airfoil's chord. This will be enforced by thickening the trailing edge of the airfoils if:
:param necessary. This is useful for avoiding numerical issues in CAD software that can arise from extremely:
:param thin:
:type thin: i.e., <1e-6 meters
:param tol: The geometric tolerance (meters) to use when generating the CAD geometry. This is passed directly to the CADQuery
Returns: A CADQuery Workplane object containing the CAD geometry of the airplane.
.. py:method:: export_cadquery_geometry(filename, minimum_airfoil_TE_thickness = 0.001)
Exports the airplane geometry to a STEP file.
:param filename: The filename to export to. Should include the ".step" extension.
:param minimum_airfoil_TE_thickness: The minimum thickness of the trailing edge of the airfoils, as a fraction
:param of each airfoil's chord. This will be enforced by thickening the trailing edge of the airfoils if:
:param necessary. This is useful for avoiding numerical issues in CAD software that can arise from extremely:
:param thin:
:type thin: i.e., <1e-6 meters
Returns: None, but exports the airplane geometry to a STEP file.
.. py:method:: export_AVL(filename, include_fuselages = True)
.. py:method:: export_XFLR(*args, **kwargs)
.. py:method:: export_XFLR5_xml(filename, mass_props = None, include_fuselages = False, mainwing = None, elevator = None, fin = None)
Exports the airplane geometry to an XFLR5 `.xml` file. To import the `.xml` file into XFLR5, go to File ->
Import -> Import from XML.
:param filename: The filename to export to. Should include the ".xml" extension.
:param mass_props: The MassProperties object to use when exporting the airplane. If not specified, will default to
a 1 kg point mass at the origin.
- Note: XFLR5 does not natively support user-defined inertia tensors, so we have to synthesize an equivalent
set of point masses to represent the inertia tensor.
:param include_fuselages: Whether to include fuselages in the export.
:param mainwing: The main wing of the airplane. If not specified, will default to the first wing in the airplane.
:param elevator: The elevator of the airplane. If not specified, will default to the second wing in the airplane.
:param fin: The fin of the airplane. If not specified, will default to the third wing in the airplane.
Returns: None, but exports the airplane geometry to an XFLR5 `.xml` file.
To import the `.xml` file into XFLR5, go to File -> Import -> Import from XML.
.. py:method:: export_OpenVSP_vspscript(filename)
Exports the airplane geometry to a `*.vspscript` file compatible with OpenVSP. To import the `.vspscript`
file into OpenVSP:
Open OpenVSP, then File -> Run Script -> Select the `.vspscript` file.
:param filename: The filename to export to, given as a string or Path. Should include the ".vspscript" extension.
Returns: A string of the file contents, and also saves the file to the specified filename
.. py:class:: Propulsor(name = 'Untitled', xyz_c = None, xyz_normal = None, radius = 1.0, length = 0.0, color = None, analysis_specific_options = None)
Bases: :py:obj:`aerosandbox.AeroSandboxObject`
Definition for a Propulsor, which could be a propeller, a rotor, or a jet engine.
Assumes a disk- or cylinder-shaped propulsor.
.. py:attribute:: name
:value: 'Untitled'
.. py:attribute:: xyz_c
.. py:attribute:: xyz_normal
.. py:attribute:: radius
:value: 1.0
.. py:attribute:: length
:value: 0.0
.. py:attribute:: color
:value: None
.. py:attribute:: analysis_specific_options
:value: None
.. py:method:: __repr__()
.. py:method:: xsec_area()
Returns the cross-sectional area of the propulsor, in m^2.
.. py:method:: xsec_perimeter()
Returns the cross-sectional perimeter of the propulsor, in m.
.. py:method:: volume()
Returns the volume of the propulsor, in m^3.
.. py:method:: compute_frame()
Computes the local coordinate frame of the propulsor, in aircraft geometry axes.
xg_local is aligned with the propulsor's normal vector.
zg_local is roughly aligned with the z-axis of the aircraft geometry axes, but projected onto the propulsor's plane.
yg_local is the cross product of zg_local and xg_local.
Returns: A tuple:
xg_local: The x-axis of the local coordinate frame, in aircraft geometry axes.
yg_local: The y-axis of the local coordinate frame, in aircraft geometry axes.
zg_local: The z-axis of the local coordinate frame, in aircraft geometry axes.
.. py:method:: get_disk_3D_coordinates(theta = None, l_over_length = None)
.. py:method:: translate(xyz)
Returns a copy of this propulsor that has been translated by `xyz`.
:param xyz: The amount to translate the propulsor, in meters. Given in aircraft geometry axes, as with everything else.
Returns: A copy of this propulsor, translated by `xyz`.
.. py:function:: is_casadi_type(object, recursive = True)
Returns a boolean of whether an object is a CasADi data type or not. If the recursive flag is True,
iterates recursively, returning True if any subelement (at any depth) is a CasADi type.
:param object: The object to evaluate.
:param recursive: If the object is a list or tuple, recursively iterate through every subelement. If any of the
:param subelements:
:type subelements: at any depth
Returns: A boolean if the object is (or contains, if recursive=True) a CasADi data type.
.. py:function:: reflect_over_XZ_plane(input_vector)
Takes in a vector or an array and flips the y-coordinates.
:param input_vector: A vector or list of vectors to flip.
:return: Vector with flipped sign on y-coordinate.
.. py:class:: Atmosphere(altitude = 0.0, method = 'differentiable', temperature_deviation = 0.0)
Bases: :py:obj:`aerosandbox.common.AeroSandboxObject`
All models here are smoothed fits to the 1976 COESA model;
see AeroSandbox\studies\Atmosphere Fitting for details.
.. py:attribute:: altitude
:value: 0.0
.. py:attribute:: method
:value: 'differentiable'
.. py:attribute:: temperature_deviation
:value: 0.0
.. py:attribute:: _valid_altitude_range
:value: (0, 80000)
.. py:method:: __repr__()
.. py:method:: __getitem__(index)
Indexes one item from each attribute of an Atmosphere instance.
Returns a new Atmosphere instance.
:param index: The index that is being called; e.g.,:
>>> first_atmosphere = atmosphere[0]
Returns: A new Atmosphere instance, where each attribute is subscripted at the given value, if possible.
.. py:method:: __len__()
.. py:method:: __array__(dtype='O')
Allows NumPy array creation without infinite recursion in __len__ and __getitem__.
.. py:method:: pressure()
Returns the pressure, in Pascals.
.. py:method:: temperature()
Returns the temperature, in Kelvin.
.. py:method:: density()
Returns the density, in kg/m^3.
.. py:method:: density_altitude(method = 'approximate')
Returns the density altitude, in meters.
See https://en.wikipedia.org/wiki/Density_altitude
.. py:method:: speed_of_sound()
Returns the speed of sound, in m/s.
.. py:method:: dynamic_viscosity()
Returns the dynamic viscosity (mu), in kg/(m*s).
Based on Sutherland's Law, citing `https://www.cfd-online.com/Wiki/Sutherland's_law`.
According to Rathakrishnan, E. (2013). Theoretical aerodynamics. John Wiley & Sons.:
This relationship is valid from 0.01 to 100 atm, and between 0 and 3000K.
According to White, F. M., & Corfield, I. (2006). Viscous fluid flow (Vol. 3, pp. 433-434). New York: McGraw-Hill.:
The error is no more than approximately 2% for air between 170K and 1900K.
.. py:method:: kinematic_viscosity()
Returns the kinematic viscosity (nu), in m^2/s.
Definitional.
.. py:method:: ratio_of_specific_heats()
.. py:method:: mean_free_path()
Returns the mean free path of an air molecule, in meters.
To find the collision radius, assumes "a hard-sphere gas that has the same viscosity as the actual gas being considered".
From Vincenti, W. G. and Kruger, C. H. (1965). Introduction to physical gas dynamics. Krieger Publishing Company. p. 414.
.. py:method:: knudsen(length)
Computes the Knudsen number for a given length.
.. py:function:: mass_properties_from_radius_of_gyration(mass, x_cg = 0, y_cg = 0, z_cg = 0, radius_of_gyration_x = 0, radius_of_gyration_y = 0, radius_of_gyration_z = 0)
Returns the mass properties of an object, given its radius of gyration.
It's assumed that the principle axes of the inertia tensor are aligned with the coordinate axes.
This is a shorthand convenience function for common usage of the MassProperties constructor. For more detailed
use, use the MassProperties object directly.
:param mass: Mass [kg]
:param x_cg: x-position of the center of gravity
:param y_cg: y-position of the center of gravity
:param z_cg: z-position of the center of gravity
:param radius_of_gyration_x: Radius of gyration along the x-axis, about the center of gravity [m]
:param radius_of_gyration_y: Radius of gyration along the y-axis, about the center of gravity [m]
:param radius_of_gyration_z: Radius of gyration along the z-axis, about the center of gravity [m]
Returns: MassProperties object.
.. py:class:: AeroSandboxObject
Bases: :py:obj:`abc.ABC`
Helper class that provides a standard way to create an ABC using
inheritance.
.. py:attribute:: _asb_metadata
:type: Dict[str, str]
:value: None
.. py:method:: __eq__(other)
Checks if two AeroSandbox objects are value-equivalent. A more sensible default for classes that represent
physical objects than checking for memory equivalence.
This is done by checking if the two objects are of the same type and have the same __dict__.
:param other: Another object.
:returns: True if the objects are equal, False otherwise.
:rtype: bool
.. py:method:: save(filename = None, verbose = True, automatically_add_extension = True)
Saves the object to a binary file, using the `dill` library.
Creates a .asb file, which is a binary file that can be loaded with `aerosandbox.load()`. This can be loaded
into memory in a different Python session or a different computer, and it will be exactly the same as when it
was saved.
:param filename: The filename to save this object to. Should be a .asb file.
:param verbose: If True, prints messages to console on successful save.
:param automatically_add_extension: If True, automatically adds the .asb extension to the filename if it doesn't
already have it. If False, does not add the extension.
Returns: None (writes to file)
.. py:method:: copy()
Returns a shallow copy of the object.
.. py:method:: deepcopy()
Returns a deep copy of the object.
.. py:method:: substitute_solution(sol, inplace = None)
Substitutes a solution from CasADi's solver recursively as an in-place operation.
In-place operation. To make it not in-place, do `y = copy.deepcopy(x)` or similar first.
:param sol: OptiSol object.
:return:
.. py:function:: trim_string(string, length = 80)
Trims a string to be less than a given length. If the string would exceed the length, makes it end in ellipses ("…").
:param string: The string to be trimmed.
:param length: The length to trim the string to, including any ellipses that may be added.
Returns: The trimmed string, including ellipses if needed.
.. py:class:: MassProperties(mass = None, x_cg = 0.0, y_cg = 0.0, z_cg = 0.0, Ixx = 0.0, Iyy = 0.0, Izz = 0.0, Ixy = 0.0, Iyz = 0.0, Ixz = 0.0)
Bases: :py:obj:`aerosandbox.common.AeroSandboxObject`
Mass properties of a rigid 3D object.
## Notes on Inertia Tensor Definition
This class uses the standard mathematical definition of the inertia tensor, which is different from the
alternative definition used by some CAD and CAE applications (such as SolidWorks, NX, etc.). These differ by a
sign flip in the products of inertia.
Specifically, we define the inertia tensor using the standard convention:
[ I11 I12 I13 ] [ Ixx Ixy Ixz ] [sum(m*(y^2+z^2)) -sum(m*x*y) -sum(m*x*z) ]
I = [ I21 I22 I23 ] = [ Ixy Iyy Iyz ] = [-sum(m*x*y) sum(m*(x^2+z^2)) -sum(m*y*z) ]
[ I31 I32 I33 ] [ Ixz Iyz Izz ] [-sum(m*x*z) -sum(m*y*z) sum(m*(x^2+y^2))]
Whereas SolidWorks, NX, etc. define the inertia tensor as:
[ I11 I12 I13 ] [ Ixx -Ixy -Ixz ] [sum(m*(y^2+z^2)) -sum(m*x*y) -sum(m*x*z) ]
I = [ I21 I22 I23 ] = [-Ixy Iyy -Iyz ] = [-sum(m*x*y) sum(m*(x^2+z^2)) -sum(m*y*z) ]
[ I31 I32 I33 ] [-Ixz -Iyz Izz ] [-sum(m*x*z) -sum(m*y*z) sum(m*(x^2+y^2))]
See also: https://en.wikipedia.org/wiki/Moment_of_inertia#Inertia_tensor
.. py:attribute:: mass
:value: None
.. py:attribute:: x_cg
:value: 0.0
.. py:attribute:: y_cg
:value: 0.0
.. py:attribute:: z_cg
:value: 0.0
.. py:attribute:: Ixx
:value: 0.0
.. py:attribute:: Iyy
:value: 0.0
.. py:attribute:: Izz
:value: 0.0
.. py:attribute:: Ixy
:value: 0.0
.. py:attribute:: Iyz
:value: 0.0
.. py:attribute:: Ixz
:value: 0.0
.. py:method:: __repr__()
.. py:method:: __getitem__(index)
Indexes one item from each attribute of an MassProperties instance.
Returns a new MassProperties instance.
:param index: The index that is being called; e.g.,:
>>> first_mass_props = mass_props[0]
Returns: A new MassProperties instance, where each attribute is subscripted at the given value, if possible.
.. py:method:: __len__()
.. py:method:: __array__(dtype='O')
Allows NumPy array creation without infinite recursion in __len__ and __getitem__.
.. py:method:: __neg__()
.. py:method:: __add__(other)
Combines one MassProperties object with another.
.. py:method:: __radd__(other)
Allows sum() to work with MassProperties objects.
Basically, makes addition commutative.
.. py:method:: __sub__(other)
Subtracts one MassProperties object from another. (opposite of __add__() )
.. py:method:: __mul__(other)
Returns a new MassProperties object that is equivalent to if you had summed together N (with `other`
interpreted as N) identical MassProperties objects.
.. py:method:: __rmul__(other)
Allows multiplication of a scalar by a MassProperties object. Makes multiplication commutative.
.. py:method:: __truediv__(other)
Returns a new MassProperties object that is equivalent to if you had divided the mass of the current
MassProperties object by a factor.
.. py:method:: allclose(other, rtol=1e-05, atol=1e-08, equal_nan=False)
.. py:property:: xyz_cg
.. py:property:: inertia_tensor
.. py:method:: inv_inertia_tensor()
Computes the inverse of the inertia tensor, in a slightly more efficient way than raw inversion by exploiting its known structure.
If you are effectively using this inertia tensor to solve a linear system, you should use a linear algebra
solve() method (ideally via Cholesky decomposition) instead, for best speed.
.. py:method:: get_inertia_tensor_about_point(x = 0.0, y = 0.0, z = 0.0, return_tensor = True)
Returns the inertia tensor about an arbitrary point.
Using https://en.wikipedia.org/wiki/Parallel_axis_theorem#Tensor_generalization
:param x: x-position of the new point, in the same axes as this MassProperties instance is specified in.
:param y: y-position of the new point, in the same axes as this MassProperties instance is specified in.
:param z: z-position of the new point, in the same axes as this MassProperties instance is specified in.
:param return_tensor: A switch for the desired return type; see below for details. [boolean]
:returns: Returns the new inertia tensor, as a 2D numpy ndarray.
If `return_tensor` is False:
Returns the components of the new inertia tensor, as a tuple.
If J is the new inertia tensor, the tuple returned is:
(Jxx, Jyy, Jzz, Jxy, Jyz, Jxz)
:rtype: If `return_tensor` is True
.. py:method:: is_physically_possible()
Checks whether it's possible for this MassProperties object to correspond to the mass properties of a real
physical object.
Assumes that all physically-possible objects have a positive mass (or density).
Some special edge cases:
- A MassProperties object with mass of 0 (i.e., null object) will return True. Note: this will return
True even if the inertia tensor is not zero (which would basically be infinitesimal point masses at
infinite distance).
- A MassProperties object that is a point mass (i.e., inertia tensor is all zeros) will return True.
:returns: True if the MassProperties object is physically possible, False otherwise.
.. py:method:: is_point_mass()
Returns True if this MassProperties object corresponds to a point mass, False otherwise.
.. py:method:: generate_possible_set_of_point_masses(method='optimization', check_if_already_a_point_mass = True)
Generates a set of point masses (represented as MassProperties objects with zero inertia tensors), that, when
combined, would yield this MassProperties object.
Note that there are an infinite number of possible sets of point masses that could yield this MassProperties
object. This method returns one possible set of point masses, but there are many others.
.. rubric:: Example
>>> mp = MassProperties(mass=1, Ixx=1, Iyy=1, Izz=1, Ixy=0.1, Iyz=-0.1, Ixz=0.1)
>>> point_masses = mp.generate_possible_set_of_point_masses()
>>> mp.allclose(sum(point_masses)) # Asserts these are equal, within tolerance
True
:param method: The method to use to generate the set of point masses. Currently, only "optimization" is supported.
:returns: A list of MassProperties objects, each of which is a point mass (i.e., zero inertia tensor).
.. py:method:: export_AVL_mass_file(filename)
Exports this MassProperties object to an AVL mass file.
Note: AVL uses the SolidWorks convention for inertia tensors, which is different from the typical
mathematical convention, and the convention used by this MassProperties class. In short, these differ by a
sign flip in the products of inertia. More details available in the MassProperties docstring. See also:
https://en.wikipedia.org/wiki/Moment_of_inertia#Inertia_tensor
:param filename: The filename to export to.
Returns: None
.. py:data:: mp1
.. py:class:: OperatingPoint(atmosphere = Atmosphere(altitude=0), velocity = 1.0, alpha = 0.0, beta = 0.0, p = 0.0, q = 0.0, r = 0.0)
Bases: :py:obj:`aerosandbox.common.AeroSandboxObject`
Helper class that provides a standard way to create an ABC using
inheritance.
.. py:attribute:: atmosphere
.. py:attribute:: velocity
:value: 1.0
.. py:attribute:: alpha
:value: 0.0
.. py:attribute:: beta
:value: 0.0
.. py:attribute:: p
:value: 0.0
.. py:attribute:: q
:value: 0.0
.. py:attribute:: r
:value: 0.0
.. py:property:: state
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
Returns the state variables of this OperatingPoint instance as a Dict.
Keys are strings that give the name of the variables.
Values are the variables themselves.
.. py:method:: get_new_instance_with_state(new_state = None)
Creates a new instance of the OperatingPoint class from the given state.
:param new_state: The new state to be used for the new instance. Ideally, this is represented as a Dict in identical format to the `state` of a OperatingPoint instance.
Returns: A new instance of this same OperatingPoint class.
.. py:method:: _set_state(new_state = None)
Force-overwrites all state variables with a new set (either partial or complete) of state variables.
Warning: this is *not* the intended public usage of OperatingPoint instances.
If you want a new state yourself, you should instantiate a new one either:
a) manually, or
b) by using OperatingPoint.get_new_instance_with_state()
Hence, this function is meant for PRIVATE use only - be careful how you use this!
.. py:method:: unpack_state(dict_like_state = None)
'Unpacks' a Dict-like state into an array-like that represents the state of the OperatingPoint.
:param dict_like_state: Takes in a dict-like representation of the state.
Returns: The array representation of the state that you gave.
.. py:method:: pack_state(array_like_state = None)
'Packs' an array into a Dict that represents the state of the OperatingPoint.
:param array_like_state: Takes in an iterable that must have the same number of entries as the state vector of the OperatingPoint.
Returns: The Dict representation of the state that you gave.
.. py:method:: __repr__()
.. py:method:: __getitem__(index)
Indexes one item from each attribute of an OperatingPoint instance.
Returns a new OperatingPoint instance.
:param index: The index that is being called; e.g.,:
>>> first_op_point = op_point[0]
Returns: A new OperatingPoint instance, where each attribute is subscripted at the given value, if possible.
.. py:method:: __len__()
.. py:method:: __array__(dtype='O')
Allows NumPy array creation without infinite recursion in __len__ and __getitem__.
.. py:method:: dynamic_pressure()
Dynamic pressure of the working fluid
:returns: Dynamic pressure of the working fluid. [Pa]
:rtype: float
.. py:method:: total_pressure()
Total (stagnation) pressure of the working fluid.
Assumes a calorically perfect gas (i.e. specific heats do not change across the isentropic deceleration).
Note that `total pressure != static pressure + dynamic pressure`, due to compressibility effects.
Returns: Total pressure of the working fluid. [Pa]
.. py:method:: total_temperature()
Total (stagnation) temperature of the working fluid.
Assumes a calorically perfect gas (i.e. specific heats do not change across the isentropic deceleration).
Returns: Total temperature of the working fluid [K]
.. py:method:: reynolds(reference_length)
Computes a Reynolds number with respect to a given reference length.
:param reference_length: A reference length you choose [m]
:return: Reynolds number [unitless]
.. py:method:: mach()
Returns the Mach number associated with the current flight condition.
.. py:method:: indicated_airspeed()
Returns the indicated airspeed associated with the current flight condition, in meters per second.
.. py:method:: equivalent_airspeed()
Returns the equivalent airspeed associated with the current flight condition, in meters per second.
.. py:method:: energy_altitude()
Returns the energy altitude associated with the current flight condition, in meters.
The energy altitude is the altitude at which a stationary aircraft would have the same total energy (kinetic
+ gravitational potential) as the aircraft at the current flight condition.
.. py:method:: convert_axes(x_from, y_from, z_from, from_axes, to_axes)
Converts a vector [x_from, y_from, z_from], as given in the `from_axes` frame, to an equivalent vector [x_to,
y_to, z_to], as given in the `to_axes` frame.
Both `from_axes` and `to_axes` should be a string, one of:
* "geometry"
* "body"
* "wind"
* "stability"
This whole function is vectorized, both over the vector and the OperatingPoint (e.g., a vector of
`OperatingPoint.alpha` values)
Wind axes rotations are taken from Eq. 6.7 in Sect. 6.2.2 of Drela's Flight Vehicle Aerodynamics textbook,
with axes corrections to go from [D, Y, L] to true wind axes (and same for geometry to body axes).
:param x_from: x-component of the vector, in `from_axes` frame.
:param y_from: y-component of the vector, in `from_axes` frame.
:param z_from: z-component of the vector, in `from_axes` frame.
:param from_axes: The axes to convert from.
:param to_axes: The axes to convert to.
Returns: The x-, y-, and z-components of the vector, in `to_axes` frame. Given as a tuple.
.. py:method:: compute_rotation_matrix_wind_to_geometry()
Computes the 3x3 rotation matrix that transforms from wind axes to geometry axes.
Returns: a 3x3 rotation matrix.
.. py:method:: compute_freestream_direction_geometry_axes()
.. py:method:: compute_freestream_velocity_geometry_axes()
.. py:method:: compute_rotation_velocity_geometry_axes(points)
.. py:class:: DynamicsPointMass1DHorizontal(mass_props = None, x_e = 0, u_e = 0)
Bases: :py:obj:`aerosandbox.dynamics.point_mass.point_3D.cartesian.DynamicsPointMass3DCartesian`
Dynamics instance:
* simulating a point mass
* in 1D, oriented horizontally (i.e., the .add_gravity() method will have no effect)
State variables:
x_e: x-position, in Earth axes. [meters]
u_e: x-velocity, in Earth axes. [m/s]
Control variables:
Fx_e: Force along the Earth-x axis. [N]
.. py:attribute:: mass_props
For each state variable, self.state_var = state_var
For each indirect control variable, self.indirect_control_var = indirect_control_var
For each control variable, self.control_var = 0
.. py:attribute:: x_e
:value: 0
.. py:attribute:: y_e
:value: 0
.. py:attribute:: z_e
:value: 0
.. py:attribute:: u_e
:value: 0
.. py:attribute:: v_e
:value: 0
.. py:attribute:: w_e
:value: 0
.. py:attribute:: alpha
:value: 0
.. py:attribute:: beta
:value: 0
.. py:attribute:: bank
:value: 0
.. py:attribute:: Fx_e
:value: 0
.. py:attribute:: Fy_e
:value: 0
.. py:attribute:: Fz_e
:value: 0
.. py:property:: state
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
Returns the state variables of this Dynamics instance as a Dict.
Keys are strings that give the name of the variables.
Values are the variables themselves.
This method should look something like:
>>> {
>>> "x_e": self.x_e,
>>> "u_e": self.u_e,
>>> ...
>>> }
.. py:property:: control_variables
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
.. py:method:: state_derivatives()
A function that returns the derivatives with respect to time of the state specified in the `state` property.
Should return a Dict with the same keys as the `state` property.
.. py:class:: DynamicsPointMass1DVertical(mass_props = None, z_e = 0, w_e = 0)
Bases: :py:obj:`aerosandbox.dynamics.point_mass.point_3D.cartesian.DynamicsPointMass3DCartesian`
Dynamics instance:
* simulating a point mass
* in 1D, oriented vertically (i.e., the .add_gravity() method will have an effect)
State variables:
z_e: z-position, in Earth axes. [meters]
w_e: z-velocity, in Earth axes. [m/s]
Control variables:
Fz_e: Force along the Earth-x axis. [N]
.. py:attribute:: mass_props
For each state variable, self.state_var = state_var
For each indirect control variable, self.indirect_control_var = indirect_control_var
For each control variable, self.control_var = 0
.. py:attribute:: x_e
:value: 0
.. py:attribute:: y_e
:value: 0
.. py:attribute:: z_e
:value: 0
.. py:attribute:: u_e
:value: 0
.. py:attribute:: v_e
:value: 0
.. py:attribute:: w_e
:value: 0
.. py:attribute:: alpha
:value: 0
.. py:attribute:: beta
:value: 0
.. py:attribute:: bank
:value: 0
.. py:attribute:: Fx_e
:value: 0
.. py:attribute:: Fy_e
:value: 0
.. py:attribute:: Fz_e
:value: 0
.. py:property:: state
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
Returns the state variables of this Dynamics instance as a Dict.
Keys are strings that give the name of the variables.
Values are the variables themselves.
This method should look something like:
>>> {
>>> "x_e": self.x_e,
>>> "u_e": self.u_e,
>>> ...
>>> }
.. py:property:: control_variables
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
.. py:method:: state_derivatives()
A function that returns the derivatives with respect to time of the state specified in the `state` property.
Should return a Dict with the same keys as the `state` property.
.. py:class:: DynamicsPointMass2DCartesian(mass_props = None, x_e = 0, z_e = 0, u_e = 0, w_e = 0, alpha = 0)
Bases: :py:obj:`aerosandbox.dynamics.point_mass.point_3D.cartesian.DynamicsPointMass3DCartesian`
Dynamics instance:
* simulating a point mass
* in 2D
* with velocity parameterized in Cartesian coordinates
State variables:
x_e: x-position, in Earth axes. [meters]
z_e: z-position, in Earth axes. [meters]
u_e: x-velocity, in Earth axes. [m/s]
w_e: z-velocity, in Earth axes. [m/s]
Indirect control variables:
alpha: Angle of attack. [degrees]
Control variables:
Fx_e: Force along the Earth-x axis. [N]
Fz_e: Force along the Earth-z axis. [N]
.. py:attribute:: mass_props
For each state variable, self.state_var = state_var
For each indirect control variable, self.indirect_control_var = indirect_control_var
For each control variable, self.control_var = 0
.. py:attribute:: x_e
:value: 0
.. py:attribute:: y_e
:value: 0
.. py:attribute:: z_e
:value: 0
.. py:attribute:: u_e
:value: 0
.. py:attribute:: v_e
:value: 0
.. py:attribute:: w_e
:value: 0
.. py:attribute:: alpha
:value: 0
.. py:attribute:: beta
:value: 0
.. py:attribute:: bank
:value: 0
.. py:attribute:: Fx_e
:value: 0
.. py:attribute:: Fy_e
:value: 0
.. py:attribute:: Fz_e
:value: 0
.. py:property:: state
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
Returns the state variables of this Dynamics instance as a Dict.
Keys are strings that give the name of the variables.
Values are the variables themselves.
This method should look something like:
>>> {
>>> "x_e": self.x_e,
>>> "u_e": self.u_e,
>>> ...
>>> }
.. py:property:: control_variables
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
.. py:method:: state_derivatives()
A function that returns the derivatives with respect to time of the state specified in the `state` property.
Should return a Dict with the same keys as the `state` property.
.. py:class:: DynamicsPointMass2DSpeedGamma(mass_props = None, x_e = 0, z_e = 0, speed = 0, gamma = 0, alpha = 0)
Bases: :py:obj:`aerosandbox.dynamics.point_mass.point_3D.speed_gamma_track.DynamicsPointMass3DSpeedGammaTrack`
Dynamics instance:
* simulating a point mass
* in 2D
* with velocity parameterized in speed-gamma space.
State variables:
x_e: x-position, in Earth axes. [meters]
z_e: z-position, in Earth axes. [meters]
speed: Speed; equivalent to u_w, the x-velocity in wind axes. [m/s]
gamma: Flight path angle. [rad]
Indirect control variables:
alpha: Angle of attack. [degrees]
Control variables:
Fx_w: Force along the wind-x axis. [N]
Fz_w: Force along the wind-z axis. [N]
.. py:attribute:: mass_props
For each state variable, self.state_var = state_var
For each indirect control variable, self.indirect_control_var = indirect_control_var
For each control variable, self.control_var = 0
.. py:attribute:: x_e
:value: 0
.. py:attribute:: y_e
:value: 0
.. py:attribute:: z_e
:value: 0
.. py:attribute:: speed
:value: 0
.. py:attribute:: gamma
:value: 0
.. py:attribute:: track
:value: 0
.. py:attribute:: bank
:value: 0
.. py:attribute:: alpha
:value: 0
.. py:attribute:: beta
:value: 0
.. py:attribute:: Fx_w
:value: 0
.. py:attribute:: Fy_w
:value: 0
.. py:attribute:: Fz_w
:value: 0
.. py:property:: state
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
Returns the state variables of this Dynamics instance as a Dict.
Keys are strings that give the name of the variables.
Values are the variables themselves.
This method should look something like:
>>> {
>>> "x_e": self.x_e,
>>> "u_e": self.u_e,
>>> ...
>>> }
.. py:property:: control_variables
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
.. py:method:: state_derivatives()
A function that returns the derivatives with respect to time of the state specified in the `state` property.
Should return a Dict with the same keys as the `state` property.
.. py:class:: DynamicsPointMass3DCartesian(mass_props = None, x_e = 0, y_e = 0, z_e = 0, u_e = 0, v_e = 0, w_e = 0, alpha = 0, beta = 0, bank = 0)
Bases: :py:obj:`aerosandbox.dynamics.point_mass.common_point_mass._DynamicsPointMassBaseClass`
Dynamics instance:
* simulating a point mass
* in 3D
* with velocity parameterized in Cartesian coordinates
State variables:
x_e: x-position, in Earth axes. [meters]
y_e: y-position, in Earth axes. [meters]
z_e: z-position, in Earth axes. [meters]
u_e: x-velocity, in Earth axes. [m/s]
v_e: v-velocity, in Earth axes. [m/s]
w_e: z-velocity, in Earth axes. [m/s]
Indirect control variables:
alpha: Angle of attack. [degrees]
beta: Sideslip angle. [degrees]
bank: Bank angle. [radians]
Control variables:
Fx_e: Force along the Earth-x axis. [N]
Fy_e: Force along the Earth-y axis. [N]
Fz_e: Force along the Earth-z axis. [N]
.. py:attribute:: mass_props
For each state variable, self.state_var = state_var
For each indirect control variable, self.indirect_control_var = indirect_control_var
For each control variable, self.control_var = 0
.. py:attribute:: x_e
:value: 0
.. py:attribute:: y_e
:value: 0
.. py:attribute:: z_e
:value: 0
.. py:attribute:: u_e
:value: 0
.. py:attribute:: v_e
:value: 0
.. py:attribute:: w_e
:value: 0
.. py:attribute:: alpha
:value: 0
.. py:attribute:: beta
:value: 0
.. py:attribute:: bank
:value: 0
.. py:attribute:: Fx_e
:value: 0
.. py:attribute:: Fy_e
:value: 0
.. py:attribute:: Fz_e
:value: 0
.. py:property:: state
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
Returns the state variables of this Dynamics instance as a Dict.
Keys are strings that give the name of the variables.
Values are the variables themselves.
This method should look something like:
>>> {
>>> "x_e": self.x_e,
>>> "u_e": self.u_e,
>>> ...
>>> }
.. py:property:: control_variables
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
.. py:method:: state_derivatives()
A function that returns the derivatives with respect to time of the state specified in the `state` property.
Should return a Dict with the same keys as the `state` property.
.. py:property:: speed
:type: float
.. py:property:: gamma
Returns the flight path angle, in radians.
Positive flight path angle indicates positive vertical speed.
.. py:property:: track
Returns the track angle, in radians.
* Track of 0 == North == aligned with x_e axis
* Track of np.pi / 2 == East == aligned with y_e axis
.. py:method:: convert_axes(x_from, y_from, z_from, from_axes, to_axes)
Converts a vector [x_from, y_from, z_from], as given in the `from_axes` frame, to an equivalent vector [x_to,
y_to, z_to], as given in the `to_axes` frame.
Identical to OperatingPoint.convert_axes(), but adds in "earth" as a valid axis frame. For more documentation,
see the docstring of OperatingPoint.convert_axes().
Both `from_axes` and `to_axes` should be a string, one of:
* "geometry"
* "body"
* "wind"
* "stability"
* "earth"
:param x_from: x-component of the vector, in `from_axes` frame.
:param y_from: y-component of the vector, in `from_axes` frame.
:param z_from: z-component of the vector, in `from_axes` frame.
:param from_axes: The axes to convert from. See above for options.
:param to_axes: The axes to convert to. See above for options.
Returns: The x-, y-, and z-components of the vector, in `to_axes` frame. Given as a tuple.
.. py:method:: add_force(Fx = 0, Fy = 0, Fz = 0, axes='earth')
Adds a force (in whichever axis system you choose) to this Dynamics instance.
:param Fx: Force in the x-direction in the axis system chosen. [N]
:param Fy: Force in the y-direction in the axis system chosen. [N]
:param Fz: Force in the z-direction in the axis system chosen. [N]
:param axes: The axis system that the specified force is in. One of:
* "geometry"
* "body"
* "wind"
* "stability"
* "earth"
Returns: None (in-place)
.. py:class:: DynamicsPointMass3DSpeedGammaTrack(mass_props = None, x_e = 0, y_e = 0, z_e = 0, speed = 0, gamma = 0, track = 0, alpha = 0, beta = 0, bank = 0)
Bases: :py:obj:`aerosandbox.dynamics.point_mass.common_point_mass._DynamicsPointMassBaseClass`
Dynamics instance:
* simulating a point mass
* in 3D
* with velocity parameterized in speed-gamma-track space
State variables:
x_e: x-position, in Earth axes. [meters]
y_e: y-position, in Earth axes. [meters]
z_e: z-position, in Earth axes. [meters]
speed: Speed; equivalent to u_w, the x-velocity in wind axes. [m/s]
gamma: Flight path angle. [radians]
track: Track angle. [radians]
* Track of 0 == North == aligned with x_e axis
* Track of np.pi / 2 == East == aligned with y_e axis
Indirect control variables:
alpha: Angle of attack. [degrees]
beta: Sideslip angle. [degrees]
bank: Bank angle. [radians]
Control variables:
Fx_w: Force along the wind-x axis. [N]
Fy_w: Force along the wind-y axis. [N]
Fz_w: Force along the wind-z axis. [N]
.. py:attribute:: mass_props
For each state variable, self.state_var = state_var
For each indirect control variable, self.indirect_control_var = indirect_control_var
For each control variable, self.control_var = 0
.. py:attribute:: x_e
:value: 0
.. py:attribute:: y_e
:value: 0
.. py:attribute:: z_e
:value: 0
.. py:attribute:: speed
:value: 0
.. py:attribute:: gamma
:value: 0
.. py:attribute:: track
:value: 0
.. py:attribute:: alpha
:value: 0
.. py:attribute:: beta
:value: 0
.. py:attribute:: bank
:value: 0
.. py:attribute:: Fx_w
:value: 0
.. py:attribute:: Fy_w
:value: 0
.. py:attribute:: Fz_w
:value: 0
.. py:property:: state
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
Returns the state variables of this Dynamics instance as a Dict.
Keys are strings that give the name of the variables.
Values are the variables themselves.
This method should look something like:
>>> {
>>> "x_e": self.x_e,
>>> "u_e": self.u_e,
>>> ...
>>> }
.. py:property:: control_variables
:type: Dict[str, Union[float, aerosandbox.numpy.ndarray]]
.. py:method:: state_derivatives()
A function that returns the derivatives with respect to time of the state specified in the `state` property.
Should return a Dict with the same keys as the `state` property.
.. py:property:: u_e
.. py:property:: v_e
.. py:property:: w_e
.. py:method:: convert_axes(x_from, y_from, z_from, from_axes, to_axes)
Converts a vector [x_from, y_from, z_from], as given in the `from_axes` frame, to an equivalent vector [x_to,
y_to, z_to], as given in the `to_axes` frame.
Identical to OperatingPoint.convert_axes(), but adds in "earth" as a valid axis frame. For more documentation,
see the docstring of OperatingPoint.convert_axes().
Both `from_axes` and `to_axes` should be a string, one of:
* "geometry"
* "body"
* "wind"
* "stability"
* "earth"
:param x_from: x-component of the vector, in `from_axes` frame.
:param y_from: y-component of the vector, in `from_axes` frame.
:param z_from: z-component of the vector, in `from_axes` frame.
:param from_axes: The axes to convert from. See above for options.
:param to_axes: The axes to convert to. See above for options.
Returns: The x-, y-, and z-components of the vector, in `to_axes` frame. Given as a tuple.
.. py:method:: add_force(Fx = 0, Fy = 0, Fz = 0, axes='wind')
Adds a force (in whichever axis system you choose) to this Dynamics instance.
:param Fx: Force in the x-direction in the axis system chosen. [N]
:param Fy: Force in the y-direction in the axis system chosen. [N]
:param Fz: Force in the z-direction in the axis system chosen. [N]
:param axes: The axis system that the specified force is in. One of:
* "geometry"
* "body"
* "wind"
* "stability"
* "earth"
Returns: None (in-place)
.. py:class:: DynamicsRigidBody2DBody(mass_props = None, x_e = 0, z_e = 0, u_b = 0, w_b = 0, theta = 0, q = 0)
Bases: :py:obj:`aerosandbox.dynamics.rigid_body.rigid_3D.body_euler.DynamicsRigidBody3DBodyEuler`
Dynamics instance:
* simulating a rigid body
* in 2D
* with velocity parameterized in body axes
State variables:
x_e: x-position, in Earth axes. [meters]
z_e: z-position, in Earth axes. [meters]
u_b: x-velocity, in body axes. [m/s]
w_b: z-velocity, in body axes. [m/s]
theta: pitch angle. [rad]
q: y-angular-velocity, in body axes. [rad/sec]
Control variables:
Fx_b: Force along the body-x axis. [N]
Fz_b: Force along the body-z axis. [N]
My_b: Moment about the body-y axis. [Nm]
.. py:attribute:: mass_props
For each state variable, self.state_var = state_var
For each indirect control variable, self.indirect_control_var = indirect_control_var
For each control variable, self.control_var = 0
.. py:attribute:: x_e
:value: 0
.. py:attribute:: y_e
:value: 0
.. py:attribute:: z_e
:value: 0
.. py:attribute:: u_b
:value: 0
.. py:attribute:: v_b
:value: 0
.. py:attribute:: w_b
:value: 0
.. py:attribute:: phi
:value: 0
.. py:attribute:: theta
:value: 0
.. py:attribute:: psi
:value: 0
.. py:attribute:: p
:value: 0
.. py:attribute:: q
:value: 0
.. py:attribute:: r
:value: 0
.. py:attribute:: Fx_b
:value: 0
.. py:attribute:: Fy_b
:value: 0
.. py:attribute:: Fz_b
:value: 0
.. py:attribute:: Mx_b
:value: 0
.. py:attribute:: My_b
:value: 0
.. py:attribute:: Mz_b
:value: 0
.. py:attribute:: hx_b
:value: 0
.. py:attribute:: hy_b
:value: 0
.. py:attribute:: hz_b
:value: 0
.. py:property:: state
Returns the state variables of this Dynamics instance as a Dict.
Keys are strings that give the name of the variables.
Values are the variables themselves.
This method should look something like:
>>> {
>>> "x_e": self.x_e,
>>> "u_e": self.u_e,
>>> ...
>>> }
.. py:property:: control_variables
.. py:method:: state_derivatives()
Computes the state derivatives (i.e. equations of motion) for a body in 3D space.
Based on Section 9.8.2 of Flight Vehicle Aerodynamics by Mark Drela.
:returns:
{
"xe" : d_xe,
"ye" : d_ye,
"ze" : d_ze,
"u" : d_u,
"v" : d_v,
"w" : d_w,
"phi" : d_phi,
"theta": d_theta,
"psi" : d_psi,
"p" : d_p,
"q" : d_q,
"r" : d_r,
}
:rtype: Time derivatives of each of the 12 state variables, given in a dictionary
.. py:class:: DynamicsRigidBody3DBodyEuler(mass_props = None, x_e = 0, y_e = 0, z_e = 0, u_b = 0, v_b = 0, w_b = 0, phi = 0, theta = 0, psi = 0, p = 0, q = 0, r = 0)
Bases: :py:obj:`aerosandbox.dynamics.rigid_body.common_rigid_body._DynamicsRigidBodyBaseClass`
Dynamics instance:
* simulating a rigid body
* in 3D
* with velocity parameterized in body axes
* and angle parameterized in Euler angles
State variables:
x_e: x-position, in Earth axes. [meters]
y_e: y-position, in Earth axes. [meters]
z_e: z-position, in Earth axes. [meters]
u_b: x-velocity, in body axes. [m/s]
v_b: y-velocity, in body axes. [m/s]
w_b: z-velocity, in body axes. [m/s]
phi: roll angle. Uses yaw-pitch-roll Euler angle convention. [rad]
theta: pitch angle. Uses yaw-pitch-roll Euler angle convention. [rad]
psi: yaw angle. Uses yaw-pitch-roll Euler angle convention. [rad]
p: x-angular-velocity, in body axes. [rad/sec]
q: y-angular-velocity, in body axes. [rad/sec]
r: z-angular-velocity, in body axes. [rad/sec]
Control variables:
Fx_b: Force along the body-x axis. [N]
Fy_b: Force along the body-y axis. [N]
Fz_b: Force along the body-z axis. [N]
Mx_b: Moment about the body-x axis. [Nm]
My_b: Moment about the body-y axis. [Nm]
Mz_b: Moment about the body-z axis. [Nm]
hx_b: Angular momentum (e.g., propellers) about the body-x axis. [kg*m^2/sec]
hy_b: Angular momentum (e.g., propellers) about the body-y axis. [kg*m^2/sec]
hz_b: Angular momentum (e.g., propellers) about the body-z axis. [kg*m^2/sec]
.. py:attribute:: mass_props
For each state variable, self.state_var = state_var
For each indirect control variable, self.indirect_control_var = indirect_control_var
For each control variable, self.control_var = 0
.. py:attribute:: x_e
:value: 0
.. py:attribute:: y_e
:value: 0
.. py:attribute:: z_e
:value: 0
.. py:attribute:: u_b
:value: 0
.. py:attribute:: v_b
:value: 0
.. py:attribute:: w_b
:value: 0
.. py:attribute:: phi
:value: 0
.. py:attribute:: theta
:value: 0
.. py:attribute:: psi
:value: 0
.. py:attribute:: p
:value: 0
.. py:attribute:: q
:value: 0
.. py:attribute:: r
:value: 0
.. py:attribute:: Fx_b
:value: 0
.. py:attribute:: Fy_b
:value: 0
.. py:attribute:: Fz_b
:value: 0
.. py:attribute:: Mx_b
:value: 0
.. py:attribute:: My_b
:value: 0
.. py:attribute:: Mz_b
:value: 0
.. py:attribute:: hx_b
:value: 0
.. py:attribute:: hy_b
:value: 0
.. py:attribute:: hz_b
:value: 0
.. py:property:: state
Returns the state variables of this Dynamics instance as a Dict.
Keys are strings that give the name of the variables.
Values are the variables themselves.
This method should look something like:
>>> {
>>> "x_e": self.x_e,
>>> "u_e": self.u_e,
>>> ...
>>> }
.. py:property:: control_variables
.. py:method:: state_derivatives()
Computes the state derivatives (i.e. equations of motion) for a body in 3D space.
Based on Section 9.8.2 of Flight Vehicle Aerodynamics by Mark Drela.
:returns:
{
"xe" : d_xe,
"ye" : d_ye,
"ze" : d_ze,
"u" : d_u,
"v" : d_v,
"w" : d_w,
"phi" : d_phi,
"theta": d_theta,
"psi" : d_psi,
"p" : d_p,
"q" : d_q,
"r" : d_r,
}
:rtype: Time derivatives of each of the 12 state variables, given in a dictionary
.. py:method:: convert_axes(x_from, y_from, z_from, from_axes, to_axes)
Converts a vector [x_from, y_from, z_from], as given in the `from_axes` frame, to an equivalent vector [x_to,
y_to, z_to], as given in the `to_axes` frame.
Identical to OperatingPoint.convert_axes(), but adds in "earth" as a valid axis frame. For more documentation,
see the docstring of OperatingPoint.convert_axes().
Both `from_axes` and `to_axes` should be a string, one of:
* "geometry"
* "body"
* "wind"
* "stability"
* "earth"
:param x_from: x-component of the vector, in `from_axes` frame.
:param y_from: y-component of the vector, in `from_axes` frame.
:param z_from: z-component of the vector, in `from_axes` frame.
:param from_axes: The axes to convert from.
:param to_axes: The axes to convert to.
Returns: The x-, y-, and z-components of the vector, in `to_axes` frame. Given as a tuple.
.. py:method:: add_force(Fx = 0, Fy = 0, Fz = 0, axes='body')
Adds a force (in whichever axis system you choose) to this Dynamics instance.
:param Fx: Force in the x-direction in the axis system chosen. [N]
:param Fy: Force in the y-direction in the axis system chosen. [N]
:param Fz: Force in the z-direction in the axis system chosen. [N]
:param axes: The axis system that the specified force is in. One of:
* "geometry"
* "body"
* "wind"
* "stability"
* "earth"
Returns: None (in-place)
.. py:method:: add_moment(Mx = 0, My = 0, Mz = 0, axes='body')
Adds a moment (in whichever axis system you choose) to this Dynamics instance.
:param Mx: Moment about the x-axis in the axis system chosen. Assumed these moments are applied about the center of mass. [Nm]
:param My: Moment about the y-axis in the axis system chosen. Assumed these moments are applied about the center of mass. [Nm]
:param Mz: Moment about the z-axis in the axis system chosen. Assumed these moments are applied about the center of mass. [Nm]
:param axes: The axis system that the specified moment is in. One of:
* "geometry"
* "body"
* "wind"
* "stability"
* "earth"
Returns: None (in-place)
.. py:property:: speed
The speed of the object, expressed as a scalar.
.. py:property:: alpha
The angle of attack, in degrees.
.. py:property:: beta
The sideslip angle, in degrees.
.. py:class:: AirfoilInviscid(airfoil, op_point, ground_effect = False)
Bases: :py:obj:`aerosandbox.common.ImplicitAnalysis`
An implicit analysis for inviscid analysis of an airfoil (or family of airfoils).
Key outputs:
* AirfoilInviscid.Cl
.. py:attribute:: op_point
.. py:attribute:: ground_effect
:value: False
.. py:method:: __repr__()
.. py:method:: _setup_unknowns()
.. py:method:: calculate_velocity(x_field, y_field)
.. py:method:: _enforce_governing_equations()
.. py:method:: _calculate_forces()
.. py:method:: draw_streamlines(res=200, show=True)
.. py:method:: draw_cp(show=True)
.. py:class:: XFoil(airfoil, Re = 0.0, mach = 0.0, n_crit = 9.0, xtr_upper = 1.0, xtr_lower = 1.0, hinge_point_x = 0.75, full_potential = False, max_iter = 100, xfoil_command = 'xfoil', xfoil_repanel = True, xfoil_repanel_n_points = 279, include_bl_data = False, verbose = False, timeout = 30, working_directory = None)
Bases: :py:obj:`aerosandbox.common.ExplicitAnalysis`
An interface to XFoil, a 2D airfoil analysis tool developed by Mark Drela at MIT.
Requires XFoil to be on your computer; XFoil is available here: https://web.mit.edu/drela/Public/web/xfoil/
It is recommended (but not required) that you add XFoil to your system PATH environment variable such that it can
be called with the command `xfoil`. If this is not the case, you need to specify the path to your XFoil
executable using the `xfoil_command` argument of the constructor.
Usage example:
>>> xf = XFoil(
>>> airfoil=Airfoil("naca2412").repanel(n_points_per_side=100),
>>> Re=1e6,
>>> )
>>>
>>> result_at_single_alpha = xf.alpha(5)
>>> result_at_several_CLs = xf.cl([0.5, 0.7, 0.8, 0.9])
>>> result_at_multiple_alphas = xf.alpha([3, 5, 60]) # Note: if a result does not converge (such as the 60 degree case here), it will not be included in the results.
.. py:exception:: XFoilError
Bases: :py:obj:`Exception`
Common base class for all non-exit exceptions.
.. py:attribute:: airfoil
.. py:attribute:: Re
:value: 0.0
.. py:attribute:: mach
:value: 0.0
.. py:attribute:: n_crit
:value: 9.0
.. py:attribute:: xtr_upper
:value: 1.0
.. py:attribute:: xtr_lower
:value: 1.0
.. py:attribute:: hinge_point_x
:value: 0.75
.. py:attribute:: full_potential
:value: False
.. py:attribute:: max_iter
:value: 100
.. py:attribute:: xfoil_command
:value: 'xfoil'
.. py:attribute:: xfoil_repanel
:value: True
.. py:attribute:: xfoil_repanel_n_points
:value: 279
.. py:attribute:: include_bl_data
:value: False
.. py:attribute:: verbose
:value: False
.. py:attribute:: timeout
:value: 30
.. py:method:: __repr__()
.. py:method:: _default_keystrokes(airfoil_filename, output_filename)
Returns a list of XFoil keystrokes that are common to all XFoil runs.
:returns: A list of strings, each of which is a single XFoil keystroke to be followed by .
.. py:method:: _run_xfoil(run_command, read_bl_data_from = None)
Private function to run XFoil.
Args: run_command: A string with any XFoil keystroke inputs that you'd like. By default, you start off within the OPER
menu. All of the inputs indicated in the constructor have been set already, but you can override them here (for
this run only) if you want.
Returns: A dictionary containing all converged solutions obtained with your inputs.
.. py:method:: open_interactive()
Opens a new terminal window and runs XFoil interactively. This is useful for detailed analysis or debugging.
Returns: None
.. py:method:: alpha(alpha, start_at = 0)
Execute XFoil at a given angle of attack, or at a sequence of angles of attack.
:param alpha: The angle of attack [degrees]. Can be either a float or an iterable of floats, such as an array.
:param start_at: Chooses whether to split a large sweep into two runs that diverge away from some central value,
:param to improve convergence. As an example:
:param if you wanted to sweep from alpha=-20 to alpha=20:
:param you might want:
:param to instead do two sweeps and stitch them together: 0 to 20, and 0 to -20. `start_at` can be either:
* None, in which case the alpha inputs are run as a single sequence in the order given.
* A float that corresponds to an angle of attack (in degrees), in which case the alpha inputs are
split into two sequences that diverge from the `start_at` value. Successful runs are then sorted by
`alpha` before returning.
Returns: A dictionary with the XFoil results. Dictionary values are arrays; they may not be the same shape as
your input array if some points did not converge.
.. py:method:: cl(cl, start_at = 0)
Execute XFoil at a given lift coefficient, or at a sequence of lift coefficients.
:param cl: The lift coefficient [-]. Can be either a float or an iterable of floats, such as an array.
:param start_at: Chooses whether to split a large sweep into two runs that diverge away from some central value,
:param to improve convergence. As an example:
:param if you wanted to sweep from cl=-1.5 to cl=1.5:
:param you might want to:
:param instead do two sweeps and stitch them together: 0 to 1.5, and 0 to -1.5. `start_at` can be either:
* None, in which case the cl inputs are run as a single sequence in the order given.
* A float that corresponds to an lift coefficient, in which case the cl inputs are
split into two sequences that diverge from the `start_at` value. Successful runs are then sorted by
`alpha` before returning.
Returns: A dictionary with the XFoil results. Dictionary values are arrays; they may not be the same shape as
your input array if some points did not converge.
.. py:class:: MSES(airfoil, n_crit = 9.0, xtr_upper = 1.0, xtr_lower = 1.0, max_iter = 100, mset_command = 'mset', mses_command = 'mses', mplot_command = 'mplot', use_xvfb = None, xvfb_command = 'xvfb-run -a', verbosity = 1, timeout_mset = 10, timeout_mses = 60, timeout_mplot = 10, working_directory = None, behavior_after_unconverged_run = 'reinitialize', mset_alpha = 0, mset_n = 141, mset_e = 0.4, mset_io = 37, mset_x = 0.85, mses_mcrit = 0.99, mses_mucon = -1.0)
Bases: :py:obj:`aerosandbox.common.ExplicitAnalysis`
An interface to MSES, MSET, and MPLOT, a 2D airfoil analysis system developed by Mark Drela at MIT.
Requires compiled binaries for all the programs to be on your computer;
MSES is available here: https://web.mit.edu/drela/Public/web/mses/
Academics can get a copy by emailing the MIT Tech. Licensing Office;
MIT affiliates can find a copy on Athena.
It is recommended (but not required) that you add MSES, MSET, and MPLOT to your system PATH environment variable
such that they can be called with the commands `mses`, `mset`, and `mplot`. If this is not the case, you need to
specify the path to these executables using the command arguments of the constructor.
-----
X11 Notes:
Note that MSES, MSET, and MPLOT by default open up X11 windows on your computer. If you prefer that this doesn't
happen (for extra speed), or if you cannot have this happen (e.g., you are computing in an environment without
proper X11 support, like Windows Subsystem for Linux), you should use XVFB. https://en.wikipedia.org/wiki/Xvfb
XVFB is a virtual "display" server that can receive X11 output and safely dump it. (If you don't use XVFB and you
don't have proper X11 support on your computer, this AeroSandbox MSES module will simply error out during the
MSET call - probably not what you want.)
To install XVFB on a Linux machine, use:
```bash
sudo apt-get install xvfb
```
Then, when instantiating this MSES instance in AeroSandbox, pass the `use_xvfb` flag to be True. Default behavior
here is that this class will look for the XVFB executable, `xvfb-run`, on your machine. If it finds it,
it will run with XVFB enabled. If it does not, it will run without XVFB.
-----
Usage example:
>>> ms = MSES(
>>> airfoil=Airfoil("naca2412").repanel(n_points_per_side=100),
>>> Re=1e6,
>>> mach=0.2,
>>> )
>>>
>>> result_at_single_alpha = ms.alpha(5)
>>> #result_at_several_CLs = ms.cl([0.5, 0.7, 0.8, 0.9])
>>> result_at_multiple_alphas = ms.alpha([3, 5, 60]) # Note: if a result does not converge (such as the 60 degree case here), it will not be included in the results.
.. py:attribute:: airfoil
.. py:attribute:: n_crit
:value: 9.0
.. py:attribute:: xtr_upper
:value: 1.0
.. py:attribute:: xtr_lower
:value: 1.0
.. py:attribute:: max_iter
:value: 100
.. py:attribute:: mset_command
:value: 'mset'
.. py:attribute:: mses_command
:value: 'mses'
.. py:attribute:: mplot_command
:value: 'mplot'
.. py:attribute:: use_xvfb
:value: None
.. py:attribute:: xvfb_command
:value: 'xvfb-run -a'
.. py:attribute:: verbosity
:value: 1
.. py:attribute:: timeout_mses
:value: 60
.. py:attribute:: timeout_mset
:value: 10
.. py:attribute:: timeout_mplot
:value: 10
.. py:attribute:: working_directory
:value: None
.. py:attribute:: behavior_after_unconverged_run
:value: 'reinitialize'
.. py:attribute:: mset_alpha
:value: 0
.. py:attribute:: mset_n
:value: 141
.. py:attribute:: mset_e
:value: 0.4
.. py:attribute:: mset_io
:value: 37
.. py:attribute:: mset_x
:value: 0.85
.. py:attribute:: mses_mcrit
:value: 0.99
.. py:attribute:: mses_mucon
:value: -1.0
.. py:method:: run(alpha = 0.0, Re = 0.0, mach = 0.01)
.. py:class:: VortexLatticeMethod(airplane, op_point, xyz_ref = None, run_symmetric_if_possible = False, verbose = False, spanwise_resolution = 10, spanwise_spacing_function = np.cosspace, chordwise_resolution = 10, chordwise_spacing_function = np.cosspace, vortex_core_radius = 1e-08, align_trailing_vortices_with_wind = False)
Bases: :py:obj:`aerosandbox.ExplicitAnalysis`
An explicit (linear) vortex-lattice-method aerodynamics analysis.
Usage example:
>>> analysis = asb.VortexLatticeMethod(
>>> airplane=my_airplane,
>>> op_point=asb.OperatingPoint(
>>> velocity=100, # m/s
>>> alpha=5, # deg
>>> beta=4, # deg
>>> p=0.01, # rad/sec
>>> q=0.02, # rad/sec
>>> r=0.03, # rad/sec
>>> )
>>> )
>>> aero_data = analysis.run()
>>> analysis.draw()
.. py:attribute:: airplane
.. py:attribute:: op_point
.. py:attribute:: xyz_ref
:value: None
.. py:attribute:: verbose
:value: False
.. py:attribute:: spanwise_resolution
:value: 10
.. py:attribute:: spanwise_spacing_function
.. py:attribute:: chordwise_resolution
:value: 10
.. py:attribute:: chordwise_spacing_function
.. py:attribute:: vortex_core_radius
:value: 1e-08
.. py:attribute:: align_trailing_vortices_with_wind
:value: False
.. py:attribute:: run_symmetric
:value: False
.. py:method:: __repr__()
.. py:method:: run()
Computes the aerodynamic forces.
Returns a dictionary with keys:
- 'F_g' : an [x, y, z] list of forces in geometry axes [N]
- 'F_b' : an [x, y, z] list of forces in body axes [N]
- 'F_w' : an [x, y, z] list of forces in wind axes [N]
- 'M_g' : an [x, y, z] list of moments about geometry axes [Nm]
- 'M_b' : an [x, y, z] list of moments about body axes [Nm]
- 'M_w' : an [x, y, z] list of moments about wind axes [Nm]
- 'L' : the lift force [N]. Definitionally, this is in wind axes.
- 'Y' : the side force [N]. This is in wind axes.
- 'D' : the drag force [N]. Definitionally, this is in wind axes.
- 'l_b', the rolling moment, in body axes [Nm]. Positive is roll-right.
- 'm_b', the pitching moment, in body axes [Nm]. Positive is pitch-up.
- 'n_b', the yawing moment, in body axes [Nm]. Positive is nose-right.
- 'CL', the lift coefficient [-]. Definitionally, this is in wind axes.
- 'CY', the sideforce coefficient [-]. This is in wind axes.
- 'CD', the drag coefficient [-]. Definitionally, this is in wind axes.
- 'Cl', the rolling coefficient [-], in body axes
- 'Cm', the pitching coefficient [-], in body axes
- 'Cn', the yawing coefficient [-], in body axes
Nondimensional values are nondimensionalized using reference values in the VortexLatticeMethod.airplane object.
.. py:method:: run_with_stability_derivatives(alpha=True, beta=True, p=True, q=True, r=True)
Computes the aerodynamic forces and moments on the airplane, and the stability derivatives.
Arguments essentially determine which stability derivatives are computed. If a stability derivative is not
needed, leaving it False will speed up the computation.
:param - alpha: If True, compute the stability derivatives with respect to the angle of attack (alpha).
:type - alpha: bool
:param - beta: If True, compute the stability derivatives with respect to the sideslip angle (beta).
:type - beta: bool
:param - p: If True, compute the stability derivatives with respect to the body-axis roll rate (p).
:type - p: bool
:param - q: If True, compute the stability derivatives with respect to the body-axis pitch rate (q).
:type - q: bool
:param - r: If True, compute the stability derivatives with respect to the body-axis yaw rate (r).
:type - r: bool
Returns: a dictionary with keys:
- 'F_g' : an [x, y, z] list of forces in geometry axes [N]
- 'F_b' : an [x, y, z] list of forces in body axes [N]
- 'F_w' : an [x, y, z] list of forces in wind axes [N]
- 'M_g' : an [x, y, z] list of moments about geometry axes [Nm]
- 'M_b' : an [x, y, z] list of moments about body axes [Nm]
- 'M_w' : an [x, y, z] list of moments about wind axes [Nm]
- 'L' : the lift force [N]. Definitionally, this is in wind axes.
- 'Y' : the side force [N]. This is in wind axes.
- 'D' : the drag force [N]. Definitionally, this is in wind axes.
- 'l_b', the rolling moment, in body axes [Nm]. Positive is roll-right.
- 'm_b', the pitching moment, in body axes [Nm]. Positive is pitch-up.
- 'n_b', the yawing moment, in body axes [Nm]. Positive is nose-right.
- 'CL', the lift coefficient [-]. Definitionally, this is in wind axes.
- 'CY', the sideforce coefficient [-]. This is in wind axes.
- 'CD', the drag coefficient [-]. Definitionally, this is in wind axes.
- 'Cl', the rolling coefficient [-], in body axes
- 'Cm', the pitching coefficient [-], in body axes
- 'Cn', the yawing coefficient [-], in body axes
Along with additional keys, depending on the value of the `alpha`, `beta`, `p`, `q`, and `r` arguments. For
example, if `alpha=True`, then the following additional keys will be present:
- 'CLa', the lift coefficient derivative with respect to alpha [1/rad]
- 'CDa', the drag coefficient derivative with respect to alpha [1/rad]
- 'CYa', the sideforce coefficient derivative with respect to alpha [1/rad]
- 'Cla', the rolling moment coefficient derivative with respect to alpha [1/rad]
- 'Cma', the pitching moment coefficient derivative with respect to alpha [1/rad]
- 'Cna', the yawing moment coefficient derivative with respect to alpha [1/rad]
- 'x_np', the neutral point location in the x direction [m]
Nondimensional values are nondimensionalized using reference values in the
VortexLatticeMethod.airplane object.
Data types:
- The "L", "Y", "D", "l_b", "m_b", "n_b", "CL", "CY", "CD", "Cl", "Cm", and "Cn" keys are:
- floats if the OperatingPoint object is not vectorized (i.e., if all attributes of OperatingPoint
are floats, not arrays).
- arrays if the OperatingPoint object is vectorized (i.e., if any attribute of OperatingPoint is an
array).
- The "F_g", "F_b", "F_w", "M_g", "M_b", and "M_w" keys are always lists, which will contain either
floats or arrays, again depending on whether the OperatingPoint object is vectorized or not.
.. py:method:: get_induced_velocity_at_points(points)
Computes the induced velocity at a set of points in the flowfield.
:param points: A Nx3 array of points that you would like to know the induced velocities at. Given in geometry axes.
Returns: A Nx3 of the induced velocity at those points. Given in geometry axes.
.. py:method:: get_velocity_at_points(points)
Computes the velocity at a set of points in the flowfield.
:param points: A Nx3 array of points that you would like to know the velocities at. Given in geometry axes.
Returns: A Nx3 of the velocity at those points. Given in geometry axes.
.. py:method:: calculate_streamlines(seed_points = None, n_steps = 300, length = None)
Computes streamlines, starting at specific seed points.
After running this function, a new instance variable `VortexLatticeFilaments.streamlines` is computed
Uses simple forward-Euler integration with a fixed spatial stepsize (i.e., velocity vectors are normalized
before ODE integration). After investigation, it's not worth doing fancier ODE integration methods (adaptive
schemes, RK substepping, etc.), due to the near-singular conditions near vortex filaments.
:param seed_points: A Nx3 ndarray that contains a list of points where streamlines are started. Will be
:param auto-calculated if not specified.:
:param n_steps: The number of individual streamline steps to trace. Minimum of 2.
:param length: The approximate total length of the streamlines desired, in meters. Will be auto-calculated if
:param not specified.:
:returns: a 3D array with dimensions: (n_seed_points) x (3) x (n_steps).
Consists of streamlines data.
Result is also saved as an instance variable, VortexLatticeMethod.streamlines.
:rtype: streamlines
.. py:method:: draw(c = None, cmap = None, colorbar_label = None, show = True, show_kwargs = None, draw_streamlines=True, recalculate_streamlines=False, backend = 'pyvista')
Draws the solution. Note: Must be called on a SOLVED AeroProblem object.
To solve an AeroProblem, use opti.solve(). To substitute a solved solution, use ap = sol(ap).
:return:
.. py:class:: LiftingLine(airplane, op_point, xyz_ref = None, model_size = 'medium', run_symmetric_if_possible = False, verbose = False, spanwise_resolution = 4, spanwise_spacing_function = np.cosspace, vortex_core_radius = 1e-08, align_trailing_vortices_with_wind = False)
Bases: :py:obj:`aerosandbox.ExplicitAnalysis`
An implicit aerodynamics analysis based on lifting line theory, with modifications for nonzero sweep
and dihedral + multiple wings.
Nonlinear, and includes viscous effects based on 2D data.
Usage example:
>>> analysis = asb.LiftingLine(
>>> airplane=my_airplane,
>>> op_point=asb.OperatingPoint(
>>> velocity=100, # m/s
>>> alpha=5, # deg
>>> beta=4, # deg
>>> p=0.01, # rad/sec
>>> q=0.02, # rad/sec
>>> r=0.03, # rad/sec
>>> )
>>> )
>>> outputs = analysis.run()
.. py:attribute:: airplane
.. py:attribute:: op_point
.. py:attribute:: xyz_ref
:value: None
.. py:attribute:: model_size
:value: 'medium'
.. py:attribute:: verbose
:value: False
.. py:attribute:: spanwise_resolution
:value: 4
.. py:attribute:: spanwise_spacing_function
.. py:attribute:: vortex_core_radius
:value: 1e-08
.. py:attribute:: align_trailing_vortices_with_wind
:value: False
.. py:attribute:: run_symmetric
:value: False
.. py:method:: __repr__()
.. py:class:: AeroComponentResults
.. py:attribute:: s_ref
:type: float
.. py:attribute:: c_ref
:type: float
.. py:attribute:: b_ref
:type: float
.. py:attribute:: op_point
:type: aerosandbox.performance.OperatingPoint
.. py:attribute:: F_g
:type: List[Union[float, aerosandbox.geometry.np.ndarray]]
.. py:attribute:: M_g
:type: List[Union[float, aerosandbox.geometry.np.ndarray]]
.. py:method:: __repr__()
.. py:property:: F_b
:type: List[Union[float, aerosandbox.geometry.np.ndarray]]
An [x, y, z] list of forces in body axes [N]
.. py:property:: F_w
:type: List[Union[float, aerosandbox.geometry.np.ndarray]]
An [x, y, z] list of forces in wind axes [N]
.. py:property:: M_b
:type: List[Union[float, aerosandbox.geometry.np.ndarray]]
An [x, y, z] list of moments about body axes [Nm]
.. py:property:: M_w
:type: List[Union[float, aerosandbox.geometry.np.ndarray]]
An [x, y, z] list of moments about wind axes [Nm]
.. py:property:: L
:type: Union[float, aerosandbox.geometry.np.ndarray]
The lift force [N]. Definitionally, this is in wind axes.
.. py:property:: Y
:type: Union[float, aerosandbox.geometry.np.ndarray]
The side force [N]. Definitionally, this is in wind axes.
.. py:property:: D
:type: Union[float, aerosandbox.geometry.np.ndarray]
The drag force [N]. Definitionally, this is in wind axes.
.. py:property:: l_b
:type: Union[float, aerosandbox.geometry.np.ndarray]
The rolling moment [Nm] in body axes. Positive is roll-right.
.. py:property:: m_b
:type: Union[float, aerosandbox.geometry.np.ndarray]
The pitching moment [Nm] in body axes. Positive is nose-up.
.. py:property:: n_b
:type: Union[float, aerosandbox.geometry.np.ndarray]
The yawing moment [Nm] in body axes. Positive is nose-right.
.. py:method:: run()
Computes the aerodynamic forces.
Returns a dictionary with keys:
- 'F_g' : an [x, y, z] list of forces in geometry axes [N]
- 'F_b' : an [x, y, z] list of forces in body axes [N]
- 'F_w' : an [x, y, z] list of forces in wind axes [N]
- 'M_g' : an [x, y, z] list of moments about geometry axes [Nm]
- 'M_b' : an [x, y, z] list of moments about body axes [Nm]
- 'M_w' : an [x, y, z] list of moments about wind axes [Nm]
- 'L' : the lift force [N]. Definitionally, this is in wind axes.
- 'Y' : the side force [N]. This is in wind axes.
- 'D' : the drag force [N]. Definitionally, this is in wind axes.
- 'l_b', the rolling moment, in body axes [Nm]. Positive is roll-right.
- 'm_b', the pitching moment, in body axes [Nm]. Positive is pitch-up.
- 'n_b', the yawing moment, in body axes [Nm]. Positive is nose-right.
- 'CL', the lift coefficient [-]. Definitionally, this is in wind axes.
- 'CY', the sideforce coefficient [-]. This is in wind axes.
- 'CD', the drag coefficient [-]. Definitionally, this is in wind axes.
- 'Cl', the rolling coefficient [-], in body axes
- 'Cm', the pitching coefficient [-], in body axes
- 'Cn', the yawing coefficient [-], in body axes
Nondimensional values are nondimensionalized using reference values in the LiftingLine.airplane object.
Data types:
- The "L", "Y", "D", "l_b", "m_b", "n_b", "CL", "CY", "CD", "Cl", "Cm", and "Cn" keys are:
- floats if the OperatingPoint object is not vectorized (i.e., if all attributes of OperatingPoint
are floats, not arrays).
- arrays if the OperatingPoint object is vectorized (i.e., if any attribute of OperatingPoint is an
array).
- The "F_g", "F_b", "F_w", "M_g", "M_b", and "M_w" keys are always lists, which will contain either
floats or arrays, again depending on whether the OperatingPoint object is vectorized or not.
.. py:method:: run_with_stability_derivatives(alpha=True, beta=True, p=True, q=True, r=True)
Computes the aerodynamic forces and moments on the airplane, and the stability derivatives.
Arguments essentially determine which stability derivatives are computed. If a stability derivative is not
needed, leaving it False will speed up the computation.
:param - alpha: If True, compute the stability derivatives with respect to the angle of attack (alpha).
:type - alpha: bool
:param - beta: If True, compute the stability derivatives with respect to the sideslip angle (beta).
:type - beta: bool
:param - p: If True, compute the stability derivatives with respect to the body-axis roll rate (p).
:type - p: bool
:param - q: If True, compute the stability derivatives with respect to the body-axis pitch rate (q).
:type - q: bool
:param - r: If True, compute the stability derivatives with respect to the body-axis yaw rate (r).
:type - r: bool
Returns: a dictionary with keys:
- 'F_g' : an [x, y, z] list of forces in geometry axes [N]
- 'F_b' : an [x, y, z] list of forces in body axes [N]
- 'F_w' : an [x, y, z] list of forces in wind axes [N]
- 'M_g' : an [x, y, z] list of moments about geometry axes [Nm]
- 'M_b' : an [x, y, z] list of moments about body axes [Nm]
- 'M_w' : an [x, y, z] list of moments about wind axes [Nm]
- 'L' : the lift force [N]. Definitionally, this is in wind axes.
- 'Y' : the side force [N]. This is in wind axes.
- 'D' : the drag force [N]. Definitionally, this is in wind axes.
- 'l_b' : the rolling moment, in body axes [Nm]. Positive is roll-right.
- 'm_b' : the pitching moment, in body axes [Nm]. Positive is pitch-up.
- 'n_b' : the yawing moment, in body axes [Nm]. Positive is nose-right.
- 'CL' : the lift coefficient [-]. Definitionally, this is in wind axes.
- 'CY' : the sideforce coefficient [-]. This is in wind axes.
- 'CD' : the drag coefficient [-]. Definitionally, this is in wind axes.
- 'Cl' : the rolling coefficient [-], in body axes
- 'Cm' : the pitching coefficient [-], in body axes
- 'Cn' : the yawing coefficient [-], in body axes
Along with additional keys, depending on the value of the `alpha`, `beta`, `p`, `q`, and `r` arguments. For
example, if `alpha=True`, then the following additional keys will be present:
- 'CLa' : the lift coefficient derivative with respect to alpha [1/rad]
- 'CDa' : the drag coefficient derivative with respect to alpha [1/rad]
- 'CYa' : the sideforce coefficient derivative with respect to alpha [1/rad]
- 'Cla' : the rolling moment coefficient derivative with respect to alpha [1/rad]
- 'Cma' : the pitching moment coefficient derivative with respect to alpha [1/rad]
- 'Cna' : the yawing moment coefficient derivative with respect to alpha [1/rad]
- 'x_np': the neutral point location in the x direction [m]
Nondimensional values are nondimensionalized using reference values in the AeroBuildup.airplane object.
Data types:
- The "L", "Y", "D", "l_b", "m_b", "n_b", "CL", "CY", "CD", "Cl", "Cm", and "Cn" keys are:
- floats if the OperatingPoint object is not vectorized (i.e., if all attributes of OperatingPoint
are floats, not arrays).
- arrays if the OperatingPoint object is vectorized (i.e., if any attribute of OperatingPoint is an
array).
- The "F_g", "F_b", "F_w", "M_g", "M_b", and "M_w" keys are always lists, which will contain either
floats or arrays, again depending on whether the OperatingPoint object is vectorized or not.
.. py:method:: wing_aerodynamics()
.. py:method:: get_induced_velocity_at_points(points, vortex_strengths = None)
Computes the induced velocity at a set of points in the flowfield.
:param points: A Nx3 array of points that you would like to know the induced velocities at. Given in geometry axes.
Returns: A Nx3 of the induced velocity at those points. Given in geometry axes.
.. py:method:: get_velocity_at_points(points, vortex_strengths = None)
Computes the velocity at a set of points in the flowfield.
:param points: A Nx3 array of points that you would like to know the velocities at. Given in geometry axes.
Returns: A Nx3 of the velocity at those points. Given in geometry axes.
.. py:method:: calculate_fuselage_influences(points)
.. py:method:: calculate_streamlines(seed_points = None, n_steps = 300, length = None)
Computes streamlines, starting at specific seed points.
After running this function, a new instance variable `VortexLatticeFilaments.streamlines` is computed
Uses simple forward-Euler integration with a fixed spatial stepsize (i.e., velocity vectors are normalized
before ODE integration). After investigation, it's not worth doing fancier ODE integration methods (adaptive
schemes, RK substepping, etc.), due to the near-singular conditions near vortex filaments.
:param seed_points: A Nx3 ndarray that contains a list of points where streamlines are started. Will be
:param auto-calculated if not specified.:
:param n_steps: The number of individual streamline steps to trace. Minimum of 2.
:param length: The approximate total length of the streamlines desired, in meters. Will be auto-calculated if
:param not specified.:
:returns: a 3D array with dimensions: (n_seed_points) x (3) x (n_steps).
Consists of streamlines data.
Result is also saved as an instance variable, VortexLatticeMethod.streamlines.
:rtype: streamlines
.. py:method:: draw(c = None, cmap = None, colorbar_label = None, show = True, show_kwargs = None, draw_streamlines=True, recalculate_streamlines=False, backend = 'pyvista')
Draws the solution. Note: Must be called on a SOLVED AeroProblem object.
To solve an AeroProblem, use opti.solve(). To substitute a solved solution, use ap = sol(ap).
:return:
.. py:class:: NonlinearLiftingLine(airplane, op_point, xyz_ref = None, run_symmetric_if_possible = False, verbose = False, spanwise_resolution=8, spanwise_spacing_function = np.cosspace, vortex_core_radius = 1e-08, align_trailing_vortices_with_wind = False)
Bases: :py:obj:`aerosandbox.ImplicitAnalysis`
An implicit aerodynamics analysis based on lifting line theory, with modifications for nonzero sweep
and dihedral + multiple wings.
Nonlinear, and includes viscous effects based on 2D data.
Usage example:
>>> analysis = asb.NonlinearLiftingLine(
>>> airplane=my_airplane,
>>> op_point=asb.OperatingPoint(
>>> velocity=100, # m/s
>>> alpha=5, # deg
>>> beta=4, # deg
>>> p=0.01, # rad/sec
>>> q=0.02, # rad/sec
>>> r=0.03, # rad/sec
>>> )
>>> )
>>> outputs = analysis.run()
.. py:attribute:: airplane
.. py:attribute:: op_point
.. py:attribute:: xyz_ref
:value: None
.. py:attribute:: verbose
:value: False
.. py:attribute:: spanwise_resolution
:value: 8
.. py:attribute:: spanwise_spacing_function
.. py:attribute:: vortex_core_radius
:value: 1e-08
.. py:attribute:: align_trailing_vortices_with_wind
:value: False
.. py:attribute:: run_symmetric
:value: False
.. py:method:: __repr__()
.. py:method:: run(solve = True)
Computes the aerodynamic forces.
Returns a dictionary with keys:
- 'residuals': a list of residuals for each horseshoe element
- 'F_g' : an [x, y, z] list of forces in geometry axes [N]
- 'F_b' : an [x, y, z] list of forces in body axes [N]
- 'F_w' : an [x, y, z] list of forces in wind axes [N]
- 'M_g' : an [x, y, z] list of moments about geometry axes [Nm]
- 'M_b' : an [x, y, z] list of moments about body axes [Nm]
- 'M_w' : an [x, y, z] list of moments about wind axes [Nm]
- 'L' : the lift force [N]. Definitionally, this is in wind axes.
- 'Y' : the side force [N]. This is in wind axes.
- 'D' : the drag force [N]. Definitionally, this is in wind axes.
- 'l_b', the rolling moment, in body axes [Nm]. Positive is roll-right.
- 'm_b', the pitching moment, in body axes [Nm]. Positive is pitch-up.
- 'n_b', the yawing moment, in body axes [Nm]. Positive is nose-right.
- 'CL', the lift coefficient [-]. Definitionally, this is in wind axes.
- 'CY', the sideforce coefficient [-]. This is in wind axes.
- 'CD', the drag coefficient [-]. Definitionally, this is in wind axes.
- 'CDi' the induced drag coefficient
- 'CDp' the profile drag coefficient
- 'Cl', the rolling coefficient [-], in body axes
- 'Cm', the pitching coefficient [-], in body axes
- 'Cn', the yawing coefficient [-], in body axes
Nondimensional values are nondimensionalized using reference values in the VortexLatticeMethod.airplane object.
.. py:method:: get_induced_velocity_at_points(points, vortex_strengths = None)
Computes the induced velocity at a set of points in the flowfield.
:param points: A Nx3 array of points that you would like to know the induced velocities at. Given in geometry axes.
Returns: A Nx3 of the induced velocity at those points. Given in geometry axes.
.. py:method:: get_velocity_at_points(points, vortex_strengths = None)
Computes the velocity at a set of points in the flowfield.
:param points: A Nx3 array of points that you would like to know the velocities at. Given in geometry axes.
Returns: A Nx3 of the velocity at those points. Given in geometry axes.
.. py:method:: calculate_fuselage_influences(points)
.. py:method:: calculate_streamlines(seed_points = None, n_steps = 300, length = None)
Computes streamlines, starting at specific seed points.
After running this function, a new instance variable `VortexLatticeFilaments.streamlines` is computed
Uses simple forward-Euler integration with a fixed spatial stepsize (i.e., velocity vectors are normalized
before ODE integration). After investigation, it's not worth doing fancier ODE integration methods (adaptive
schemes, RK substepping, etc.), due to the near-singular conditions near vortex filaments.
:param seed_points: A Nx3 ndarray that contains a list of points where streamlines are started. Will be
:param auto-calculated if not specified.:
:param n_steps: The number of individual streamline steps to trace. Minimum of 2.
:param length: The approximate total length of the streamlines desired, in meters. Will be auto-calculated if
:param not specified.:
:returns: a 3D array with dimensions: (n_seed_points) x (3) x (n_steps).
Consists of streamlines data.
Result is also saved as an instance variable, VortexLatticeMethod.streamlines.
:rtype: streamlines
.. py:method:: draw(c = None, cmap = None, colorbar_label = None, show = True, show_kwargs = None, draw_streamlines=True, recalculate_streamlines=False, backend = 'pyvista')
Draws the solution. Note: Must be called on a SOLVED AeroProblem object.
To solve an AeroProblem, use opti.solve(). To substitute a solved solution, use ap = sol(ap).
:return:
.. py:class:: AeroBuildup(airplane, op_point, xyz_ref = None, model_size = 'small', include_wave_drag = True)
Bases: :py:obj:`aerosandbox.ExplicitAnalysis`
A workbook-style aerodynamics buildup.
Example usage:
>>> import aerosandbox as asb
>>> ab = asb.AeroBuildup( # This sets up the analysis, but doesn't execute calculation
>>> airplane=my_airplane, # type: asb.Airplane
>>> op_point=my_operating_point, # type: asb.OperatingPoint
>>> xyz_ref=[0.1, 0.2, 0.3], # Moment reference and center of rotation.
>>> )
>>> aero = ab.run() # This executes the actual aero analysis.
>>> aero_with_stability_derivs = ab.run_with_stability_derivatives() # Same, but also gets stability derivatives.
.. py:attribute:: default_analysis_specific_options
This is part of AeroSandbox's "analysis-specific options" feature, which lets you "tag" geometry objects with
flags that change how different analyses act on them.
This variable, `default_analysis_specific_options`, allows you to specify default values for options that can be used for
specific problems.
This should be a dictionary, where: * keys are the geometry-like types that you might be interested in defining
parameters for. * values are dictionaries, where: * keys are strings that label a given option * values are
anything. These are used as the default values, in the event that the associated geometry doesn't override those.
An example of what this variable might look like, for a vortex-lattice method aerodynamic analysis:
>>> default_analysis_specific_options = {
>>> Airplane: dict(
>>> profile_drag_coefficient=0
>>> ),
>>> Wing : dict(
>>> wing_level_spanwise_spacing=True,
>>> spanwise_resolution=12,
>>> spanwise_spacing="cosine",
>>> chordwise_resolution=12,
>>> chordwise_spacing="cosine",
>>> component=None, # type: int
>>> no_wake=False,
>>> no_alpha_beta=False,
>>> no_load=False,
>>> drag_polar=dict(
>>> CL1=0,
>>> CD1=0,
>>> CL2=0,
>>> CD2=0,
>>> CL3=0,
>>> CD3=0,
>>> ),
>>> )
>>> }
.. py:attribute:: airplane
.. py:attribute:: op_point
.. py:attribute:: xyz_ref
:value: None
.. py:attribute:: model_size
:value: 'small'
.. py:attribute:: include_wave_drag
:value: True
.. py:method:: __repr__()
.. py:class:: AeroComponentResults
.. py:attribute:: s_ref
:type: float
.. py:attribute:: c_ref
:type: float
.. py:attribute:: b_ref
:type: float
.. py:attribute:: op_point
:type: aerosandbox.performance.OperatingPoint
.. py:attribute:: F_g
:type: List[Union[float, aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.fuselage_aerodynamics_utilities.np.ndarray]]
.. py:attribute:: M_g
:type: List[Union[float, aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.fuselage_aerodynamics_utilities.np.ndarray]]
.. py:attribute:: span_effective
:type: float
.. py:attribute:: oswalds_efficiency
:type: float
.. py:method:: __repr__()
.. py:property:: F_b
:type: List[Union[float, aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.fuselage_aerodynamics_utilities.np.ndarray]]
An [x, y, z] list of forces in body axes [N]
.. py:property:: F_w
:type: List[Union[float, aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.fuselage_aerodynamics_utilities.np.ndarray]]
An [x, y, z] list of forces in wind axes [N]
.. py:property:: M_b
:type: List[Union[float, aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.fuselage_aerodynamics_utilities.np.ndarray]]
An [x, y, z] list of moments about body axes [Nm]
.. py:property:: M_w
:type: List[Union[float, aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.fuselage_aerodynamics_utilities.np.ndarray]]
An [x, y, z] list of moments about wind axes [Nm]
.. py:property:: L
:type: Union[float, aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.fuselage_aerodynamics_utilities.np.ndarray]
The lift force [N]. Definitionally, this is in wind axes.
.. py:property:: Y
:type: Union[float, aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.fuselage_aerodynamics_utilities.np.ndarray]
The side force [N]. Definitionally, this is in wind axes.
.. py:property:: D
:type: Union[float, aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.fuselage_aerodynamics_utilities.np.ndarray]
The drag force [N]. Definitionally, this is in wind axes.
.. py:property:: l_b
:type: Union[float, aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.fuselage_aerodynamics_utilities.np.ndarray]
The rolling moment [Nm] in body axes. Positive is roll-right.
.. py:property:: m_b
:type: Union[float, aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.fuselage_aerodynamics_utilities.np.ndarray]
The pitching moment [Nm] in body axes. Positive is nose-up.
.. py:property:: n_b
:type: Union[float, aerosandbox.aerodynamics.aero_3D.aero_buildup_submodels.fuselage_aerodynamics_utilities.np.ndarray]
The yawing moment [Nm] in body axes. Positive is nose-right.
.. py:method:: run()
Computes the aerodynamic forces and moments on the airplane.
Returns: a dictionary with keys:
- 'F_g' : an [x, y, z] list of forces in geometry axes [N]
- 'F_b' : an [x, y, z] list of forces in body axes [N]
- 'F_w' : an [x, y, z] list of forces in wind axes [N]
- 'M_g' : an [x, y, z] list of moments about geometry axes [Nm]
- 'M_b' : an [x, y, z] list of moments about body axes [Nm]
- 'M_w' : an [x, y, z] list of moments about wind axes [Nm]
- 'L' : the lift force [N]. Definitionally, this is in wind axes.
- 'Y' : the side force [N]. This is in wind axes.
- 'D' : the drag force [N]. Definitionally, this is in wind axes.
- 'l_b', the rolling moment, in body axes [Nm]. Positive is roll-right.
- 'm_b', the pitching moment, in body axes [Nm]. Positive is pitch-up.
- 'n_b', the yawing moment, in body axes [Nm]. Positive is nose-right.
- 'CL', the lift coefficient [-]. Definitionally, this is in wind axes.
- 'CY', the sideforce coefficient [-]. This is in wind axes.
- 'CD', the drag coefficient [-]. Definitionally, this is in wind axes.
- 'Cl', the rolling coefficient [-], in body axes
- 'Cm', the pitching coefficient [-], in body axes
- 'Cn', the yawing coefficient [-], in body axes
Nondimensional values are nondimensionalized using reference values in the AeroBuildup.airplane object.
Data types:
- The "L", "Y", "D", "l_b", "m_b", "n_b", "CL", "CY", "CD", "Cl", "Cm", and "Cn" keys are:
- floats if the OperatingPoint object is not vectorized (i.e., if all attributes of OperatingPoint
are floats, not arrays).
- arrays if the OperatingPoint object is vectorized (i.e., if any attribute of OperatingPoint is an
array).
- The "F_g", "F_b", "F_w", "M_g", "M_b", and "M_w" keys are always lists, which will contain either
floats or arrays, again depending on whether the OperatingPoint object is vectorized or not.
.. py:method:: run_with_stability_derivatives(alpha=True, beta=True, p=True, q=True, r=True)
Computes the aerodynamic forces and moments on the airplane, and the stability derivatives.
Arguments essentially determine which stability derivatives are computed. If a stability derivative is not
needed, leaving it False will speed up the computation.
:param - alpha: If True, compute the stability derivatives with respect to the angle of attack (alpha).
:type - alpha: bool
:param - beta: If True, compute the stability derivatives with respect to the sideslip angle (beta).
:type - beta: bool
:param - p: If True, compute the stability derivatives with respect to the body-axis roll rate (p).
:type - p: bool
:param - q: If True, compute the stability derivatives with respect to the body-axis pitch rate (q).
:type - q: bool
:param - r: If True, compute the stability derivatives with respect to the body-axis yaw rate (r).
:type - r: bool
Returns: a dictionary with keys:
- 'F_g' : an [x, y, z] list of forces in geometry axes [N]
- 'F_b' : an [x, y, z] list of forces in body axes [N]
- 'F_w' : an [x, y, z] list of forces in wind axes [N]
- 'M_g' : an [x, y, z] list of moments about geometry axes [Nm]
- 'M_b' : an [x, y, z] list of moments about body axes [Nm]
- 'M_w' : an [x, y, z] list of moments about wind axes [Nm]
- 'L' : the lift force [N]. Definitionally, this is in wind axes.
- 'Y' : the side force [N]. This is in wind axes.
- 'D' : the drag force [N]. Definitionally, this is in wind axes.
- 'l_b' : the rolling moment, in body axes [Nm]. Positive is roll-right.
- 'm_b' : the pitching moment, in body axes [Nm]. Positive is pitch-up.
- 'n_b' : the yawing moment, in body axes [Nm]. Positive is nose-right.
- 'CL' : the lift coefficient [-]. Definitionally, this is in wind axes.
- 'CY' : the sideforce coefficient [-]. This is in wind axes.
- 'CD' : the drag coefficient [-]. Definitionally, this is in wind axes.
- 'Cl' : the rolling coefficient [-], in body axes
- 'Cm' : the pitching coefficient [-], in body axes
- 'Cn' : the yawing coefficient [-], in body axes
Along with additional keys, depending on the value of the `alpha`, `beta`, `p`, `q`, and `r` arguments. For
example, if `alpha=True`, then the following additional keys will be present:
- 'CLa' : the lift coefficient derivative with respect to alpha [1/rad]
- 'CDa' : the drag coefficient derivative with respect to alpha [1/rad]
- 'CYa' : the sideforce coefficient derivative with respect to alpha [1/rad]
- 'Cla' : the rolling moment coefficient derivative with respect to alpha [1/rad]
- 'Cma' : the pitching moment coefficient derivative with respect to alpha [1/rad]
- 'Cna' : the yawing moment coefficient derivative with respect to alpha [1/rad]
- 'x_np': the neutral point location in the x direction [m]
Nondimensional values are nondimensionalized using reference values in the AeroBuildup.airplane object.
Data types:
- The "L", "Y", "D", "l_b", "m_b", "n_b", "CL", "CY", "CD", "Cl", "Cm", and "Cn" keys are:
- floats if the OperatingPoint object is not vectorized (i.e., if all attributes of OperatingPoint
are floats, not arrays).
- arrays if the OperatingPoint object is vectorized (i.e., if any attribute of OperatingPoint is an
array).
- The "F_g", "F_b", "F_w", "M_g", "M_b", and "M_w" keys are always lists, which will contain either
floats or arrays, again depending on whether the OperatingPoint object is vectorized or not.
.. py:method:: wing_aerodynamics(wing, include_induced_drag = True)
Estimates the aerodynamic forces, moments, and derivatives on a wing in isolation.
Moments are given with the reference at Wing [0, 0, 0].
:param wing: A Wing object that you wish to analyze.
:param op_point: The OperatingPoint that you wish to analyze the fuselage at.
Returns:
.. py:method:: fuselage_aerodynamics(fuselage, include_induced_drag = True)
Estimates the aerodynamic forces, moments, and derivatives on a fuselage in isolation.
Assumes:
* The fuselage is a body of revolution aligned with the x_b axis.
* The angle between the nose and the freestream is less than 90 degrees.
Moments are given with the reference at Fuselage [0, 0, 0].
Uses methods from Jorgensen, Leland Howard. "Prediction of Static Aerodynamic Characteristics for Slender Bodies
Alone and with Lifting Surfaces to Very High Angles of Attack". NASA TR R-474. 1977.
:param fuselage: A Fuselage object that you wish to analyze.
Returns:
.. py:class:: AVL(airplane, op_point, xyz_ref = None, avl_command = 'avl', verbose = False, timeout = 5, working_directory = None, ground_effect = False, ground_effect_height = 0)
Bases: :py:obj:`aerosandbox.common.ExplicitAnalysis`
An interface to AVL, a 3D vortex lattice aerodynamics code developed by Mark Drela at MIT.
Requires AVL to be on your computer; AVL is available here: https://web.mit.edu/drela/Public/web/avl/
It is recommended (but not required) that you add AVL to your system PATH environment variable such that it can
be called with the command `avl`. If this is not the case, you need to specify the path to your AVL
executable using the `avl_command` argument of the constructor.
Usage example:
>>> avl = asb.AVL(
>>> airplane=my_airplane,
>>> op_point=asb.OperatingPoint(
>>> velocity=100, # m/s
>>> alpha=5, # deg
>>> beta=4, # deg
>>> p=0.01, # rad/sec
>>> q=0.02, # rad/sec
>>> r=0.03, # rad/sec
>>> )
>>> )
>>> outputs = avl.run()
.. py:attribute:: default_analysis_specific_options
This is part of AeroSandbox's "analysis-specific options" feature, which lets you "tag" geometry objects with
flags that change how different analyses act on them.
This variable, `default_analysis_specific_options`, allows you to specify default values for options that can be used for
specific problems.
This should be a dictionary, where: * keys are the geometry-like types that you might be interested in defining
parameters for. * values are dictionaries, where: * keys are strings that label a given option * values are
anything. These are used as the default values, in the event that the associated geometry doesn't override those.
An example of what this variable might look like, for a vortex-lattice method aerodynamic analysis:
>>> default_analysis_specific_options = {
>>> Airplane: dict(
>>> profile_drag_coefficient=0
>>> ),
>>> Wing : dict(
>>> wing_level_spanwise_spacing=True,
>>> spanwise_resolution=12,
>>> spanwise_spacing="cosine",
>>> chordwise_resolution=12,
>>> chordwise_spacing="cosine",
>>> component=None, # type: int
>>> no_wake=False,
>>> no_alpha_beta=False,
>>> no_load=False,
>>> drag_polar=dict(
>>> CL1=0,
>>> CD1=0,
>>> CL2=0,
>>> CD2=0,
>>> CL3=0,
>>> CD3=0,
>>> ),
>>> )
>>> }
.. py:attribute:: AVL_spacing_parameters
.. py:attribute:: airplane
.. py:attribute:: op_point
.. py:attribute:: xyz_ref
:value: None
.. py:attribute:: avl_command
:value: 'avl'
.. py:attribute:: verbose
:value: False
.. py:attribute:: timeout
:value: 5
.. py:attribute:: working_directory
:value: None
.. py:attribute:: ground_effect
:value: False
.. py:attribute:: ground_effect_height
:value: 0
.. py:method:: __repr__()
.. py:method:: open_interactive()
Opens a new terminal window and runs AVL interactively. This is useful for detailed analysis or debugging.
Returns: None
.. py:method:: run(run_command = None)
Private function to run AVL.
Args: run_command: A string with any AVL keystroke inputs that you'd like. By default, you start off within the OPER
menu. All of the inputs indicated in the constructor have been set already, but you can override them here (
for this run only) if you want.
Returns: A dictionary containing all of your results.
.. py:method:: _default_keystroke_file_contents()
.. py:method:: write_avl(filepath = None)
Writes a .avl file corresponding to this airplane to a filepath.
For use with the AVL vortex-lattice-method aerodynamics analysis tool by Mark Drela at MIT.
AVL is available here: https://web.mit.edu/drela/Public/web/avl/
:param filepath: filepath (including the filename and .avl extension) [string]
If None, this function returns the .avl file as a string.
Returns: None
.. py:method:: write_avl_bfile(fuselage, filepath = None, include_name = True)
:staticmethod:
Writes an AVL-compatible BFILE corresponding to this fuselage to a filepath.
For use with the AVL vortex-lattice-method aerodynamics analysis tool by Mark Drela at MIT.
AVL is available here: https://web.mit.edu/drela/Public/web/avl/
:param filepath: filepath (including the filename and .avl extension) [string]
If None, this function returns the would-be file contents as a string.
:param include_name: Should the name of the fuselage be included in the .dat file? (This should be True for use with AVL.)
Returns:
.. py:method:: parse_unformatted_data_output(s, data_identifier = ' = ', cast_outputs_to_float = True, overwrite = None)
:staticmethod:
Parses a (multiline) string of unformatted data into a nice and tidy dictionary.
The expected input string looks like what you might get as an output from AVL (or many other Drela codes),
which may list data in ragged order.
An example input `s` that you might want to parse could look like the following:
```
Standard axis orientation, X fwd, Z down
Run case: -unnamed-
Alpha = 0.43348 pb/2V = -0.00000 p'b/2V = -0.00000
Beta = 0.00000 qc/2V = 0.00000
Mach = 0.003 rb/2V = -0.00000 r'b/2V = -0.00000
CXtot = -0.02147 Cltot = 0.00000 Cl'tot = 0.00000
CYtot = 0.00000 Cmtot = 0.28149
CZtot = -1.01474 Cntot = -0.00000 Cn'tot = -0.00000
CLtot = 1.01454
CDtot = 0.02915
CDvis = 0.00000 CDind = 0.0291513
CLff = 1.00050 CDff = 0.0297201 | Trefftz
CYff = 0.00000 e = 0.9649 | Plane
```
Here, this function will go through this string and extract each key-value pair, as denoted by the data
identifier (by default, " = "). It will pull the next whole word without spaces to the left as the key,
and it will pull the next whole word without spaces to the right as the value. Together, these will be
returned as a Dict.
So, the output for the input above would be:
{
'Alpha' : 0.43348,
'pb/2V' : -0.00000,
'p'b/2V' : -0.00000,
'Beta' : 0.00000,
# and so on...
}
:param s: The input string to identify. Can be multiline.
:param data_identifier: The triggering substring for a new key-value pair. By default, it's " = ",
:param which is convention in many output files from Mark Drela's codes. Be careful if you decide to change this:
:param to "=":
:type to "=": '======='
:param as you could pick up on heading separators:
:type as you could pick up on heading separators: '======='
:param cast_outputs_to_float: If this boolean flag is set true, the values of the key-value pairs are cast to
:param floating-point numbers before returning:
:type floating-point numbers before returning: as opposed to the default type, string
:param cast:
:param a NaN is returned (guaranteeing that you can do floating-point math with the outputs in downstream:
:param applications.):
:param overwrite: Determines the behavior if you find a key that's already in the dictionary.
* By default, value is None. In this case, an error is raised.
* If you set it to True, the new value will overwrite the old one. Thus, your dictionary will have
the last matching value from the string.
* If you set it to False, the new value will be discarded. Thus, your dictionary will have the first
matching value from the string.
Returns: A dictionary of key-value pairs, corresponding to the unformatted data in the input string.
Keys are strings, values are floats if `cast_outputs_to_float` is True, otherwise also strings.
.. py:data:: __version__
:value: '4.2.6'
.. py:function:: docs()
Opens the AeroSandbox documentation.
.. py:function:: run_tests()
Runs all of the AeroSandbox internal unit tests on this computer.