diff --git a/.github/workflows/oas.yml b/.github/workflows/oas.yml index c11171576..43820afa2 100644 --- a/.github/workflows/oas.yml +++ b/.github/workflows/oas.yml @@ -20,6 +20,7 @@ jobs: test: runs-on: ubuntu-latest strategy: + fail-fast: false # continue other jobs even if one of the jobs in matrix fails matrix: dep-versions: ["oldest", "latest"] # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Set versions to test here ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -163,12 +164,13 @@ jobs: coverage xml - name: Upload Coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: fail_ci_if_error: true - token: ${{ secrets.CODECOV_TOKEN }} verbose: true - + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + # --- linting and formatting --- black: uses: mdolab/.github/.github/workflows/black.yaml@main diff --git a/openaerostruct/__init__.py b/openaerostruct/__init__.py index 7a38ae062..7f9085047 100644 --- a/openaerostruct/__init__.py +++ b/openaerostruct/__init__.py @@ -1 +1 @@ -__version__ = "2.7.1" +__version__ = "2.7.2" diff --git a/openaerostruct/docs/user_reference/mesh_surface_dict.rst b/openaerostruct/docs/user_reference/mesh_surface_dict.rst index baf3a0d21..9f53ccbcd 100644 --- a/openaerostruct/docs/user_reference/mesh_surface_dict.rst +++ b/openaerostruct/docs/user_reference/mesh_surface_dict.rst @@ -19,7 +19,7 @@ Here is a list of the keys and default values of the ``mesh_dict``, which is use * - num_x - 3 - - - Number of chordwise vertices. Needs to be 2 or an odd number. + - Number of chordwise vertices. * - num_y - 5 - diff --git a/openaerostruct/geometry/tests/test_vsp_mesh.py b/openaerostruct/geometry/tests/test_vsp_mesh.py index 014bd0f54..24c9784da 100644 --- a/openaerostruct/geometry/tests/test_vsp_mesh.py +++ b/openaerostruct/geometry/tests/test_vsp_mesh.py @@ -27,6 +27,7 @@ def test_full(self): "span": 10.0, "root_chord": 1, "span_cos_spacing": 0.0, + "offset": [-0.5, 0, 0], # chordwise coordinate goes from -0.5 to 0.5 } oas_mesh = generate_mesh(mesh_dict) @@ -46,6 +47,7 @@ def test_symm(self): "span": 10.0, "root_chord": 1, "span_cos_spacing": 0.0, + "offset": [-0.5, 0, 0], # chordwise coordinate goes from -0.5 to 0.5 } oas_mesh = generate_mesh(mesh_dict) diff --git a/openaerostruct/geometry/utils.py b/openaerostruct/geometry/utils.py index 19063f366..643c02d83 100644 --- a/openaerostruct/geometry/utils.py +++ b/openaerostruct/geometry/utils.py @@ -375,6 +375,7 @@ def gen_rect_mesh(num_x, num_y, span, chord, span_cos_spacing=0.0, chord_cos_spa mesh = np.zeros((num_x, num_y, 3)) ny2 = (num_y + 1) // 2 + # --- spanwise discretization --- # Hotfix a special case for spacing bunched at the root and tips if span_cos_spacing == 2.0: beta = np.linspace(0, np.pi, ny2) @@ -396,23 +397,17 @@ def gen_rect_mesh(num_x, num_y, span, chord, span_cos_spacing=0.0, chord_cos_spa half_wing = cosine * span_cos_spacing + (1 - span_cos_spacing) * uniform full_wing = np.hstack((-half_wing[:-1], half_wing[::-1])) * span - nx2 = (num_x + 1) // 2 - beta = np.linspace(0, np.pi / 2, nx2) - - # mixed spacing with span_cos_spacing as a weighting factor - # this is for the chordwise spacing - cosine = 0.5 * np.cos(beta) # cosine spacing - uniform = np.linspace(0, 0.5, nx2)[::-1] # uniform spacing - half_wing = cosine * chord_cos_spacing + (1 - chord_cos_spacing) * uniform - full_wing_x = np.hstack((-half_wing[:-1], half_wing[::-1])) * chord - - # Special case if there are only 2 chordwise nodes - if num_x <= 2: - full_wing_x = np.array([0.0, chord]) + # --- chordwise discretization --- + cosine = 0.5 * (1 - np.cos(np.linspace(0, np.pi, num_x))) # cosine spacing from 0 to 1 + uniform = np.linspace(0, 1, num_x) # uniform spacing + # mixed spacing with chord_cos_spacing as a weighting factor + wing_x = cosine * chord_cos_spacing + (1 - chord_cos_spacing) * uniform + wing_x *= chord # apply chord length + # --- form 3D mesh array --- for ind_x in range(num_x): for ind_y in range(num_y): - mesh[ind_x, ind_y, :] = [full_wing_x[ind_x], full_wing[ind_y], 0] + mesh[ind_x, ind_y, :] = [wing_x[ind_x], full_wing[ind_y], 0] return mesh @@ -557,24 +552,12 @@ def add_chordwise_panels(mesh, num_x, chord_cos_spacing): # Obtain mesh and num properties num_y = mesh.shape[1] - nx2 = (num_x + 1) // 2 - - # Create beta, an array of linear sampling points to pi/2 - beta = np.linspace(0, np.pi / 2, nx2) - - # Obtain the two spacings that we will use to blend - cosine = 0.5 * np.cos(beta) # cosine spacing - uniform = np.linspace(0, 0.5, nx2)[::-1] # uniform spacing - - # Create half of the wing in the chordwise direction - half_wing = cosine * chord_cos_spacing + (1 - chord_cos_spacing) * uniform - if chord_cos_spacing == 0.0: - full_wing_x = np.linspace(0, 1.0, num_x) - - else: - # Mirror this half wing into a full wing; offset by 0.5 so it goes 0 to 1 - full_wing_x = np.hstack((-half_wing[:-1], half_wing[::-1])) + 0.5 + # chordwise discretization + cosine = 0.5 * (1 - np.cos(np.linspace(0, np.pi, num_x))) # cosine spacing from 0 to 1 + uniform = np.linspace(0, 1, num_x) # uniform spacing + # mixed spacing with chord_cos_spacing as a weighting factor + wing_x = cosine * chord_cos_spacing + (1 - chord_cos_spacing) * uniform # Obtain the leading and trailing edges le = mesh[0, :, :] @@ -586,7 +569,7 @@ def add_chordwise_panels(mesh, num_x, chord_cos_spacing): new_mesh[-1, :, :] = te for i in range(1, num_x - 1): - w = full_wing_x[i] + w = wing_x[i] new_mesh[i, :, :] = (1 - w) * le + w * te return new_mesh @@ -635,6 +618,29 @@ def get_default_geo_dict(): def generate_mesh(input_dict): + """ + Generate an OAS mesh. + + Parameters + ---------- + input_dict : dict + Dictionary containing user-provided parameters for the surface definition. + See the following for more information: + https://mdolab-openaerostruct.readthedocs-hosted.com/en/latest/user_reference/mesh_surface_dict.html#mesh-dict + + Returns + ------- + mesh : numpy array + Nodal coordinates defining the mesh. + shape = (nx, ny, 3), + where nx is the number of chordwise discretization nodes, ny is the number of spanwise discretization nodes. + If input_dict["symmetry"] is True, mesh defines left half of wing. + twist : numpy array, optional + Only for CRM wing (input_dict["wing_type"] == "CRM"). + Twist values at the spanwise locations. + + """ + # Get defaults and update surface with the user-provided input surf_dict = get_default_geo_dict() @@ -677,10 +683,6 @@ def generate_mesh(input_dict): if not num_y % 2: raise ValueError("num_y must be an odd number.") - # Check to make sure that an odd number of chordwise points (num_x) was provided - if not num_x % 2 and not num_x == 2: - raise ValueError("num_x must be an odd number.") - # Generate rectangular mesh if surf_dict["wing_type"] == "rect": span = surf_dict["span"] diff --git a/openaerostruct/tests/test_multiple_rect.py b/openaerostruct/tests/test_multiple_rect.py index 02e3aa072..a6e4e3dde 100644 --- a/openaerostruct/tests/test_multiple_rect.py +++ b/openaerostruct/tests/test_multiple_rect.py @@ -44,7 +44,13 @@ def test(self): } # Create a dictionary to store options about the surface - mesh_dict = {"num_y": 5, "num_x": 3, "wing_type": "rect", "symmetry": True, "offset": np.array([50, 0.0, 0.0])} + mesh_dict = { + "num_y": 5, + "num_x": 3, + "wing_type": "rect", + "symmetry": True, + "offset": np.array([49.5, 0.0, 0.0]), + } mesh = generate_mesh(mesh_dict) diff --git a/openaerostruct/tests/test_scaneagle.py b/openaerostruct/tests/test_scaneagle.py index 5cf43b454..72d65e04b 100644 --- a/openaerostruct/tests/test_scaneagle.py +++ b/openaerostruct/tests/test_scaneagle.py @@ -115,7 +115,7 @@ def test(self): indep_var_comp.add_output("W0", val=10.0, units="kg") indep_var_comp.add_output("speed_of_sound", val=322.2, units="m/s") indep_var_comp.add_output("load_factor", val=1.0) - indep_var_comp.add_output("empty_cg", val=np.array([0.2, 0.0, 0.0]), units="m") + indep_var_comp.add_output("empty_cg", val=np.array([0.35, 0.0, 0.0]), units="m") prob.model.add_subsystem("prob_vars", indep_var_comp, promotes=["*"]) diff --git a/openaerostruct/tests/test_struct_point_masses.py b/openaerostruct/tests/test_struct_point_masses.py index b5ef926ae..7e138165a 100644 --- a/openaerostruct/tests/test_struct_point_masses.py +++ b/openaerostruct/tests/test_struct_point_masses.py @@ -11,7 +11,7 @@ class Test(unittest.TestCase): def test(self): # Create a dictionary to store options about the surface - mesh_dict = {"num_y": 31, "wing_type": "rect", "span": 10, "symmetry": True} + mesh_dict = {"num_y": 31, "num_x": 3, "wing_type": "rect", "span": 10, "symmetry": True} mesh = generate_mesh(mesh_dict) @@ -48,7 +48,7 @@ def test(self): point_masses = np.array([[10.0]]) - point_mass_locations = np.array([[1.0, -10.0, 0.0]]) + point_mass_locations = np.array([[1.5, -10.0, 0.0]]) indep_var_comp.add_output("point_masses", val=point_masses, units="kg") indep_var_comp.add_output("point_mass_locations", val=point_mass_locations, units="m") @@ -69,7 +69,7 @@ def test(self): def test_multiple_masses(self): # Create a dictionary to store options about the surface - mesh_dict = {"num_y": 31, "wing_type": "rect", "span": 10, "symmetry": True} + mesh_dict = {"num_y": 31, "num_x": 3, "wing_type": "rect", "span": 10, "symmetry": True} mesh = generate_mesh(mesh_dict) @@ -106,7 +106,7 @@ def test_multiple_masses(self): point_masses = np.array([[10.0, 20.0]]) - point_mass_locations = np.array([[1.0, -1.0, 0.0], [1.0, -2.0, 0.0]]) + point_mass_locations = np.array([[1.5, -1.0, 0.0], [1.5, -2.0, 0.0]]) indep_var_comp.add_output("point_masses", val=point_masses, units="kg") indep_var_comp.add_output("point_mass_locations", val=point_mass_locations, units="m")