├── .coveragerc ├── .flake8 ├── .github ├── CODEOWNERS ├── azure-pipelines.yaml ├── build_real.sh └── test_real.sh ├── .gitignore ├── .readthedocs.yaml ├── CITATION.cff ├── LICENSE.md ├── README.md ├── openaerostruct ├── __init__.py ├── aerodynamics │ ├── __init__.py │ ├── aero_groups.py │ ├── coeffs.py │ ├── collocation_points.py │ ├── compressible_states.py │ ├── convert_velocity.py │ ├── eval_mtx.py │ ├── eval_velocities.py │ ├── functionals.py │ ├── geometry.py │ ├── get_vectors.py │ ├── horseshoe_circulations.py │ ├── lift_coeff_2D.py │ ├── lift_drag.py │ ├── mesh_point_forces.py │ ├── mtx_rhs.py │ ├── panel_forces.py │ ├── panel_forces_surf.py │ ├── pg_scale.py │ ├── pg_transform.py │ ├── pg_wind_rotation.py │ ├── rotational_velocity.py │ ├── solve_matrix.py │ ├── states.py │ ├── total_drag.py │ ├── total_lift.py │ ├── viscous_drag.py │ ├── vortex_mesh.py │ └── wave_drag.py ├── common │ ├── __init__.py │ ├── atmos_comp.py │ ├── atmos_group.py │ └── reynolds_comp.py ├── docs │ ├── Makefile │ ├── __init__.py │ ├── _n2html │ │ └── generate_n2_n2.html │ ├── _static │ │ └── copybutton.js │ ├── _utils │ │ ├── __init__.py │ │ ├── generate_sourcedocs.py │ │ └── patch.py │ ├── advanced_features.rst │ ├── advanced_features │ │ ├── aerostruct_ffd.rst │ │ ├── custom_mesh_example.rst │ │ ├── customizing_prob_setup.rst │ │ ├── figs │ │ │ ├── CRM_esque.svg │ │ │ ├── bspline.svg │ │ │ ├── chord.svg │ │ │ ├── dihedral.svg │ │ │ ├── ground_effect_correction.png │ │ │ ├── ground_effect_polars.png │ │ │ ├── groundplane.svg │ │ │ ├── inverted_gull-wing.svg │ │ │ ├── mesh-diagram.svg │ │ │ ├── multi_section_2_sym.png │ │ │ ├── oas_vsp_mesh.png │ │ │ ├── radius.svg │ │ │ ├── sweep.svg │ │ │ ├── tacs-oas_coupling.png │ │ │ ├── taper.svg │ │ │ ├── thickness.svg │ │ │ ├── twist.svg │ │ │ ├── two_part_mesh.png │ │ │ ├── vsp777.png │ │ │ ├── vsp_chordwise.png │ │ │ ├── vsp_spanwise.png │ │ │ ├── wing_and_tail.png │ │ │ ├── wingbox_Q400.png │ │ │ ├── xshear.svg │ │ │ └── zshear.svg │ │ ├── geometry_manipulation.rst │ │ ├── ground_effect.rst │ │ ├── mphys_coupling.rst │ │ ├── multi_section_surfaces.rst │ │ ├── multiple_surfaces.rst │ │ ├── multipoint.rst │ │ ├── multipoint_parallel.rst │ │ ├── openconcept_coupling.rst │ │ ├── openvsp_mesh_example.rst │ │ ├── scripts │ │ │ ├── Boeing_777-9x_ref.vsp3 │ │ │ ├── basic_2_sec.py │ │ │ ├── basic_2_sec_AS.py │ │ │ ├── basic_2_sec_construction.py │ │ │ ├── basic_2_sec_visc.py │ │ │ ├── mphys_opt_chord.py │ │ │ ├── rect_wing.vsp3 │ │ │ ├── run_vsp_777.py │ │ │ ├── two_part_wing_custom_mesh.py │ │ │ └── wingbox_mpt_Q400_example.py │ │ └── solver.rst │ ├── aero_walkthrough.rst │ ├── aero_walkthrough │ │ ├── generate_n2.py │ │ ├── part_1.py │ │ ├── part_2.py │ │ ├── part_3.py │ │ ├── part_4.py │ │ ├── part_5.py │ │ ├── part_6.py │ │ └── part_7.py │ ├── aerostructural_index.rst │ ├── aerostructural_tube_walkthrough.rst │ ├── aerostructural_wingbox_walkthrough.rst │ ├── conf.py │ ├── figures │ │ ├── OptView.png │ │ ├── aero.png │ │ ├── aero_sample.png │ │ ├── aerostruct_sample.png │ │ ├── aerostruct_xdsm.pdf │ │ ├── aerostruct_xdsm.png │ │ ├── collapsed_aerostruct_diagram.png │ │ ├── example.png │ │ ├── plotall.png │ │ ├── problem_diagram.png │ │ ├── wingbox_fine.png │ │ └── wingbox_opt.png │ ├── how_to_contribute.rst │ ├── index.rst │ ├── installation.rst │ ├── quick_example.rst │ ├── struct_example.rst │ ├── user_reference.rst │ ├── user_reference │ │ ├── debugging_tips.rst │ │ ├── mesh_surface_dict.rst │ │ └── v1_v2_conversion.rst │ └── wingbox_mpt_opt_example.py ├── examples │ ├── black_box_example.py │ ├── drag_polar.py │ ├── drag_polar_ground_effect.py │ ├── example_deriv_check.py │ ├── rectangular_wing │ │ ├── drag_polar.py │ │ ├── mphys_example.py │ │ ├── mphys_opt_chord.py │ │ ├── mphys_opt_twist.py │ │ ├── opt_chord.py │ │ ├── opt_twist.py │ │ ├── rect_wing.vsp3 │ │ ├── run_rect_wing.py │ │ └── run_roll.py │ ├── run_CRM.py │ ├── run_aerostruct_uCRM_multipoint.py │ └── run_scaneagle.py ├── functionals │ ├── __init__.py │ ├── breguet_range.py │ ├── center_of_gravity.py │ ├── equilibrium.py │ ├── moment_coefficient.py │ ├── sum_areas.py │ ├── total_aero_performance.py │ ├── total_lift_drag.py │ └── total_performance.py ├── geometry │ ├── __init__.py │ ├── ffd_component.py │ ├── geometry_group.py │ ├── geometry_mesh.py │ ├── geometry_mesh_transformations.py │ ├── geometry_multi_join.py │ ├── geometry_unification.py │ ├── monotonic_constraint.py │ ├── radius_comp.py │ └── utils.py ├── integration │ ├── __init__.py │ ├── aerostruct_groups.py │ └── multipoint_comps.py ├── meshing │ ├── CRM_definitions.py │ ├── mesh_generator.py │ ├── section_mesh_generator.py │ └── utils.py ├── mphys │ ├── __init__.py │ ├── aero_builder.py │ ├── aero_funcs_group.py │ ├── aero_mesh.py │ ├── aero_solver_group.py │ ├── demux_surface_mesh.py │ ├── lift_distribution.py │ ├── mux_surface_forces.py │ ├── surface_contours.py │ └── utils.py ├── structures │ ├── __init__.py │ ├── assemble_k_group.py │ ├── compute_nodes.py │ ├── compute_point_mass_loads.py │ ├── compute_thrust_loads.py │ ├── create_rhs.py │ ├── disp.py │ ├── energy.py │ ├── failure_exact.py │ ├── failure_ks.py │ ├── fem.py │ ├── fuel_loads.py │ ├── fuel_vol.py │ ├── length.py │ ├── local_stiff.py │ ├── local_stiff_permuted.py │ ├── local_stiff_transformed.py │ ├── non_intersecting_thickness.py │ ├── section_properties_tube.py │ ├── section_properties_wingbox.py │ ├── spar_within_wing.py │ ├── spatial_beam_functionals.py │ ├── spatial_beam_setup.py │ ├── spatial_beam_states.py │ ├── struct_groups.py │ ├── structural_cg.py │ ├── total_loads.py │ ├── transform.py │ ├── tube_group.py │ ├── utils.py │ ├── vonmises_tube.py │ ├── vonmises_wingbox.py │ ├── weight.py │ ├── wing_weight_loads.py │ ├── wingbox_fuel_vol_delta.py │ ├── wingbox_geometry.py │ └── wingbox_group.py ├── transfer │ ├── __init__.py │ ├── compute_transformation_matrix.py │ ├── displacement_transfer.py │ ├── displacement_transfer_group.py │ └── load_transfer.py └── utils │ ├── __init__.py │ ├── check_surface_dict.py │ ├── constants.py │ ├── interpolation.py │ ├── plot_wing.py │ ├── plot_wingbox.py │ ├── testing.py │ └── vector_algebra.py ├── setup.py └── tests ├── aerodynamics_tests ├── test_2d_lift.py ├── test_coeffs.py ├── test_collocation_points.py ├── test_convert_velocity.py ├── test_eval_mtx.py ├── test_eval_velocities.py ├── test_functionals.py ├── test_geometry.py ├── test_get_vectors.py ├── test_horseshoe_circulations.py ├── test_lift_drag.py ├── test_mesh_point_forces.py ├── test_mtx_rhs.py ├── test_panel_forces.py ├── test_panel_forces_surf.py ├── test_pg_transform.py ├── test_rotational_velocity.py ├── test_solve_matrix.py ├── test_total_drag.py ├── test_total_lift.py ├── test_viscous_drag.py ├── test_vortex_mesh.py └── test_wave_drag.py ├── common_tests ├── test_atmos_comp.py └── test_reynolds_comp.py ├── functionals_tests ├── test_breguet_range.py ├── test_center_of_gravity.py ├── test_equilibrium.py ├── test_moment_coefficient.py ├── test_sum_areas.py └── test_total_lift_drag.py ├── geometry_tests ├── rect_wing.vsp3 ├── test_geometry_mesh.py ├── test_geometry_mesh_transformations.py ├── test_joining_comp.py ├── test_monotonic_constraint.py ├── test_radius_comp.py ├── test_unification_comp.py └── test_vsp_mesh.py ├── integration_tests ├── test_aero.py ├── test_aero_all_geom.py ├── test_aero_analysis.py ├── test_aero_analysis_Sref.py ├── test_aero_analysis_compressible.py ├── test_aero_analysis_no_symmetry.py ├── test_aero_analysis_no_symmetry_wavedrag.py ├── test_aero_atmos.py ├── test_aero_ffd.py ├── test_aero_ground_effect.py ├── test_aero_ground_effect_right.py ├── test_aero_morphing_multipoint.py ├── test_aero_opt_no_symmetry.py ├── test_aero_opt_wavedrag.py ├── test_aerostruct.py ├── test_aerostruct_analysis.py ├── test_aerostruct_analysis_Sref.py ├── test_aerostruct_analysis_compressible.py ├── test_aerostruct_engine_thrusts.py ├── test_aerostruct_ffd.py ├── test_aerostruct_groundeffect.py ├── test_aerostruct_point_loads.py ├── test_aerostruct_wingbox_+weight_analysis.py ├── test_aerostruct_wingbox_analysis.py ├── test_aerostruct_wingbox_fuel_vol_constraint_opt.py ├── test_aerostruct_wingbox_opt.py ├── test_aerostruct_wingbox_wave_fuel_vol_constraint_opt.py ├── test_morphing_aerostruct.py ├── test_multi_2_sec.py ├── test_multi_2_sec_AS.py ├── test_multi_3_sec.py ├── test_multi_single.py ├── test_multi_single_AS.py ├── test_multiple_aero_analysis.py ├── test_multiple_rect.py ├── test_multipoint_aero.py ├── test_multipoint_aero_ffd.py ├── test_multipoint_parallel.py ├── test_multipoint_wingbox_aerostruct.py ├── test_multipoint_wingbox_derivs.py ├── test_scaneagle.py ├── test_simple_rect_AS.py ├── test_simple_rect_aero.py ├── test_simple_rect_aero_roll.py ├── test_simple_rect_aero_sideslip.py ├── test_simple_rect_aero_wo_symmetry.py ├── test_simple_rect_mphys_aero.py ├── test_simple_rect_mphys_aero_compressible.py ├── test_simple_rect_right_aero.py ├── test_struct.py ├── test_struct_analysis.py ├── test_struct_analysis_2g.py ├── test_struct_engine_thrusts.py ├── test_struct_no_loads.py ├── test_struct_point_masses.py ├── test_v1_aero_analysis.py ├── test_v1_aero_opt.py ├── test_v1_aero_viscous.py ├── test_v1_aero_viscous_opt.py ├── test_v1_aerostruct_analysis.py ├── test_v1_aerostruct_opt.py ├── test_v1_struct_analysis.py ├── test_v1_struct_opt.py ├── test_vsp_aero_analysis.py ├── test_wingbox_analysis.py ├── test_wingbox_derivs.py ├── test_wingbox_distributed_fuel.py └── vsp_model.vsp3 ├── meshing_tests ├── test_mesh_generator.py ├── test_mesh_regen.py └── test_meshing_imports.py ├── mphys_tests ├── test_aero_builder.py ├── test_aero_funcs_group.py ├── test_aero_solver_group.py ├── test_demux_mesh.py └── test_mux_forces.py ├── structures_tests ├── test_add_point_masses.py ├── test_compute_nodes.py ├── test_create_rhs.py ├── test_disp.py ├── test_energy.py ├── test_failure_exact.py ├── test_failure_ks.py ├── test_fem.py ├── test_fuel_loads.py ├── test_materials_tube.py ├── test_materials_wingbox.py ├── test_non_intersecting_thickness.py ├── test_spar_within_wing.py ├── test_structural_cg.py ├── test_thrust_loads.py ├── test_total_load_derivs.py ├── test_vonmises_tube.py ├── test_vonmises_wingbox.py ├── test_weight.py └── test_wingbox_geometry.py └── transfer_tests ├── test_compute_transformation_matrix.py ├── test_displacement_transfer.py └── test_load_transfer.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | */openaerostruct/utils/plot_wing.py 4 | */openaerostruct/utils/plot_wingbox.py 5 | */tests/* 6 | */docs/* 7 | exclude_lines = 8 | pragma: no cover 9 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | # OpenAeroStruct-specific settings in addition to the MDO Lab standard config file. 3 | extend-exclude = 4 | openaerostruct/docs/conf.py, 5 | openaerostruct/docs/aero_walkthrough/part*.py, 6 | openaerostruct/docs/_utils/docutil.py, 7 | openaerostruct/docs/_utils/patch.py, 8 | openaerostruct/utils/plot_*.py, 9 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mdolab/openaerostruct_maintainers 2 | -------------------------------------------------------------------------------- /.github/azure-pipelines.yaml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - main 5 | tags: 6 | include: 7 | - v*.*.* 8 | 9 | pr: 10 | - main 11 | 12 | resources: 13 | repositories: 14 | - repository: azure_template 15 | type: github 16 | name: mdolab/.github 17 | endpoint: mdolab 18 | 19 | stages: 20 | - template: azure/azure_template.yaml@azure_template 21 | parameters: 22 | REPO_NAME: OpenAeroStruct 23 | COVERAGE: true 24 | 25 | - stage: 26 | dependsOn: 27 | - Test_Real 28 | - Style 29 | displayName: PyPI 30 | condition: and(succeeded(), contains(variables['build.sourceBranch'], 'tags')) 31 | jobs: 32 | - template: azure/azure_pypi.yaml@azure_template 33 | -------------------------------------------------------------------------------- /.github/build_real.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | sed -i '/mphys/d' "$HOME/.config/pip/constraints.txt" # Remove the pip constraint on the mphys version 4 | sed -i '/numpy/d; /openmdao/d' "$HOME/.config/pip/constraints.txt" # Remove the pip constraint on the numpy and openmdao version 5 | pip install .[testing,mphys] 6 | -------------------------------------------------------------------------------- /.github/test_real.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | testflo -n 2 -v . --coverage --coverpkg openaerostruct 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore OpenMDAO reports folders 2 | **/reports/ 3 | 4 | *.pyc 5 | *.py~ 6 | *.db 7 | *~ 8 | *.aux 9 | *.log 10 | *.pdf 11 | *.synctex.gz 12 | *.f90~ 13 | *.so 14 | *.out 15 | *.0 16 | *.o 17 | *.mod 18 | openaerostruct/docs/_build/ 19 | openaerostruct/docs/*.rst 20 | openaerostruct/docs/_srcdocs/ 21 | *.a 22 | *.msg 23 | src/adjoint/tempReverse/ 24 | src/adjoint/tempForward/ 25 | openaerostruct.egg-info/ 26 | .vscode 27 | *.plt 28 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # File: .readthedocs.yaml 2 | 3 | version: 2 4 | 5 | sphinx: 6 | # Path to your Sphinx configuration file. 7 | configuration: openaerostruct/docs/conf.py 8 | 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | sphinx: 15 | configuration: openaerostruct/docs/conf.py 16 | 17 | python: 18 | install: 19 | - method: pip 20 | path: . 21 | extra_requirements: 22 | - docs 23 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: "1.2.0" 2 | authors: 3 | - family-names: Jasa 4 | given-names: John P. 5 | orcid: "https://orcid.org/0000-0001-5442-2792" 6 | - family-names: Hwang 7 | given-names: John T. 8 | orcid: "https://orcid.org/0009-0002-5789-6357" 9 | - family-names: Martins 10 | given-names: Joaquim R. R. A. 11 | orcid: "https://orcid.org/0000-0003-2143-1478" 12 | doi: 10.1007/s00158-018-1912-8 13 | message: If you use this software, please cite the following paper. 14 | preferred-citation: 15 | authors: 16 | - family-names: Jasa 17 | given-names: John P. 18 | orcid: "https://orcid.org/0000-0001-5442-2792" 19 | - family-names: Hwang 20 | given-names: John T. 21 | orcid: "https://orcid.org/0009-0002-5789-6357" 22 | - family-names: Martins 23 | given-names: Joaquim R. R. A. 24 | orcid: "https://orcid.org/0000-0003-2143-1478" 25 | title: "Open-source coupled aerostructural optimization using Python" 26 | journal: "Structural and Multidisciplinary Optimization" 27 | volume: "57" 28 | number: "4" 29 | pages: "1815--1827" 30 | date: "2018-04" 31 | doi: "10.1007/s00158-018-1912-8" -------------------------------------------------------------------------------- /openaerostruct/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.10.1" 2 | -------------------------------------------------------------------------------- /openaerostruct/aerodynamics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/aerodynamics/__init__.py -------------------------------------------------------------------------------- /openaerostruct/aerodynamics/coeffs.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | 4 | class Coeffs(om.ExplicitComponent): 5 | """Compute lift and drag coefficients for each individual lifting surface. 6 | 7 | Parameters 8 | ---------- 9 | S_ref : float 10 | The reference areas of the lifting surface. 11 | L : float 12 | Total lift for the lifting surface. 13 | D : float 14 | Total drag for the lifting surface. 15 | v : float 16 | Freestream air velocity in m/s. 17 | rho : float 18 | Air density in kg/m^3. 19 | 20 | Returns 21 | ------- 22 | CL1 : float 23 | Induced coefficient of lift (CL) for the lifting surface. 24 | CDi : float 25 | Induced coefficient of drag (CD) for the lifting surface. 26 | """ 27 | 28 | def setup(self): 29 | self.add_input("S_ref", val=1.0, units="m**2", tags=["mphys_coupling"]) 30 | self.add_input("L", val=1.0, units="N") 31 | self.add_input("D", val=1.0, units="N") 32 | self.add_input("v", val=1.0, units="m/s", tags=["mphys_input"]) 33 | self.add_input("rho", val=1.0, units="kg/m**3", tags=["mphys_input"]) 34 | 35 | self.add_output("CL1", val=0.0) 36 | self.add_output("CDi", val=0.0) 37 | 38 | self.declare_partials("CL1", "L") 39 | self.declare_partials("CDi", "D") 40 | self.declare_partials("CL1", "v") 41 | self.declare_partials("CDi", "v") 42 | self.declare_partials("CL1", "rho") 43 | self.declare_partials("CDi", "rho") 44 | self.declare_partials("CL1", "S_ref") 45 | self.declare_partials("CDi", "S_ref") 46 | 47 | def compute(self, inputs, outputs): 48 | S_ref = inputs["S_ref"] 49 | rho = inputs["rho"] 50 | v = inputs["v"] 51 | L = inputs["L"] 52 | D = inputs["D"] 53 | 54 | outputs["CL1"] = L / (0.5 * rho * v**2 * S_ref) 55 | outputs["CDi"] = D / (0.5 * rho * v**2 * S_ref) 56 | 57 | def compute_partials(self, inputs, partials): 58 | S_ref = inputs["S_ref"] 59 | rho = inputs["rho"] 60 | v = inputs["v"] 61 | L = inputs["L"] 62 | D = inputs["D"] 63 | 64 | partials["CL1", "L"] = 1.0 / (0.5 * rho * v**2 * S_ref) 65 | partials["CDi", "D"] = 1.0 / (0.5 * rho * v**2 * S_ref) 66 | 67 | partials["CL1", "v"] = -2.0 * L / (0.5 * rho * v**3 * S_ref) 68 | partials["CDi", "v"] = -2.0 * D / (0.5 * rho * v**3 * S_ref) 69 | 70 | partials["CL1", "rho"] = -L / (0.5 * rho**2 * v**2 * S_ref) 71 | partials["CDi", "rho"] = -D / (0.5 * rho**2 * v**2 * S_ref) 72 | 73 | partials["CL1", "S_ref"] = -L / (0.5 * rho * v**2 * S_ref**2) 74 | partials["CDi", "S_ref"] = -D / (0.5 * rho * v**2 * S_ref**2) 75 | -------------------------------------------------------------------------------- /openaerostruct/aerodynamics/functionals.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | from openaerostruct.aerodynamics.lift_drag import LiftDrag 3 | from openaerostruct.aerodynamics.coeffs import Coeffs 4 | from openaerostruct.aerodynamics.total_lift import TotalLift 5 | from openaerostruct.aerodynamics.total_drag import TotalDrag 6 | from openaerostruct.aerodynamics.viscous_drag import ViscousDrag 7 | from openaerostruct.aerodynamics.wave_drag import WaveDrag 8 | from openaerostruct.aerodynamics.lift_coeff_2D import LiftCoeff2D 9 | 10 | 11 | class VLMFunctionals(om.Group): 12 | """ 13 | Group that contains the aerodynamic functionals used to evaluate 14 | performance. These are not included in the coupled aerostructural group, 15 | but are only used to compute aerodynamic performance. This includes 16 | computing lift, drag, CL, CD, viscous CD, and wave drag CD. 17 | """ 18 | 19 | def initialize(self): 20 | self.options.declare("surface", types=dict) 21 | 22 | def setup(self): 23 | surface = self.options["surface"] 24 | 25 | self.add_subsystem( 26 | "liftcoeff", 27 | LiftCoeff2D(surface=surface), 28 | promotes_inputs=["v", "alpha", "rho", "widths", "chords", "sec_forces"], 29 | promotes_outputs=["Cl"], 30 | ) 31 | 32 | self.add_subsystem( 33 | "liftdrag", 34 | LiftDrag(surface=surface), 35 | promotes_inputs=["alpha", "beta", "sec_forces"], 36 | promotes_outputs=["L", "D"], 37 | ) 38 | 39 | self.add_subsystem( 40 | "coeffs", Coeffs(), promotes_inputs=["v", "rho", "S_ref", "L", "D"], promotes_outputs=["CL1", "CDi"] 41 | ) 42 | 43 | self.add_subsystem("CL", TotalLift(surface=surface), promotes_inputs=["CL1"], promotes_outputs=["CL"]) 44 | 45 | self.add_subsystem( 46 | "viscousdrag", 47 | ViscousDrag(surface=surface), 48 | promotes_inputs=["Mach_number", "re", "widths", "lengths_spanwise", "lengths", "S_ref", "t_over_c"], 49 | promotes_outputs=["CDv"], 50 | ) 51 | 52 | self.add_subsystem( 53 | "wavedrag", 54 | WaveDrag(surface=surface), 55 | promotes_inputs=["Mach_number", "lengths_spanwise", "widths", "CL", "chords", "t_over_c"], 56 | promotes_outputs=["CDw"], 57 | ) 58 | 59 | self.add_subsystem( 60 | "CD", TotalDrag(surface=surface), promotes_inputs=["CDv", "CDi", "CDw"], promotes_outputs=["CD"] 61 | ) 62 | -------------------------------------------------------------------------------- /openaerostruct/aerodynamics/panel_forces_surf.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | class PanelForcesSurf(om.ExplicitComponent): 7 | """ 8 | Take in the computed panel forces and convert them to sectional forces 9 | for each surface. Basically just takes the long array that has info 10 | for all surfaces and creates a new output for each surface with only 11 | that surface's panel forces. 12 | 13 | Parameters 14 | ---------- 15 | panel_forces[system_size, 3] : numpy array 16 | All of the forces acting on all panels in the total system. 17 | 18 | Returns 19 | ------- 20 | sec_forces[nx-1, ny-1, 3] : numpy array 21 | Only the panel forces for one individual lifting surface. 22 | There is one of these per surface. 23 | """ 24 | 25 | def initialize(self): 26 | self.options.declare("surfaces", types=list) 27 | 28 | def setup(self): 29 | surfaces = self.options["surfaces"] 30 | 31 | system_size = 0 32 | 33 | # Loop through the surfaces to get the total system size 34 | for surface in surfaces: 35 | mesh = surface["mesh"] 36 | nx = mesh.shape[0] 37 | ny = mesh.shape[1] 38 | 39 | system_size += (nx - 1) * (ny - 1) 40 | 41 | arange = np.arange(3 * system_size) 42 | 43 | self.add_input("panel_forces", shape=(system_size, 3), units="N") 44 | 45 | # Loop through the surfaces and add the output of sec_forces based on 46 | # the size of each surface. Here we keep track of the total indices 47 | # from panel_forces to make sure the forces go to the correct output 48 | ind1, ind2 = 0, 0 49 | for surface in surfaces: 50 | mesh = surface["mesh"] 51 | nx = mesh.shape[0] 52 | ny = mesh.shape[1] 53 | name = surface["name"] 54 | 55 | sec_forces_name = "{}_sec_forces".format(name) 56 | 57 | ind2 += (nx - 1) * (ny - 1) * 3 58 | 59 | self.add_output(sec_forces_name, shape=(nx - 1, ny - 1, 3), units="N", tags=["mphys_coupling"]) 60 | 61 | rows = np.arange((nx - 1) * (ny - 1) * 3) 62 | cols = arange[ind1:ind2] 63 | self.declare_partials(sec_forces_name, "panel_forces", val=1.0, rows=rows, cols=cols) 64 | 65 | ind1 += (nx - 1) * (ny - 1) * 3 66 | 67 | def compute(self, inputs, outputs): 68 | surfaces = self.options["surfaces"] 69 | 70 | ind1, ind2 = 0, 0 71 | for surface in surfaces: 72 | mesh = surface["mesh"] 73 | nx = mesh.shape[0] 74 | ny = mesh.shape[1] 75 | name = surface["name"] 76 | 77 | sec_forces_name = "{}_sec_forces".format(name) 78 | 79 | ind2 += (nx - 1) * (ny - 1) 80 | 81 | # Just pluck out the relevant forces and reshape them 82 | outputs[sec_forces_name] = inputs["panel_forces"][ind1:ind2].reshape((nx - 1, ny - 1, 3)) 83 | 84 | ind1 += (nx - 1) * (ny - 1) 85 | -------------------------------------------------------------------------------- /openaerostruct/aerodynamics/pg_transform.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | from openaerostruct.aerodynamics.pg_wind_rotation import RotateToWindFrame, RotateFromWindFrame 4 | from openaerostruct.aerodynamics.pg_scale import ScaleToPrandtlGlauert, ScaleFromPrandtlGlauert 5 | 6 | 7 | class PGTransform(om.Group): 8 | """ 9 | This group is responsible for transforming the VLM geometries from 10 | physical coordinates to Prandtl-Glauert coordinates. This allows the 11 | compressible aerodynamic problem to be solved as an equivelent 12 | incompressible problem. 13 | 14 | The transform can be broken down into two steps: 15 | 16 | 1. Rotate the geometry from the body frame to the wind frame so 17 | x-axis is parallel to freestream velocity. 18 | 19 | 2. Scale wind frame coordinates by Prandtl-Glauert factor to retrieve 20 | equivalent Prandtl-Glauert geometry. 21 | """ 22 | 23 | def initialize(self): 24 | self.options.declare("surfaces", types=list) 25 | self.options.declare( 26 | "rotational", False, types=bool, desc="Set to True to turn on support for computing angular velocities" 27 | ) 28 | 29 | def setup(self): 30 | surfaces = self.options["surfaces"] 31 | rotational = self.options["rotational"] 32 | 33 | # Create component to rotate mesh geometry to wind frame, 34 | # s.t. the freestream velocity is along the x-axis 35 | self.add_subsystem( 36 | "rotate", 37 | RotateToWindFrame(surfaces=surfaces, rotational=rotational), 38 | promotes_inputs=["*"], 39 | promotes_outputs=["*"], 40 | ) 41 | 42 | # Scale y and z direction by Prandtl Glauert factor 43 | self.add_subsystem( 44 | "scale", 45 | ScaleToPrandtlGlauert(surfaces=surfaces, rotational=rotational), 46 | promotes_inputs=["*"], 47 | promotes_outputs=["*"], 48 | ) 49 | 50 | 51 | class InversePGTransform(om.Group): 52 | """ 53 | This group is responsible for transforming the solved incompressible 54 | forces in the Prandtl-Glauert domain to the compressible forces in the 55 | physical aerodynamic frame. This is the reverse procedure used in the 56 | PGTransform group. 57 | 58 | The inverse transform can be broken down into two steps: 59 | 60 | 1. Scale Prandtl-Glauert force vectors to physical force vectors in wind 61 | frame. 62 | 63 | 2. Rotate physical forces from wind frame to the aerodynamic frame. 64 | """ 65 | 66 | def initialize(self): 67 | self.options.declare("surfaces", types=list) 68 | 69 | def setup(self): 70 | surfaces = self.options["surfaces"] 71 | 72 | # Scale force vectors back to compressible values 73 | scale_sys = ScaleFromPrandtlGlauert(surfaces=surfaces) 74 | self.add_subsystem("scale", scale_sys, promotes_inputs=["*"], promotes_outputs=["*"]) 75 | 76 | # Rotate forces back to aerodynamic frame 77 | rot_sys = RotateFromWindFrame(surfaces=surfaces) 78 | self.add_subsystem("rotate", rot_sys, promotes_inputs=["*"], promotes_outputs=["*"]) 79 | -------------------------------------------------------------------------------- /openaerostruct/aerodynamics/solve_matrix.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.linalg import lu_factor, lu_solve 3 | 4 | import openmdao.api as om 5 | 6 | 7 | class SolveMatrix(om.ImplicitComponent): 8 | """ 9 | Solve the AIC linear system to obtain the vortex ring circulations. 10 | 11 | Parameters 12 | ---------- 13 | mtx[system_size, system_size] : numpy array 14 | Final fully assembled AIC matrix that is used to solve for the 15 | circulations. 16 | rhs[system_size] : numpy array 17 | Right-hand side of the AIC linear system, constructed from the 18 | freestream velocities and panel normals. 19 | 20 | Returns 21 | ------- 22 | circulations[system_size] : numpy array 23 | The vortex ring circulations obtained by solving the AIC linear system. 24 | 25 | """ 26 | 27 | def initialize(self): 28 | self.options.declare("surfaces", types=list) 29 | 30 | def setup(self): 31 | system_size = 0 32 | 33 | for surface in self.options["surfaces"]: 34 | mesh = surface["mesh"] 35 | nx = mesh.shape[0] 36 | ny = mesh.shape[1] 37 | 38 | system_size += (nx - 1) * (ny - 1) 39 | 40 | self.system_size = system_size 41 | 42 | self.add_input("mtx", shape=(system_size, system_size), units="1/m") 43 | self.add_input("rhs", shape=system_size, units="m/s") 44 | self.add_output("circulations", shape=system_size, units="m**2/s", tags=["mphys_coupling"]) 45 | 46 | self.declare_partials( 47 | "circulations", 48 | "circulations", 49 | rows=np.outer(np.arange(system_size), np.ones(system_size, int)).flatten(), 50 | cols=np.outer(np.ones(system_size, int), np.arange(system_size)).flatten(), 51 | ) 52 | self.declare_partials( 53 | "circulations", 54 | "mtx", 55 | rows=np.outer(np.arange(system_size), np.ones(system_size, int)).flatten(), 56 | cols=np.arange(system_size**2), 57 | ) 58 | self.declare_partials( 59 | "circulations", 60 | "rhs", 61 | val=-1.0, 62 | rows=np.arange(system_size), 63 | cols=np.arange(system_size), 64 | ) 65 | 66 | def apply_nonlinear(self, inputs, outputs, residuals): 67 | residuals["circulations"] = inputs["mtx"].dot(outputs["circulations"]) - inputs["rhs"] 68 | 69 | def solve_nonlinear(self, inputs, outputs): 70 | self.lu = lu_factor(inputs["mtx"]) 71 | 72 | outputs["circulations"] = lu_solve(self.lu, inputs["rhs"]) 73 | 74 | def linearize(self, inputs, outputs, partials): 75 | system_size = self.system_size 76 | self.lu = lu_factor(inputs["mtx"]) 77 | 78 | partials["circulations", "circulations"] = inputs["mtx"].flatten() 79 | partials["circulations", "mtx"] = np.outer(np.ones(system_size), outputs["circulations"]).flatten() 80 | 81 | def solve_linear(self, d_outputs, d_residuals, mode): 82 | if mode == "fwd": 83 | d_outputs["circulations"] = lu_solve(self.lu, d_residuals["circulations"], trans=0) 84 | else: 85 | d_residuals["circulations"] = lu_solve(self.lu, d_outputs["circulations"], trans=1) 86 | -------------------------------------------------------------------------------- /openaerostruct/aerodynamics/total_drag.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | 4 | class TotalDrag(om.ExplicitComponent): 5 | """Calculate total drag in force units. 6 | 7 | parameters 8 | ---------- 9 | CDi : float 10 | Induced coefficient of drag (CD) for the lifting surface. 11 | CDv : float 12 | Calculated coefficient of viscous drag for the lifting surface. 13 | 14 | Returns 15 | ------- 16 | CD : float 17 | Total coefficient of drag (CD) for the lifting surface. 18 | """ 19 | 20 | def initialize(self): 21 | self.options.declare("surface", types=dict) 22 | 23 | def setup(self): 24 | surface = self.options["surface"] 25 | 26 | self.add_input("CDi", val=1.0) 27 | self.add_input("CDv", val=1.0) 28 | self.add_input("CDw", val=1.0) 29 | 30 | self.add_output("CD", val=1.0, tags=["mphys_result"]) 31 | 32 | self.CD0 = surface["CD0"] 33 | 34 | self.declare_partials("CD", "CDi", val=1.0) 35 | self.declare_partials("CD", "CDv", val=1.0) 36 | self.declare_partials("CD", "CDw", val=1.0) 37 | 38 | def compute(self, inputs, outputs): 39 | outputs["CD"] = inputs["CDi"] + inputs["CDv"] + inputs["CDw"] + self.CD0 40 | -------------------------------------------------------------------------------- /openaerostruct/aerodynamics/total_lift.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | 4 | class TotalLift(om.ExplicitComponent): 5 | """ 6 | Calculate total lift in force units by summing the induced CL 7 | with the CL0. 8 | 9 | Parameters 10 | ---------- 11 | CL1 : float 12 | Induced coefficient of lift (CL) for the lifting surface. 13 | 14 | Returns 15 | ------- 16 | CL : float 17 | Total coefficient of lift (CL) for the lifting surface. 18 | """ 19 | 20 | def initialize(self): 21 | self.options.declare("surface", types=dict) 22 | 23 | def setup(self): 24 | surface = self.options["surface"] 25 | 26 | self.add_input("CL1", val=1.0) 27 | 28 | self.add_output("CL", val=1.0, tags=["mphys_result"]) 29 | 30 | self.CL0 = surface["CL0"] 31 | 32 | self.declare_partials("CL", "CL1", val=1.0) 33 | 34 | def compute(self, inputs, outputs): 35 | outputs["CL"] = inputs["CL1"] + self.CL0 36 | -------------------------------------------------------------------------------- /openaerostruct/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/common/__init__.py -------------------------------------------------------------------------------- /openaerostruct/common/atmos_group.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | from openaerostruct.common.reynolds_comp import ReynoldsComp 3 | from openaerostruct.common.atmos_comp import AtmosComp 4 | 5 | 6 | class AtmosGroup(om.Group): 7 | def setup(self): 8 | self.add_subsystem( 9 | "atmos", 10 | AtmosComp(), 11 | promotes_inputs=["altitude", "Mach_number"], 12 | promotes_outputs=["T", "P", "rho", "speed_of_sound", "mu", "v"], 13 | ) 14 | 15 | self.add_subsystem("reynolds", ReynoldsComp(), promotes_inputs=["rho", "mu", "v"], promotes_outputs=["re"]) 16 | -------------------------------------------------------------------------------- /openaerostruct/common/reynolds_comp.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | 4 | class ReynoldsComp(om.ExplicitComponent): 5 | def setup(self): 6 | self.add_input("rho", val=1.0, units="slug/ft**3") 7 | self.add_input("mu", val=1.0, units="lbf*s/ft**2") 8 | self.add_input("v", val=1.0, units="ft/s") 9 | 10 | self.add_output("re", val=1.0, units="1/ft") 11 | 12 | self.declare_partials("re", ["rho", "mu", "v"]) 13 | 14 | def compute(self, inputs, outputs): 15 | outputs["re"] = inputs["rho"] * inputs["v"] / inputs["mu"] 16 | 17 | def compute_partials(self, inputs, partials): 18 | partials["re", "rho"] = inputs["v"] / inputs["mu"] 19 | partials["re", "v"] = inputs["rho"] / inputs["mu"] 20 | partials["re", "mu"] = -inputs["rho"] * inputs["v"] / inputs["mu"] ** 2 21 | -------------------------------------------------------------------------------- /openaerostruct/docs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/__init__.py -------------------------------------------------------------------------------- /openaerostruct/docs/_static/copybutton.js: -------------------------------------------------------------------------------- 1 | // originally taken from scikit-learn's Sphinx theme 2 | $(document).ready(function() { 3 | /* Add a [>>>] button on the top-right corner of code samples to hide 4 | * the >>> and ... prompts and the output and thus make the code 5 | * copyable. 6 | * Note: This JS snippet was taken from the official python.org 7 | * documentation site.*/ 8 | var div = $('.highlight-python .highlight,' + 9 | '.highlight-python3 .highlight,' + 10 | '.highlight-pycon .highlight') 11 | var pre = div.find('pre'); 12 | 13 | // get the styles from the current theme 14 | pre.parent().parent().css('position', 'relative'); 15 | var hide_text = 'Hide the prompts and output'; 16 | var show_text = 'Show the prompts and output'; 17 | var border_width = pre.css('border-top-width'); 18 | var border_style = pre.css('border-top-style'); 19 | var border_color = pre.css('border-top-color'); 20 | var button_styles = { 21 | 'cursor':'pointer', 'position': 'absolute', 'top': '0', 'right': '0', 22 | 'border-color': border_color, 'border-style': border_style, 23 | 'border-width': border_width, 'color': border_color, 'text-size': '75%', 24 | 'font-family': 'monospace', 'padding-left': '0.2em', 'padding-right': '0.2em' 25 | } 26 | 27 | // create and add the button to all the code blocks that contain >>> 28 | div.each(function(index) { 29 | var jthis = $(this); 30 | if (jthis.find('.gp').length > 0) { 31 | var button = $('>>>'); 32 | button.css(button_styles) 33 | button.attr('title', hide_text); 34 | jthis.prepend(button); 35 | } 36 | // tracebacks (.gt) contain bare text elements that need to be 37 | // wrapped in a span to work with .nextUntil() (see later) 38 | jthis.find('pre:has(.gt)').contents().filter(function() { 39 | return ((this.nodeType == 3) && (this.data.trim().length > 0)); 40 | }).wrap(''); 41 | }); 42 | 43 | // define the behavior of the button when it's clicked 44 | $('.copybutton').toggle( 45 | function() { 46 | var button = $(this); 47 | button.parent().find('.go, .gp, .gt').hide(); 48 | button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'hidden'); 49 | button.css('text-decoration', 'line-through'); 50 | button.attr('title', show_text); 51 | }, 52 | function() { 53 | var button = $(this); 54 | button.parent().find('.go, .gp, .gt').show(); 55 | button.next('pre').find('.gt').nextUntil('.gp, .go').css('visibility', 'visible'); 56 | button.css('text-decoration', 'none'); 57 | button.attr('title', hide_text); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /openaerostruct/docs/_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/_utils/__init__.py -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features.rst: -------------------------------------------------------------------------------- 1 | .. _Advanced_Features: 2 | 3 | Advanced Features 4 | ================= 5 | 6 | These examples show some advanced features in OpenAeroStruct. 7 | 8 | .. toctree:: 9 | :maxdepth: 1 10 | 11 | advanced_features/solver.rst 12 | advanced_features/geometry_manipulation.rst 13 | advanced_features/custom_mesh_example.rst 14 | advanced_features/openvsp_mesh_example.rst 15 | advanced_features/multi_section_surfaces.rst 16 | advanced_features/multiple_surfaces.rst 17 | advanced_features/multipoint.rst 18 | advanced_features/multipoint_parallel.rst 19 | advanced_features/aerostruct_ffd.rst 20 | advanced_features/ground_effect.rst 21 | advanced_features/openconcept_coupling.rst 22 | advanced_features/customizing_prob_setup.rst 23 | advanced_features/mphys_coupling.rst 24 | -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/aerostruct_ffd.rst: -------------------------------------------------------------------------------- 1 | .. _Aerostruct_ffd: 2 | 3 | Aerostructural FFD 4 | ================== 5 | 6 | In additional to OpenAeroStruct's internal geometry manipulation group, you can also use `pyGeo` to perform free-form deformation (FFD) manipulation on the mesh. 7 | This allows for more general design shape changes and helps sync up geometry changes between meshes from different levels of fidelity. 8 | 9 | .. warning:: 10 | This example requires `pyGeo`, an external code developed by the MDO Lab. You can install it from `here `_. 11 | 12 | .. literalinclude:: /../../tests/integration_tests/test_aerostruct_ffd.py 13 | :start-after: docs checkpoint 0 14 | :end-before: docs checkpoint 1 15 | :dedent: 8 16 | -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/custom_mesh_example.rst: -------------------------------------------------------------------------------- 1 | .. _Custom_Mesh: 2 | 3 | User-Provided Mesh Example 4 | ========================== 5 | 6 | Here is an example script with a custom mesh provided as an array of coordinates. 7 | This should help you understand how meshes are defined in OpenAeroStruct and how to create them for your own custom planform shapes. 8 | This is an alternative to the helper-function approach described in :ref:`Aerodynamic_Optimization_Walkthrough`. 9 | 10 | The following shows the portion of the example script in which the user provides the coordinates for the mesh. 11 | This example is for a wing with a kink and two distinct trapezoidal segments. 12 | 13 | .. literalinclude:: /advanced_features/scripts/two_part_wing_custom_mesh.py 14 | :start-after: checkpoint 0 15 | :end-before: checkpoint 1 16 | 17 | The following shows a visualization of the mesh. 18 | 19 | .. image:: /advanced_features/figs/two_part_mesh.png 20 | 21 | The complete script for the optimization is as follows. 22 | Make sure you go through the :ref:`Aerostructural_with_Wingbox_Walkthrough` before trying to understand this setup. 23 | 24 | .. embed-code:: 25 | advanced_features/scripts/two_part_wing_custom_mesh.py 26 | :layout: interleave 27 | 28 | There is plenty of room for improvement. 29 | A finer mesh and a tighter optimization tolerance should be used. -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/customizing_prob_setup.rst: -------------------------------------------------------------------------------- 1 | .. _Customizing_Prob_Setup: 2 | 3 | Customizing Problem Setup 4 | ========================= 5 | 6 | Specifying a Reference Area 7 | --------------------------- 8 | 9 | In order to use a specific reference area for calculating coefficients, rather than the default approach of area-weighted averaging of each lifting surface, a few modifications must be done. First, an option need to be passed to either ``AeroPoint`` or ``AeroStructPoint`` when it's intialized: 10 | 11 | .. code-block:: python 12 | 13 | aero_group = AeroPoint(surfaces=surfaces, user_specified_Sref=True) 14 | 15 | or in the aerostructural case, 16 | 17 | .. code-block:: python 18 | 19 | AS_point = AerostructPoint(surfaces=surfaces,user_specified_Sref=True) 20 | 21 | Next, a new independent variable called ``S_ref_total`` needs to be created in the run script. Here it is set to the value of ``areaRef``. Then, for each analysis point, this variable needs to be connected to each analysis point for performance computation. 22 | 23 | .. code-block:: python 24 | 25 | indep_var_comp.add_output('S_ref_total', val=areaRef, units='m**2') 26 | prob.model.connect('S_ref_total', point_name + '.S_ref_total') -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/figs/ground_effect_correction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/advanced_features/figs/ground_effect_correction.png -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/figs/ground_effect_polars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/advanced_features/figs/ground_effect_polars.png -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/figs/multi_section_2_sym.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/advanced_features/figs/multi_section_2_sym.png -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/figs/oas_vsp_mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/advanced_features/figs/oas_vsp_mesh.png -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/figs/tacs-oas_coupling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/advanced_features/figs/tacs-oas_coupling.png -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/figs/two_part_mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/advanced_features/figs/two_part_mesh.png -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/figs/vsp777.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/advanced_features/figs/vsp777.png -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/figs/vsp_chordwise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/advanced_features/figs/vsp_chordwise.png -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/figs/vsp_spanwise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/advanced_features/figs/vsp_spanwise.png -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/figs/wing_and_tail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/advanced_features/figs/wing_and_tail.png -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/figs/wingbox_Q400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/advanced_features/figs/wingbox_Q400.png -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/ground_effect.rst: -------------------------------------------------------------------------------- 1 | .. _Ground Effect: 2 | 3 | Ground Effect 4 | ============= 5 | 6 | For certain flight conditions or types of aircraft, incorporating ground effect may be important. 7 | Mathematically, the ground effect is simply an extra boundary condition imposed such that the velocities normal to the ground plane are zero. 8 | In vortex lattice methods, this is accomplished by mirroring a second copy of the mesh across the ground plane such that the vortex induced velocities cancel out at the ground. 9 | Some VLM solvers, such as AVL, model ground effect by mirroring across an x-y plane. 10 | This is simple to implement but is not strictly correct because of the influence of the angle of attack. 11 | 12 | In OpenAeroStruct, the ground effect mirroring plane is parallel to the freestream (influenced by angle of attack). 13 | This means that configurations deep in ground effect (small altitude compared to wingspan) or at higher angles of attack will obtain the correct ground effect correction. 14 | 15 | .. image:: /advanced_features/figs/groundplane.svg 16 | :width: 600 17 | 18 | To enable ground effect, add a :code:`groundplane: True` attribute to your aerosurfaces, like so: 19 | 20 | .. literalinclude:: /../../tests/integration_tests/test_aero_ground_effect.py 21 | :start-after: docs checkpoint 0 22 | :end-before: docs checkpoint 1 23 | :dedent: 8 24 | 25 | If groundplane is turned on for an AeroPoint or AeroStructPoint, a new input will be created (height_agl) which represents the distance from the origin (in airplane coordinates) to the ground plane. 26 | The default value, 8000 meters, produces essentially zero ground effect. 27 | 28 | Note that symmetry must be turned on for the ground effect correction to be used. 29 | Also, crosswind (beta) may not be used when ground effect is turned on. 30 | Finally, take care when defining geometry and run cases that your baseline mesh does not end up below the ground plane. 31 | This can occur for wings with long chord, anhedral, shear, tail surfaces located far behind the wing, high angles of attack, or some combination. 32 | 33 | The following plots (generated using the :code:`examples/drag_polar_ground_effect.py` file) illustrate the effect of the ground plane on a rectangular wing with aspect ratio 12. 34 | As the wing approaches the ground, induced drag is significantly reduced compared to the free-flight induced drag. 35 | These results are consistent with published values in the literature, for example "Lifting-Line Predictions for Induced Drag and Lift in Ground Effect" by Phillips and Hunsaker. 36 | 37 | .. image:: /advanced_features/figs/ground_effect_correction.png 38 | :width: 600 39 | 40 | .. image:: /advanced_features/figs/ground_effect_polars.png 41 | :width: 600 42 | 43 | 44 | -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/mphys_coupling.rst: -------------------------------------------------------------------------------- 1 | .. _MPhys Integration: 2 | 3 | MPhys Integration 4 | ================= 5 | 6 | OpenAeroStruct provides an interface for coupling its internal solvers with `MPhys `_. 7 | MPhys is a package that standardizes multiphysics problems in OpenMDAO across multiple fidelity levels. 8 | MPhys eases the problem set up, provides straightforward extension to new disciplines, and has a library of OpenMDAO groups for multidisciplinary problems addressed by its standard. 9 | The MPhys library currently supports from the following libraries: `ADFlow `_, `DAFoam `_, `FunToFEM `_, 10 | `pyCycle `_, `pyGeo `_, and `TACS `_. 11 | 12 | .. note:: Currently, only OpenAeroStruct's aerodynamic solver is supported in the MPhys wrapper. The structural solver has yet to be added. 13 | 14 | This potentially gives OpenAeroStruct the capability to be coupled with any of the above mentioned libraries. 15 | One could, for example, couple a 3D high-fidelity wingbox structural model in TACS with a VLM aerodynamic wing model in OpenAerostruct using FunToFEM's load/displacement coupling scheme. 16 | The following shows an example of a result from a from such an analysis. 17 | 18 | .. image:: /advanced_features/figs/tacs-oas_coupling.png 19 | 20 | The script below shows an example of how to setup and run the MPhys interface. 21 | The example couples pyGeo's OpenVSP parameterization with OpenAeroStruct's aerodynamic solver to perform an aerodynamic optimization. 22 | The interface will also write out aerodynamic solutions in a ``.plt`` file format that can open and viewed in TecPlot. 23 | 24 | .. embed-code:: 25 | advanced_features/scripts/mphys_opt_chord.py -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/multiple_surfaces.rst: -------------------------------------------------------------------------------- 1 | .. _Multiple_Lifting_Surfaces: 2 | 3 | Multiple Lifting Surfaces 4 | ========================= 5 | 6 | It's easily possible to simulate multiple lifting surfaces simultaneously in OpenAeroStruct. 7 | The most straightforward example is a wing and a tail for a conventional airplane, as shown below, though OpenAeroStruct can handle any arbitrary collection of lifting surfaces. 8 | Note that the moment coefficient is always normalized with respect to the MAC of the first surface in the list; therefore, the main lifting surface (i.e., wing) should always be the first entry in the list. 9 | 10 | .. literalinclude:: /../../tests/integration_tests/test_multiple_aero_analysis.py 11 | :start-after: docs checkpoint 0 12 | :end-before: docs checkpoint 1 13 | :dedent: 8 14 | -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/multipoint.rst: -------------------------------------------------------------------------------- 1 | .. _Multipoint Optimization: 2 | 3 | Multipoint Optimization 4 | ======================= 5 | 6 | To simulate multiple flight conditions in a single analysis or optimization, you can add multiple `AeroPoint` or `AerostructPoint` groups to the problem. 7 | This allows you to analyze the performance of the aircraft at multiple flight conditions simultaneously, such as at different cruise and maneuver conditions. 8 | 9 | 10 | Aerodynamic Optimization Example 11 | -------------------------------- 12 | We optimize the aircraft at two cruise flight conditions below. 13 | 14 | .. literalinclude:: /../../tests/integration_tests/test_multipoint_aero.py 15 | :start-after: docs checkpoint 0 16 | :end-before: docs checkpoint 1 17 | :dedent: 8 18 | 19 | 20 | Aerostructural Optimization Example (Q400) 21 | ------------------------------------------ 22 | 23 | This is an additional example of a multipoint aerostructural optimization with the wingbox model using a wing based on the Bombardier Q400. 24 | Here we also create a custom mesh instead of using one provided by OpenAeroStruct. 25 | Make sure you go through the :ref:`Aerostructural_with_Wingbox_Walkthrough` before trying to understand this example. 26 | 27 | .. embed-code:: 28 | advanced_features/scripts/wingbox_mpt_Q400_example.py 29 | 30 | The following shows a visualization of the results. 31 | As can be seen, there is plenty of room for improvement. 32 | A finer mesh and a tighter optimization tolerance should be used. 33 | 34 | .. image:: /advanced_features/figs/wingbox_Q400.png -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/openconcept_coupling.rst: -------------------------------------------------------------------------------- 1 | .. _Mission Analysis using OpenConcept: 2 | 3 | Integrating Mission Analysis 4 | ============================ 5 | 6 | You can perform aerodynamic or aerostructural wing design optimization considering full mission analysis by integrating OpenAeroStruct with `OpenConcept `_. 7 | OpenConcept is a toolkit for aircraft conceptual design, which is also built on top of the OpenMDAO framework. 8 | 9 | Examples of the OpenAeroStruct-OpenConcept coupled work can be found in OpenConcept `repository `_ and `documentation `_. -------------------------------------------------------------------------------- /openaerostruct/docs/advanced_features/openvsp_mesh_example.rst: -------------------------------------------------------------------------------- 1 | .. _Openvsp_Mesh: 2 | 3 | OpenVSP-generated Mesh Example 4 | ============================== 5 | 6 | OpenAeroStruct also has the ability to generate VLM meshes from an OpenVSP ``.vsp3`` file. 7 | In order to use this feature, users must have the OpenVSP Python API installed in their Python environment. 8 | For instructions on how to install OpenVSP's Python API see `here `_. 9 | 10 | Here is an example script with an aero surface mesh generated for a Boeing 777 aircraft defined in an OpenVSP model. 11 | This should help you understand how meshes are defined in OpenAeroStruct and how to create them for your own custom planform shapes. 12 | This is yet another alternative to the mesh definition approaches described in :ref:`Aerodynamic_Optimization_Walkthrough` and :ref:`Custom_Mesh`. 13 | 14 | The OpenVSP model used in this example is shown below: 15 | 16 | .. image:: /advanced_features/figs/vsp777.png 17 | 18 | The following shows the portion of the example script in which the user provides the OpenVSP model for the mesh. 19 | In this example we'll only be importing the wing, horizontal, and vertical tail from the OpenVSP model shown above. 20 | 21 | .. literalinclude:: /advanced_features/scripts/run_vsp_777.py 22 | :start-after: checkpoint 0 23 | :end-before: checkpoint 1 24 | 25 | The following shows a visualization of the mesh. 26 | 27 | .. image:: /advanced_features/figs/oas_vsp_mesh.png 28 | 29 | The complete script for the aerodynamic analysis is as follows. 30 | 31 | .. embed-code:: 32 | advanced_features/scripts/run_vsp_777.py 33 | 34 | The user may have noticed that the VLM mesh density is never explicitly defined in the script. 35 | The discretization of the VLM is based on the tessellation refinement define in OpenVSP model. 36 | To get an idea of what the discretization will look like, users can turn on the wireframe model for each surface in OpenVSP. 37 | The spanwise and chordwise lines will roughly correspond to panels in the exported VLM mesh. 38 | More or fewer VLM panels can be added in the chordwise and spanwise direction for each surface by toggling the following OpenVSP parameters. 39 | 40 | .. image:: /advanced_features/figs/vsp_chordwise.png 41 | .. image:: /advanced_features/figs/vsp_spanwise.png 42 | -------------------------------------------------------------------------------- /openaerostruct/docs/aero_walkthrough/part_1.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | from openaerostruct.meshing.mesh_generator import generate_mesh 6 | from openaerostruct.geometry.geometry_group import Geometry 7 | from openaerostruct.aerodynamics.aero_groups import AeroPoint 8 | 9 | # Create a dictionary to store options about the mesh 10 | mesh_dict = {"num_y": 7, "num_x": 2, "wing_type": "CRM", "symmetry": True, "num_twist_cp": 5} 11 | 12 | # Generate the aerodynamic mesh based on the previous dictionary 13 | mesh, twist_cp = generate_mesh(mesh_dict) 14 | -------------------------------------------------------------------------------- /openaerostruct/docs/aero_walkthrough/part_2.py: -------------------------------------------------------------------------------- 1 | # Create a dictionary with info and options about the aerodynamic 2 | # lifting surface 3 | surface = { 4 | # Wing definition 5 | "name": "wing", # name of the surface 6 | "symmetry": True, # if true, model one half of wing 7 | # reflected across the plane y = 0 8 | "S_ref_type": "wetted", # how we compute the wing area, 9 | # can be 'wetted' or 'projected' 10 | "fem_model_type": "tube", 11 | "twist_cp": twist_cp, 12 | "mesh": mesh, 13 | # Aerodynamic performance of the lifting surface at 14 | # an angle of attack of 0 (alpha=0). 15 | # These CL0 and CD0 values are added to the CL and CD 16 | # obtained from aerodynamic analysis of the surface to get 17 | # the total CL and CD. 18 | # These CL0 and CD0 values do not vary wrt alpha. 19 | "CL0": 0.0, # CL of the surface at alpha=0 20 | "CD0": 0.015, # CD of the surface at alpha=0 21 | # Airfoil properties for viscous drag calculation 22 | "k_lam": 0.05, # percentage of chord with laminar 23 | # flow, used for viscous drag 24 | "t_over_c_cp": np.array([0.15]), # thickness over chord ratio (NACA0015) 25 | "c_max_t": 0.303, # chordwise location of maximum (NACA0015) 26 | # thickness 27 | "with_viscous": True, # if true, compute viscous drag 28 | "with_wave": False, # if true, compute wave drag 29 | } 30 | -------------------------------------------------------------------------------- /openaerostruct/docs/aero_walkthrough/part_3.py: -------------------------------------------------------------------------------- 1 | # Create the OpenMDAO problem 2 | prob = om.Problem() 3 | 4 | # Create an independent variable component that will supply the flow 5 | # conditions to the problem. 6 | indep_var_comp = om.IndepVarComp() 7 | indep_var_comp.add_output("v", val=248.136, units="m/s") 8 | indep_var_comp.add_output("alpha", val=5.0, units="deg") 9 | indep_var_comp.add_output("Mach_number", val=0.84) 10 | indep_var_comp.add_output("re", val=1.0e6, units="1/m") 11 | indep_var_comp.add_output("rho", val=0.38, units="kg/m**3") 12 | indep_var_comp.add_output("cg", val=np.zeros((3)), units="m") 13 | 14 | # Add this IndepVarComp to the problem model 15 | prob.model.add_subsystem("prob_vars", indep_var_comp, promotes=["*"]) 16 | -------------------------------------------------------------------------------- /openaerostruct/docs/aero_walkthrough/part_4.py: -------------------------------------------------------------------------------- 1 | # Create and add a group that handles the geometry for the 2 | # aerodynamic lifting surface 3 | geom_group = Geometry(surface=surface) 4 | prob.model.add_subsystem(surface["name"], geom_group) 5 | 6 | # Create the aero point group, which contains the actual aerodynamic 7 | # analyses 8 | aero_group = AeroPoint(surfaces=[surface]) 9 | point_name = "aero_point_0" 10 | prob.model.add_subsystem(point_name, aero_group, promotes_inputs=["v", "alpha", "Mach_number", "re", "rho", "cg"]) 11 | 12 | name = surface["name"] 13 | 14 | # Connect the mesh from the geometry component to the analysis point 15 | prob.model.connect(name + ".mesh", point_name + "." + name + ".def_mesh") 16 | 17 | # Perform the connections with the modified names within the 18 | # 'aero_states' group. 19 | prob.model.connect(name + ".mesh", point_name + ".aero_states." + name + "_def_mesh") 20 | 21 | prob.model.connect(name + ".t_over_c", point_name + "." + name + "_perf." + "t_over_c") 22 | -------------------------------------------------------------------------------- /openaerostruct/docs/aero_walkthrough/part_5.py: -------------------------------------------------------------------------------- 1 | # Import the Scipy Optimizer and set the driver of the problem to use 2 | # it, which defaults to an SLSQP optimization method 3 | import openmdao.api as om 4 | 5 | prob.driver = om.ScipyOptimizeDriver() 6 | prob.driver.options["tol"] = 1e-9 7 | 8 | recorder = om.SqliteRecorder("aero_analysis_test.db") 9 | prob.driver.add_recorder(recorder) 10 | prob.driver.recording_options["record_derivatives"] = True 11 | prob.driver.recording_options["includes"] = ["*"] 12 | 13 | # Setup problem and add design variables, constraint, and objective 14 | prob.model.add_design_var("wing.twist_cp", lower=-10.0, upper=15.0) 15 | prob.model.add_constraint(point_name + ".wing_perf.CL", equals=0.5) 16 | prob.model.add_objective(point_name + ".wing_perf.CD", scaler=1e4) 17 | -------------------------------------------------------------------------------- /openaerostruct/docs/aero_walkthrough/part_6.py: -------------------------------------------------------------------------------- 1 | # Set up and run the optimization problem 2 | prob.setup() 3 | prob.run_driver() 4 | -------------------------------------------------------------------------------- /openaerostruct/docs/aero_walkthrough/part_7.py: -------------------------------------------------------------------------------- 1 | assert_near_equal(prob["aero_point_0.wing_perf.CD"][0], 0.033389699871650073, 1e-6) 2 | assert_near_equal(prob["aero_point_0.wing_perf.CL"][0], 0.5, 1e-6) 3 | assert_near_equal(prob["aero_point_0.CM"][1], -1.7885550372372376, 1e-6) 4 | -------------------------------------------------------------------------------- /openaerostruct/docs/aerostructural_index.rst: -------------------------------------------------------------------------------- 1 | .. _Aerostructural Optimization: 2 | 3 | Aerostructural Optimization 4 | =========================== 5 | 6 | OpenAeroStruct implements two wing structural models: tubular spar and wingbox. 7 | We recommend going through the basic tubular spar case first. 8 | 9 | .. toctree:: 10 | :maxdepth: 1 11 | 12 | aerostructural_tube_walkthrough.rst 13 | 14 | Then, you can review the wingbox walkthrough to understand how OAS models more detailed wing structures. 15 | 16 | .. toctree:: 17 | :maxdepth: 1 18 | 19 | aerostructural_wingbox_walkthrough.rst -------------------------------------------------------------------------------- /openaerostruct/docs/aerostructural_tube_walkthrough.rst: -------------------------------------------------------------------------------- 1 | .. _Aerostructural_Walkthrough: 2 | 3 | Aerostructural Optimization with Tubular Spar 4 | ============================================= 5 | 6 | With aerodynamic- and structural-only analyses done, we now examine an aerostructural design problem. 7 | The construction of the problem follows the same logic as outlined in :ref:`Aerodynamic_Optimization_Walkthrough`, though with some added details. 8 | For example, we use an `AerostructPoint` group instead of an `AeroGroup` because it contains the additional components needed for aerostructural optimization. 9 | Additionally, we have more variable connections due to the more complex problem formulation. 10 | 11 | For more details about ``mesh_dict`` and ``surface`` in the following script, see :ref:`Mesh and Surface Dict`. 12 | 13 | .. literalinclude:: /../../tests/integration_tests/test_aerostruct.py 14 | :start-after: docs checkpoint 0 15 | :end-before: docs checkpoint 1 16 | :dedent: 8 17 | -------------------------------------------------------------------------------- /openaerostruct/docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # This file is execfile()d with the current directory set to its 3 | # containing dir. 4 | import os 5 | import sys 6 | import importlib 7 | from unittest.mock import Mock 8 | from openaerostruct.docs._utils.generate_sourcedocs import generate_docs 9 | from sphinx_mdolab_theme.config import * 10 | 11 | sys.path.insert(0, os.path.abspath("..")) 12 | sys.path.insert(0, os.path.abspath(".")) 13 | sys.path.insert(0, os.path.join("./_exts")) 14 | 15 | # Only mock the ones that don't import. 16 | MOCK_MODULES = ["h5py", "petsc4py", "pyoptsparse", "pyDOE2"] 17 | for mod_name in MOCK_MODULES: 18 | try: 19 | importlib.import_module(mod_name) 20 | except ImportError: 21 | sys.modules[mod_name] = Mock() 22 | 23 | # --- General configuration --- 24 | 25 | # Add any Sphinx extension module names here, as strings. They can be 26 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 27 | # ones. 28 | extensions = [ 29 | "sphinx.ext.autodoc", 30 | "sphinx.ext.doctest", 31 | "sphinx.ext.coverage", 32 | "sphinx.ext.mathjax", 33 | "sphinx.ext.viewcode", 34 | "sphinx.ext.githubpages", 35 | "numpydoc", 36 | "sphinx_copybutton", 37 | "sphinx_mdolab_theme.ext.embed_code", 38 | "sphinx_mdolab_theme.ext.embed_compare", 39 | "sphinx_mdolab_theme.ext.embed_n2", 40 | ] 41 | 42 | # directories for which to generate sourcedocs 43 | packages = [ 44 | "aerodynamics", 45 | "functionals", 46 | "geometry", 47 | "integration", 48 | "structures", 49 | "transfer", 50 | ] 51 | 52 | generate_docs("..", "../..", packages, project_name="openaerostruct") 53 | 54 | # If your documentation needs a minimal Sphinx version, state it here. 55 | needs_sphinx = "1.6.2" 56 | 57 | numpydoc_show_class_members = False 58 | 59 | # General information about the project. 60 | project = "OpenAeroStruct" 61 | copyright = "2018, John Jasa, Dr. John Hwang, Justin S. Gray" 62 | author = "John Jasa, Dr. John Hwang, Justin S. Gray" 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | 68 | import re 69 | 70 | __version__ = re.findall( 71 | r"""__version__ = ["']+([0-9\.]*)["']+""", 72 | open("../__init__.py").read(), 73 | )[0] 74 | 75 | # The short X.Y version. 76 | version = __version__ 77 | 78 | # The full version, including alpha/beta/rc tags. 79 | release = __version__ 80 | 81 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 82 | # using the given strftime format. 83 | html_last_updated_fmt = "%b %d, %Y" 84 | 85 | # Output file base name for HTML help builder. 86 | htmlhelp_basename = "OpenAeroStructdoc" 87 | 88 | html_extra_path = ["_n2html"] 89 | 90 | # The master toctree document. 91 | master_doc = "index" 92 | 93 | # One entry per manual page. List of tuples 94 | # (source start file, name, description, authors, manual section). 95 | man_pages = [(master_doc, "openaerostruct", "OpenAeroStruct Documentation", [author], 1)] 96 | -------------------------------------------------------------------------------- /openaerostruct/docs/figures/OptView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/figures/OptView.png -------------------------------------------------------------------------------- /openaerostruct/docs/figures/aero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/figures/aero.png -------------------------------------------------------------------------------- /openaerostruct/docs/figures/aero_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/figures/aero_sample.png -------------------------------------------------------------------------------- /openaerostruct/docs/figures/aerostruct_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/figures/aerostruct_sample.png -------------------------------------------------------------------------------- /openaerostruct/docs/figures/aerostruct_xdsm.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/figures/aerostruct_xdsm.pdf -------------------------------------------------------------------------------- /openaerostruct/docs/figures/aerostruct_xdsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/figures/aerostruct_xdsm.png -------------------------------------------------------------------------------- /openaerostruct/docs/figures/collapsed_aerostruct_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/figures/collapsed_aerostruct_diagram.png -------------------------------------------------------------------------------- /openaerostruct/docs/figures/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/figures/example.png -------------------------------------------------------------------------------- /openaerostruct/docs/figures/plotall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/figures/plotall.png -------------------------------------------------------------------------------- /openaerostruct/docs/figures/problem_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/figures/problem_diagram.png -------------------------------------------------------------------------------- /openaerostruct/docs/figures/wingbox_fine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/figures/wingbox_fine.png -------------------------------------------------------------------------------- /openaerostruct/docs/figures/wingbox_opt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/docs/figures/wingbox_opt.png -------------------------------------------------------------------------------- /openaerostruct/docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _Installation: 2 | 3 | Installation 4 | ============ 5 | 6 | To use OpenAeroStruct, you must first install Python 3. 7 | 8 | The easiest way to get started is to install OpenAeroStruct from PyPI : 9 | 10 | .. code-block:: bash 11 | 12 | pip install openaerostruct 13 | 14 | If you'd like easier access to the examples and source code, you can also install OpenAeroStruct by cloning the OpenAeroStruct repository: 15 | 16 | .. code-block:: bash 17 | 18 | git clone https://github.com/mdolab/OpenAeroStruct.git 19 | 20 | Then from within the OpenAeroStruct folder, pip install the package: 21 | 22 | .. code-block:: bash 23 | 24 | cd openaerostruct 25 | pip install -e . 26 | 27 | Both methods will automatically install the dependencies: numpy, scipy, matplotlib, and OpenMDAO. 28 | 29 | The oldest and latest versions of the dependencies that we test regularly are the following (other versions may work, but no guarantees): 30 | 31 | .. list-table:: 32 | :header-rows: 1 33 | 34 | * - Dependency 35 | - oldest 36 | - latest 37 | * - Python 38 | - 3.9 39 | - 3.11 40 | * - NumPy 41 | - 1.21 42 | - 1.26 43 | * - SciPy 44 | - 1.7 45 | - 1.15 46 | * - OpenMDAO 47 | - 3.35 48 | - latest 49 | * - pyGeo (optional) 50 | - 1.15.0 51 | - latest 52 | * - OpenVSP (optional) 53 | - 3.33 54 | - 3.33 55 | * - MPhys (optional) 56 | - 2.0 57 | - latest 58 | 59 | Numpy 2.0 or later should also work, but we currently do not run tests with Numpy 2. 60 | 61 | If you are unfamiliar with OpenMDAO and wish to modify the internals of OpenAeroStruct, you should examine the OpenMDAO documentation at https://openmdao.org/newdocs/versions/latest/main.html. The tutorials provided with OpenMDAO are helpful to understand the basics of using OpenMDAO to solve an optimization problem. 62 | 63 | Advanced Options 64 | ~~~~~~~~~~~~~~~~ 65 | 66 | To run the tests on your machine, use the [test] option. This will install the pytest package. 67 | 68 | .. code-block:: bash 69 | 70 | pip install -e .[test] 71 | 72 | Then run the tests from the OpenAeroStruct root directory by calling: 73 | 74 | .. code-block:: bash 75 | 76 | testflo -v . 77 | 78 | To install the dependencies to build the documentation locally, run: 79 | 80 | .. code-block:: bash 81 | 82 | pip install -e .[docs] 83 | -------------------------------------------------------------------------------- /openaerostruct/docs/quick_example.rst: -------------------------------------------------------------------------------- 1 | .. _Quick_Example: 2 | 3 | Quick Example 4 | ============= 5 | 6 | Here is an example run script to perform aerodynamic optimization. 7 | We'll go into more detail later about how to set up a model, define the optimization problem, and postprocess results. 8 | 9 | .. literalinclude:: /../../tests/integration_tests/test_aero.py 10 | :start-after: docs checkpoint 0 11 | :end-before: docs checkpoint 1 12 | :dedent: 8 13 | -------------------------------------------------------------------------------- /openaerostruct/docs/struct_example.rst: -------------------------------------------------------------------------------- 1 | .. _Structural_Optimization_Example: 2 | 3 | Structural Optimization 4 | ======================= 5 | 6 | OpenAeroStruct can also handle structural-only optimization problems. 7 | Here we prescribe a load on the spar and allow the optimizer to vary the structural thickness to minimize weight subject to failure constraints. 8 | Although doing structural-only optimizations is relatively rare, this is a building block towards aerostructural optimization. 9 | 10 | For more details about ``mesh_dict`` and ``surf_dict`` in the following script, see :ref:`Mesh and Surface Dict`. 11 | 12 | .. literalinclude:: /../../tests/integration_tests/test_struct.py 13 | :start-after: docs checkpoint 0 14 | :end-before: docs checkpoint 1 15 | :dedent: 8 16 | -------------------------------------------------------------------------------- /openaerostruct/docs/user_reference.rst: -------------------------------------------------------------------------------- 1 | .. _User Reference: 2 | 3 | User Reference 4 | ============== 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | 10 | user_reference/mesh_surface_dict.rst 11 | user_reference/debugging_tips.rst 12 | user_reference/v1_v2_conversion.rst -------------------------------------------------------------------------------- /openaerostruct/docs/user_reference/debugging_tips.rst: -------------------------------------------------------------------------------- 1 | .. _Debugging Tips: 2 | 3 | Debugging Tips 4 | ============== 5 | 6 | Here are some common issues and possible solutions for them. 7 | 8 | - When debugging, you should use the smallest mesh size possible to minimize time invested. 9 | 10 | - Start with a simple optimization case and gradually add design variables and constraints while examining the change in optimized result. The design trade-offs in a simpler optimization problem should be easier to understand. 11 | 12 | - Use `plot_wing` often to visualize your results. Make sure your wing geometry and structure are set up like you expect. 13 | 14 | - After running an optimization case, always check to see if your constraints are satisfied. pyOptSparse prints out this information automatically, but if you're using Scipy's optimizer, you may need to manually print the constraints. 15 | 16 | - Check out https://openmdao.org/newdocs/versions/latest/features/recording/case_reader_data.html for more info about how to access the saved data in the outputted `.db` file. 17 | 18 | - If you are unsure of where a parameter or unknown is within the problem, view the N2 diagram. You can generate an html file by writing `openmdao.n2(prob)` in the runscript. You can use the search function to look for specific variables. 19 | 20 | - Use the `thickness_intersects` constraint when varying thickness to maintain a physically possible solution. -------------------------------------------------------------------------------- /openaerostruct/examples/example_deriv_check.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import openmdao.api as om 4 | 5 | from numpy.testing import assert_almost_equal 6 | 7 | from openaerostruct.structures.section_properties_tube import SectionPropertiesTube 8 | from openaerostruct.utils.testing import get_default_surfaces, run_test, view_mat 9 | 10 | 11 | class Test(unittest.TestCase): 12 | """ 13 | This class contains two tests on a single component. 14 | Each test verifies the derivatives found within the component by using 15 | OpenMDAO's derivative approximation functions to compare against 16 | finite-difference or complex-step. 17 | """ 18 | 19 | def test_quick(self): 20 | """Short pre-setup test to compare component derivs.""" 21 | surfaces = get_default_surfaces() 22 | 23 | comp = SectionPropertiesTube(surface=surfaces[0]) 24 | 25 | run_test(self, comp, complex_flag=True) 26 | 27 | def test_detailed(self): 28 | """This is a longer version of the previous method, with plotting.""" 29 | 30 | # Load in default lifting surfaces to setup the comparison 31 | surfaces = get_default_surfaces() 32 | 33 | # Instantiate an OpenMDAO problem and add the component we want to test 34 | # as asubsystem, giving that component a default lifting surface 35 | prob = om.Problem() 36 | prob.model.add_subsystem("tube", SectionPropertiesTube(surface=surfaces[0])) 37 | 38 | # Set up the problem and ensure it uses complex arrays so we can check 39 | # the derivatives using complex step 40 | prob.setup(force_alloc_complex=True) 41 | 42 | # Actually run the model, which is just a component in this case, then 43 | # check the derivatives and store the results in the `check` dict 44 | prob.run_model() 45 | check = prob.check_partials(compact_print=False) 46 | 47 | # Loop through this `check` dictionary and visualize the approximated 48 | # and computed derivatives 49 | for key, subjac in check[list(check.keys())[0]].items(): 50 | print() 51 | print(key) 52 | view_mat(subjac["J_fd"], subjac["J_fwd"], key) 53 | 54 | # Loop through the `check` dictionary and perform assert that the 55 | # approximated deriv must be very close to the computed deriv 56 | for key, subjac in check[list(check.keys())[0]].items(): 57 | if subjac["magnitude"].fd > 1e-6: 58 | assert_almost_equal(subjac["rel error"].forward, 0.0, err_msg="deriv of %s wrt %s" % key) 59 | assert_almost_equal(subjac["rel error"].reverse, 0.0, err_msg="deriv of %s wrt %s" % key) 60 | 61 | 62 | # Run the tests included in this script 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /openaerostruct/functionals/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/functionals/__init__.py -------------------------------------------------------------------------------- /openaerostruct/functionals/sum_areas.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | 4 | class SumAreas(om.ExplicitComponent): 5 | """ 6 | Compute the total surface area of the entire aircraft as a sum of its 7 | individual surfaces' surface areas. 8 | 9 | Parameters 10 | ---------- 11 | S_ref : float 12 | Surface area for one lifting surface. 13 | 14 | Returns 15 | ------- 16 | S_ref_total : float 17 | Total surface area of the aircraft based on the sum of individual 18 | surface areas. 19 | 20 | """ 21 | 22 | def initialize(self): 23 | self.options.declare("surfaces", types=list) 24 | 25 | def setup(self): 26 | for surface in self.options["surfaces"]: 27 | name = surface["name"] 28 | self.add_input(name + "_S_ref", val=1.0, units="m**2") 29 | 30 | self.add_output("S_ref_total", val=0.0, units="m**2", tags=["mphys_result"]) 31 | 32 | self.declare_partials("*", "*", val=1.0) 33 | 34 | def compute(self, inputs, outputs): 35 | outputs["S_ref_total"] = 0.0 36 | for surface in self.options["surfaces"]: 37 | name = surface["name"] 38 | S_ref = inputs[name + "_S_ref"] 39 | outputs["S_ref_total"] += S_ref 40 | -------------------------------------------------------------------------------- /openaerostruct/functionals/total_aero_performance.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | from openaerostruct.functionals.moment_coefficient import MomentCoefficient 4 | from openaerostruct.functionals.total_lift_drag import TotalLiftDrag 5 | from openaerostruct.functionals.sum_areas import SumAreas 6 | 7 | 8 | class TotalAeroPerformance(om.Group): 9 | """ 10 | Group to contain the total aerodynamic performance components. 11 | """ 12 | 13 | def initialize(self): 14 | self.options.declare("surfaces", types=list) 15 | self.options.declare("user_specified_Sref", types=bool) 16 | 17 | def setup(self): 18 | surfaces = self.options["surfaces"] 19 | 20 | if not self.options["user_specified_Sref"]: 21 | self.add_subsystem( 22 | "sum_areas", SumAreas(surfaces=surfaces), promotes_inputs=["*S_ref"], promotes_outputs=["S_ref_total"] 23 | ) 24 | 25 | self.add_subsystem( 26 | "CL_CD", 27 | TotalLiftDrag(surfaces=surfaces), 28 | promotes_inputs=["*CL", "*CD", "*S_ref", "S_ref_total", "rho", "v"], 29 | promotes_outputs=["CL", "CD", "L", "D"], 30 | ) 31 | 32 | self.add_subsystem( 33 | "moment", 34 | MomentCoefficient(surfaces=surfaces), 35 | promotes_inputs=["v", "cg", "rho", "*S_ref", "*b_pts", "*widths", "*chords", "*sec_forces", "S_ref_total"], 36 | promotes_outputs=["CM"], 37 | ) 38 | -------------------------------------------------------------------------------- /openaerostruct/functionals/total_performance.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | from openaerostruct.functionals.breguet_range import BreguetRange 4 | from openaerostruct.functionals.equilibrium import Equilibrium 5 | from openaerostruct.functionals.center_of_gravity import CenterOfGravity 6 | from openaerostruct.functionals.moment_coefficient import MomentCoefficient 7 | from openaerostruct.functionals.total_lift_drag import TotalLiftDrag 8 | from openaerostruct.functionals.sum_areas import SumAreas 9 | 10 | 11 | class TotalPerformance(om.Group): 12 | """ 13 | Group to contain the total aerostructural performance components. 14 | """ 15 | 16 | def initialize(self): 17 | self.options.declare("surfaces", types=list) 18 | self.options.declare("user_specified_Sref", types=bool) 19 | self.options.declare("internally_connect_fuelburn", types=bool, default=True) 20 | 21 | def setup(self): 22 | surfaces = self.options["surfaces"] 23 | 24 | if not self.options["user_specified_Sref"]: 25 | self.add_subsystem( 26 | "sum_areas", SumAreas(surfaces=surfaces), promotes_inputs=["*S_ref"], promotes_outputs=["S_ref_total"] 27 | ) 28 | 29 | if self.options["internally_connect_fuelburn"]: 30 | promote_fuelburn = ["fuelburn"] 31 | else: 32 | promote_fuelburn = [] 33 | 34 | self.add_subsystem( 35 | "CL_CD", 36 | TotalLiftDrag(surfaces=surfaces), 37 | promotes_inputs=["*CL", "*CD", "*S_ref", "S_ref_total", "rho", "v"], 38 | promotes_outputs=["CL", "CD", "L", "D"], 39 | ) 40 | 41 | self.add_subsystem( 42 | "fuelburn", 43 | BreguetRange(surfaces=surfaces), 44 | promotes_inputs=["*structural_mass", "CL", "CD", "CT", "speed_of_sound", "R", "Mach_number", "W0"], 45 | promotes_outputs=["fuelburn"], 46 | ) 47 | 48 | self.add_subsystem( 49 | "L_equals_W", 50 | Equilibrium(surfaces=surfaces), 51 | promotes_inputs=["CL", "*structural_mass", "S_ref_total", "W0", "load_factor", "rho", "v"] 52 | + promote_fuelburn, 53 | promotes_outputs=["L_equals_W", "total_weight"], 54 | ) 55 | 56 | self.add_subsystem( 57 | "CG", 58 | CenterOfGravity(surfaces=surfaces), 59 | promotes_inputs=["*structural_mass", "*cg_location", "total_weight", "W0", "empty_cg", "load_factor"] 60 | + promote_fuelburn, 61 | promotes_outputs=["cg"], 62 | ) 63 | 64 | self.add_subsystem( 65 | "moment", 66 | MomentCoefficient(surfaces=surfaces), 67 | promotes_inputs=["v", "rho", "cg", "S_ref_total", "*b_pts", "*widths", "*chords", "*sec_forces", "*S_ref"], 68 | promotes_outputs=["CM"], 69 | ) 70 | -------------------------------------------------------------------------------- /openaerostruct/geometry/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/geometry/__init__.py -------------------------------------------------------------------------------- /openaerostruct/geometry/ffd_component.py: -------------------------------------------------------------------------------- 1 | """ Manipulate geometry mesh based on high-level design parameters. """ 2 | 3 | import numpy as np 4 | 5 | import openmdao.api as om 6 | 7 | try: 8 | from pygeo import DVGeometry 9 | except ImportError: 10 | pass 11 | 12 | 13 | class GeometryMesh(om.ExplicitComponent): 14 | """ 15 | OpenMDAO component that performs mesh manipulation functions. It reads in 16 | the initial mesh from the surface dictionary and outputs the altered 17 | mesh based on the geometric design variables. 18 | 19 | Depending on the design variables selected or the supplied geometry information, 20 | only some of the follow parameters will actually be given to this component. 21 | If parameters are not active (they do not deform the mesh), then 22 | they will not be given to this component. 23 | 24 | Parameters 25 | ---------- 26 | 27 | Returns 28 | ------- 29 | mesh[nx, ny, 3] : numpy array 30 | Modified mesh based on the initial mesh in the surface dictionary and 31 | the geometric design variables. 32 | """ 33 | 34 | def initialize(self): 35 | self.options.declare("surface", types=dict) 36 | self.options.declare("DVGeo", types=DVGeometry) 37 | 38 | def setup(self): 39 | self.surface = surface = self.options["surface"] 40 | self.mx, self.my = self.surface["mx"], self.surface["my"] 41 | 42 | self.DVGeo = self.options["DVGeo"] 43 | 44 | # Associate a 'reference axis' for large-scale manipulation 45 | self.DVGeo.addRefAxis("wing_axis", xFraction=0.25, alignIndex="i") 46 | 47 | # Now add local (shape) variables 48 | self.DVGeo.addLocalDV("shape", lower=-0.5, upper=0.5, axis="z") 49 | 50 | pts = surface["mesh"].reshape(-1, 3) 51 | 52 | self.DVGeo.addPointSet(pts, "surface") 53 | 54 | coords = self.DVGeo.getLocalIndex(0) 55 | self.inds = coords[:, 0, :] 56 | self.inds2 = coords[:, 1, :] 57 | 58 | self.add_input("shape", val=np.zeros((self.mx, self.my)), units="m") 59 | 60 | self.add_output("mesh", val=surface["mesh"], units="m") 61 | 62 | self.declare_partials("*", "*") 63 | 64 | def compute(self, inputs, outputs): 65 | surface = self.surface 66 | 67 | dvs = self.DVGeo.getValues() 68 | 69 | for i, row in enumerate(self.inds): 70 | for j, ind in enumerate(row): 71 | ind2 = self.inds2[i, j] 72 | dvs["shape"][ind] = inputs["shape"][i, j] 73 | dvs["shape"][ind2] = inputs["shape"][i, j] 74 | 75 | self.DVGeo.setDesignVars(dvs) 76 | coords = self.DVGeo.update("surface") 77 | 78 | mesh = coords.copy() 79 | mesh = mesh.reshape(surface["mesh"].shape) 80 | outputs["mesh"] = mesh 81 | 82 | def compute_partials(self, inputs, partials): 83 | self.DVGeo.computeTotalJacobian("surface") 84 | jac = self.DVGeo.JT["surface"].toarray().T 85 | my_jac = partials["mesh", "shape"] 86 | my_jac[:, :] = 0.0 87 | 88 | for i, ind in enumerate(self.inds.flatten()): 89 | ind2 = self.inds2.flatten()[i] 90 | my_jac[:, i] += jac[:, ind] 91 | my_jac[:, i] += jac[:, ind2] 92 | 93 | partials["mesh", "shape"] = my_jac 94 | -------------------------------------------------------------------------------- /openaerostruct/geometry/monotonic_constraint.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | class MonotonicConstraint(om.ExplicitComponent): 7 | """ 8 | Produce a constraint that is violated if a user-chosen measure on the 9 | wing does not decrease monotonically from the root to the tip. 10 | 11 | Parameters 12 | ---------- 13 | var_name : string 14 | The variable to which the user would like to apply the monotonic constraint. 15 | 16 | Returns 17 | ------- 18 | monotonic[ny-1] : numpy array 19 | Values are greater than 0 if the constraint is violated. 20 | 21 | """ 22 | 23 | def initialize(self): 24 | self.options.declare("var_name", types=str) 25 | self.options.declare("surface", types=dict) 26 | 27 | def setup(self): 28 | self.surface = surface = self.options["surface"] 29 | self.var_name = self.options["var_name"] 30 | self.con_name = "monotonic_" + self.var_name 31 | 32 | self.symmetry = surface["symmetry"] 33 | self.ny = surface["mesh"].shape[1] 34 | 35 | self.add_input(self.var_name, val=np.zeros(self.ny)) 36 | self.add_output(self.con_name, val=np.zeros(self.ny - 1)) 37 | 38 | rows = np.arange(0, self.ny - 1) 39 | rows = np.vstack((rows, rows)).flatten(order="F") 40 | 41 | cols = np.arange(1, self.ny - 1) 42 | cols = np.vstack((cols, cols)).flatten(order="F") 43 | cols = np.insert(cols, 0, 0) 44 | cols = np.append(cols, self.ny - 1) 45 | 46 | sparse_val = np.ones_like(rows) 47 | sparse_val[1::2] = -1 48 | if not self.symmetry: 49 | if self.ny % 2 == 0: 50 | sparse_val[self.ny - 2 :] *= -1 51 | else: 52 | sparse_val[self.ny - 1 :] *= -1 53 | 54 | self.declare_partials(self.con_name, self.var_name, rows=rows, cols=cols, val=sparse_val) 55 | 56 | def compute(self, inputs, outputs): 57 | # Compute the difference between adjacent variable values 58 | diff = inputs[self.var_name][:-1] - inputs[self.var_name][1:] 59 | if self.symmetry: 60 | outputs[self.con_name] = diff 61 | else: 62 | ny2 = (self.ny - 1) // 2 63 | outputs[self.con_name][:ny2] = diff[:ny2] 64 | outputs[self.con_name][ny2:] = -diff[ny2:] 65 | -------------------------------------------------------------------------------- /openaerostruct/geometry/radius_comp.py: -------------------------------------------------------------------------------- 1 | """ Manipulate geometry mesh based on high-level design parameters. """ 2 | 3 | import numpy as np 4 | 5 | import openmdao.api as om 6 | from openaerostruct.structures.utils import radii 7 | 8 | 9 | class RadiusComp(om.ExplicitComponent): 10 | """ 11 | Compute the radius of a structural spar based on the mesh and thickness over 12 | chord ratio. 13 | 14 | Parameters 15 | ---------- 16 | mesh[nx, ny, 3] : numpy array 17 | Nodal mesh defining the initial aerodynamic surface.. 18 | t_over_c[ny-1] : numpy array 19 | The streamwise thickness-to-chord ratio of each VLM panel. 20 | 21 | Returns 22 | ------- 23 | radius[ny-1] : numpy array 24 | Radius of each element of the FEM spar. 25 | 26 | """ 27 | 28 | def initialize(self): 29 | self.options.declare("surface", types=dict) 30 | 31 | def setup(self): 32 | surface = self.options["surface"] 33 | 34 | nx, ny = self.nx, self.ny = surface["mesh"].shape[:2] 35 | 36 | self.add_input("mesh", val=np.zeros((nx, ny, 3)), units="m") 37 | self.add_input("t_over_c", val=np.ones((ny - 1))) 38 | self.add_output("radius", val=np.ones((ny - 1)), units="m") 39 | 40 | arange = np.arange(ny - 1) 41 | self.declare_partials("radius", "t_over_c", rows=arange, cols=arange) 42 | 43 | row = np.tile(np.zeros(6), ny - 1) + np.repeat(arange, 6) 44 | rows = np.concatenate([row, row]) 45 | col = np.tile(np.arange(6), ny - 1) + np.repeat(3 * arange, 6) 46 | cols = np.concatenate([col, col + (nx - 1) * 3 * ny]) 47 | 48 | self.declare_partials("radius", "mesh", rows=rows, cols=cols) 49 | 50 | def compute(self, inputs, outputs): 51 | outputs["radius"] = radii(inputs["mesh"], inputs["t_over_c"]) 52 | 53 | def compute_partials(self, inputs, partials): 54 | """ 55 | Obtain the radii of the FEM element based on local chord. 56 | """ 57 | mesh = inputs["mesh"] 58 | vectors = mesh[-1, :, :] - mesh[0, :, :] 59 | chords = np.sqrt(np.sum(vectors**2, axis=1)) 60 | t_c = inputs["t_over_c"] 61 | 62 | dr_dtoc = 0.25 * (chords[:-1] + chords[1:]) 63 | partials["radius", "t_over_c"] = dr_dtoc 64 | 65 | dr_dchords = 0.25 * t_c 66 | dr = mesh[0, :] - mesh[-1, :] 67 | 68 | length = np.sqrt(np.sum(dr**2, axis=1)) 69 | dr = dr / length[:, np.newaxis] 70 | 71 | drad = np.empty((self.ny - 1, 6)) 72 | drad[:, :3] = np.einsum("i, ij -> ij", dr_dchords, dr[:-1, :]) 73 | drad[:, 3:] = np.einsum("i, ij -> ij", dr_dchords, dr[1:, :]) 74 | drad = drad.flatten() 75 | 76 | nn = 6 * (self.ny - 1) 77 | partials["radius", "mesh"][:nn] = drad 78 | partials["radius", "mesh"][nn:] = -drad 79 | -------------------------------------------------------------------------------- /openaerostruct/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/integration/__init__.py -------------------------------------------------------------------------------- /openaerostruct/integration/multipoint_comps.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | 4 | class MultiCD(om.ExplicitComponent): 5 | def initialize(self): 6 | self.options.declare("n_points", types=int) 7 | 8 | def setup(self): 9 | self.n_points = self.options["n_points"] 10 | for i in range(self.n_points): 11 | self.add_input(str(i) + "_CD", val=0.0) 12 | 13 | self.add_output("CD", val=0.0) 14 | self.declare_partials("*", "*", val=1.0) 15 | 16 | def compute(self, inputs, outputs): 17 | outputs["CD"] = 0.0 18 | for i in range(self.n_points): 19 | outputs["CD"] += inputs[str(i) + "_CD"] 20 | -------------------------------------------------------------------------------- /openaerostruct/mphys/__init__.py: -------------------------------------------------------------------------------- 1 | from .aero_builder import AeroBuilder 2 | 3 | __all__ = ["AeroBuilder"] 4 | -------------------------------------------------------------------------------- /openaerostruct/mphys/aero_mesh.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import openmdao.api as om 3 | from mphys.core import MPhysVariables 4 | 5 | from openaerostruct.mphys.utils import get_number_of_nodes, get_src_indices 6 | 7 | 8 | class AeroMesh(om.IndepVarComp): 9 | """ 10 | Component to read the initial mesh coordinates with OAS. 11 | Only the root will be responsible for this information. 12 | The mesh will be broadcasted to all other processors in a following step. 13 | """ 14 | 15 | def initialize(self): 16 | self.options.declare("surfaces", default=None, desc="oas surface dicts", recordable=False) 17 | 18 | def setup(self): 19 | if self.comm.rank == 0: 20 | self.surfaces = self.options["surfaces"] 21 | nnodes = get_number_of_nodes(self.surfaces) 22 | src_indices = get_src_indices(self.surfaces) 23 | xpts = np.zeros(nnodes * 3) 24 | for surface in self.surfaces: 25 | surf_name = surface["name"] 26 | xpts[src_indices[surf_name]] = surface["mesh"] 27 | else: 28 | xpts = np.zeros(0) 29 | self.add_output( 30 | MPhysVariables.Aerodynamics.Surface.Mesh.COORDINATES, 31 | distributed=True, 32 | val=xpts, 33 | shape=xpts.size, 34 | units="m", 35 | desc="aero node coordinates", 36 | tags=["mphys_coordinates"], 37 | ) 38 | -------------------------------------------------------------------------------- /openaerostruct/mphys/aero_solver_group.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | from mphys.core import MPhysVariables 3 | 4 | from openaerostruct.aerodynamics.compressible_states import CompressibleVLMStates 5 | from openaerostruct.aerodynamics.geometry import VLMGeometry 6 | from openaerostruct.aerodynamics.states import VLMStates 7 | 8 | 9 | class AeroSolverGroup(om.Group): 10 | """ 11 | Group that contains the states for a incompresible/compressible aerodynamic analysis. 12 | """ 13 | 14 | def initialize(self): 15 | self.options.declare("surfaces", default=None, desc="oas surface dicts", recordable=False) 16 | self.options.declare("compressible", default=True, desc="prandtl glauert compressibiity flag", recordable=True) 17 | 18 | def setup(self): 19 | self.surfaces = self.options["surfaces"] 20 | 21 | # Loop through each surface and promote relevant parameters 22 | proms_in = [ 23 | ("alpha", MPhysVariables.Aerodynamics.FlowConditions.ANGLE_OF_ATTACK), 24 | ("beta", MPhysVariables.Aerodynamics.FlowConditions.YAW_ANGLE), 25 | ] 26 | proms_out = [] 27 | for surface in self.surfaces: 28 | name = surface["name"] 29 | 30 | proms_in.append((name + "_normals", name + ".normals")) 31 | proms_out.append((name + "_sec_forces", name + ".sec_forces")) 32 | 33 | self.add_subsystem(name, VLMGeometry(surface=surface), promotes_inputs=[("def_mesh", name + "_def_mesh")]) 34 | 35 | if self.options["compressible"]: 36 | proms_in.append(("Mach_number", MPhysVariables.Aerodynamics.FlowConditions.MACH_NUMBER)) 37 | aero_states = CompressibleVLMStates(surfaces=self.surfaces) 38 | else: 39 | aero_states = VLMStates(surfaces=self.surfaces) 40 | 41 | self.add_subsystem( 42 | "solver", 43 | aero_states, 44 | promotes_inputs=proms_in + ["*"], 45 | promotes_outputs=proms_out + ["circulations", "*_mesh_point_forces"], 46 | ) 47 | -------------------------------------------------------------------------------- /openaerostruct/mphys/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def get_number_of_nodes(surfaces): 5 | """ 6 | Get the total number of nodes over all surfaces. 7 | 8 | Parameters 9 | ---------- 10 | surfaces : list[dict] 11 | List of all surfaces. 12 | 13 | Returns 14 | ------- 15 | nnodes : int 16 | Total number of nodes across all surfaces. 17 | 18 | """ 19 | nnodes = 0 20 | for surface in surfaces: 21 | nnodes += surface["mesh"].size // 3 22 | return nnodes 23 | 24 | 25 | def get_src_indices(surfaces): 26 | """ 27 | Get src indices for each surface that will project each mesh into a single flattened array. 28 | 29 | Parameters 30 | ---------- 31 | surfaces : list[dict] 32 | List of all surfaces. 33 | 34 | Returns 35 | ------- 36 | src_indices : dict 37 | Dictionary holding source indices sorted by surface name. 38 | 39 | """ 40 | src_indices = {} 41 | nindices = 0 42 | for surface in surfaces: 43 | surf_name = surface["name"] 44 | mesh = surface["mesh"] 45 | nx, ny, _ = mesh.shape 46 | surf_indices = np.arange(mesh.size) + nindices 47 | src_indices[surf_name] = surf_indices.reshape(nx, ny, 3) 48 | nindices += mesh.size 49 | return src_indices 50 | 51 | 52 | def get_node_indices(surfaces): 53 | """ 54 | Get node indices for each surface that define the ordering of each node for all surfaces. 55 | 56 | Parameters 57 | ---------- 58 | surfaces : list[dict] 59 | List of all surfaces. 60 | 61 | Returns 62 | ------- 63 | node_indices : dict 64 | Dictionary holding node indices sorted by surface name. 65 | 66 | """ 67 | node_indices = {} 68 | nnodes = 0 69 | for surface in surfaces: 70 | surf_name = surface["name"] 71 | mesh = surface["mesh"] 72 | nx, ny, _ = mesh.shape 73 | surf_indices = np.arange(nx * ny) + nnodes 74 | node_indices[surf_name] = surf_indices.reshape(nx, ny) 75 | nnodes += nx * ny 76 | return node_indices 77 | -------------------------------------------------------------------------------- /openaerostruct/structures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/structures/__init__.py -------------------------------------------------------------------------------- /openaerostruct/structures/assemble_k_group.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | from openaerostruct.structures.transform import Transform 3 | from openaerostruct.structures.length import Length 4 | from openaerostruct.structures.local_stiff import LocalStiff 5 | from openaerostruct.structures.local_stiff_permuted import LocalStiffPermuted 6 | from openaerostruct.structures.local_stiff_transformed import LocalStiffTransformed 7 | 8 | 9 | class AssembleKGroup(om.Group): 10 | """Assemble that there compact local stiffness matrix.""" 11 | 12 | def initialize(self): 13 | self.options.declare("surface", types=dict) 14 | 15 | def setup(self): 16 | surface = self.options["surface"] 17 | 18 | comp = Transform(surface=surface) 19 | self.add_subsystem("transform", comp, promotes=["*"]) 20 | 21 | comp = Length(surface=surface) 22 | self.add_subsystem("length", comp, promotes=["*"]) 23 | 24 | comp = LocalStiff(surface=surface) 25 | self.add_subsystem("local_stiff", comp, promotes=["*"]) 26 | 27 | comp = LocalStiffPermuted(surface=surface) 28 | self.add_subsystem("local_stiff_permuted", comp, promotes=["*"]) 29 | 30 | comp = LocalStiffTransformed(surface=surface) 31 | self.add_subsystem("local_stiff_transformed", comp, promotes=["*"]) 32 | -------------------------------------------------------------------------------- /openaerostruct/structures/compute_nodes.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | class ComputeNodes(om.ExplicitComponent): 7 | """ 8 | Compute FEM nodes based on aerodynamic mesh. 9 | 10 | The FEM nodes are placed at fem_origin * chord, 11 | with the default fem_origin = 0.35. 12 | 13 | Parameters 14 | ---------- 15 | mesh[nx, ny, 3] : numpy array 16 | Array defining the nodal points of the lifting surface. 17 | 18 | Returns 19 | ------- 20 | nodes[ny, 3] : numpy array 21 | Flattened array with coordinates for each FEM node. 22 | 23 | """ 24 | 25 | def initialize(self): 26 | self.options.declare("surface", types=dict) 27 | 28 | def setup(self): 29 | surface = self.options["surface"] 30 | mesh = surface["mesh"] 31 | nx = mesh.shape[0] 32 | ny = mesh.shape[1] 33 | 34 | if surface["fem_model_type"] == "tube": 35 | self.fem_origin = surface["fem_origin"] 36 | else: 37 | y_upper = surface["data_y_upper"] 38 | x_upper = surface["data_x_upper"] 39 | y_lower = surface["data_y_lower"] 40 | 41 | self.fem_origin = (x_upper[0] * (y_upper[0] - y_lower[0]) + x_upper[-1] * (y_upper[-1] - y_lower[-1])) / ( 42 | (y_upper[0] - y_lower[0]) + (y_upper[-1] - y_lower[-1]) 43 | ) 44 | 45 | self.add_input("mesh", val=np.zeros((nx, ny, 3)), units="m") 46 | self.add_output("nodes", val=np.zeros((ny, 3)), units="m") 47 | 48 | w = self.fem_origin 49 | n = ny * 3 50 | 51 | data = np.zeros((2 * n)) 52 | data[:n] = 1 - w 53 | data[n:] = w 54 | 55 | arange = np.arange(n) 56 | 57 | rows = np.hstack((arange, arange)) 58 | cols = np.hstack((arange, arange + (nx - 1) * n)) 59 | 60 | self.declare_partials("nodes", "mesh", rows=rows, cols=cols, val=data) 61 | 62 | def compute(self, inputs, outputs): 63 | w = self.fem_origin 64 | mesh = inputs["mesh"] 65 | outputs["nodes"] = (1 - w) * mesh[0, :, :] + w * mesh[-1, :, :] 66 | -------------------------------------------------------------------------------- /openaerostruct/structures/create_rhs.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | class CreateRHS(om.ExplicitComponent): 7 | """ 8 | Compute the right-hand-side of the K * u = f linear system to solve for the displacements. 9 | The RHS is based on the loads. For the aerostructural case, these are 10 | recomputed at each design point based on the aerodynamic loads. 11 | 12 | Parameters 13 | ---------- 14 | loads[ny, 6] : numpy array 15 | Flattened array containing the loads applied on the FEM component, 16 | computed from the sectional forces. 17 | 18 | Returns 19 | ------- 20 | forces[6*(ny+1)] : numpy array 21 | Right-hand-side of the linear system. The loads from the aerodynamic 22 | analysis or the user-defined loads. 23 | """ 24 | 25 | def initialize(self): 26 | self.options.declare("surface", types=dict) 27 | 28 | def setup(self): 29 | surface = self.options["surface"] 30 | 31 | self.ny = surface["mesh"].shape[1] 32 | 33 | self.add_input("total_loads", val=np.zeros((self.ny, 6)), units="N") 34 | self.add_output("forces", val=np.ones(((self.ny + 1) * 6)), units="N") 35 | 36 | n = self.ny * 6 37 | arange = np.arange((n)) 38 | self.declare_partials("forces", "total_loads", val=1.0, rows=arange, cols=arange) 39 | 40 | def compute(self, inputs, outputs): 41 | outputs["forces"][:] = 0.0 42 | 43 | # Populate the right-hand side of the linear system using the 44 | # prescribed or computed loads 45 | outputs["forces"][: 6 * self.ny] += inputs["total_loads"].reshape(self.ny * 6) 46 | 47 | # Remove extremely small values from the RHS so the linear system 48 | # can more easily be solved 49 | outputs["forces"][np.abs(outputs["forces"]) < 1e-6] = 0.0 50 | -------------------------------------------------------------------------------- /openaerostruct/structures/disp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | class Disp(om.ExplicitComponent): 7 | """ 8 | Reshape the flattened displacements from the linear system solution into 9 | a 2D array so we can more easily use the results. 10 | 11 | The solution to the linear system has meaingless entires due to the 12 | constraints on the FEM model. The displacements from this portion of 13 | the linear system are not needed, so we select only the relevant 14 | portion of the displacements for further calculations. 15 | 16 | Parameters 17 | ---------- 18 | disp_aug[6*(ny+1)] : numpy array 19 | Augmented displacement array. Obtained by solving the system 20 | K * disp_aug = forces, where forces is a flattened version of loads. 21 | 22 | Returns 23 | ------- 24 | disp[6*ny] : numpy array 25 | Actual displacement array formed by truncating disp_aug. 26 | 27 | """ 28 | 29 | def initialize(self): 30 | self.options.declare("surface", types=dict) 31 | 32 | def setup(self): 33 | surface = self.options["surface"] 34 | 35 | self.ny = surface["mesh"].shape[1] 36 | 37 | self.add_input("disp_aug", val=np.zeros(((self.ny + 1) * 6)), units="m") 38 | self.add_output("disp", val=np.zeros((self.ny, 6)), units="m") 39 | 40 | n = self.ny * 6 41 | arange = np.arange((n)) 42 | self.declare_partials("disp", "disp_aug", val=1.0, rows=arange, cols=arange) 43 | 44 | def compute(self, inputs, outputs): 45 | # Obtain the relevant portions of disp_aug and store the reshaped 46 | # displacements in disp 47 | outputs["disp"] = inputs["disp_aug"][:-6].reshape((-1, 6)) 48 | -------------------------------------------------------------------------------- /openaerostruct/structures/energy.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | class Energy(om.ExplicitComponent): 7 | """Compute strain energy. 8 | 9 | Parameters 10 | ---------- 11 | disp[ny, 6] : numpy array 12 | Actual displacement array formed by truncating disp_aug. 13 | loads[ny, 6] : numpy array 14 | Array containing the loads applied on the FEM component, 15 | computed from the sectional forces. 16 | 17 | Returns 18 | ------- 19 | energy : float 20 | Total strain energy of the structural component. 21 | 22 | """ 23 | 24 | def initialize(self): 25 | self.options.declare("surface", types=dict) 26 | 27 | def setup(self): 28 | surface = self.options["surface"] 29 | 30 | ny = surface["mesh"].shape[1] 31 | 32 | self.add_input("disp", val=np.zeros((ny, 6)), units="m") 33 | self.add_input("loads", val=np.zeros((ny, 6)), units="N") 34 | self.add_output("energy", val=0.0, units="N*m") 35 | 36 | self.declare_partials("*", "*") 37 | 38 | def compute(self, inputs, outputs): 39 | outputs["energy"] = np.sum(inputs["disp"] * inputs["loads"]) 40 | 41 | def compute_partials(self, inputs, partials): 42 | partials["energy", "disp"][0, :] = inputs["loads"].real.flatten() 43 | partials["energy", "loads"][0, :] = inputs["disp"].real.flatten() 44 | -------------------------------------------------------------------------------- /openaerostruct/structures/failure_exact.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | class FailureExact(om.ExplicitComponent): 7 | """ 8 | Output individual failure constraints on each FEM element. 9 | 10 | Parameters 11 | ---------- 12 | vonmises[ny-1, 2] : numpy array 13 | von Mises stress magnitudes for each FEM element. 14 | 15 | Returns 16 | ------- 17 | failure[ny-1, 2] : numpy array 18 | Array of failure conditions. Positive if element has failed. 19 | 20 | """ 21 | 22 | def initialize(self): 23 | self.options.declare("surface", types=dict) 24 | 25 | def setup(self): 26 | surface = self.options["surface"] 27 | 28 | if surface["fem_model_type"] == "tube": 29 | num_failure_criteria = 2 30 | elif surface["fem_model_type"] == "wingbox": 31 | num_failure_criteria = 4 32 | 33 | self.ny = surface["mesh"].shape[1] 34 | self.sigma = surface["yield"] 35 | 36 | self.add_input("vonmises", val=np.zeros((self.ny - 1, num_failure_criteria)), units="N/m**2") 37 | self.add_output("failure", val=np.zeros((self.ny - 1, num_failure_criteria))) 38 | 39 | self.declare_partials("failure", "vonmises", val=np.eye(((self.ny - 1) * num_failure_criteria)) / self.sigma) 40 | 41 | def compute(self, inputs, outputs): 42 | outputs["failure"] = inputs["vonmises"] / self.sigma - 1 43 | -------------------------------------------------------------------------------- /openaerostruct/structures/fuel_loads.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | from openaerostruct.utils.constants import grav_constant 5 | 6 | 7 | def norm(vec): 8 | return np.sqrt(np.sum(vec**2)) 9 | 10 | 11 | class FuelLoads(om.ExplicitComponent): 12 | """ 13 | Compute the nodal loads from the distributed fuel within the wing 14 | to be applied to the wing structure. 15 | 16 | Parameters 17 | ---------- 18 | 19 | Returns 20 | ------- 21 | 22 | """ 23 | 24 | def initialize(self): 25 | self.options.declare("surface", types=dict) 26 | 27 | def setup(self): 28 | self.surface = surface = self.options["surface"] 29 | self.ny = surface["mesh"].shape[1] 30 | 31 | self.add_input("fuel_vols", val=np.ones((self.ny - 1)), units="m**3") 32 | self.add_input("nodes", val=np.zeros((self.ny, 3)), units="m") 33 | self.add_input("fuel_mass", val=1.0, units="kg") 34 | self.add_input("load_factor", val=1.0) 35 | self.add_output("fuel_weight_loads", val=np.zeros((self.ny, 6)), units="N") 36 | 37 | self.declare_partials("*", "*", method="cs") 38 | 39 | def compute(self, inputs, outputs): 40 | nodes = inputs["nodes"] 41 | 42 | element_lengths = np.ones(self.ny - 1, dtype=complex) 43 | for i in range(self.ny - 1): 44 | element_lengths[i] = norm(nodes[i + 1] - nodes[i]) 45 | 46 | # And we also need the deltas between consecutive nodes 47 | deltas = nodes[1:, :] - nodes[:-1, :] 48 | 49 | # Fuel weight 50 | fuel_weight = (inputs["fuel_mass"] + self.surface["Wf_reserve"]) * grav_constant * inputs["load_factor"] 51 | 52 | if self.surface["symmetry"]: 53 | fuel_weight /= 2.0 54 | 55 | vols = inputs["fuel_vols"] 56 | sum_vols = np.sum(vols) 57 | 58 | # Now we need the fuel weight per segment 59 | # Assume it's divided evenly based on vols 60 | z_weights = vols * fuel_weight / sum_vols 61 | 62 | # Assume weight coincides with the elastic axis 63 | z_forces_for_each = z_weights / 2.0 64 | z_moments_for_each = ( 65 | z_weights * element_lengths / 12.0 * (deltas[:, 0] ** 2 + deltas[:, 1] ** 2) ** 0.5 / element_lengths 66 | ) 67 | 68 | loads = np.zeros((self.ny, 6), dtype=complex) 69 | 70 | # Loads in z-direction 71 | loads[:-1, 2] = loads[:-1, 2] - z_forces_for_each 72 | loads[1:, 2] = loads[1:, 2] - z_forces_for_each 73 | 74 | # Bending moments for consistency 75 | loads[:-1, 3] = loads[:-1, 3] - z_moments_for_each * deltas[:, 1] / element_lengths 76 | loads[1:, 3] = loads[1:, 3] + z_moments_for_each * deltas[:, 1] / element_lengths 77 | 78 | loads[:-1, 4] = loads[:-1, 4] - z_moments_for_each * deltas[:, 0] / element_lengths 79 | loads[1:, 4] = loads[1:, 4] + z_moments_for_each * deltas[:, 0] / element_lengths 80 | 81 | outputs["fuel_weight_loads"] = loads 82 | -------------------------------------------------------------------------------- /openaerostruct/structures/fuel_vol.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | def norm(vec): 7 | return np.sqrt(np.sum(vec**2)) 8 | 9 | 10 | class WingboxFuelVol(om.ExplicitComponent): 11 | """ 12 | Computes the internal volumes of the wingbox segments. 13 | 14 | parameters 15 | ---------- 16 | nodes[ny, 3] : numpy array 17 | Coordinates of FEM nodes. 18 | A_int[ny-1] : numpy array 19 | Internal cross-sectional area of each wingbox segment. 20 | 21 | Returns 22 | ------- 23 | fuel_vols[ny-1] : numpy array 24 | Internal volume of each wingbox segment. 25 | """ 26 | 27 | def initialize(self): 28 | self.options.declare("surface", types=dict) 29 | 30 | def setup(self): 31 | self.surface = surface = self.options["surface"] 32 | 33 | self.ny = surface["mesh"].shape[1] 34 | 35 | self.add_input("nodes", val=np.zeros((self.ny, 3)), units="m") 36 | self.add_input("A_int", val=np.zeros((self.ny - 1)), units="m**2") 37 | self.add_output("fuel_vols", val=np.zeros((self.ny - 1)), units="m**3") 38 | 39 | self.declare_partials("*", "*", method="cs") 40 | 41 | def compute(self, inputs, outputs): 42 | nodes = inputs["nodes"] 43 | 44 | element_lengths = np.zeros(self.ny - 1, dtype=type(nodes[0, 0])) 45 | 46 | for i in range(self.ny - 1): 47 | element_lengths[i] = norm(nodes[i + 1] - nodes[i]) 48 | 49 | # Next we multiply the element lengths with the A_int for the internal volumes of the wingobox segments 50 | vols = element_lengths * inputs["A_int"] 51 | 52 | outputs["fuel_vols"] = vols 53 | -------------------------------------------------------------------------------- /openaerostruct/structures/length.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | from openaerostruct.utils.vector_algebra import compute_norm 6 | 7 | 8 | class Length(om.ExplicitComponent): 9 | def initialize(self): 10 | self.options.declare("surface", types=dict) 11 | 12 | def setup(self): 13 | surface = self.options["surface"] 14 | 15 | self.ny = ny = surface["mesh"].shape[1] 16 | 17 | self.add_input("nodes", shape=(ny, 3), units="m") 18 | self.add_output("element_lengths", shape=ny - 1, units="m") 19 | 20 | mesh_indices = np.arange(3 * ny).reshape((ny, 3)) 21 | length_indices = np.arange(ny - 1) 22 | 23 | rows = np.tile(np.outer(length_indices, np.ones(3, int)).flatten(), 2) 24 | cols = np.concatenate( 25 | [ 26 | mesh_indices[:-1, :].flatten(), 27 | mesh_indices[1:, :].flatten(), 28 | ] 29 | ) 30 | self.declare_partials("element_lengths", "nodes", rows=rows, cols=cols) 31 | self.set_check_partial_options(wrt="nodes", method="fd", step=1e-6) 32 | 33 | def compute(self, inputs, outputs): 34 | vec = inputs["nodes"][1:, :] - inputs["nodes"][:-1, :] 35 | 36 | outputs["element_lengths"] = np.linalg.norm(vec, axis=-1) 37 | 38 | def compute_partials(self, inputs, partials): 39 | ny = self.ny 40 | 41 | vec = inputs["nodes"][1:, :] - inputs["nodes"][:-1, :] 42 | 43 | derivs = partials["element_lengths", "nodes"].reshape((2, ny - 1, 3)) 44 | derivs[0, :, :] = -vec / compute_norm(vec) 45 | derivs[1, :, :] = vec / compute_norm(vec) 46 | -------------------------------------------------------------------------------- /openaerostruct/structures/local_stiff_permuted.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | row_indices = np.arange(12) 7 | col_indices = np.array( 8 | [ 9 | 0, 10 | 6, 11 | 3, 12 | 9, 13 | 2, 14 | 4, 15 | 8, 16 | 10, 17 | 1, 18 | 5, 19 | 7, 20 | 11, 21 | ] 22 | ) 23 | mtx = np.zeros((12, 12)) 24 | mtx[row_indices, col_indices] = 1.0 25 | 26 | 27 | class LocalStiffPermuted(om.ExplicitComponent): 28 | def initialize(self): 29 | self.options.declare("surface", types=dict) 30 | 31 | def setup(self): 32 | surface = self.options["surface"] 33 | 34 | self.ny = ny = surface["mesh"].shape[1] 35 | 36 | self.add_input("local_stiff", shape=(ny - 1, 12, 12)) 37 | self.add_output("local_stiff_permuted", shape=(ny - 1, 12, 12)) 38 | 39 | indices = np.arange(144 * (ny - 1)).reshape((ny - 1, 12, 12)) 40 | ones = np.ones((12, 12), int) 41 | 42 | # data = np.empty((ny - 1, 12, 12, 12, 12)) 43 | # for row1 in range(12): 44 | # for col1 in range(12): 45 | # for row2 in range(12): 46 | # for col2 in range(12): 47 | # data[:, row1, col1, row2, col2] = mtx[row1, row2] * mtx[col2, col1] 48 | data = np.einsum("i,jl,mk->ijklm", np.ones(ny - 1), mtx.T, mtx) 49 | 50 | data = data.flatten() 51 | rows = np.einsum("ijk,lm->ijklm", indices, ones).flatten() 52 | cols = np.einsum("ilm,jk->ijklm", indices, ones).flatten() 53 | self.declare_partials("local_stiff_permuted", "local_stiff", val=data, rows=rows, cols=cols) 54 | 55 | def compute(self, inputs, outputs): 56 | outputs["local_stiff_permuted"] = np.einsum("jl,ilm,mk->ijk", mtx.T, inputs["local_stiff"], mtx) 57 | -------------------------------------------------------------------------------- /openaerostruct/structures/local_stiff_transformed.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | class LocalStiffTransformed(om.ExplicitComponent): 7 | def initialize(self): 8 | self.options.declare("surface", types=dict) 9 | 10 | def setup(self): 11 | surface = self.options["surface"] 12 | 13 | self.ny = ny = surface["mesh"].shape[1] 14 | 15 | self.add_input("transform", shape=(ny - 1, 12, 12)) 16 | self.add_input("local_stiff_permuted", shape=(ny - 1, 12, 12)) 17 | self.add_output("local_stiff_transformed", shape=(ny - 1, 12, 12)) 18 | 19 | indices = np.arange(144 * (ny - 1)).reshape((ny - 1, 12, 12)) 20 | ones = np.ones((12, 12), int) 21 | 22 | rows = np.einsum("ijk,lm->ijklm", indices, ones).flatten() 23 | cols = np.einsum("ilm,jk->ijklm", indices, ones).flatten() 24 | 25 | self.declare_partials("local_stiff_transformed", "transform", rows=rows, cols=cols) 26 | self.declare_partials("local_stiff_transformed", "local_stiff_permuted", rows=rows, cols=cols) 27 | 28 | def compute(self, inputs, outputs): 29 | outputs["local_stiff_transformed"] = np.einsum( 30 | "ilj,ilm,imk->ijk", inputs["transform"], inputs["local_stiff_permuted"], inputs["transform"] 31 | ) 32 | 33 | def compute_partials(self, inputs, partials): 34 | partials["local_stiff_transformed", "local_stiff_permuted"] = np.einsum( 35 | "ilj,imk->ijklm", inputs["transform"], inputs["transform"] 36 | ).flatten() 37 | 38 | partials["local_stiff_transformed", "transform"] = ( 39 | np.einsum("ilj,ilp,kq->ijkpq", inputs["transform"], inputs["local_stiff_permuted"], np.eye(12)) 40 | + np.einsum("jq,ipm,imk->ijkpq", np.eye(12), inputs["local_stiff_permuted"], inputs["transform"]) 41 | ).flatten() 42 | -------------------------------------------------------------------------------- /openaerostruct/structures/non_intersecting_thickness.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | class NonIntersectingThickness(om.ExplicitComponent): 7 | """ 8 | Create a constraint so the thickness of the spar does not intersect 9 | itself in the center of the spar. Basically, the thickness must be less 10 | than or equal to the radius. 11 | 12 | parameters 13 | ---------- 14 | thickness[ny-1] : numpy array 15 | Thickness of each element of the FEM spar. 16 | radius[ny-1] : numpy array 17 | Radius of each element of the FEM spar. 18 | 19 | Returns 20 | ------- 21 | thickness_intersects[ny-1] : numpy array 22 | If all the values are negative, each element does not intersect itself. 23 | If a value is positive, then the thickness within the hollow spar 24 | intersects itself and presents an impossible design. 25 | Add a constraint as 26 | `OASProblem.add_constraint('thickness_intersects', upper=0.)` 27 | """ 28 | 29 | def initialize(self): 30 | self.options.declare("surface", types=dict) 31 | 32 | def setup(self): 33 | self.surface = surface = self.options["surface"] 34 | 35 | self.ny = surface["mesh"].shape[1] 36 | 37 | self.add_input("thickness", val=np.zeros((self.ny - 1)), units="m") 38 | self.add_input("radius", val=np.zeros((self.ny - 1)), units="m") 39 | self.add_output("thickness_intersects", val=np.zeros((self.ny - 1)), units="m") 40 | 41 | arange = np.arange(self.ny - 1) 42 | self.declare_partials("thickness_intersects", "thickness", rows=arange, cols=arange, val=1.0) 43 | self.declare_partials("thickness_intersects", "radius", rows=arange, cols=arange, val=-1.0) 44 | 45 | def compute(self, inputs, outputs): 46 | outputs["thickness_intersects"] = inputs["thickness"] - inputs["radius"] 47 | -------------------------------------------------------------------------------- /openaerostruct/structures/spar_within_wing.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | from openaerostruct.structures.utils import radii 5 | 6 | 7 | class SparWithinWing(om.ExplicitComponent): 8 | """ 9 | Create a constraint to see if the spar is within the wing. 10 | This is based on the wing's t/c and the spar radius. 11 | 12 | .. warning:: 13 | This component has not been extensively tested. 14 | It may require additional coding to work as intended. 15 | 16 | parameters 17 | ---------- 18 | mesh[nx, ny, 3] : numpy array 19 | Array defining the nodal points of the lifting surface. 20 | radius[ny-1] : numpy array 21 | Radius of each element of the FEM spar. 22 | t_over_c[ny-1] : numpy array 23 | The streamwise thickness-to-chord ratio of each VLM panel. 24 | 25 | Returns 26 | ------- 27 | spar_within_wing[ny-1] : numpy array 28 | If all the values are negative, each element is within the wing, 29 | based on the surface's t_over_c value and the current chord. 30 | Set a constraint with 31 | `OASProblem.add_constraint('spar_within_wing', upper=0.)` 32 | """ 33 | 34 | def initialize(self): 35 | self.options.declare("surface", types=dict) 36 | 37 | def setup(self): 38 | self.surface = surface = self.options["surface"] 39 | 40 | self.ny = surface["mesh"].shape[1] 41 | nx = surface["mesh"].shape[0] 42 | 43 | self.add_input("mesh", val=np.zeros((nx, self.ny, 3)), units="m") 44 | self.add_input("radius", val=np.zeros((self.ny - 1)), units="m") 45 | self.add_input("t_over_c", val=np.zeros((self.ny - 1))) 46 | self.add_output("spar_within_wing", val=np.zeros((self.ny - 1)), units="m") 47 | 48 | self.declare_partials("spar_within_wing", "mesh", method="cs") 49 | 50 | arange = np.arange(self.ny - 1) 51 | self.declare_partials("spar_within_wing", "radius", rows=arange, cols=arange, val=1.0) 52 | 53 | def compute(self, inputs, outputs): 54 | mesh = inputs["mesh"] 55 | t_over_c = inputs["t_over_c"] 56 | max_radius = radii(mesh, t_over_c) 57 | outputs["spar_within_wing"] = inputs["radius"] - max_radius 58 | -------------------------------------------------------------------------------- /openaerostruct/structures/spatial_beam_functionals.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | 3 | # from openaerostruct.structures.energy import Energy 4 | # from openaerostruct.structures.weight import Weight 5 | # from openaerostruct.structures.spar_within_wing import SparWithinWing 6 | from openaerostruct.structures.vonmises_tube import VonMisesTube 7 | from openaerostruct.structures.vonmises_wingbox import VonMisesWingbox 8 | from openaerostruct.structures.non_intersecting_thickness import NonIntersectingThickness 9 | from openaerostruct.structures.failure_exact import FailureExact 10 | from openaerostruct.structures.failure_ks import FailureKS 11 | 12 | 13 | class SpatialBeamFunctionals(om.Group): 14 | """Group that contains the spatial beam functionals used to evaluate 15 | performance.""" 16 | 17 | def initialize(self): 18 | self.options.declare("surface", types=dict) 19 | 20 | def setup(self): 21 | surface = self.options["surface"] 22 | 23 | # Commented out energy for now since we haven't ever used its output 24 | # self.add_subsystem('energy', 25 | # Energy(surface=surface), 26 | # promotes=['*']) 27 | 28 | if surface["fem_model_type"] == "tube": 29 | self.add_subsystem( 30 | "thicknessconstraint", 31 | NonIntersectingThickness(surface=surface), 32 | promotes_inputs=["thickness", "radius"], 33 | promotes_outputs=["thickness_intersects"], 34 | ) 35 | 36 | self.add_subsystem( 37 | "vonmises", 38 | VonMisesTube(surface=surface), 39 | promotes_inputs=["radius", "nodes", "disp"], 40 | promotes_outputs=["vonmises"], 41 | ) 42 | elif surface["fem_model_type"] == "wingbox": 43 | self.add_subsystem( 44 | "vonmises", 45 | VonMisesWingbox(surface=surface), 46 | promotes_inputs=[ 47 | "Qz", 48 | "J", 49 | "A_enc", 50 | "spar_thickness", 51 | "htop", 52 | "hbottom", 53 | "hfront", 54 | "hrear", 55 | "nodes", 56 | "disp", 57 | ], 58 | promotes_outputs=["vonmises"], 59 | ) 60 | else: 61 | raise NameError("Please select a valid `fem_model_type` from either `tube` or `wingbox`.") 62 | 63 | # The following component has not been fully tested so we leave it 64 | # commented out for now. Use at your own risk. 65 | # self.add_subsystem('sparconstraint', 66 | # SparWithinWing(surface=surface), 67 | # promotes=['*']) 68 | 69 | if surface["exact_failure_constraint"]: 70 | self.add_subsystem( 71 | "failure", FailureExact(surface=surface), promotes_inputs=["vonmises"], promotes_outputs=["failure"] 72 | ) 73 | else: 74 | self.add_subsystem( 75 | "failure", FailureKS(surface=surface), promotes_inputs=["vonmises"], promotes_outputs=["failure"] 76 | ) 77 | -------------------------------------------------------------------------------- /openaerostruct/structures/spatial_beam_setup.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | from openaerostruct.structures.compute_nodes import ComputeNodes 3 | from openaerostruct.structures.assemble_k_group import AssembleKGroup 4 | from openaerostruct.structures.weight import Weight 5 | from openaerostruct.structures.structural_cg import StructuralCG 6 | from openaerostruct.structures.fuel_vol import WingboxFuelVol 7 | 8 | 9 | class SpatialBeamSetup(om.Group): 10 | """Group that sets up the spatial beam components and assembles the 11 | stiffness matrix.""" 12 | 13 | def initialize(self): 14 | self.options.declare("surface", types=dict) 15 | 16 | def setup(self): 17 | surface = self.options["surface"] 18 | 19 | self.add_subsystem("nodes", ComputeNodes(surface=surface), promotes_inputs=["mesh"], promotes_outputs=["nodes"]) 20 | 21 | self.add_subsystem( 22 | "assembly", 23 | AssembleKGroup(surface=surface), 24 | promotes_inputs=["A", "Iy", "Iz", "J", "nodes"], 25 | promotes_outputs=["local_stiff_transformed"], 26 | ) 27 | 28 | self.add_subsystem( 29 | "structural_mass", 30 | Weight(surface=surface), 31 | promotes_inputs=["A", "nodes"], 32 | promotes_outputs=["structural_mass", "element_mass"], 33 | ) 34 | 35 | self.add_subsystem( 36 | "structural_cg", 37 | StructuralCG(surface=surface), 38 | promotes_inputs=["nodes", "structural_mass", "element_mass"], 39 | promotes_outputs=["cg_location"], 40 | ) 41 | 42 | if surface["fem_model_type"] == "wingbox": 43 | self.add_subsystem( 44 | "fuel_vol", 45 | WingboxFuelVol(surface=surface), 46 | promotes_inputs=["nodes", "A_int"], 47 | promotes_outputs=["fuel_vols"], 48 | ) 49 | -------------------------------------------------------------------------------- /openaerostruct/structures/spatial_beam_states.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | from openaerostruct.structures.create_rhs import CreateRHS 3 | from openaerostruct.structures.fem import FEM 4 | from openaerostruct.structures.disp import Disp 5 | from openaerostruct.structures.wing_weight_loads import StructureWeightLoads 6 | from openaerostruct.structures.fuel_loads import FuelLoads 7 | from openaerostruct.structures.total_loads import TotalLoads 8 | from openaerostruct.structures.compute_point_mass_loads import ComputePointMassLoads 9 | from openaerostruct.structures.compute_thrust_loads import ComputeThrustLoads 10 | 11 | 12 | class SpatialBeamStates(om.Group): 13 | """Group that contains the spatial beam states.""" 14 | 15 | def initialize(self): 16 | self.options.declare("surface", types=dict) 17 | 18 | def setup(self): 19 | surface = self.options["surface"] 20 | 21 | promotes = [] 22 | if surface["struct_weight_relief"]: 23 | self.add_subsystem( 24 | "struct_weight_loads", 25 | StructureWeightLoads(surface=surface), 26 | promotes_inputs=["element_mass", "nodes", "load_factor"], 27 | promotes_outputs=["struct_weight_loads"], 28 | ) 29 | promotes.append("struct_weight_loads") 30 | 31 | if surface["distributed_fuel_weight"]: 32 | self.add_subsystem( 33 | "fuel_loads", 34 | FuelLoads(surface=surface), 35 | promotes_inputs=["nodes", "load_factor", "fuel_vols", "fuel_mass"], 36 | promotes_outputs=["fuel_weight_loads"], 37 | ) 38 | promotes.append("fuel_weight_loads") 39 | 40 | if "n_point_masses" in surface.keys(): 41 | self.add_subsystem( 42 | "point_masses", 43 | ComputePointMassLoads(surface=surface), 44 | promotes_inputs=["point_mass_locations", "point_masses", "nodes", "load_factor"], 45 | promotes_outputs=["loads_from_point_masses"], 46 | ) 47 | promotes.append("loads_from_point_masses") 48 | 49 | self.add_subsystem( 50 | "thrust_loads", 51 | ComputeThrustLoads(surface=surface), 52 | promotes_inputs=["point_mass_locations", "engine_thrusts", "nodes"], 53 | promotes_outputs=["loads_from_thrusts"], 54 | ) 55 | promotes.append("loads_from_thrusts") 56 | 57 | self.add_subsystem( 58 | "total_loads", 59 | TotalLoads(surface=surface), 60 | promotes_inputs=["loads"] + promotes, 61 | promotes_outputs=["total_loads"], 62 | ) 63 | 64 | self.add_subsystem( 65 | "create_rhs", CreateRHS(surface=surface), promotes_inputs=["total_loads"], promotes_outputs=["forces"] 66 | ) 67 | 68 | self.add_subsystem("fem", FEM(surface=surface), promotes_inputs=["*"], promotes_outputs=["*"]) 69 | 70 | self.add_subsystem("disp", Disp(surface=surface), promotes_inputs=["*"], promotes_outputs=["*"]) 71 | -------------------------------------------------------------------------------- /openaerostruct/structures/total_loads.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | class TotalLoads(om.ExplicitComponent): 7 | """ 8 | Add the loads from the aerodynamics, structural weight, and fuel weight. 9 | 10 | Parameters 11 | ---------- 12 | loads[ny, 6] : numpy array 13 | Array containing the loads applied on the FEM component, 14 | computed from the sectional forces. 15 | struct_weight_loads[ny, 6] : numpy array 16 | Array containing the loads applied on the FEM component, 17 | computed from the weight of the wing-structure segments. 18 | fuel_weight_loads[ny, 6] : numpy array 19 | Array containing the loads applied on the FEM component, 20 | computed from the weight of the fuel. 21 | loads_from_point_masses[ny, 6] : numpy array 22 | The cumulative loads from all point masses. 23 | 24 | Returns 25 | ------- 26 | total_loads[ny, 6] : numpy array 27 | Flattened array containing the total loads applied on the FEM. 28 | """ 29 | 30 | def initialize(self): 31 | self.options.declare("surface", types=dict) 32 | 33 | def setup(self): 34 | self.surface = surface = self.options["surface"] 35 | self.ny = surface["mesh"].shape[1] 36 | 37 | self.add_input("loads", val=np.ones((self.ny, 6)), units="N") 38 | if surface["struct_weight_relief"]: 39 | self.add_input("struct_weight_loads", val=np.zeros((self.ny, 6)), units="N") 40 | if surface["distributed_fuel_weight"]: 41 | self.add_input("fuel_weight_loads", val=np.zeros((self.ny, 6)), units="N") 42 | if "n_point_masses" in surface.keys(): 43 | self.add_input("loads_from_point_masses", val=np.zeros((self.ny, 6)), units="N") 44 | self.add_input("loads_from_thrusts", val=np.zeros((self.ny, 6)), units="N") 45 | 46 | self.add_output("total_loads", val=np.ones((self.ny, 6)), units="N") 47 | 48 | arange = np.arange(self.ny * 6) 49 | 50 | self.declare_partials("total_loads", "loads", rows=arange, cols=arange, val=1.0) 51 | 52 | if self.surface["struct_weight_relief"]: 53 | self.declare_partials("total_loads", "struct_weight_loads", rows=arange, cols=arange, val=1.0) 54 | 55 | if self.surface["distributed_fuel_weight"]: 56 | self.declare_partials("total_loads", "fuel_weight_loads", rows=arange, cols=arange, val=1.0) 57 | 58 | if "n_point_masses" in surface.keys(): 59 | self.declare_partials("total_loads", "loads_from_point_masses", rows=arange, cols=arange, val=1.0) 60 | self.declare_partials("total_loads", "loads_from_thrusts", rows=arange, cols=arange, val=1.0) 61 | 62 | def compute(self, inputs, outputs): 63 | outputs["total_loads"] = inputs["loads"] 64 | 65 | if self.surface["struct_weight_relief"]: 66 | outputs["total_loads"] += inputs["struct_weight_loads"] 67 | 68 | if self.surface["distributed_fuel_weight"]: 69 | outputs["total_loads"] += inputs["fuel_weight_loads"] 70 | 71 | if "n_point_masses" in self.surface.keys(): 72 | outputs["total_loads"] += inputs["loads_from_point_masses"] 73 | outputs["total_loads"] += inputs["loads_from_thrusts"] 74 | -------------------------------------------------------------------------------- /openaerostruct/structures/tube_group.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | from openaerostruct.structures.section_properties_tube import SectionPropertiesTube 3 | from openaerostruct.geometry.radius_comp import RadiusComp 4 | from openaerostruct.utils.interpolation import get_normalized_span_coords 5 | 6 | 7 | class TubeGroup(om.Group): 8 | """Group that contains everything needed for a structural-only problem.""" 9 | 10 | def initialize(self): 11 | self.options.declare("surface", types=dict) 12 | self.options.declare("connect_geom_DVs", default=True) 13 | # The option "connect_geom_DVs" is no longer necessary, but we still keep it to be backward compatible. 14 | 15 | def setup(self): 16 | surface = self.options["surface"] 17 | 18 | if "thickness_cp" in surface.keys(): 19 | n_cp = len(surface["thickness_cp"]) 20 | # Add bspline components for active bspline geometric variables. 21 | x_interp = get_normalized_span_coords(surface, mid_panel=True) 22 | comp = self.add_subsystem( 23 | "thickness_bsp", 24 | om.SplineComp( 25 | method="bsplines", 26 | x_interp_val=x_interp, 27 | num_cp=n_cp, 28 | interp_options={"order": min(n_cp, 4), "x_cp_start": 0, "x_cp_end": 1}, 29 | ), 30 | promotes_inputs=["thickness_cp"], 31 | promotes_outputs=["thickness"], 32 | ) 33 | comp.add_spline(y_cp_name="thickness_cp", y_interp_name="thickness", y_units="m") 34 | self.set_input_defaults("thickness_cp", val=surface["thickness_cp"], units="m") 35 | 36 | if "radius_cp" in surface.keys(): 37 | n_cp = len(surface["radius_cp"]) 38 | # Add bspline components for active bspline geometric variables. 39 | x_interp = get_normalized_span_coords(surface, mid_panel=True) 40 | comp = self.add_subsystem( 41 | "radius_bsp", 42 | om.SplineComp( 43 | method="bsplines", 44 | x_interp_val=x_interp, 45 | num_cp=n_cp, 46 | interp_options={"order": min(n_cp, 4), "x_cp_start": 0, "x_cp_end": 1}, 47 | ), 48 | promotes_inputs=["radius_cp"], 49 | promotes_outputs=["radius"], 50 | ) 51 | comp.add_spline(y_cp_name="radius_cp", y_interp_name="radius", y_units="m") 52 | self.set_input_defaults("radius_cp", val=surface["radius_cp"], units="m") 53 | 54 | else: 55 | self.add_subsystem( 56 | "radius_comp", 57 | RadiusComp(surface=surface), 58 | promotes_inputs=["mesh", "t_over_c"], 59 | promotes_outputs=["radius"], 60 | ) 61 | 62 | self.add_subsystem( 63 | "tube", 64 | SectionPropertiesTube(surface=surface), 65 | promotes_inputs=["thickness", "radius"], 66 | promotes_outputs=["A", "Iy", "Iz", "J"], 67 | ) 68 | -------------------------------------------------------------------------------- /openaerostruct/structures/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def norm(vec, axis=None): 5 | return np.sqrt(np.sum(vec**2, axis=axis)) 6 | 7 | 8 | def unit(vec): 9 | return vec / norm(vec) 10 | 11 | 12 | def norm_d(vec): 13 | vec_d = vec / norm(vec) 14 | return vec_d 15 | 16 | 17 | def unit_d(vec): 18 | n_d = norm_d(vec) 19 | normvec = norm(vec) 20 | vec_d = np.outer((-vec / (normvec * normvec)), n_d) + 1 / normvec * np.eye(len(vec)) 21 | 22 | return vec_d 23 | 24 | 25 | # This is a limited cross product definition for 3 vectors 26 | def cross_d(a, b): 27 | if not isinstance(a, np.ndarray): 28 | a = np.array(a) 29 | if a.shape != (3,): 30 | raise ValueError("a must be a (3,) nd array") 31 | if not isinstance(b, np.ndarray): 32 | b = np.array(b) 33 | if b.shape != (3,): 34 | raise ValueError("b must be a (3,) nd array") 35 | 36 | dcda = np.zeros([3, 3]) 37 | dcdb = np.zeros([3, 3]) 38 | 39 | dcda[0, 1] = b[2] 40 | dcda[0, 2] = -b[1] 41 | dcda[1, 0] = -b[2] 42 | dcda[1, 2] = b[0] 43 | dcda[2, 0] = b[1] 44 | dcda[2, 1] = -b[0] 45 | 46 | dcdb[0, 1] = -a[2] 47 | dcdb[0, 2] = a[1] 48 | dcdb[1, 0] = a[2] 49 | dcdb[1, 2] = -a[0] 50 | dcdb[2, 0] = -a[1] 51 | dcdb[2, 1] = a[0] 52 | 53 | return dcda, dcdb 54 | 55 | 56 | def radii(mesh, t_c=0.15): 57 | """ 58 | Obtain the radii of the FEM element based on local chord. 59 | """ 60 | vectors = mesh[-1, :, :] - mesh[0, :, :] 61 | chords = np.sqrt(np.sum(vectors**2, axis=1)) 62 | mean_chords = 0.5 * chords[:-1] + 0.5 * chords[1:] 63 | return t_c * mean_chords * 0.5 64 | -------------------------------------------------------------------------------- /openaerostruct/structures/wingbox_fuel_vol_delta.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import openmdao.api as om 4 | 5 | 6 | def norm(vec): 7 | return np.sqrt(np.sum(vec**2)) 8 | 9 | 10 | class WingboxFuelVolDelta(om.ExplicitComponent): 11 | """ 12 | Create a constraint to ensure the wingbox has enough internal volume to 13 | store the required fuel. 14 | 15 | Parameters 16 | ---------- 17 | fuelburn : float 18 | Fuel weight 19 | fuel_vols[ny-1] : numpy array 20 | The magnitude of each individual panel's fuel-carrying volumes. 21 | 22 | Returns 23 | ------- 24 | fuel_vol_delta : float 25 | If the value is negative, then there isn't enough volume for the fuel. 26 | """ 27 | 28 | def initialize(self): 29 | self.options.declare("surface", types=dict) 30 | 31 | def setup(self): 32 | self.surface = surface = self.options["surface"] 33 | 34 | self.ny = surface["mesh"].shape[1] 35 | 36 | self.add_input("fuelburn", val=0.0, units="kg") 37 | self.add_input("fuel_vols", val=np.zeros((self.ny - 1)), units="m**3") 38 | self.add_output("fuel_vol_delta", val=0.0, units="m**3") 39 | 40 | self.declare_partials("*", "*", method="cs") 41 | 42 | def compute(self, inputs, outputs): 43 | fuel_weight = inputs["fuelburn"] 44 | reserves = self.surface["Wf_reserve"] 45 | fuel_density = self.surface["fuel_density"] 46 | vols = inputs["fuel_vols"] 47 | 48 | if self.surface["symmetry"] is True: 49 | fuel_weight /= 2.0 50 | reserves /= 2.0 51 | 52 | sum_vols = np.sum(vols) 53 | 54 | # This is used for the fuel-volume constraint. It should be positive for fuel to fit. 55 | outputs["fuel_vol_delta"] = sum_vols - (fuel_weight + reserves) / fuel_density 56 | -------------------------------------------------------------------------------- /openaerostruct/transfer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/transfer/__init__.py -------------------------------------------------------------------------------- /openaerostruct/transfer/displacement_transfer_group.py: -------------------------------------------------------------------------------- 1 | import openmdao.api as om 2 | from openaerostruct.transfer.displacement_transfer import DisplacementTransfer 3 | from openaerostruct.transfer.compute_transformation_matrix import ComputeTransformationMatrix 4 | 5 | 6 | class DisplacementTransferGroup(om.Group): 7 | """ 8 | These components take the displacements and rotations obtained by 9 | solving the FEM problem and applies them to the aerodynamic mesh 10 | to produce a deformed aerodynamic mesh. 11 | """ 12 | 13 | def initialize(self): 14 | self.options.declare("surface", types=dict) 15 | 16 | def setup(self): 17 | surface = self.options["surface"] 18 | 19 | self.add_subsystem( 20 | "compute_transformation_matrix", ComputeTransformationMatrix(surface=surface), promotes=["*"] 21 | ) 22 | 23 | self.add_subsystem("displacement_transfer", DisplacementTransfer(surface=surface), promotes=["*"]) 24 | -------------------------------------------------------------------------------- /openaerostruct/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/OpenAeroStruct/7ae28a82779ad0e3bd027e9bd0afb5ce26f9a672/openaerostruct/utils/__init__.py -------------------------------------------------------------------------------- /openaerostruct/utils/check_surface_dict.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | 4 | def check_surface_dict_keys(surface): 5 | """ 6 | Key valication function for the OAS surface dict. 7 | Shows a warning if a user provided a key that is (likely) not implemented in OAS. 8 | 9 | Parameters 10 | ---------- 11 | surface : dict 12 | User-defined surface dict 13 | """ 14 | 15 | # NOTE: make sure this is consistent to the documentation's surface dict page 16 | keys_implemented = [ 17 | # wing definition 18 | "name", 19 | "symmetry", 20 | "S_ref_type", 21 | "mesh", 22 | "span", 23 | "taper", 24 | "sweep", 25 | "dihedral", 26 | "twist_cp", 27 | "chord_cp", 28 | "xshear_cp", 29 | "yshear_cp", 30 | "zshear_cp", 31 | "ref_axis_pos", 32 | # aerodynamics 33 | "CL0", 34 | "CD0", 35 | "with_viscous", 36 | "with_wave", 37 | "groundplane", 38 | "k_lam", 39 | "t_over_c_cp", 40 | "c_max_t", 41 | # structure 42 | "fem_model_type", 43 | "E", 44 | "G", 45 | "yield", 46 | "mrho", 47 | "fem_origin", 48 | "wing_weight_ratio", 49 | "exact_failure_constraint", 50 | "struct_weight_relief", 51 | "distributed_fuel_weight", 52 | "fuel_density", 53 | "Wf_reserve", 54 | "n_point_masses", 55 | # tube structure 56 | "thickness_cp", 57 | "radius_cp", 58 | # wingbox structure 59 | "spar_thickness_cp", 60 | "skin_thickness_cp", 61 | "original_wingbox_airfoil_t_over_c", 62 | "strength_factor_for_upper_skin", 63 | "data_x_upper", 64 | "data_y_upper", 65 | "data_x_lower", 66 | "data_y_lower", 67 | # FFD 68 | "mx", 69 | "my", 70 | # Multisection 71 | "is_multi_section", 72 | "num_sections", 73 | "sec_name", 74 | "meshes", 75 | "root_chord", 76 | "span", 77 | "ny", 78 | "nx", 79 | "bpanels", 80 | "cpanels", 81 | "root_section", 82 | ] 83 | 84 | for key in surface.keys(): 85 | if key not in keys_implemented: 86 | warnings.warn( 87 | "Key `{}` in surface dict is (likely) not supported in OAS and will be ignored".format(key), 88 | category=RuntimeWarning, 89 | stacklevel=2, 90 | ) 91 | -------------------------------------------------------------------------------- /openaerostruct/utils/constants.py: -------------------------------------------------------------------------------- 1 | grav_constant = 9.80665 2 | -------------------------------------------------------------------------------- /openaerostruct/utils/interpolation.py: -------------------------------------------------------------------------------- 1 | def get_normalized_span_coords(surface, mid_panel=False): 2 | """Get the normalised coordinates used for interpolating values along the wingspan 3 | 4 | These normalized coordinates range from 0 at the tip of the wing to 1 at the root. 5 | 6 | Parameters 7 | ---------- 8 | surface : OpenAeroStruct surface dictionary 9 | Surface to generate coordinates for 10 | mid_panel : bool, optional 11 | Whether the normalized coordinate should be of the panel midpoints rather than the mesh nodes, by default False 12 | 13 | Returns 14 | ------- 15 | np.array 16 | Normalized coordinate values 17 | """ 18 | spanwise_coord = surface["mesh"][0, :, 1] 19 | span_range = spanwise_coord[-1] - spanwise_coord[0] 20 | span_offset = spanwise_coord[0] 21 | if mid_panel: 22 | x_real = (spanwise_coord[:-1] + spanwise_coord[1:]) / 2 23 | else: 24 | x_real = spanwise_coord 25 | x_norm = (x_real - span_offset) / span_range 26 | return x_norm 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | import re 4 | import os 5 | 6 | __version__ = re.findall( 7 | r"""__version__ = ["']+([0-9\.]*)["']+""", 8 | open("openaerostruct/__init__.py").read(), 9 | )[0] 10 | 11 | optional_dependencies = { 12 | "docs": ["sphinx_mdolab_theme"], 13 | "test": ["testflo"], 14 | "ffd": ["pygeo>=1.6.0"], 15 | "mphys": ["mphys>=2.0.0", "pygeo>=1.15.0"], 16 | } 17 | 18 | # Add an optional dependency that concatenates all others 19 | optional_dependencies["all"] = sorted( 20 | [dependency for dependencies in optional_dependencies.values() for dependency in dependencies] 21 | ) 22 | 23 | this_directory = os.path.abspath(os.path.dirname(__file__)) 24 | with open(os.path.join(this_directory, "README.md"), encoding="utf-8") as f: 25 | long_description = f.read() 26 | 27 | setup( 28 | name="openaerostruct", 29 | version=__version__, 30 | description="OpenAeroStruct", 31 | long_description=long_description, 32 | long_description_content_type="text/markdown", 33 | url="https://github.com/mdolab/OpenAeroStruct", 34 | keywords="", 35 | author="", 36 | author_email="", 37 | license="BSD-3", 38 | packages=[ 39 | "openaerostruct", 40 | "openaerostruct/docs", 41 | "openaerostruct/docs/_utils", 42 | "openaerostruct/geometry", 43 | "openaerostruct/structures", 44 | "openaerostruct/aerodynamics", 45 | "openaerostruct/transfer", 46 | "openaerostruct/functionals", 47 | "openaerostruct/integration", 48 | "openaerostruct/meshing", 49 | "openaerostruct/common", 50 | "openaerostruct/utils", 51 | "openaerostruct/mphys", 52 | ], 53 | # Test files 54 | package_data={"openaerostruct": ["tests/*.py", "*/tests/*.py", "*/*/tests/*.py"]}, 55 | install_requires=[ 56 | # Remember to update the oldest versions in docs/installation.rst 57 | "openmdao>=3.35", 58 | "numpy>=1.21", 59 | "scipy>=1.7", 60 | "matplotlib", 61 | ], 62 | extras_require=optional_dependencies, 63 | zip_safe=False, 64 | # ext_modules=ext, 65 | entry_points=""" 66 | [console_scripts] 67 | plot_wing=openaerostruct.utils.plot_wing:disp_plot 68 | plot_wingbox=openaerostruct.utils.plot_wingbox:disp_plot 69 | """, 70 | ) 71 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_2d_lift.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.lift_coeff_2D import LiftCoeff2D 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = LiftCoeff2D(surface=surfaces[0]) 12 | 13 | run_test(self, comp, method="cs", complex_flag=True) 14 | 15 | def test2(self): 16 | surfaces = get_default_surfaces() 17 | 18 | comp = LiftCoeff2D(surface=surfaces[1]) 19 | 20 | run_test(self, comp, method="cs", complex_flag=True) 21 | 22 | 23 | if __name__ == "__main__": 24 | unittest.main() 25 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_coeffs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.coeffs import Coeffs 4 | from openaerostruct.utils.testing import run_test 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | comp = Coeffs() 10 | 11 | run_test(self, comp) 12 | 13 | 14 | if __name__ == "__main__": 15 | unittest.main() 16 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_collocation_points.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.collocation_points import CollocationPoints 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = CollocationPoints(surfaces=surfaces) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_convert_velocity.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | import openmdao.api as om 6 | from openmdao.utils.assert_utils import assert_check_partials 7 | 8 | from openaerostruct.aerodynamics.convert_velocity import ConvertVelocity 9 | from openaerostruct.utils.testing import run_test, get_default_surfaces 10 | 11 | 12 | class Test(unittest.TestCase): 13 | def test(self): 14 | surfaces = get_default_surfaces() 15 | 16 | comp = ConvertVelocity(surfaces=surfaces) 17 | 18 | run_test(self, comp) 19 | 20 | def test_rotation_option_derivatives(self): 21 | surfaces = get_default_surfaces() 22 | 23 | comp = ConvertVelocity(surfaces=surfaces, rotational=True) 24 | 25 | prob = om.Problem() 26 | prob.model.add_subsystem("comp", comp) 27 | prob.setup(force_alloc_complex=True) 28 | 29 | rng = np.random.default_rng(0) 30 | prob["comp.rotational_velocities"] = rng.random(prob["comp.rotational_velocities"].shape) 31 | prob["comp.beta"] = 15.0 32 | prob.run_model() 33 | 34 | check = prob.check_partials(compact_print=True, method="cs", step=1e-40) 35 | 36 | assert_check_partials(check) 37 | 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_eval_velocities.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.eval_velocities import EvalVelocities 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = EvalVelocities(surfaces=surfaces, eval_name="TestEval", num_eval_points=11) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_functionals.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.functionals import VLMFunctionals 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = VLMFunctionals(surface=surfaces[0]) 12 | 13 | run_test(self, comp, complex_flag=True, step=1e-8) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_get_vectors.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.get_vectors import GetVectors 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces, get_ground_effect_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = GetVectors(surfaces=surfaces, num_eval_points=10, eval_name="test_name") 12 | 13 | run_test(self, comp) 14 | 15 | def test_groundplane(self): 16 | surfaces = get_ground_effect_surfaces() 17 | 18 | comp = GetVectors(surfaces=surfaces, num_eval_points=10, eval_name="test_name") 19 | 20 | run_test(self, comp) 21 | 22 | 23 | if __name__ == "__main__": 24 | unittest.main() 25 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_horseshoe_circulations.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.horseshoe_circulations import HorseshoeCirculations 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = HorseshoeCirculations(surfaces=surfaces) 12 | run_test(self, comp) 13 | 14 | 15 | if __name__ == "__main__": 16 | unittest.main() 17 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_lift_drag.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | import openmdao.api as om 6 | from openmdao.utils.assert_utils import assert_check_partials 7 | 8 | from openaerostruct.aerodynamics.lift_drag import LiftDrag 9 | from openaerostruct.utils.testing import run_test, get_default_surfaces 10 | 11 | 12 | class Test(unittest.TestCase): 13 | def test(self): 14 | surfaces = get_default_surfaces() 15 | 16 | comp = LiftDrag(surface=surfaces[0]) 17 | 18 | run_test(self, comp) 19 | 20 | def test_derivs_with_sideslip(self): 21 | surfaces = get_default_surfaces() 22 | 23 | # Use Tail since it is not symmetric. 24 | comp = LiftDrag(surface=surfaces[1]) 25 | 26 | prob = om.Problem() 27 | prob.model.add_subsystem("comp", comp) 28 | prob.setup(force_alloc_complex=True) 29 | 30 | prob["comp.alpha"] = 3.0 31 | prob["comp.beta"] = 15.0 32 | rng = np.random.default_rng(0) 33 | prob["comp.sec_forces"] = 10.0 * rng.random(prob["comp.sec_forces"].shape) 34 | 35 | prob.run_model() 36 | 37 | check = prob.check_partials(compact_print=True, method="cs", step=1e-40) 38 | 39 | assert_check_partials(check) 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_mesh_point_forces.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | import openmdao.api as om 6 | from openmdao.utils.assert_utils import assert_check_partials 7 | 8 | from openaerostruct.aerodynamics.mesh_point_forces import MeshPointForces 9 | from openaerostruct.meshing.mesh_generator import generate_mesh 10 | from openaerostruct.utils.testing import run_test, get_default_surfaces 11 | 12 | 13 | class Test(unittest.TestCase): 14 | def test(self): 15 | surfaces = get_default_surfaces() 16 | 17 | comp = MeshPointForces(surfaces=surfaces) 18 | 19 | run_test(self, comp) 20 | 21 | def test_derivatives(self): 22 | # For this test, we bump up the number of x and y points so that we are sure the 23 | # derivatives are correct. 24 | mesh_dict = {"num_y": 7, "num_x": 5, "wing_type": "CRM", "symmetry": True, "num_twist_cp": 5} 25 | 26 | # Generate the aerodynamic mesh based on the previous dictionary 27 | mesh, twist_cp = generate_mesh(mesh_dict) 28 | 29 | rng = np.random.default_rng(0) 30 | mesh[:, :, 2] = rng.random(mesh[:, :, 2].shape) 31 | 32 | # Create a dictionary with info and options about the aerodynamic 33 | # lifting surface 34 | surface = { 35 | # Wing definition 36 | "name": "wing", # name of the surface 37 | "twist_cp": twist_cp, 38 | "mesh": mesh, 39 | } 40 | 41 | # surfaces = get_default_surfaces() 42 | surfaces = [surface] 43 | 44 | prob = om.Problem() 45 | group = prob.model 46 | 47 | comp = MeshPointForces(surfaces=surfaces) 48 | group.add_subsystem("comp", comp) 49 | 50 | prob.setup() 51 | 52 | prob["comp.wing_sec_forces"] = rng.random(prob["comp.wing_sec_forces"].shape) 53 | 54 | prob.run_model() 55 | 56 | check = prob.check_partials(compact_print=True) 57 | assert_check_partials(check, atol=3e-5, rtol=1e-5) 58 | 59 | 60 | if __name__ == "__main__": 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_mtx_rhs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.mtx_rhs import VLMMtxRHSComp 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = VLMMtxRHSComp(surfaces=surfaces) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_panel_forces.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.panel_forces import PanelForces 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = PanelForces(surfaces=surfaces) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_panel_forces_surf.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.panel_forces_surf import PanelForcesSurf 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = PanelForcesSurf(surfaces=surfaces) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_rotational_velocity.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | import openmdao.api as om 6 | from openmdao.utils.assert_utils import assert_check_partials 7 | 8 | from openaerostruct.aerodynamics.rotational_velocity import RotationalVelocity 9 | from openaerostruct.utils.testing import run_test, get_default_surfaces 10 | 11 | 12 | class Test(unittest.TestCase): 13 | def test(self): 14 | surfaces = get_default_surfaces() 15 | 16 | comp = RotationalVelocity(surfaces=surfaces) 17 | 18 | run_test(self, comp) 19 | 20 | def test_rotation_option_derivatives(self): 21 | surfaces = get_default_surfaces() 22 | 23 | comp = RotationalVelocity(surfaces=surfaces) 24 | 25 | prob = om.Problem() 26 | prob.model.add_subsystem("comp", comp) 27 | prob.setup(force_alloc_complex=True) 28 | 29 | prob["comp.omega"] = np.array([0.3, 0.4, -0.1]) 30 | prob["comp.cg"] = np.array([0.1, 0.6, 0.4]) 31 | rng = np.random.default_rng(0) 32 | prob["comp.coll_pts"] = rng.random(prob["comp.coll_pts"].shape) 33 | prob.run_model() 34 | 35 | check = prob.check_partials(compact_print=True, method="cs", step=1e-40) 36 | 37 | assert_check_partials(check) 38 | 39 | 40 | if __name__ == "__main__": 41 | unittest.main() 42 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_solve_matrix.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | import openmdao.api as om 5 | 6 | from openaerostruct.aerodynamics.solve_matrix import SolveMatrix 7 | from openaerostruct.utils.testing import run_test, get_default_surfaces 8 | 9 | 10 | class Test(unittest.TestCase): 11 | def test(self): 12 | surfaces = get_default_surfaces() 13 | 14 | group = om.Group() 15 | comp = SolveMatrix(surfaces=surfaces) 16 | 17 | indep_var_comp = om.IndepVarComp() 18 | 19 | system_size = 0 20 | for surface in surfaces: 21 | nx = surface["mesh"].shape[0] 22 | ny = surface["mesh"].shape[1] 23 | system_size += (nx - 1) * (ny - 1) 24 | 25 | indep_var_comp.add_output("rhs", val=np.ones((system_size)), units="m/s") 26 | indep_var_comp.add_output("mtx", val=np.identity((system_size)), units="1/m") 27 | 28 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 29 | group.add_subsystem("solve_matrix", comp, promotes=["*"]) 30 | 31 | run_test(self, group) 32 | 33 | 34 | if __name__ == "__main__": 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_total_drag.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.total_drag import TotalDrag 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surface = get_default_surfaces()[0] 10 | 11 | comp = TotalDrag(surface=surface) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_total_lift.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.total_lift import TotalLift 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surface = get_default_surfaces()[0] 10 | 11 | comp = TotalLift(surface=surface) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_viscous_drag.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.viscous_drag import ViscousDrag 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | import openmdao.api as om 6 | import numpy as np 7 | 8 | 9 | class Test(unittest.TestCase): 10 | def test(self): 11 | surface = get_default_surfaces()[0] 12 | surface["t_over_c_cp"] = np.array([0.1, 0.15, 0.2]) 13 | 14 | ny = surface["mesh"].shape[1] 15 | n_cp = len(surface["t_over_c_cp"]) 16 | 17 | group = om.Group() 18 | 19 | indep_var_comp = om.IndepVarComp() 20 | indep_var_comp.add_output("t_over_c_cp", val=surface["t_over_c_cp"]) 21 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 22 | 23 | x_interp = np.linspace(0.0, 1.0, int(ny - 1)) 24 | comp = group.add_subsystem( 25 | "t_over_c_bsp", 26 | om.SplineComp( 27 | method="bsplines", x_interp_val=x_interp, num_cp=n_cp, interp_options={"order": min(n_cp, 4)} 28 | ), 29 | promotes_inputs=["t_over_c_cp"], 30 | promotes_outputs=["t_over_c"], 31 | ) 32 | comp.add_spline(y_cp_name="t_over_c_cp", y_interp_name="t_over_c", y_cp_val=np.zeros(n_cp)) 33 | 34 | comp = ViscousDrag(surface=surface, with_viscous=True) 35 | group.add_subsystem("viscousdrag", comp, promotes=["*"]) 36 | 37 | run_test(self, group, complex_flag=True) 38 | 39 | def test_2(self): 40 | surface = get_default_surfaces()[0] 41 | surface["k_lam"] = 0.5 42 | 43 | ny = surface["mesh"].shape[1] 44 | n_cp = len(surface["t_over_c_cp"]) 45 | 46 | group = om.Group() 47 | 48 | indep_var_comp = om.IndepVarComp() 49 | indep_var_comp.add_output("t_over_c_cp", val=surface["t_over_c_cp"]) 50 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 51 | 52 | x_interp = np.linspace(0.0, 1.0, int(ny - 1)) 53 | comp = group.add_subsystem( 54 | "t_over_c_bsp", 55 | om.SplineComp( 56 | method="bsplines", x_interp_val=x_interp, num_cp=n_cp, interp_options={"order": min(n_cp, 4)} 57 | ), 58 | promotes_inputs=["t_over_c_cp"], 59 | promotes_outputs=["t_over_c"], 60 | ) 61 | comp.add_spline(y_cp_name="t_over_c_cp", y_interp_name="t_over_c") 62 | 63 | comp = ViscousDrag(surface=surface, with_viscous=True) 64 | group.add_subsystem("viscousdrag", comp, promotes=["*"]) 65 | 66 | run_test(self, group, complex_flag=True) 67 | 68 | 69 | if __name__ == "__main__": 70 | unittest.main() 71 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_vortex_mesh.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.aerodynamics.vortex_mesh import VortexMesh 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces, get_ground_effect_surfaces 5 | 6 | import numpy as np 7 | 8 | np.set_printoptions(linewidth=200) 9 | 10 | 11 | class Test(unittest.TestCase): 12 | def test(self): 13 | surfaces = get_default_surfaces() 14 | 15 | comp = VortexMesh(surfaces=surfaces) 16 | 17 | run_test(self, comp) 18 | 19 | def test_groundplane(self): 20 | surfaces = get_ground_effect_surfaces() 21 | 22 | comp = VortexMesh(surfaces=surfaces) 23 | 24 | run_test(self, comp, atol=1e6) 25 | 26 | def test_right_wing(self): 27 | surfaces = get_default_surfaces() 28 | 29 | # flip each surface to lie on right 30 | for surface in surfaces: 31 | surface["mesh"] = surface["mesh"][:, ::-1, :] 32 | surface["mesh"][:, :, 1] *= -1.0 33 | 34 | comp = VortexMesh(surfaces=surfaces) 35 | 36 | run_test(self, comp, atol=1e6) 37 | 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /tests/aerodynamics_tests/test_wave_drag.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | from openaerostruct.aerodynamics.wave_drag import WaveDrag 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | import openmdao.api as om 6 | 7 | 8 | class Test(unittest.TestCase): 9 | def test(self): 10 | surface = get_default_surfaces()[0] 11 | surface["with_wave"] = True 12 | surface["t_over_c_cp"] = np.array([0.15, 0.21, 0.03, 0.05]) 13 | 14 | ny = surface["mesh"].shape[1] 15 | n_cp = len(surface["t_over_c_cp"]) 16 | 17 | group = om.Group() 18 | 19 | indep_var_comp = om.IndepVarComp() 20 | indep_var_comp.add_output("t_over_c_cp", val=surface["t_over_c_cp"]) 21 | indep_var_comp.add_output("Mach_number", val=0.95) 22 | indep_var_comp.add_output("CL", val=0.7) 23 | indep_var_comp.add_output("lengths_spanwise", val=np.array([12.14757848, 11.91832712, 11.43730892]), units="m") 24 | indep_var_comp.add_output("widths", val=np.array([10.01555924, 9.80832351, 9.79003729]), units="m") 25 | indep_var_comp.add_output("chords", val=np.array([2.72835132, 5.12528179, 7.88916016, 13.6189974]), units="m") 26 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 27 | 28 | x_interp = np.linspace(0.0, 1.0, int(ny - 1)) 29 | comp = group.add_subsystem( 30 | "t_over_c_bsp", 31 | om.SplineComp( 32 | method="bsplines", 33 | x_interp_val=x_interp, 34 | num_cp=n_cp, 35 | interp_options={"order": min(n_cp, 4)}, 36 | ), 37 | promotes_inputs=["t_over_c_cp"], 38 | promotes_outputs=["t_over_c"], 39 | ) 40 | comp.add_spline(y_cp_name="t_over_c_cp", y_interp_name="t_over_c") 41 | 42 | comp = WaveDrag(surface=surface) 43 | group.add_subsystem("wavedrag", comp, promotes=["*"]) 44 | 45 | run_test(self, group, complex_flag=True) 46 | 47 | 48 | if __name__ == "__main__": 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /tests/common_tests/test_atmos_comp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.common.atmos_comp import AtmosComp 4 | from openaerostruct.utils.testing import run_test 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | comp = AtmosComp() 10 | 11 | run_test(self, comp, method="fd", atol=1e20, rtol=1e-4) 12 | 13 | 14 | if __name__ == "__main__": 15 | unittest.main() 16 | -------------------------------------------------------------------------------- /tests/common_tests/test_reynolds_comp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | import openmdao.api as om 4 | from openmdao.utils.assert_utils import assert_check_partials 5 | from openaerostruct.common.reynolds_comp import ReynoldsComp 6 | 7 | 8 | class Test(unittest.TestCase): 9 | def test_reynolds_derivs(self): 10 | comp = ReynoldsComp() 11 | 12 | prob = om.Problem() 13 | prob.model.add_subsystem("comp", comp, promotes=["*"]) 14 | prob.setup(force_alloc_complex=True) 15 | 16 | rng = np.random.default_rng(0) 17 | prob["rho"] = rng.random() 18 | prob["mu"] = rng.random() 19 | prob["v"] = rng.random() 20 | prob.run_model() 21 | 22 | check = prob.check_partials(compact_print=True, method="cs", step=1e-40) 23 | 24 | assert_check_partials(check) 25 | 26 | 27 | if __name__ == "__main__": 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /tests/functionals_tests/test_breguet_range.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.functionals.breguet_range import BreguetRange 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = BreguetRange(surfaces=surfaces) 12 | 13 | run_test(self, comp, complex_flag=True) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/functionals_tests/test_center_of_gravity.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.functionals.center_of_gravity import CenterOfGravity 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = CenterOfGravity(surfaces=surfaces) 12 | 13 | run_test(self, comp, compact_print=True) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/functionals_tests/test_equilibrium.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.functionals.equilibrium import Equilibrium 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = Equilibrium(surfaces=surfaces) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/functionals_tests/test_moment_coefficient.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | import openmdao.api as om 5 | 6 | from openaerostruct.functionals.moment_coefficient import MomentCoefficient 7 | from openaerostruct.utils.testing import run_test, get_default_surfaces 8 | 9 | 10 | class Test(unittest.TestCase): 11 | def test(self): 12 | wing_dict = {"name": "wing", "mesh": np.zeros((2, 7)), "symmetry": True} 13 | tail_dict = {"name": "tail", "mesh": np.zeros((3, 5)), "symmetry": False} 14 | 15 | surfaces = [wing_dict, tail_dict] 16 | 17 | comp = MomentCoefficient(surfaces=surfaces) 18 | 19 | run_test(self, comp, complex_flag=True, method="cs") 20 | 21 | def test2(self): 22 | surfaces = get_default_surfaces() 23 | 24 | group = om.Group() 25 | 26 | comp = MomentCoefficient(surfaces=surfaces) 27 | 28 | indep_var_comp = om.IndepVarComp() 29 | 30 | indep_var_comp.add_output("S_ref_total", val=1e4, units="m**2") 31 | indep_var_comp.add_output("cg", val=np.array([-10.0, 10.0, -10.0]), units="m") 32 | 33 | group.add_subsystem("moment_calc", comp) 34 | group.add_subsystem("indep_var_comp", indep_var_comp) 35 | 36 | group.connect("indep_var_comp.S_ref_total", "moment_calc.S_ref_total") 37 | group.connect("indep_var_comp.cg", "moment_calc.cg") 38 | 39 | run_test(self, group, complex_flag=True, method="cs") 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tests/functionals_tests/test_sum_areas.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.functionals.sum_areas import SumAreas 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = SumAreas(surfaces=surfaces) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/functionals_tests/test_total_lift_drag.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import openmdao.api as om 4 | 5 | from openaerostruct.functionals.total_lift_drag import TotalLiftDrag 6 | from openaerostruct.utils.testing import run_test, get_default_surfaces 7 | 8 | 9 | class Test(unittest.TestCase): 10 | def test(self): 11 | wing_dict = {"name": "wing", "num_y": 7, "num_x": 2, "symmetry": True} 12 | tail_dict = {"name": "tail", "num_y": 5, "num_x": 3, "symmetry": False} 13 | 14 | surfaces = [wing_dict, tail_dict] 15 | 16 | comp = TotalLiftDrag(surfaces=surfaces) 17 | 18 | run_test(self, comp) 19 | 20 | # This is known to have some issues for sufficiently small values of S_ref_total 21 | # There is probably a derivative bug somewhere in the moment_coefficient.py calcs 22 | def test2(self): 23 | surfaces = get_default_surfaces() 24 | 25 | group = om.Group() 26 | 27 | comp = TotalLiftDrag(surfaces=surfaces) 28 | 29 | indep_var_comp = om.IndepVarComp() 30 | 31 | indep_var_comp.add_output("S_ref_total", val=10.0, units="m**2") 32 | 33 | group.add_subsystem("moment_calc", comp) 34 | group.add_subsystem("indep_var_comp", indep_var_comp) 35 | 36 | group.connect("indep_var_comp.S_ref_total", "moment_calc.S_ref_total") 37 | 38 | run_test(self, group) 39 | 40 | 41 | if __name__ == "__main__": 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /tests/geometry_tests/test_geometry_mesh.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | 4 | from openaerostruct.geometry.geometry_mesh import GeometryMesh 5 | from openaerostruct.utils.testing import run_test 6 | 7 | 8 | class Test(unittest.TestCase): 9 | def test(self): 10 | surface = {} 11 | surface["symmetry"] = False 12 | surface["type"] = "aero" 13 | span = 1 14 | ny = 5 15 | mesh = np.zeros((2, ny, 3)) 16 | mesh[0, :, 0] = -1.0 17 | mesh[:, :, 1] = np.linspace(0, span, ny) 18 | surface["mesh"] = mesh 19 | surface["name"] = "wing" 20 | 21 | # The way this is currently set up, we don't actually use the values here 22 | surface["twist_cp"] = np.zeros((5)) 23 | surface["chord_cp"] = np.zeros((5)) 24 | surface["chord_scaling_pos"] = 0.25 25 | surface["xshear_cp"] = np.zeros((5)) 26 | surface["yshear_cp"] = np.zeros((5)) 27 | surface["zshear_cp"] = np.zeros((5)) 28 | 29 | surface["span"] = span 30 | surface["t_over_c_cp"] = np.array([0.12]) 31 | 32 | comp = GeometryMesh(surface=surface) 33 | 34 | run_test(self, comp, complex_flag=True, method="cs") 35 | 36 | 37 | if __name__ == "__main__": 38 | unittest.main() 39 | -------------------------------------------------------------------------------- /tests/geometry_tests/test_joining_comp.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import unittest 3 | 4 | from openaerostruct.geometry.geometry_multi_join import GeomMultiJoin 5 | from openaerostruct.geometry.utils import build_section_dicts 6 | from openaerostruct.utils.testing import run_test, get_three_section_surface 7 | 8 | 9 | class Test(unittest.TestCase): 10 | def test(self): 11 | (surface, chord_bspline) = get_three_section_surface() 12 | sec_dicts = build_section_dicts(surface) 13 | 14 | comp = GeomMultiJoin(sections=sec_dicts, dim_constr=[np.ones(3), np.ones(3), np.ones(3)]) 15 | 16 | run_test(self, comp, complex_flag=True, method="cs") 17 | 18 | 19 | if __name__ == "__main__": 20 | unittest.main() 21 | -------------------------------------------------------------------------------- /tests/geometry_tests/test_monotonic_constraint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from openaerostruct.geometry.monotonic_constraint import MonotonicConstraint 6 | 7 | from openaerostruct.utils.testing import run_test 8 | 9 | 10 | class Test(unittest.TestCase): 11 | def test_sym1(self): 12 | surface = {"symmetry": True, "mesh": np.zeros((1, 5, 3))} 13 | 14 | comp = MonotonicConstraint(var_name="x", surface=surface) 15 | run_test(self, comp) 16 | 17 | def test_sym2(self): 18 | surface = {"symmetry": True, "mesh": np.zeros((1, 16, 3))} 19 | comp = MonotonicConstraint(var_name="x", surface=surface) 20 | 21 | run_test(self, comp) 22 | 23 | def test_assym1(self): 24 | surface = {"symmetry": False, "mesh": np.zeros((1, 5, 3))} 25 | comp = MonotonicConstraint(var_name="x", surface=surface) 26 | run_test(self, comp) 27 | 28 | def test_assym2(self): 29 | surface = {"symmetry": False, "mesh": np.zeros((1, 16, 3))} 30 | comp = MonotonicConstraint(var_name="x", surface=surface) 31 | run_test(self, comp) 32 | 33 | 34 | if __name__ == "__main__": 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /tests/geometry_tests/test_radius_comp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import openmdao.api as om 4 | 5 | from openaerostruct.geometry.radius_comp import RadiusComp 6 | from openaerostruct.utils.testing import run_test, get_default_surfaces 7 | 8 | import numpy as np 9 | 10 | 11 | class Test(unittest.TestCase): 12 | def test(self): 13 | surfaces = get_default_surfaces() 14 | 15 | group = om.Group() 16 | 17 | comp = RadiusComp(surface=surfaces[0]) 18 | ny = surfaces[0]["mesh"].shape[1] 19 | 20 | indep_var_comp = om.IndepVarComp() 21 | indep_var_comp.add_output("mesh", val=surfaces[0]["mesh"], units="m") 22 | indep_var_comp.add_output("t_over_c", val=np.linspace(0.1, 0.5, num=ny - 1)) 23 | 24 | group.add_subsystem("radius", comp) 25 | group.add_subsystem("indep_var_comp", indep_var_comp) 26 | 27 | group.connect("indep_var_comp.mesh", "radius.mesh") 28 | group.connect("indep_var_comp.t_over_c", "radius.t_over_c") 29 | 30 | run_test(self, group) 31 | 32 | 33 | if __name__ == "__main__": 34 | unittest.main() 35 | -------------------------------------------------------------------------------- /tests/geometry_tests/test_unification_comp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import openmdao.api as om 4 | from openaerostruct.geometry.geometry_unification import GeomMultiUnification 5 | from openaerostruct.geometry.utils import build_section_dicts 6 | from openaerostruct.geometry.utils import stretch, sweep, dihedral 7 | from openaerostruct.utils.testing import run_test, get_three_section_surface 8 | 9 | 10 | class Test(unittest.TestCase): 11 | def test_no_shift(self): 12 | (surface, chord_bspline) = get_three_section_surface() 13 | sec_dicts = build_section_dicts(surface) 14 | 15 | comp = GeomMultiUnification(sections=sec_dicts, surface_name=surface["name"], shift_uni_mesh=False) 16 | 17 | group = om.Group() 18 | 19 | group.add_subsystem("comp", comp, promotes=["*"]) 20 | 21 | group.set_input_defaults("sec0_def_mesh", sec_dicts[0]["mesh"]) 22 | group.set_input_defaults("sec1_def_mesh", sec_dicts[1]["mesh"]) 23 | group.set_input_defaults("sec2_def_mesh", sec_dicts[2]["mesh"]) 24 | 25 | run_test(self, group, complex_flag=True, method="cs") 26 | 27 | def test_shift(self): 28 | (surface, chord_bspline) = get_three_section_surface() 29 | 30 | # Apply some scalar mesh transformations 31 | surface["span"] = [5.0, 5.0, 3.0] 32 | surface["sweep"] = [-10.0, 10.0, -20.0] 33 | surface["dihedral"] = [-10.0, 10.0, -20.0] 34 | 35 | sec_dicts = build_section_dicts(surface) 36 | 37 | for i in range(surface["num_sections"]): 38 | sweep(sec_dicts[i]["mesh"], surface["sweep"][i], True) 39 | stretch(sec_dicts[i]["mesh"], surface["span"][i], True) 40 | dihedral(sec_dicts[i]["mesh"], surface["dihedral"][i], True) 41 | 42 | comp = GeomMultiUnification(sections=sec_dicts, surface_name=surface["name"], shift_uni_mesh=True) 43 | 44 | group = om.Group() 45 | 46 | group.add_subsystem("comp", comp, promotes=["*"]) 47 | 48 | group.set_input_defaults("sec0_def_mesh", sec_dicts[0]["mesh"]) 49 | group.set_input_defaults("sec1_def_mesh", sec_dicts[1]["mesh"]) 50 | group.set_input_defaults("sec2_def_mesh", sec_dicts[2]["mesh"]) 51 | 52 | run_test(self, group, complex_flag=True, method="cs") 53 | 54 | 55 | if __name__ == "__main__": 56 | unittest.main() 57 | -------------------------------------------------------------------------------- /tests/geometry_tests/test_vsp_mesh.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from openmdao.utils.assert_utils import assert_near_equal 4 | 5 | from openaerostruct.meshing.mesh_generator import generate_mesh 6 | from openaerostruct.geometry.utils import generate_vsp_surfaces 7 | 8 | vsp_file = os.path.join(os.path.dirname(__file__), "rect_wing.vsp3") 9 | 10 | # check if openvsp is available 11 | try: 12 | generate_vsp_surfaces(vsp_file) 13 | 14 | openvsp_flag = True 15 | except ImportError: 16 | openvsp_flag = False 17 | 18 | 19 | @unittest.skipUnless(openvsp_flag, "OpenVSP is required.") 20 | class Test(unittest.TestCase): 21 | def test_full(self): 22 | # Create a rectangular wing with uniform spacing 23 | mesh_dict = { 24 | "num_y": 9, 25 | "num_x": 3, 26 | "wing_type": "rect", 27 | "symmetry": False, 28 | "span": 10.0, 29 | "root_chord": 1, 30 | "span_cos_spacing": 0.0, 31 | "offset": [-0.5, 0, 0], # chordwise coordinate goes from -0.5 to 0.5 32 | } 33 | 34 | oas_mesh = generate_mesh(mesh_dict) 35 | 36 | # Read in equivilent wing from vsp 37 | vsp_surf_list = generate_vsp_surfaces(vsp_file, symmetry=False) 38 | 39 | assert_near_equal(vsp_surf_list[0]["mesh"], oas_mesh) 40 | 41 | def test_symm(self): 42 | # Create a rectangular wing with uniform spacing 43 | mesh_dict = { 44 | "num_y": 9, 45 | "num_x": 3, 46 | "wing_type": "rect", 47 | "symmetry": True, 48 | "span": 10.0, 49 | "root_chord": 1, 50 | "span_cos_spacing": 0.0, 51 | "offset": [-0.5, 0, 0], # chordwise coordinate goes from -0.5 to 0.5 52 | } 53 | 54 | oas_mesh = generate_mesh(mesh_dict) 55 | # Flip mesh to be right-handed 56 | oas_mesh = oas_mesh[:, ::-1] 57 | oas_mesh[:, :, 1] *= -1.0 58 | 59 | # Read in equivilent wing from vsp 60 | vsp_surf_list = generate_vsp_surfaces(vsp_file, symmetry=True) 61 | 62 | assert_near_equal(vsp_surf_list[0]["mesh"], oas_mesh) 63 | 64 | 65 | if __name__ == "__main__": 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /tests/integration_tests/test_struct_analysis.py: -------------------------------------------------------------------------------- 1 | from openmdao.utils.assert_utils import assert_near_equal 2 | import unittest 3 | import numpy as np 4 | 5 | from openaerostruct.meshing.mesh_generator import generate_mesh 6 | from openaerostruct.structures.struct_groups import SpatialBeamAlone 7 | 8 | import openmdao.api as om 9 | 10 | 11 | class Test(unittest.TestCase): 12 | def test(self): 13 | # Create a dictionary to store options about the surface 14 | mesh_dict = {"num_y": 7, "wing_type": "CRM", "symmetry": True, "num_twist_cp": 5} 15 | 16 | mesh, twist_cp = generate_mesh(mesh_dict) 17 | 18 | surf_dict = { 19 | # Wing definition 20 | "name": "wing", # name of the surface 21 | "symmetry": True, # if true, model one half of wing 22 | # reflected across the plane y = 0 23 | "fem_model_type": "tube", 24 | "mesh": mesh, 25 | "radius_cp": np.ones((5)) * 0.5, 26 | # Structural values are based on aluminum 7075 27 | "E": 70.0e9, # [Pa] Young's modulus of the spar 28 | "G": 30.0e9, # [Pa] shear modulus of the spar 29 | "yield": 500.0e6 / 2.5, # [Pa] yield stress divided by 2.5 for limiting case 30 | "mrho": 3.0e3, # [kg/m^3] material density 31 | "fem_origin": 0.35, # normalized chordwise location of the spar 32 | "t_over_c_cp": np.array([0.15]), # maximum airfoil thickness 33 | "thickness_cp": np.ones((3)) * 0.1, 34 | "wing_weight_ratio": 2.0, 35 | "struct_weight_relief": False, # True to add the weight of the structure to the loads on the structure 36 | "distributed_fuel_weight": False, 37 | "exact_failure_constraint": False, 38 | } 39 | 40 | # Create the problem and assign the model group 41 | prob = om.Problem() 42 | 43 | ny = surf_dict["mesh"].shape[1] 44 | 45 | indep_var_comp = om.IndepVarComp() 46 | indep_var_comp.add_output("loads", val=np.ones((ny, 6)) * 2e5, units="N") 47 | indep_var_comp.add_output("load_factor", val=1.0) 48 | 49 | struct_group = SpatialBeamAlone(surface=surf_dict) 50 | 51 | # Add indep_vars to the structural group 52 | struct_group.add_subsystem("indep_vars", indep_var_comp, promotes=["*"]) 53 | 54 | prob.model.add_subsystem(surf_dict["name"], struct_group) 55 | 56 | # Set up the problem 57 | prob.setup() 58 | 59 | prob.run_model() 60 | 61 | assert_near_equal(prob["wing.structural_mass"][0], 117819.798089, 1e-4) 62 | 63 | 64 | if __name__ == "__main__": 65 | unittest.main() 66 | -------------------------------------------------------------------------------- /tests/integration_tests/test_struct_analysis_2g.py: -------------------------------------------------------------------------------- 1 | from openmdao.utils.assert_utils import assert_near_equal 2 | import unittest 3 | import numpy as np 4 | 5 | from openaerostruct.meshing.mesh_generator import generate_mesh 6 | from openaerostruct.structures.struct_groups import SpatialBeamAlone 7 | 8 | import openmdao.api as om 9 | 10 | 11 | class Test(unittest.TestCase): 12 | def test(self): 13 | # Create a dictionary to store options about the surface 14 | mesh_dict = {"num_y": 7, "wing_type": "CRM", "symmetry": True, "num_twist_cp": 5} 15 | 16 | mesh, twist_cp = generate_mesh(mesh_dict) 17 | 18 | surf_dict = { 19 | # Wing definition 20 | "name": "wing", # name of the surface 21 | "symmetry": True, # if true, model one half of wing 22 | # reflected across the plane y = 0 23 | "fem_model_type": "tube", 24 | "mesh": mesh, 25 | # Structural values are based on aluminum 7075 26 | "E": 70.0e9, # [Pa] Young's modulus of the spar 27 | "G": 30.0e9, # [Pa] shear modulus of the spar 28 | "yield": 500.0e6 / 2.5, # [Pa] yield stress divided by 2.5 for limiting case 29 | "mrho": 3.0e3, # [kg/m^3] material density 30 | "fem_origin": 0.35, # normalized chordwise location of the spar 31 | "t_over_c_cp": np.array([0.15]), # maximum airfoil thickness 32 | "thickness_cp": np.ones((3)) * 0.1, 33 | "wing_weight_ratio": 2.0, 34 | "struct_weight_relief": False, # True to add the weight of the structure to the loads on the structure 35 | "distributed_fuel_weight": False, 36 | "exact_failure_constraint": False, 37 | } 38 | 39 | # Create the problem and assign the model group 40 | prob = om.Problem() 41 | 42 | ny = surf_dict["mesh"].shape[1] 43 | 44 | indep_var_comp = om.IndepVarComp() 45 | indep_var_comp.add_output("loads", val=np.ones((ny, 6)) * 2e5, units="N") 46 | indep_var_comp.add_output("load_factor", val=2.0) 47 | 48 | struct_group = SpatialBeamAlone(surface=surf_dict) 49 | 50 | # Add indep_vars to the structural group 51 | struct_group.add_subsystem("indep_vars", indep_var_comp, promotes=["*"]) 52 | 53 | prob.model.add_subsystem(surf_dict["name"], struct_group) 54 | 55 | # Set up the problem 56 | prob.setup() 57 | 58 | prob.run_model() 59 | 60 | assert_near_equal(prob["wing.structural_mass"][0], 124229.646011, 1e-4) 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /tests/integration_tests/test_struct_no_loads.py: -------------------------------------------------------------------------------- 1 | from openmdao.utils.assert_utils import assert_near_equal 2 | import unittest 3 | import numpy as np 4 | 5 | from openaerostruct.meshing.mesh_generator import generate_mesh 6 | from openaerostruct.structures.struct_groups import SpatialBeamAlone 7 | 8 | import openmdao.api as om 9 | 10 | 11 | class Test(unittest.TestCase): 12 | def test(self): 13 | # Create a dictionary to store options about the surface 14 | mesh_dict = {"num_y": 7, "wing_type": "CRM", "symmetry": True, "num_twist_cp": 5} 15 | 16 | mesh, twist_cp = generate_mesh(mesh_dict) 17 | 18 | surf_dict = { 19 | # Wing definition 20 | "name": "wing", # name of the surface 21 | "symmetry": True, # if true, model one half of wing 22 | # reflected across the plane y = 0 23 | "fem_model_type": "tube", 24 | "mesh": mesh, 25 | # Structural values are based on aluminum 7075 26 | "E": 70.0e9, # [Pa] Young's modulus of the spar 27 | "G": 30.0e9, # [Pa] shear modulus of the spar 28 | "yield": 500.0e6 / 2.5, # [Pa] yield stress divided by 2.5 for limiting case 29 | "mrho": 3.0e3, # [kg/m^3] material density 30 | "fem_origin": 0.35, # normalized chordwise location of the spar 31 | "t_over_c_cp": np.array([0.15]), # maximum airfoil thickness 32 | "thickness_cp": np.ones((3)) * 0.1, 33 | "wing_weight_ratio": 2.0, 34 | "struct_weight_relief": False, # True to add the weight of the structure to the loads on the structure 35 | "distributed_fuel_weight": False, 36 | "exact_failure_constraint": False, 37 | } 38 | 39 | # Create the problem and assign the model group 40 | prob = om.Problem() 41 | 42 | ny = surf_dict["mesh"].shape[1] 43 | 44 | indep_var_comp = om.IndepVarComp() 45 | indep_var_comp.add_output("loads", val=np.zeros((ny, 6)), units="N") 46 | indep_var_comp.add_output("load_factor", val=1.0) 47 | 48 | struct_group = SpatialBeamAlone(surface=surf_dict) 49 | 50 | # Add indep_vars to the structural group 51 | struct_group.add_subsystem("indep_vars", indep_var_comp, promotes=["*"]) 52 | 53 | prob.model.add_subsystem(surf_dict["name"], struct_group, promotes=["*"]) 54 | 55 | prob.driver = om.ScipyOptimizeDriver() 56 | 57 | # Setup problem and add design variables, constraint, and objective 58 | prob.model.add_design_var("thickness_cp", lower=0.01, upper=0.5, scaler=1e2) 59 | prob.model.add_constraint("failure", upper=0.0) 60 | prob.model.add_constraint("thickness_intersects", upper=0.0) 61 | 62 | # Add design variables, constraisnt, and objective on the problem 63 | prob.model.add_objective("structural_mass", scaler=1e-5) 64 | 65 | # Set up the problem 66 | prob.setup() 67 | 68 | prob.run_driver() 69 | 70 | assert_near_equal(prob["structural_mass"][0], 13601.162582, 1e-4) 71 | assert_near_equal(prob["disp"][1, 2], 0.0, 1e-4) 72 | 73 | 74 | if __name__ == "__main__": 75 | unittest.main() 76 | -------------------------------------------------------------------------------- /tests/meshing_tests/test_mesh_generator.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class Test(unittest.TestCase): 5 | def test_span_cos_spacing_rect(self): 6 | # Checks if a valid mesh is returned for various spanwise spacing factors 7 | import numpy as np 8 | from openaerostruct.meshing.mesh_generator import gen_rect_mesh 9 | 10 | num_x = 2 11 | num_y = 11 12 | span = 10 13 | span_cos_spacing = [0.0, 0.25, 0.5, 0.75, 1.0, 2.0, 2.25, 2.5, 2.75, 3.0] 14 | 15 | for csp in span_cos_spacing: 16 | mesh = gen_rect_mesh(num_x, num_y, span, chord=2, span_cos_spacing=csp, chord_cos_spacing=0.0) 17 | 18 | self.assertTrue(np.all(np.diff(mesh[0, :, 1]) > 0.0)) 19 | self.assertTrue(np.all(np.diff(mesh[:, 0, 0]) > 0.0)) 20 | 21 | def test_chord_cos_spacing_rect(self): 22 | # Checks if a valid mesh is returned for various spanwise spacing factors 23 | import numpy as np 24 | from openaerostruct.meshing.mesh_generator import gen_rect_mesh 25 | 26 | num_x = 11 27 | num_y = 11 28 | span = 10 29 | chord_cos_spacing = [0.0, 0.25, 0.5, 0.75, 1.0] 30 | 31 | for csp in chord_cos_spacing: 32 | mesh = gen_rect_mesh(num_x, num_y, span, chord=2, span_cos_spacing=0.0, chord_cos_spacing=csp) 33 | 34 | self.assertTrue(np.all(np.diff(mesh[0, :, 1]) > 0.0)) 35 | self.assertTrue(np.all(np.diff(mesh[:, 0, 0]) > 0.0)) 36 | 37 | def test_span_cos_spacing_crm(self): 38 | # Checks if a valid mesh is returned for various spanwise spacing factors 39 | import numpy as np 40 | from openaerostruct.meshing.mesh_generator import gen_crm_mesh 41 | 42 | num_x = 2 43 | num_y = 6 44 | span_cos_spacing = [0.0, 0.25, 0.5, 0.75, 1.0, 2.0, 2.25, 2.5, 2.75, 3.0] 45 | 46 | for csp in span_cos_spacing: 47 | mesh, _, _ = gen_crm_mesh(num_x, num_y, span_cos_spacing=csp, chord_cos_spacing=0.0) 48 | 49 | self.assertTrue(np.all(np.diff(mesh[0, :, 1]) > 0.0)) 50 | self.assertTrue(np.all(np.diff(mesh[:, 0, 0]) > 0.0)) 51 | 52 | def test_chord_cos_spacing_crm(self): 53 | # Checks if a valid mesh is returned for various spanwise spacing factors 54 | import numpy as np 55 | from openaerostruct.meshing.mesh_generator import gen_crm_mesh 56 | 57 | num_x = 6 58 | num_y = 6 59 | chord_cos_spacing = [0.0, 0.25, 0.5, 0.75, 1.0] 60 | 61 | for csp in chord_cos_spacing: 62 | mesh, _, _ = gen_crm_mesh(num_x, num_y, span_cos_spacing=0.0, chord_cos_spacing=csp) 63 | 64 | self.assertTrue(np.all(np.diff(mesh[0, :, 1]) > 0.0)) 65 | self.assertTrue(np.all(np.diff(mesh[:, 0, 0]) > 0.0)) 66 | 67 | 68 | if __name__ == "__main__": 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /tests/meshing_tests/test_mesh_regen.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from openmdao.utils.assert_utils import assert_near_equal 3 | 4 | 5 | class Test(unittest.TestCase): 6 | def test_span_regen_rect(self): 7 | # Checks if a valid mesh is returned for various spanwise spacing factors and numbers of points 8 | from openaerostruct.meshing.mesh_generator import gen_rect_mesh 9 | from openaerostruct.meshing.utils import regen_spanwise_panels 10 | 11 | num_x = 2 12 | num_y = 11 13 | span = 10 14 | span_cos_spacing = [0.0, 0.25, 0.5, 0.75, 1.0, 2.0, 2.25, 2.5, 2.75, 3.0] 15 | 16 | mesh = gen_rect_mesh(num_x, num_y, span, chord=2, span_cos_spacing=0.0, chord_cos_spacing=0.0) 17 | rc = mesh[0, -1, 0] - mesh[-1, -1, 0] 18 | tc = mesh[0, 0, 0] - mesh[-1, 0, 0] 19 | b = mesh[0, 0, 1] - mesh[0, -1, 1] 20 | 21 | new_num_y = [7, 21, 31] 22 | 23 | for nny in new_num_y: 24 | for csp in span_cos_spacing: 25 | newMesh = regen_spanwise_panels(mesh, nny, span_cos_spacing=csp) 26 | 27 | new_rc = newMesh[0, -1, 0] - newMesh[-1, -1, 0] 28 | new_tc = newMesh[0, 0, 0] - newMesh[-1, 0, 0] 29 | new_b = newMesh[0, 0, 1] - newMesh[0, -1, 1] 30 | 31 | assert_near_equal(new_rc, rc, 1e-10) 32 | assert_near_equal(new_tc, tc, 1e-10) 33 | assert_near_equal(new_b, b, 1e-10) 34 | 35 | def test_span_regen_crm(self): 36 | # Checks if a valid mesh is returned for various spanwise spacing factors and numbers of points 37 | from openaerostruct.meshing.mesh_generator import gen_crm_mesh 38 | from openaerostruct.meshing.utils import regen_spanwise_panels 39 | 40 | num_x = 2 41 | num_y = 11 42 | 43 | span_cos_spacing = [0.0, 0.25, 0.5, 0.75, 1.0, 2.0, 2.25, 2.5, 2.75, 3.0] 44 | 45 | mesh, _, _ = gen_crm_mesh(num_x, num_y, span_cos_spacing=0.0, chord_cos_spacing=0.0) 46 | rc = mesh[0, -1, 0] - mesh[-1, -1, 0] 47 | tc = mesh[0, 0, 0] - mesh[-1, 0, 0] 48 | b = mesh[0, 0, 1] - mesh[0, -1, 1] 49 | 50 | new_num_y = [7, 21, 31] 51 | 52 | for nny in new_num_y: 53 | for csp in span_cos_spacing: 54 | newMesh = regen_spanwise_panels(mesh, nny, span_cos_spacing=csp) 55 | 56 | new_rc = newMesh[0, -1, 0] - newMesh[-1, -1, 0] 57 | new_tc = newMesh[0, 0, 0] - newMesh[-1, 0, 0] 58 | new_b = newMesh[0, 0, 1] - newMesh[0, -1, 1] 59 | 60 | assert_near_equal(new_rc, rc, 1e-10) 61 | assert_near_equal(new_tc, tc, 1e-10) 62 | assert_near_equal(new_b, b, 1e-10) 63 | 64 | 65 | if __name__ == "__main__": 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /tests/mphys_tests/test_aero_builder.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import importlib 3 | import numpy as np 4 | 5 | from openaerostruct.mphys import AeroBuilder 6 | from openaerostruct.utils.testing import get_default_surfaces 7 | 8 | # check if mphys/mpi4py is available 9 | try: 10 | from mpi4py import MPI 11 | 12 | mpi_flag = True 13 | except ImportError: 14 | mpi_flag = False 15 | 16 | mphys_flag = importlib.util.find_spec("mphys") is not None 17 | 18 | 19 | @unittest.skipUnless(mphys_flag and mpi_flag, "mphys/mpi4py is required.") 20 | class Test(unittest.TestCase): 21 | def setUp(self): 22 | self.surfaces = get_default_surfaces() 23 | comm = MPI.COMM_WORLD 24 | # Create mphys builder for aero solver 25 | self.aero_builder = AeroBuilder(self.surfaces) 26 | self.aero_builder.initialize(comm) 27 | 28 | def test_tagged_indices(self): 29 | wing_nnodes = self.surfaces[0]["mesh"].size // 3 30 | tail_nnodes = self.surfaces[1]["mesh"].size // 3 31 | with self.subTest(case="wing"): 32 | wing_inds = self.aero_builder.get_tagged_indices(["wing"]) 33 | np.testing.assert_equal(wing_inds, np.arange(0, wing_nnodes)) 34 | 35 | with self.subTest(case="tail"): 36 | tail_inds = self.aero_builder.get_tagged_indices(["tail"]) 37 | np.testing.assert_equal(tail_inds, np.arange(wing_nnodes, wing_nnodes + tail_nnodes)) 38 | 39 | with self.subTest(case="wing+tail"): 40 | wt_inds = self.aero_builder.get_tagged_indices(["wing", "tail"]) 41 | np.testing.assert_equal(wt_inds, np.arange(0, wing_nnodes + tail_nnodes)) 42 | 43 | with self.subTest(case="all"): 44 | wt_inds = self.aero_builder.get_tagged_indices(-1) 45 | np.testing.assert_equal(wt_inds, np.arange(0, wing_nnodes + tail_nnodes)) 46 | 47 | 48 | if __name__ == "__main__": 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /tests/mphys_tests/test_aero_funcs_group.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import openmdao.api as om 4 | from mphys.core import MPhysVariables 5 | 6 | from openaerostruct.mphys.aero_funcs_group import AeroFuncsGroup 7 | from openaerostruct.utils.testing import run_test, get_default_surfaces 8 | 9 | FlowVars = MPhysVariables.Aerodynamics.FlowConditions 10 | 11 | 12 | class Test(unittest.TestCase): 13 | def test(self): 14 | surfaces = get_default_surfaces() 15 | 16 | group = om.Group() 17 | 18 | ivc = group.add_subsystem("ivc", om.IndepVarComp()) 19 | ivc.add_output(FlowVars.ANGLE_OF_ATTACK, val=1.0) 20 | ivc.add_output(FlowVars.MACH_NUMBER, val=0.6) 21 | ivc.add_output("wing_widths", val=[1.0, 1.0, 1.0]) 22 | ivc.add_output("v", val=1.0) 23 | ivc.add_output("rho", val=1.0) 24 | 25 | group.add_subsystem("funcs", AeroFuncsGroup(surfaces=[surfaces[0]], write_solution=False)) 26 | group.promotes("funcs", [(f"{surfaces[0]['name']}.widths", f"{surfaces[0]['name']}_widths")]) 27 | 28 | group.connect(f"ivc.{FlowVars.ANGLE_OF_ATTACK}", f"funcs.{FlowVars.ANGLE_OF_ATTACK}") 29 | group.connect(f"ivc.{FlowVars.MACH_NUMBER}", f"funcs.{FlowVars.MACH_NUMBER}") 30 | group.connect("ivc.rho", "funcs.rho") 31 | group.connect("ivc.v", "funcs.v") 32 | group.connect("ivc.wing_widths", f"{surfaces[0]['name']}_widths") 33 | 34 | run_test(self, group, complex_flag=True, method="cs") 35 | 36 | 37 | if __name__ == "__main__": 38 | unittest.main() 39 | -------------------------------------------------------------------------------- /tests/mphys_tests/test_aero_solver_group.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import openmdao.api as om 4 | from mphys.core import MPhysVariables 5 | 6 | from openaerostruct.mphys.aero_solver_group import AeroSolverGroup 7 | from openaerostruct.utils.testing import run_test, get_default_surfaces 8 | 9 | FlowVars = MPhysVariables.Aerodynamics.FlowConditions 10 | 11 | 12 | class Test(unittest.TestCase): 13 | def test_incompressible(self): 14 | group = self.setup_solver(compressible=False) 15 | run_test(self, group, complex_flag=True, method="cs", atol=1e-4, rtol=1e-5) 16 | 17 | def test_compressible(self): 18 | group = self.setup_solver(compressible=True) 19 | run_test(self, group, complex_flag=True, method="cs", atol=1e-4, rtol=1e-5) 20 | 21 | def setup_solver(self, compressible=True): 22 | surfaces = get_default_surfaces() 23 | 24 | group = om.Group() 25 | 26 | ivc = group.add_subsystem("ivc", om.IndepVarComp()) 27 | ivc.add_output(f"{surfaces[0]['name']}_def_mesh", val=surfaces[0]["mesh"]) 28 | ivc.add_output(FlowVars.ANGLE_OF_ATTACK, val=1.0) 29 | ivc.add_output(FlowVars.MACH_NUMBER, val=0.6) 30 | 31 | group.add_subsystem("solver", AeroSolverGroup(surfaces=[surfaces[0]], compressible=compressible)) 32 | group.promotes("solver", [(f"{surfaces[0]['name']}.normals", f"{surfaces[0]['name']}_normals")]) 33 | 34 | group.connect(f"ivc.{surfaces[0]['name']}_def_mesh", f"solver.{surfaces[0]['name']}_def_mesh") 35 | group.connect(f"ivc.{FlowVars.ANGLE_OF_ATTACK}", f"solver.{FlowVars.ANGLE_OF_ATTACK}") 36 | if compressible: 37 | group.connect(f"ivc.{FlowVars.MACH_NUMBER}", f"solver.{FlowVars.MACH_NUMBER}") 38 | 39 | return group 40 | 41 | 42 | if __name__ == "__main__": 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tests/mphys_tests/test_demux_mesh.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.mphys.demux_surface_mesh import DemuxSurfaceMesh 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = DemuxSurfaceMesh(surfaces=surfaces) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/mphys_tests/test_mux_forces.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.mphys.mux_surface_forces import MuxSurfaceForces 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surfaces = get_default_surfaces() 10 | 11 | comp = MuxSurfaceForces(surfaces=surfaces) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/structures_tests/test_add_point_masses.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | from openmdao.utils.assert_utils import assert_near_equal 5 | import openmdao.api as om 6 | from openaerostruct.structures.compute_point_mass_loads import ComputePointMassLoads 7 | from openaerostruct.utils.testing import run_test, get_default_surfaces 8 | 9 | 10 | derivs_added = False 11 | 12 | 13 | class Test(unittest.TestCase): 14 | @unittest.skipUnless(derivs_added, "Analytic derivs not added yet") 15 | def test_derivs(self): 16 | surface = get_default_surfaces()[0] 17 | 18 | surface["n_point_masses"] = 2 19 | 20 | comp = ComputePointMassLoads(surface=surface) 21 | 22 | group = om.Group() 23 | 24 | indep_var_comp = om.IndepVarComp() 25 | 26 | nodesval = np.array([[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 2.0, 0.0], [0.0, 3.0, 0.0]]) 27 | 28 | point_masses = np.array([[2.0, 1.0]]) 29 | 30 | point_mass_locations = np.array([[2.1, 0.1, 0.2], [3.2, 1.2, 0.3]]) 31 | 32 | indep_var_comp.add_output("nodes", val=nodesval, units="m") 33 | indep_var_comp.add_output("point_masses", val=point_masses, units="kg") 34 | indep_var_comp.add_output("point_mass_locations", val=point_mass_locations, units="m") 35 | 36 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 37 | group.add_subsystem("compute_point_mass_loads", comp, promotes=["*"]) 38 | 39 | run_test(self, group, complex_flag=True, step=1e-8, atol=1e-5, compact_print=True) 40 | 41 | @unittest.skipUnless(derivs_added, "Analytic derivs not added yet") 42 | def test_simple_values(self): 43 | surface = get_default_surfaces()[0] 44 | 45 | surface["n_point_masses"] = 1 46 | 47 | comp = ComputePointMassLoads(surface=surface) 48 | 49 | group = om.Group() 50 | 51 | indep_var_comp = om.IndepVarComp() 52 | 53 | nodesval = np.array([[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 2.0, 0.0], [0.0, 3.0, 0.0]]) 54 | 55 | point_masses = np.array([[1 / 9.8]]) 56 | 57 | point_mass_locations = np.array([[0.55012, 0.1, 0.0]]) 58 | 59 | indep_var_comp.add_output("nodes", val=nodesval, units="m") 60 | indep_var_comp.add_output("point_masses", val=point_masses, units="kg") 61 | indep_var_comp.add_output("point_mass_locations", val=point_mass_locations, units="m") 62 | 63 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 64 | group.add_subsystem("compute_point_mass_loads", comp, promotes=["*"]) 65 | 66 | prob = run_test(self, group, complex_flag=True, step=1e-8, atol=1e-5, compact_print=True) 67 | 68 | truth_array = np.array([0, 0, -1.0, 0.0, 0.55012, 0.0]) 69 | 70 | assert_near_equal(prob["comp.loads_from_point_masses"][0, :], truth_array, 1e-6) 71 | 72 | 73 | if __name__ == "__main__": 74 | unittest.main() 75 | -------------------------------------------------------------------------------- /tests/structures_tests/test_compute_nodes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.structures.compute_nodes import ComputeNodes 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surface = get_default_surfaces()[0] 10 | 11 | comp = ComputeNodes(surface=surface) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/structures_tests/test_create_rhs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.structures.create_rhs import CreateRHS 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surface = get_default_surfaces()[0] 10 | 11 | comp = CreateRHS(surface=surface) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/structures_tests/test_disp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.structures.disp import Disp 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surface = get_default_surfaces()[0] 10 | 11 | comp = Disp(surface=surface) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/structures_tests/test_energy.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.structures.energy import Energy 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surface = get_default_surfaces()[0] 10 | 11 | comp = Energy(surface=surface) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/structures_tests/test_failure_exact.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.structures.failure_exact import FailureExact 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surface = get_default_surfaces()[0] 10 | 11 | # turn down some of these properties, so the absolute deriv error isn't magnified 12 | surface["E"] = 7 13 | surface["G"] = 3 14 | surface["yield"] = 0.02 15 | 16 | comp = FailureExact(surface=surface) 17 | 18 | run_test(self, comp) 19 | 20 | 21 | if __name__ == "__main__": 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /tests/structures_tests/test_failure_ks.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.structures.failure_ks import FailureKS 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surface = get_default_surfaces()[0] 10 | 11 | # turn down some of these properties, so the absolute deriv error isn't magnified 12 | surface["E"] = 7 13 | surface["G"] = 3 14 | surface["yield"] = 0.02 15 | 16 | comp = FailureKS(surface=surface) 17 | 18 | run_test(self, comp, complex_flag=True, step=1e-40, method="cs", compact_print=False) 19 | 20 | 21 | if __name__ == "__main__": 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /tests/structures_tests/test_fem.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.structures.fem import FEM 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surface = get_default_surfaces()[0] 10 | 11 | comp = FEM(surface=surface) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/structures_tests/test_fuel_loads.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import openmdao.api as om 4 | from openaerostruct.structures.fuel_loads import FuelLoads 5 | from openaerostruct.utils.testing import run_test, get_default_surfaces 6 | import numpy as np 7 | 8 | 9 | class Test(unittest.TestCase): 10 | def test_0(self): 11 | surface = get_default_surfaces()[0] 12 | 13 | comp = FuelLoads(surface=surface) 14 | 15 | group = om.Group() 16 | 17 | indep_var_comp = om.IndepVarComp() 18 | 19 | nodesval = np.array([[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 2.0, 0.0], [0.0, 3.0, 0.0]], dtype=complex) 20 | 21 | indep_var_comp.add_output("nodes", val=nodesval, units="m") 22 | 23 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 24 | group.add_subsystem("load", comp, promotes=["*"]) 25 | 26 | run_test(self, group, complex_flag=True, atol=1e-2, rtol=1e-6) 27 | 28 | 29 | if __name__ == "__main__": 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /tests/structures_tests/test_materials_tube.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.structures.section_properties_tube import SectionPropertiesTube 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surface = get_default_surfaces()[0] 10 | 11 | comp = SectionPropertiesTube(surface=surface) 12 | 13 | run_test(self, comp, complex_flag=True) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/structures_tests/test_non_intersecting_thickness.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.structures.non_intersecting_thickness import NonIntersectingThickness 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surface = get_default_surfaces()[0] 10 | 11 | comp = NonIntersectingThickness(surface=surface) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/structures_tests/test_spar_within_wing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.structures.spar_within_wing import SparWithinWing 4 | from openaerostruct.utils.testing import run_test, get_default_surfaces 5 | 6 | 7 | class Test(unittest.TestCase): 8 | def test(self): 9 | surface = get_default_surfaces()[0] 10 | 11 | comp = SparWithinWing(surface=surface) 12 | 13 | run_test(self, comp) 14 | 15 | 16 | if __name__ == "__main__": 17 | unittest.main() 18 | -------------------------------------------------------------------------------- /tests/structures_tests/test_structural_cg.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | import openmdao.api as om 5 | from openaerostruct.structures.structural_cg import StructuralCG 6 | from openaerostruct.utils.testing import run_test, get_default_surfaces 7 | 8 | np.random.seed(1) 9 | 10 | 11 | class Test(unittest.TestCase): 12 | def test(self): 13 | surface = get_default_surfaces()[0] 14 | 15 | group = om.Group() 16 | 17 | comp = StructuralCG(surface=surface) 18 | 19 | indep_var_comp = om.IndepVarComp() 20 | 21 | ny = surface["mesh"].shape[1] 22 | 23 | rng = np.random.default_rng(0) 24 | indep_var_comp.add_output("nodes", val=rng.random((ny, 3)), units="m") 25 | indep_var_comp.add_output("structural_mass", val=1.0, units="kg") 26 | indep_var_comp.add_output("element_mass", val=np.ones((ny - 1)), units="kg") 27 | 28 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 29 | group.add_subsystem("structural_cg", comp, promotes=["*"]) 30 | 31 | run_test(self, group, complex_flag=True, compact_print=False) 32 | 33 | 34 | if __name__ == "__main__": 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /tests/structures_tests/test_total_load_derivs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from openaerostruct.structures.wing_weight_loads import StructureWeightLoads 4 | from openaerostruct.structures.total_loads import TotalLoads 5 | from openaerostruct.utils.testing import run_test, get_default_surfaces 6 | import openmdao.api as om 7 | import numpy as np 8 | 9 | 10 | class Test(unittest.TestCase): 11 | def test_0(self): 12 | surface = get_default_surfaces()[0] 13 | 14 | comp = TotalLoads(surface=surface) 15 | 16 | run_test(self, comp, complex_flag=True) 17 | 18 | def test_1(self): 19 | surface = get_default_surfaces()[0] 20 | surface["struct_weight_relief"] = True 21 | 22 | comp = TotalLoads(surface=surface) 23 | 24 | run_test(self, comp, complex_flag=True) 25 | 26 | def test_2(self): 27 | surface = get_default_surfaces()[0] 28 | surface["distributed_fuel_weight"] = True 29 | 30 | comp = TotalLoads(surface=surface) 31 | 32 | run_test(self, comp, complex_flag=True) 33 | 34 | def test_structural_mass_loads(self): 35 | surface = get_default_surfaces()[0] 36 | 37 | comp = StructureWeightLoads(surface=surface) 38 | 39 | group = om.Group() 40 | 41 | indep_var_comp = om.IndepVarComp() 42 | 43 | ny = surface["mesh"].shape[1] 44 | 45 | # carefully chosen "random" values that give non-uniform derivatives outputs that are good for testing 46 | nodesval = np.array([[1.0, 2.0, 4.0], [20.0, 22.0, 7.0], [8.0, 17.0, 14.0], [13.0, 14.0, 16.0]], dtype=complex) 47 | element_mass_val = np.arange(ny - 1) + 1 48 | 49 | indep_var_comp.add_output("nodes", val=nodesval, units="m") 50 | indep_var_comp.add_output("element_mass", val=element_mass_val, units="kg") 51 | 52 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 53 | group.add_subsystem("load", comp, promotes=["*"]) 54 | 55 | run_test(self, group, complex_flag=True, compact_print=True) 56 | 57 | 58 | if __name__ == "__main__": 59 | unittest.main() 60 | -------------------------------------------------------------------------------- /tests/structures_tests/test_vonmises_tube.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | import openmdao.api as om 5 | from openaerostruct.structures.vonmises_tube import VonMisesTube 6 | from openaerostruct.utils.testing import run_test, get_default_surfaces 7 | 8 | 9 | class Test(unittest.TestCase): 10 | def test(self): 11 | surface = get_default_surfaces()[0] 12 | 13 | group = om.Group() 14 | 15 | comp = VonMisesTube(surface=surface) 16 | 17 | indep_var_comp = om.IndepVarComp() 18 | 19 | ny = surface["mesh"].shape[1] 20 | 21 | # define the spar with y out the wing 22 | nodes = np.zeros((ny, 3)) 23 | nodes[:, 0] = np.linspace(0, 0.01, ny) 24 | nodes[:, 1] = np.linspace(0, 1, ny) 25 | 26 | radius = 0.01 * np.ones((ny - 1)) 27 | 28 | disp = np.zeros((ny, 6)) 29 | for i in range(6): 30 | disp[:, i] = np.linspace(0, 0.001, ny) 31 | 32 | indep_var_comp.add_output("nodes", val=nodes, units="m") 33 | indep_var_comp.add_output("radius", val=radius, units="m") 34 | indep_var_comp.add_output("disp", val=disp, units="m") 35 | 36 | group.add_subsystem("vm_comp", comp) 37 | group.add_subsystem("indep_var_comp", indep_var_comp) 38 | 39 | group.connect("indep_var_comp.nodes", "vm_comp.nodes") 40 | group.connect("indep_var_comp.radius", "vm_comp.radius") 41 | group.connect("indep_var_comp.disp", "vm_comp.disp") 42 | 43 | run_test(self, group, complex_flag=True, compact_print=True, method="cs", step=1e-40, atol=2e-4, rtol=1e-8) 44 | 45 | 46 | if __name__ == "__main__": 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /tests/structures_tests/test_vonmises_wingbox.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | import openmdao.api as om 5 | from openaerostruct.structures.vonmises_wingbox import VonMisesWingbox 6 | from openaerostruct.utils.testing import run_test, get_default_surfaces 7 | 8 | 9 | class Test(unittest.TestCase): 10 | def test(self): 11 | surface = get_default_surfaces()[0] 12 | 13 | # turn down some of these properties, so the absolute deriv error isn't magnified 14 | surface["E"] = 7 15 | surface["G"] = 3 16 | surface["yield"] = 0.02 17 | 18 | surface["strength_factor_for_upper_skin"] = 1.0 19 | 20 | comp = VonMisesWingbox(surface=surface) 21 | 22 | group = om.Group() 23 | 24 | indep_var_comp = om.IndepVarComp() 25 | 26 | ny = surface["mesh"].shape[1] 27 | 28 | nodesval = np.array([[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 2.0, 0.0], [0.0, 3.0, 0.0]]) 29 | 30 | indep_var_comp.add_output("nodes", val=nodesval, units="m") 31 | indep_var_comp.add_output("disp", val=np.ones((ny, 6)), units="m") 32 | indep_var_comp.add_output("Qz", val=np.ones((ny - 1)), units="m**3") 33 | indep_var_comp.add_output("Iz", val=np.ones((ny - 1))) 34 | indep_var_comp.add_output("J", val=np.ones((ny - 1)), units="m**4") 35 | indep_var_comp.add_output("A_enc", val=np.ones((ny - 1)), units="m**2") 36 | indep_var_comp.add_output("spar_thickness", val=np.ones((ny - 1)), units="m") 37 | indep_var_comp.add_output("skin_thickness", val=np.ones((ny - 1)), units="m") 38 | indep_var_comp.add_output("htop", val=np.ones((ny - 1)), units="m") 39 | indep_var_comp.add_output("hbottom", val=np.ones((ny - 1)), units="m") 40 | indep_var_comp.add_output("hfront", val=np.ones((ny - 1)), units="m") 41 | indep_var_comp.add_output("hrear", val=np.ones((ny - 1)), units="m") 42 | 43 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 44 | group.add_subsystem("vonmises_wingbox", comp, promotes=["*"]) 45 | 46 | run_test(self, group, complex_flag=True, step=1e-8, atol=2e-5, compact_print=True) 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /tests/structures_tests/test_weight.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | import openmdao.api as om 5 | from openaerostruct.structures.weight import Weight 6 | from openaerostruct.utils.testing import run_test, get_default_surfaces 7 | 8 | 9 | np.random.seed(314) 10 | 11 | 12 | class Test(unittest.TestCase): 13 | def test(self): 14 | surface = get_default_surfaces()[0] 15 | ny = surface["mesh"].shape[1] 16 | 17 | group = om.Group() 18 | 19 | ivc = om.IndepVarComp() 20 | rng = np.random.default_rng(0) 21 | ivc.add_output("nodes", val=rng.random((ny, 3)), units="m") 22 | 23 | comp = Weight(surface=surface) 24 | 25 | group.add_subsystem("ivc", ivc, promotes=["*"]) 26 | group.add_subsystem("comp", comp, promotes=["*"]) 27 | 28 | run_test(self, group, compact_print=False, complex_flag=True) 29 | 30 | 31 | if __name__ == "__main__": 32 | unittest.main() 33 | -------------------------------------------------------------------------------- /tests/transfer_tests/test_compute_transformation_matrix.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | import openmdao.api as om 4 | 5 | from openaerostruct.transfer.compute_transformation_matrix import ComputeTransformationMatrix 6 | from openaerostruct.utils.testing import run_test, get_default_surfaces 7 | 8 | 9 | np.random.seed(314) 10 | 11 | 12 | class Test(unittest.TestCase): 13 | def test(self): 14 | surface = get_default_surfaces()[0] 15 | 16 | comp = ComputeTransformationMatrix(surface=surface) 17 | 18 | group = om.Group() 19 | 20 | indep_var_comp = om.IndepVarComp() 21 | 22 | ny = surface["mesh"].shape[1] 23 | rng = np.random.default_rng(0) 24 | disp = rng.random((ny, 6)) * 100.0 25 | 26 | indep_var_comp.add_output("disp", val=disp, units="m") 27 | 28 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 29 | group.add_subsystem("trans_mtx", comp, promotes=["*"]) 30 | 31 | run_test(self, group, complex_flag=True, method="cs") 32 | 33 | 34 | if __name__ == "__main__": 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /tests/transfer_tests/test_displacement_transfer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import openmdao.api as om 3 | 4 | from openaerostruct.transfer.displacement_transfer import DisplacementTransfer 5 | from openaerostruct.utils.testing import run_test, get_default_surfaces 6 | 7 | 8 | class Test(unittest.TestCase): 9 | def test(self): 10 | surface = get_default_surfaces()[0] 11 | 12 | comp = DisplacementTransfer(surface=surface) 13 | 14 | group = om.Group() 15 | 16 | indep_var_comp = om.IndepVarComp() 17 | 18 | mesh = surface["mesh"] 19 | 20 | indep_var_comp.add_output("mesh", val=mesh, units="m") 21 | 22 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 23 | group.add_subsystem("load", comp, promotes=["*"]) 24 | 25 | run_test(self, group, complex_flag=True) 26 | 27 | 28 | if __name__ == "__main__": 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /tests/transfer_tests/test_load_transfer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | import openmdao.api as om 5 | from openaerostruct.transfer.load_transfer import LoadTransfer 6 | from openaerostruct.utils.testing import run_test, get_default_surfaces 7 | 8 | 9 | class Test(unittest.TestCase): 10 | def test(self): 11 | surface = get_default_surfaces()[0] 12 | group = om.Group() 13 | 14 | comp = LoadTransfer(surface=surface) 15 | 16 | indep_var_comp = om.IndepVarComp() 17 | 18 | nx = surface["mesh"].shape[0] 19 | ny = surface["mesh"].shape[1] 20 | 21 | rng = np.random.default_rng(0) 22 | indep_var_comp.add_output("def_mesh", val=rng.random((nx, ny, 3)), units="m") 23 | indep_var_comp.add_output("sec_forces", val=rng.random((nx - 1, ny - 1, 3)), units="N") 24 | 25 | group.add_subsystem("indep_var_comp", indep_var_comp, promotes=["*"]) 26 | group.add_subsystem("load_transfer", comp, promotes=["*"]) 27 | 28 | run_test(self, group, complex_flag=True, compact_print=False) 29 | 30 | 31 | if __name__ == "__main__": 32 | unittest.main() 33 | --------------------------------------------------------------------------------