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.