From fc34405582120f5f57173e2af2de7aa8c301520e Mon Sep 17 00:00:00 2001 From: Bernardo Pacini <61238730+bernardopacini@users.noreply.github.com> Date: Tue, 30 Aug 2022 11:50:38 -0400 Subject: [PATCH] Coupled Propeller Movement in Aerostructural Case (#322) * Brute-force implementation of coupling * Cleaning up propeller movement coupling and standardizing interface * Removing print statement * Fixes in propeller derivatives * Cleaning up propeller pre-coupling. * Removing forward-mode code and standardizing derivative mode check * Updating mphys aerostruct case to include propeller (and child FFD) * Adding comments to new propeller coupling functions * Fixes for multi-propeller case * Fixing MPHYS aerostructural with propeller test * Black formatting --- dafoam/mphys/mphys_dafoam.py | 443 +++++++++++++++++- dafoam/pyDAFoam.py | 2 +- tests/refs/DAFoam_Test_MphysAeroStructRef.txt | 8 +- tests/runTests_MphysAeroStruct.py | 107 +++-- 4 files changed, 514 insertions(+), 46 deletions(-) diff --git a/dafoam/mphys/mphys_dafoam.py b/dafoam/mphys/mphys_dafoam.py index 49d045f6..a4783417 100644 --- a/dafoam/mphys/mphys_dafoam.py +++ b/dafoam/mphys/mphys_dafoam.py @@ -7,6 +7,7 @@ from petsc4py import PETSc import numpy as np from mpi4py import MPI +from mphys import MaskedConverter, UnmaskedConverter, MaskedVariableDescription petsc4py.init(sys.argv) @@ -59,6 +60,7 @@ def __init__( # api level method for all builders def initialize(self, comm): + self.comm = comm # initialize the PYDAFOAM class, defined in pyDAFoam.py self.DASolver = PYDAFOAM(options=self.options, comm=comm) # always set the mesh @@ -90,21 +92,32 @@ def get_mesh_coordinate_subsystem(self, scenario_name=None): return DAFoamMesh(solver=self.DASolver) def get_pre_coupling_subsystem(self, scenario_name=None): - # we warp as a pre-processing step - if self.warp_in_solver: - # if we warp in the solver, then we wont have any pre-coupling systems - return None - else: - # we warp as a pre-processing step - return DAFoamWarper(solver=self.DASolver) + return DAFoamPrecouplingGroup(solver=self.DASolver, warp_in_solver=self.warp_in_solver) def get_post_coupling_subsystem(self, scenario_name=None): return DAFoamFunctions(solver=self.DASolver) def get_number_of_nodes(self, groupName=None): + # Get number of aerodynamic nodes if groupName is None: groupName = self.DASolver.designFamilyGroup - return int(self.DASolver.getSurfaceCoordinates(groupName=groupName).size / 3) + nodes = int(self.DASolver.getSurfaceCoordinates(groupName=groupName).size / 3) + + # Add fictitious nodes to root proc, if they are used + if self.comm.rank == 0: + fsiDict = self.DASolver.getOption("fsi") + fvSourceDict = self.DASolver.getOption("fvSource") + if "propMovement" in fsiDict.keys() and fsiDict["propMovement"]: + if "fvSource" in fsiDict.keys(): + # Iterate through Actuator Disks + for fvSource, parameters in fsiDict["fvSource"].items(): + # Check if Actuator Disk Exists + if fvSource not in fvSourceDict: + raise RuntimeWarning("Actuator disk {} not found when adding masked nodes".format(fvSource)) + + # Count Nodes + nodes += 1 + parameters["nNodes"] + return nodes class DAFoamGroup(Group): @@ -129,16 +142,29 @@ def setup(self): if self.prop_coupling not in ["Prop", "Wing"]: raise AnalysisError("prop_coupling can be either Wing or Prop, while %s is given!" % self.prop_coupling) + fsiDict = self.DASolver.getOption("fsi") if self.use_warper: + # Setup node masking + self.mphys_set_masking() + + # Add propeller movement, if enabled + if "propMovement" in fsiDict.keys() and fsiDict["propMovement"]: + prop_movement = DAFoamActuator(solver=self.DASolver) + self.add_subsystem("prop_movement", prop_movement, promotes_inputs=["*"], promotes_outputs=["*"]) + # if we dont have geo_disp, we also need to promote the x_a as x_a0 from the deformer component self.add_subsystem( "deformer", DAFoamWarper( solver=self.DASolver, ), - promotes_inputs=["x_aero"], + promotes_inputs=[("x_aero", "x_aero_masked")], promotes_outputs=["dafoam_vol_coords"], ) + elif "propMovement" in fsiDict.keys() and fsiDict["propMovement"]: + raise RuntimeError( + "Propeller movement not possible when the warper is outside of the solver. Check for a valid scenario." + ) if self.prop_coupling is not None: if self.prop_coupling == "Wing": @@ -171,7 +197,144 @@ def setup(self): "force", DAFoamForces(solver=self.DASolver), promotes_inputs=["dafoam_vol_coords", "dafoam_states"], - promotes_outputs=["f_aero"], + promotes_outputs=[("f_aero", "f_aero_masked")], + ) + + # Setup unmasking + self.mphys_set_unmasking(forces=self.struct_coupling) + + def mphys_compute_nodes(self): + fsiDict = self.DASolver.getOption("fsi") + fvSourceDict = self.DASolver.getOption("fvSource") + + # Check if Actuator Disk Definitions Exist, only add to Root Proc + nodes_prop = 0 + if self.comm.rank == 0: + if "propMovement" in fsiDict.keys() and fsiDict["propMovement"]: + if "fvSource" in fsiDict.keys(): + # Iterate through Actuator Disks + for fvSource, parameters in fsiDict["fvSource"].items(): + # Check if Actuator Disk Exists + if fvSource not in fvSourceDict: + raise RuntimeWarning("Actuator disk %s not found when adding masked nodes" % fvSource) + + # Count Nodes + nodes_prop += 1 + parameters["nNodes"] + + # Compute number of aerodynamic nodes + nodes_aero = int(self.DASolver.getSurfaceCoordinates(groupName=self.DASolver.designFamilyGroup).size / 3) + + # Sum nodes and return all values + nodes_total = nodes_aero + nodes_prop + return nodes_total, nodes_aero, nodes_prop + + def mphys_set_masking(self): + # Retrieve number of nodes in each category + nodes_total, nodes_aero, nodes_prop = self.mphys_compute_nodes() + + fsiDict = self.DASolver.getOption("fsi") + + mask = [] + output = [] + promotes_inputs = [] + promotes_outputs = [] + + # Mesh Coordinate Mask + mask.append(np.zeros([(nodes_total) * 3], dtype=bool)) + mask[0][:] = True + if nodes_prop > 0: + mask[0][3 * nodes_aero :] = False + output.append(MaskedVariableDescription("x_aero_masked", shape=(nodes_aero) * 3, tags=["mphys_coupling"])) + promotes_outputs.append("x_aero_masked") + + # Add Propeller Masks + if "propMovement" in fsiDict.keys() and fsiDict["propMovement"]: + if "fvSource" in fsiDict.keys(): + i_fvSource = 0 + i_start = 3 * nodes_aero + for fvSource, parameters in fsiDict["fvSource"].items(): + mask.append(np.zeros([(nodes_total) * 3], dtype=bool)) + mask[i_fvSource + 1][:] = False + + if self.comm.rank == 0: + mask[i_fvSource + 1][i_start : i_start + 3 * (1 + parameters["nNodes"])] = True + i_start += 3 * (1 + parameters["nNodes"]) + + output.append( + MaskedVariableDescription( + "x_prop_%s" % fvSource, shape=((1 + parameters["nNodes"])) * 3, tags=["mphys_coupling"] + ) + ) + else: + output.append( + MaskedVariableDescription("x_prop_%s" % fvSource, shape=(0), tags=["mphys_coupling"]) + ) + + promotes_outputs.append("x_prop_%s" % fvSource) + + i_fvSource += 1 + + # Define Mask + input = MaskedVariableDescription("x_aero", shape=(nodes_total) * 3, tags=["mphys_coupling"]) + promotes_inputs.append("x_aero") + masker = MaskedConverter(input=input, output=output, mask=mask, distributed=True, init_output=0.0) + self.add_subsystem("masker", masker, promotes_inputs=promotes_inputs, promotes_outputs=promotes_outputs) + + def mphys_set_unmasking(self, forces=False): + # Retrieve number of nodes in each category + nodes_total, nodes_aero, nodes_prop = self.mphys_compute_nodes() + + # If forces are active, generate mask + if forces: + fsiDict = self.DASolver.getOption("fsi") + + mask = [] + input = [] + promotes_inputs = [] + promotes_outputs = [] + + # Mesh Coordinate Mask + mask.append(np.zeros([(nodes_total) * 3], dtype=bool)) + mask[0][:] = True + if nodes_prop > 0: + mask[0][3 * nodes_aero :] = False + input.append(MaskedVariableDescription("f_aero_masked", shape=(nodes_aero) * 3, tags=["mphys_coupling"])) + promotes_inputs.append("f_aero_masked") + + if "propMovement" in fsiDict.keys() and fsiDict["propMovement"]: + if "fvSource" in fsiDict.keys(): + # Add Propeller Masks + i_fvSource = 0 + i_start = 3 * nodes_aero + for fvSource, parameters in fsiDict["fvSource"].items(): + mask.append(np.zeros([(nodes_total) * 3], dtype=bool)) + mask[i_fvSource + 1][:] = False + + if self.comm.rank == 0: + mask[i_fvSource + 1][i_start : i_start + 3 * (1 + parameters["nNodes"])] = True + i_start += 3 * (1 + parameters["nNodes"]) + + input.append( + MaskedVariableDescription( + "f_prop_%s" % fvSource, + shape=((1 + parameters["nNodes"])) * 3, + tags=["mphys_coordinates"], + ) + ) + else: + input.append( + MaskedVariableDescription("f_prop_%s" % fvSource, shape=(0), tags=["mphys_coupling"]) + ) + promotes_inputs.append("f_prop_%s" % fvSource) + + i_fvSource += 1 + + # Define Mask + output = MaskedVariableDescription("f_aero", shape=(nodes_total) * 3, tags=["mphys_coupling"]) + promotes_outputs.append("f_aero") + unmasker = UnmaskedConverter(input=input, output=output, mask=mask, distributed=True, default_values=0.0) + self.add_subsystem( + "force_unmasker", unmasker, promotes_inputs=promotes_inputs, promotes_outputs=promotes_outputs ) def mphys_set_options(self, optionDict): @@ -180,6 +343,112 @@ def mphys_set_options(self, optionDict): self.solver.set_options(optionDict) +class DAFoamPrecouplingGroup(Group): + """ + Pre-coupling group that configures any components that happen before the solver and post-processor. + """ + + def initialize(self): + self.options.declare("solver", default=None, recordable=False) + self.options.declare("warp_in_solver", default=None, recordable=False) + + def setup(self): + self.DASolver = self.options["solver"] + self.warp_in_solver = self.options["warp_in_solver"] + + fsiDict = self.DASolver.getOption("fsi") + + # Return the warper only if it is not in the solver + if not self.warp_in_solver: + if "propMovement" in fsiDict.keys() and fsiDict["propMovement"]: + raise RuntimeError( + "Propeller movement not possible when the warper is outside of the solver. Check for a valid scenario." + ) + + self.add_subsystem( + "warper", + DAFoamWarper(solver=self.DASolver), + promotes_inputs=["x_aero"], + promotes_outputs=["dafoam_vol_coords"], + ) + + # If the warper is in the solver, add other pre-coupling groups if desired + else: + fvSourceDict = self.DASolver.getOption("fvSource") + nodes_prop = 0 + + # Add propeller nodes and subsystem if needed + if "propMovement" in fsiDict.keys() and fsiDict["propMovement"]: + self.add_subsystem( + "prop_nodes", DAFoamPropNodes(solver=self.DASolver), promotes_inputs=["*"], promotes_outputs=["*"] + ) + + # Only add to Root Proc + if self.comm.rank == 0: + if "fvSource" in fsiDict.keys(): + # Iterate through Actuator Disks + for fvSource, parameters in fsiDict["fvSource"].items(): + # Check if Actuator Disk Exists + if fvSource not in fvSourceDict: + raise RuntimeWarning("Actuator disk %s not found when adding masked nodes" % fvSource) + + # Count Nodes + nodes_prop += 1 + parameters["nNodes"] + + nodes_aero = int(self.DASolver.getSurfaceCoordinates(groupName=self.DASolver.designFamilyGroup).size / 3) + nodes_total = nodes_aero + nodes_prop + + mask = [] + input = [] + promotes_inputs = [] + + # Mesh Coordinate Mask + mask.append(np.zeros([(nodes_total) * 3], dtype=bool)) + mask[0][:] = True + if nodes_prop > 0: + mask[0][3 * nodes_aero :] = False + input.append( + MaskedVariableDescription("x_aero0_masked", shape=(nodes_aero) * 3, tags=["mphys_coordinates"]) + ) + promotes_inputs.append("x_aero0_masked") + + # Add propeller movement nodes mask if needed + if "propMovement" in fsiDict.keys() and fsiDict["propMovement"]: + # Add Propeller Masks + if "fvSource" in fsiDict.keys(): + i_fvSource = 0 + i_start = 3 * nodes_aero + for fvSource, parameters in fsiDict["fvSource"].items(): + mask.append(np.zeros([(nodes_total) * 3], dtype=bool)) + mask[i_fvSource + 1][:] = False + + if self.comm.rank == 0: + mask[i_fvSource + 1][i_start : i_start + 3 * (1 + parameters["nNodes"])] = True + i_start += 3 * (1 + parameters["nNodes"]) + + input.append( + MaskedVariableDescription( + "x_prop0_nodes_%s" % fvSource, + shape=((1 + parameters["nNodes"])) * 3, + tags=["mphys_coordinates"], + ) + ) + else: + input.append( + MaskedVariableDescription( + "x_prop0_nodes_%s" % fvSource, shape=(0), tags=["mphys_coordinates"] + ) + ) + promotes_inputs.append("x_prop0_nodes_%s" % fvSource) + + i_fvSource += 1 + + output = MaskedVariableDescription("x_aero0", shape=(nodes_total) * 3, tags=["mphys_coordinates"]) + + unmasker = UnmaskedConverter(input=input, output=output, mask=mask, distributed=True, default_values=0.0) + self.add_subsystem("unmasker", unmasker, promotes_inputs=promotes_inputs, promotes_outputs=["x_aero0"]) + + class DAFoamSolver(ImplicitComponent): """ OpenMDAO component that wraps the DAFoam flow and adjoint solvers @@ -570,7 +839,7 @@ def setup(self): self.add_subsystem( "volume_mesh", DAFoamWarper(solver=DASolver), - promotes_inputs=[("x_aero", "x_aero0")], + promotes_inputs=[("x_aero_masked", "x_aero0")], promotes_outputs=["dafoam_vol_coords"], ) @@ -1023,6 +1292,9 @@ def compute_jacvec_product(self, inputs, d_inputs, d_outputs, mode): stateVec = DASolver.array2Vec(dafoam_states) xvVec = DASolver.array2Vec(dafoam_xv) + if mode == "fwd": + raise AnalysisError("fwd not implemented!") + if "force_profile" in d_outputs: fBar = d_outputs["force_profile"] fBarVec = DASolver.array2VecSeq(fBar) @@ -1147,6 +1419,155 @@ def compute_jacvec_product(self, inputs, d_inputs, d_outputs, mode): d_inputs["force_profile"] += fBar +class DAFoamPropNodes(ExplicitComponent): + """ + Component that computes propeller aero-node locations that link with structural nodes in aerostructural cases. + """ + + def initialize(self): + self.options.declare("solver", default=None, recordable=False) + + def setup(self): + self.DASolver = self.options["solver"] + + self.fsiDict = self.DASolver.getOption("fsi") + self.fvSourceDict = self.DASolver.getOption("fvSource") + + if "fvSource" in self.fsiDict.keys(): + # Iterate through Actuator Disks + for fvSource, parameters in self.fsiDict["fvSource"].items(): + # Check if Actuator Disk Exists + if fvSource not in self.fvSourceDict: + raise RuntimeWarning("Actuator disk %s not found when adding masked nodes" % fvSource) + + # Add Input + self.add_input("x_prop0_%s" % fvSource, shape=3, distributed=False, tags=["mphys_coordinates"]) + + # Add Output + if self.comm.rank == 0: + self.add_output( + "x_prop0_nodes_%s" % fvSource, + shape=(1 + parameters["nNodes"]) * 3, + distributed=True, + tags=["mphys_coordinates"], + ) + self.add_output( + "f_prop_%s" % fvSource, + shape=(1 + parameters["nNodes"]) * 3, + distributed=True, + tags=["mphys_coordinates"], + ) + else: + self.add_output( + "x_prop0_nodes_%s" % fvSource, shape=(0), distributed=True, tags=["mphys_coordinates"] + ) + self.add_output("f_prop_%s" % fvSource, shape=(0), distributed=True, tags=["mphys_coordinates"]) + + def compute(self, inputs, outputs): + # Loop over all actuator disks to generate ring of nodes for each + for fvSource, parameters in self.fsiDict["fvSource"].items(): + # Nodes should only be on root proc + if self.comm.rank == 0: + center = inputs["x_prop0_%s" % fvSource] + + # Compute local coordinate frame for ring of nodes + direction = self.fvSourceDict[fvSource]["direction"] + direction = direction / np.linalg.norm(direction, 2) + temp_vec = np.array([1.0, 0.0, 0.0]) + y_local = np.cross(direction, temp_vec) + if np.linalg.norm(y_local, 2) < 1e-5: + temp_vec = np.array([0.0, 1.0, 0.0]) + y_local = np.cross(direction, temp_vec) + y_local = y_local / np.linalg.norm(y_local, 2) + z_local = np.cross(direction, y_local) + z_local = z_local / np.linalg.norm(z_local, 2) + + n_theta = parameters["nNodes"] + radial_loc = parameters["radialLoc"] + + # Set ring of nodes location and force values + nodes_x = np.zeros((n_theta + 1, 3)) + nodes_x[0, :] = center + nodes_f = np.zeros((n_theta + 1, 3)) + if n_theta == 0: + nodes_f[0, :] = -self.fvSourceDict[fvSource]["targetThrust"] * direction + else: + nodes_f[0, :] = 0.0 + for i in range(n_theta): + theta = i / n_theta * 2 * np.pi + nodes_x[i + 1, :] = ( + center + radial_loc * y_local * np.cos(theta) + radial_loc * z_local * np.sin(theta) + ) + nodes_f[i + 1, :] = -self.fvSourceDict[fvSource]["targetThrust"] * direction / n_theta + + outputs["x_prop0_nodes_%s" % fvSource] = nodes_x.flatten() + outputs["f_prop_%s" % fvSource] = nodes_f.flatten() + + def compute_jacvec_product(self, inputs, d_inputs, d_outputs, mode): + if mode == "fwd": + raise AnalysisError("fwd not implemented!") + + for fvSource, parameters in self.fsiDict["fvSource"].items(): + if "x_prop0_%s" % fvSource in d_inputs: + if "x_prop0_nodes_%s" % fvSource in d_outputs: + temp = np.zeros((parameters["nNodes"] + 1) * 3) + # Take ring of node seeds, broadcast them, and add them to all procs + if self.comm.rank == 0: + temp[:] = d_outputs["x_prop0_nodes_%s" % fvSource] + self.comm.Bcast(temp, root=0) + for i in range(parameters["nNodes"]): + d_inputs["x_prop0_%s" % fvSource] += temp[3 * i : 3 * i + 3] + + +class DAFoamActuator(ExplicitComponent): + """ + Component that updates actuator disk definition variables when actuator disks are displaced in an aerostructural case. + """ + + def initialize(self): + self.options.declare("solver", recordable=False) + + def setup(self): + self.DASolver = self.options["solver"] + + self.fsiDict = self.DASolver.getOption("fsi") + self.fvSourceDict = self.DASolver.getOption("fvSource") + + for fvSource, _ in self.fsiDict["fvSource"].items(): + self.add_input("dv_actuator_%s" % fvSource, shape=(6), distributed=False, tags=["mphys_coupling"]) + self.add_input("x_prop_%s" % fvSource, shape_by_conn=True, distributed=True, tags=["mphys_coupling"]) + + self.add_output("actuator_%s" % fvSource, shape_by_conn=(9), distributed=False, tags=["mphys_coupling"]) + + def compute(self, inputs, outputs): + # Loop over all actuator disks + for fvSource, _ in self.fsiDict["fvSource"].items(): + actuator = np.zeros(9) + # Update variables on root proc + if self.comm.rank == 0: + actuator[3:] = inputs["dv_actuator_%s" % fvSource][:] + actuator[:3] = inputs["x_prop_%s" % fvSource][:3] + + # Broadcast variables to all procs and set as output + self.comm.Bcast(actuator, root=0) + outputs["actuator_%s" % fvSource] = actuator + + def compute_jacvec_product(self, inputs, d_inputs, d_outputs, mode): + if mode == "fwd": + raise AnalysisError("fwd not implemented!") + + # Loop over all actuator disks + for fvSource, _ in self.fsiDict["fvSource"].items(): + if "actuator_%s" % fvSource in d_outputs: + if "dv_actuator_%s" % fvSource in d_inputs: + # Add non-location seeds to all procs + d_inputs["dv_actuator_%s" % fvSource][:] += d_outputs["actuator_%s" % fvSource][3:] + if "x_prop_%s" % fvSource in d_inputs: + # Add location seeds to only root proc + if self.comm.rank == 0: + d_inputs["x_prop_%s" % fvSource][:3] += d_outputs["actuator_%s" % fvSource][:3] + + class OptFuncs(object): """ Some utility functions diff --git a/dafoam/pyDAFoam.py b/dafoam/pyDAFoam.py index 8714a13b..8c00abad 100755 --- a/dafoam/pyDAFoam.py +++ b/dafoam/pyDAFoam.py @@ -313,7 +313,7 @@ def __init__(self): ## Fluid-structure interatcion (FSI) options. This dictionary takes in the required values for ## an FSI case to be used throughout the simulation. - self.fsi = {"pRef": 0.0} + self.fsi = {"pRef": 0.0, "propMovement": False} ## Aero-propulsive options self.aeroPropulsive = {} diff --git a/tests/refs/DAFoam_Test_MphysAeroStructRef.txt b/tests/refs/DAFoam_Test_MphysAeroStructRef.txt index 948626f7..8bd6d817 100755 --- a/tests/refs/DAFoam_Test_MphysAeroStructRef.txt +++ b/tests/refs/DAFoam_Test_MphysAeroStructRef.txt @@ -1,4 +1,4 @@ -Dictionary Key: cruise.aero_post.CD -@value 0.03451952488734403 1e-05 1e-08 -Dictionary Key: cruise.aero_post.CL -@value 0.2999826755303601 1e-05 1e-08 +Dictionary Key: cruise.aero_post.CD +@value 0.03412277538597665 1e-06 1e-08 +Dictionary Key: cruise.aero_post.CL +@value 0.2999903644711527 1e-05 1e-08 \ No newline at end of file diff --git a/tests/runTests_MphysAeroStruct.py b/tests/runTests_MphysAeroStruct.py index 0931882d..bbecf420 100755 --- a/tests/runTests_MphysAeroStruct.py +++ b/tests/runTests_MphysAeroStruct.py @@ -49,6 +49,13 @@ "solverName": "DARhoSimpleFoam", "fsi": { "pRef": p0, + "propMovement": True, + "fvSource": { + "disk1": { + "nNodes": 4, + "radialLoc": 0.1, + }, + }, }, "primalMinResTol": 1.0e-8, "primalBC": { @@ -69,14 +76,32 @@ "rhoMax": 5.0, "rhoMin": 0.2, }, + "fvSource": { + "disk1": { + "type": "actuatorDisk", + "source": "cylinderAnnulusSmooth", + "center": [7.0, 0.0, 14.0], + "direction": [1.0, 0.0, 0.0], + "innerRadius": 0.1, + "outerRadius": 1.0, + "rotDir": "left", + "scale": 1.0, + "POD": 0.0, + "eps": 0.05, + "expM": 1.0, + "expN": 0.5, + "adjustThrust": 1, + "targetThrust": 2000.0, + }, + }, "objFunc": { "CD": { "part1": { "type": "force", "source": "patchToFace", "patches": ["wing"], - "directionMode": "parallelToFlow", - "alphaName": "aoa", + "directionMode": "fixedDirection", + "direction": [1.0, 0.0, 0.0], "scale": 1.0 / (0.5 * U0 * U0 * A0 * rho0), "addToAdjoint": True, } @@ -86,8 +111,8 @@ "type": "force", "source": "patchToFace", "patches": ["wing"], - "directionMode": "normalToFlow", - "alphaName": "aoa", + "directionMode": "fixedDirection", + "direction": [0.0, 1.0, 0.0], "scale": 1.0 / (0.5 * U0 * U0 * A0 * rho0), "addToAdjoint": True, } @@ -114,9 +139,9 @@ "maxIncorrectlyOrientedFaces": 0, }, "designVar": { - "aoa": {"designVarType": "AOA", "patches": ["inout"], "flowAxis": "x", "normalAxis": "y"}, "twist": {"designVarType": "FFD"}, "shape": {"designVarType": "FFD"}, + "actuator_disk1": {"designVarType": "ACTD", "actuatorName": "disk1"}, }, "adjPCLag": 1, } @@ -148,13 +173,13 @@ def element_callback(dvNum, compID, compDescript, elemDescripts, specialDVs, **k def problem_setup(scenario_name, fea_assembler, problem): problem.addFunction("mass", functions.StructuralMass) problem.addFunction("ks_vmfailure", functions.KSFailure, safetyFactor=1.0, ksWeight=50.0) - g = np.array([0.0, 0.0, -9.81]) + g = np.array([0.0, -9.81, 0.0]) problem.addInertialLoad(g) tacs_options = { "element_callback": element_callback, "problem_setup": problem_setup, - "mesh_file": "wingbox.bdf", + "mesh_file": "wingboxProp.bdf", } struct_builder = TacsBuilder(tacs_options) @@ -176,7 +201,7 @@ def problem_setup(scenario_name, fea_assembler, problem): dvs = self.add_subsystem("dvs", om.IndepVarComp(), promotes=["*"]) # add the geometry component, we dont need a builder because we do it here. - self.add_subsystem("geometry", OM_DVGEOCOMP(ffd_file="./FFD/wingFFD.xyz")) + self.add_subsystem("geometry", OM_DVGEOCOMP(ffd_file="./FFD/parentFFD.xyz")) # add the coupling solvers nonlinear_solver = om.NonlinearBlockGS(maxiter=25, iprint=2, use_aitken=True, rtol=1e-8, atol=1e-8) @@ -190,7 +215,9 @@ def problem_setup(scenario_name, fea_assembler, problem): linear_solver, ) - for discipline in ["aero", "struct"]: + for discipline in ["aero"]: + self.connect("geometry.x_%s0" % discipline, "cruise.x_%s0_masked" % discipline) + for discipline in ["struct"]: self.connect("geometry.x_%s0" % discipline, "cruise.x_%s0" % discipline) # add the structural thickness DVs @@ -209,56 +236,76 @@ def configure(self): # create geometric DV setup points = self.mesh_aero.mphys_get_surface_mesh() - # add pointset - self.geometry.nom_add_discipline_coords("aero", points) - self.geometry.nom_add_discipline_coords("struct") - # create constraint DV setup tri_points = self.mesh_aero.mphys_get_triangulated_surface() self.geometry.nom_setConstraintSurface(tri_points) # geometry setup - + self.geometry.nom_addChild(ffd_file="./FFD/wingFFD.xyz") # Create reference axis - nRefAxPts = self.geometry.nom_addRefAxis(name="wingAxis", xFraction=0.25, alignIndex="k") + nRefAxPts = self.geometry.nom_addRefAxis(name="wingAxis", xFraction=0.25, alignIndex="k", childIdx=0) # Set up global design variables def twist(val, geo): for i in range(1, nRefAxPts): geo.rot_z["wingAxis"].coef[i] = -val[i - 1] - def aoa(val, DASolver): - aoa = val[0] * np.pi / 180.0 - U = [float(U0 * np.cos(aoa)), float(U0 * np.sin(aoa)), 0] - DASolver.setOption("primalBC", {"U0": {"value": U}}) - DASolver.updateDAOption() + self.geometry.nom_addGlobalDV(dvName="twist", value=np.array([0] * (nRefAxPts - 1)), func=twist, childIdx=0) + nShapes = self.geometry.nom_addLocalDV(dvName="shape", childIdx=0) - self.cruise.coupling.aero.solver.add_dv_func("aoa", aoa) - self.cruise.aero_post.add_dv_func("aoa", aoa) + # add pointset + self.geometry.nom_add_discipline_coords("aero", points) + self.geometry.nom_add_discipline_coords("struct") + + def actuator(val, DASolver): + actX = float(val[0]) + actY = float(val[1]) + actZ = float(val[2]) + actR1 = float(val[3]) + actR2 = float(val[4]) + actScale = float(val[5]) + actPOD = float(val[6]) + actExpM = float(val[7]) + actExpN = float(val[8]) + DASolver.setOption( + "fvSource", + { + "disk1": { + "center": [actX, actY, actZ], + "innerRadius": actR1, + "outerRadius": actR2, + "scale": actScale, + "POD": actPOD, + "expM": actExpM, + "expN": actExpN, + }, + }, + ) + DASolver.updateDAOption() - self.geometry.nom_addGlobalDV(dvName="twist", value=np.array([0] * (nRefAxPts - 1)), func=twist) - nShapes = self.geometry.nom_addLocalDV(dvName="shape") + self.cruise.coupling.aero.solver.add_dv_func("actuator_disk1", actuator) + self.cruise.aero_post.add_dv_func("actuator_disk1", actuator) # Set up constraints leList = [[0.1, 0, 0.01], [7.5, 0, 13.9]] teList = [[4.9, 0, 0.01], [8.9, 0, 13.9]] self.geometry.nom_addThicknessConstraints2D("thickcon", leList, teList, nSpan=10, nChord=10) self.geometry.nom_addVolumeConstraint("volcon", leList, teList, nSpan=10, nChord=10) - self.geometry.nom_add_LETEConstraint("lecon", 0, "iLow") - self.geometry.nom_add_LETEConstraint("tecon", 0, "iHigh") + self.geometry.nom_add_LETEConstraint("lecon", 0, "iLow", childIdx=0) + self.geometry.nom_add_LETEConstraint("tecon", 0, "iHigh", childIdx=0) # add dvs to ivc and connect - self.dvs.add_output("twist", val=np.array([0] * (nRefAxPts - 1))) + self.dvs.add_output("twist", val=np.array([aoa0] * (nRefAxPts - 1))) self.dvs.add_output("shape", val=np.array([0] * nShapes)) - self.dvs.add_output("aoa", val=np.array([aoa0])) + self.dvs.add_output("actuator", val=np.array([7.0, 0.0, 14.0, 0.1, 1.0, 1.0, 0.0, 1.0, 0.5])) self.connect("twist", "geometry.twist") self.connect("shape", "geometry.shape") - self.connect("aoa", "cruise.aoa") + self.connect("actuator", "cruise.dv_actuator_disk1", src_indices=[3,4,5,6,7,8]) + self.connect("actuator", "cruise.x_prop0_disk1", src_indices=[0,1,2]) # define the design variables self.add_design_var("twist", lower=-10.0, upper=10.0, scaler=1.0) self.add_design_var("shape", lower=-1.0, upper=1.0, scaler=1.0) - self.add_design_var("aoa", lower=0.0, upper=10.0, scaler=1.0) # add constraints and the objective self.add_objective("cruise.aero_post.CD", scaler=1.0)