diff --git a/examples/caps_wing/7_animate_shape_vars.py b/examples/caps_wing/7_animate_shape_vars.py new file mode 100644 index 000000000..5cad71edc --- /dev/null +++ b/examples/caps_wing/7_animate_shape_vars.py @@ -0,0 +1,74 @@ +""" +Sean Engelstad, November 2023 +GT SMDO Lab, Dr. Graeme Kennedy +Caps to TACS example + +The purpose of this file is to show how to animate shape variables in the structure. +Once you run this script it will generate .f5 files in the capsStruct/Scratch/tacs work directory. +You'll need to use f5tovtk to convert to *.vtk files and open up each group of shape_var_..vtk files +using the .. shortcut which opens up a group of files at once. For each of these groups of vtk files, +click file -> save animation and save all the png files in a subfolder. Then, either use the GifWriter in the +caps2tacs module or copy the GifWriter script into that directory and use it to make a gif. Repeat for each shape variable. +""" + +from tacs import caps2tacs +from mpi4py import MPI +import openmdao.api as om + +# --------------------------------------------------------------# +# Setup CAPS Problem +# --------------------------------------------------------------# +comm = MPI.COMM_WORLD +tacs_model = caps2tacs.TacsModel.build(csm_file="large_naca_wing.csm", comm=comm) +tacs_model.mesh_aim.set_mesh( # need a refined-enough mesh for the derivative test to pass + edge_pt_min=15, + edge_pt_max=20, + global_mesh_size=0.01, + max_surf_offset=0.01, + max_dihedral_angle=5, +).register_to( + tacs_model +) + +aluminum = caps2tacs.Isotropic.aluminum().register_to(tacs_model) + +# setup the thickness design variables + automatic shell properties +nribs = int(tacs_model.get_config_parameter("nribs")) +nspars = int(tacs_model.get_config_parameter("nspars")) +nOML = nribs - 1 +for irib in range(1, nribs + 1): + caps2tacs.ThicknessVariable( + caps_group=f"rib{irib}", value=0.03, material=aluminum + ).register_to(tacs_model) +for ispar in range(1, nspars + 1): + caps2tacs.ThicknessVariable( + caps_group=f"spar{ispar}", value=0.03, material=aluminum + ).register_to(tacs_model) +for iOML in range(1, nOML + 1): + caps2tacs.ThicknessVariable( + caps_group=f"OML{iOML}", value=0.1, material=aluminum + ).register_to(tacs_model) + +# register one shape variable rib_a1 +rib_a1 = caps2tacs.ShapeVariable("rib_a1", value=1.0).register_to(tacs_model) +caps2tacs.ShapeVariable("rib_a2", value=0.0).register_to(tacs_model) +spar_a1 = caps2tacs.ShapeVariable("spar_a1", value=1.0).register_to(tacs_model) +caps2tacs.ShapeVariable("spar_a2", value=0.0).register_to(tacs_model) + +# add constraints and loads +caps2tacs.PinConstraint("root").register_to(tacs_model) +caps2tacs.GridForce("OML", direction=[0, 0, 1.0], magnitude=10.0).register_to( + tacs_model +) + +# add analysis functions to the model +caps2tacs.AnalysisFunction.mass().register_to(tacs_model) + +# run the pre analysis to build tacs input files +tacs_model.setup(include_aim=True) + +shape_var_dict = { + rib_a1: [_ * 0.1 for _ in range(6, 14)], + spar_a1: [_ * 0.1 for _ in range(6, 14)], +} +tacs_model.animate_shape_vars(shape_var_dict) diff --git a/tacs/caps2tacs/__init__.py b/tacs/caps2tacs/__init__.py index 69083d903..78ec2ed47 100644 --- a/tacs/caps2tacs/__init__.py +++ b/tacs/caps2tacs/__init__.py @@ -19,6 +19,7 @@ if openmdao_loader is not None: from .tacs_component import * +from .gif_writer import * from .analysis_function import * from .constraints import * from .egads_aim import * diff --git a/tacs/caps2tacs/gif_writer.py b/tacs/caps2tacs/gif_writer.py new file mode 100644 index 000000000..f61d7e96f --- /dev/null +++ b/tacs/caps2tacs/gif_writer.py @@ -0,0 +1,31 @@ +__all__ = ["GifWriter"] +import imageio, os + + +class GifWriter: + """ + module to write gifs from a set of pngs + """ + + def __init__(self, frames_per_second: int = 4): + self._fps = frames_per_second + + def __call__(self, gif_filename: str, path: str): + """ + call on current path to create gif of given filename + """ + gif_filepath = os.path.join(path, gif_filename) + with imageio.get_writer(gif_filepath, mode="I", fps=self._fps) as writer: + path_dir = os.listdir(path) + path_dir = sorted(path_dir) + for image_file in path_dir: + print(image_file) + if ".png" in image_file: + image = imageio.imread(image_file) + writer.append_data(image) + + +# example of how to use the GifWriter +# if __name__ == "__main__": +# my_writer = GifWriter(frames_per_second=4) +# my_writer("sizing1.gif", os.getcwd()) diff --git a/tacs/caps2tacs/materials.py b/tacs/caps2tacs/materials.py index 9241fbabd..dfde0861a 100644 --- a/tacs/caps2tacs/materials.py +++ b/tacs/caps2tacs/materials.py @@ -180,7 +180,7 @@ def aluminum(cls): cp=903, kappa=237, ) - + @classmethod def titanium(cls): return cls( @@ -282,8 +282,8 @@ def carbon_fiber(cls): alpha1=-0.3e-6, alpha2=28e-6, rho=1.6e3, - kappa1=14.5, #W/m-K - kappa2=4.8, #W/m-K - kappa3=4.8, #W/m-K - cp=1130.0 # J / kg-K + kappa1=14.5, # W/m-K + kappa2=4.8, # W/m-K + kappa3=4.8, # W/m-K + cp=1130.0, # J / kg-K ) diff --git a/tacs/caps2tacs/tacs_aim.py b/tacs/caps2tacs/tacs_aim.py index c13a7d3cf..a1b01d486 100644 --- a/tacs/caps2tacs/tacs_aim.py +++ b/tacs/caps2tacs/tacs_aim.py @@ -151,7 +151,7 @@ def setup_aim( } if len(self.variables) > 0: self.aim.input.Design_Variable = { - dv.name: dv.DV_dictionary for dv in self._design_variables + dv.name: dv.DV_dictionary for dv in self._design_variables if dv._active } if self._dict_options is not None: diff --git a/tacs/caps2tacs/tacs_model.py b/tacs/caps2tacs/tacs_model.py index 5fae9ab41..f7a24dd2b 100644 --- a/tacs/caps2tacs/tacs_model.py +++ b/tacs/caps2tacs/tacs_model.py @@ -273,6 +273,70 @@ def createTACSProbs(self, addFunctions: bool = True): ) return self.SPs + def animate_shape_vars(self, shape_vars_dict: dict): + """ + Animate the shape variables in ESP/CAPS. + + Parameters + ---------- + shape_vars_dict: dict[ShapeVariable : list[float]] + dict of the list of values for each shape variable to take + + e.g. if you wish to animate over the ShapeVariable objects var1, and var2 + create the following shape_vars_dict + shape_vars_dict = { + var1 : [_*0.1 for _ in range(1,4)], + var2 : [_*0.05 for _ in range(-3,4)], + } + """ + # make an analysis function if none have been made + if len(self.analysis_functions) == 0: + if self.comm.rank == 0: + print( + "Adding mass analysis function to enable caps2tacs postAnalysis for animating shape variables..." + ) + AnalysisFunction.mass().register_to(self) + + # set all shape variables inactive + for shape_var in self.tacs_aim.shape_variables: + shape_var._active = False + + for shape_var in shape_vars_dict: + value_list = shape_vars_dict[shape_var] + + # want only this shape variable to be active so that it doesn't + # try to do extra mesh sensitivity chain rule products in tacsAIM + shape_var._active = True + self.tacs_aim.setup_aim() + + for i, value in enumerate(value_list): + shape_var.value = value + self.geometry.despmtr[shape_var.name].value = value + + self.tacs_aim.pre_analysis() + self.SPs = self.createTACSProbs(addFunctions=True) + for caseID in self.SPs: + # note we don't solve the forward / adjoint analysis here + # as we only care about visualizing the change in the structural shape / mesh + # not the actual structural analysis results + + self.SPs[caseID].writeSolution( + baseName=shape_var.name, + outputDir=self.tacs_aim.analysis_dir, + number=i, + ) + self.SPs[caseID].writeSensFile( + evalFuncs=None, tacsAim=self.tacs_aim, + ) + self.tacs_aim.post_analysis() + + # make the shape variable inactive again + shape_var._active = False + print( + f"Done animating caps2tacs shape variables.. the f5 files are found in {self.tacs_aim.analysis_dir}." + ) + print("Use the GifWriter script in examples/caps_wing/archive to ") + def pre_analysis(self): """ call tacs aim pre_analysis to build TACS input files and mesh diff --git a/tacs/caps2tacs/variables.py b/tacs/caps2tacs/variables.py index 687ff9c62..29c20ec46 100644 --- a/tacs/caps2tacs/variables.py +++ b/tacs/caps2tacs/variables.py @@ -9,7 +9,7 @@ class ShapeVariable: shape variables in ESP/CAPS are design parameters that affect the structural geometry """ - def __init__(self, name: str, value=None): + def __init__(self, name: str, value=None, active:bool=True): """ ESP/CAPS shape variable controls a design parameter in the CSM file name: corresponds to the design parameter in the CSM file @@ -17,6 +17,7 @@ def __init__(self, name: str, value=None): """ self.name = name self._value = value + self._active = active @property def DV_dictionary(self) -> dict: @@ -52,6 +53,7 @@ def __init__( upper_bound: float = None, max_delta: float = None, material: Material = None, + active:bool=True, ): """ ESP/CAPS Thickness variable sets the thickness over a portion of the geometry in the CSM file @@ -71,6 +73,7 @@ def __init__( self.lower_bound = lower_bound self.upper_bound = upper_bound self.max_delta = max_delta + self._active = active # private variables used to create shell property self._material = material diff --git a/tacs/problems/base.py b/tacs/problems/base.py index 79231c06b..fd6610939 100644 --- a/tacs/problems/base.py +++ b/tacs/problems/base.py @@ -882,11 +882,16 @@ class which handles the sensitivity file writing for ESP/CAPS shape derivatives """ + is_dummy_file = evalFuncs is None + if is_dummy_file: + evalFuncs = ["dummy-func"] + # obtain the functions and sensitivities from TACS assembler tacs_funcs = {} tacs_sens = {} - self.evalFunctions(tacs_funcs, evalFuncs=evalFuncs) - self.evalFunctionsSens(tacs_sens, evalFuncs=evalFuncs) + if not(is_dummy_file): + self.evalFunctions(tacs_funcs, evalFuncs=evalFuncs) + self.evalFunctionsSens(tacs_sens, evalFuncs=evalFuncs) num_funcs = len(evalFuncs) assert tacsAim is not None @@ -908,30 +913,51 @@ class which handles the sensitivity file writing for ESP/CAPS shape derivatives if func_name in tacs_key: break - # get the tacs coordinate derivatives - xpts_sens = tacs_sens[tacs_key]["Xpts"] - - # write the func name, value and nnodes - hdl.write(f"{func_name}\n") - hdl.write(f"{tacs_funcs[tacs_key].real}\n") - hdl.write(f"{num_nodes}\n") - - # write the coordinate derivatives for the given function - for bdf_ind in range(num_nodes): - tacs_ind = node_ids[bdf_ind] - nastran_node = bdf_ind + 1 - hdl.write( - f"{nastran_node} {xpts_sens[3*tacs_ind].real} {xpts_sens[3*tacs_ind+1].real} {xpts_sens[3*tacs_ind+2].real}\n" - ) - - # write any struct derivatives if there are struct derivatives - if num_struct_dvs > 0: - struct_sens = tacs_sens[tacs_key]["struct"] - for idx, thick_var in enumerate( - tacsAim.thickness_variables - ): - # assumes these are sorted in tacs aim wrapper - hdl.write(f"{thick_var.name}\n") - hdl.write("1\n") - hdl.write(f"{struct_sens[idx].real}\n") - return + if not(is_dummy_file): + # get the tacs coordinate derivatives + xpts_sens = tacs_sens[tacs_key]["Xpts"] + + # write the func name, value and nnodes + hdl.write(f"{func_name}\n") + hdl.write(f"{tacs_funcs[tacs_key].real}\n") + hdl.write(f"{num_nodes}\n") + + # write the coordinate derivatives for the given function + for bdf_ind in range(num_nodes): + tacs_ind = node_ids[bdf_ind] + nastran_node = bdf_ind + 1 + hdl.write( + f"{nastran_node} {xpts_sens[3*tacs_ind].real} {xpts_sens[3*tacs_ind+1].real} {xpts_sens[3*tacs_ind+2].real}\n" + ) + + # write any struct derivatives if there are struct derivatives + if num_struct_dvs > 0: + struct_sens = tacs_sens[tacs_key]["struct"] + for idx, thick_var in enumerate( + tacsAim.thickness_variables + ): + # assumes these are sorted in tacs aim wrapper + hdl.write(f"{thick_var.name}\n") + hdl.write("1\n") + hdl.write(f"{struct_sens[idx].real}\n") + + else: # is a dummy sens file for animating the structure shape + # write the func name, value and nnodes + hdl.write(f"{func_name}\n") + hdl.write(f"{0.0}\n") + hdl.write(f"{num_nodes}\n") + + # write the coordinate derivatives for the given function + for bdf_ind in range(num_nodes): + nastran_node = bdf_ind + 1 + hdl.write(f"{nastran_node} 0.0 0.0 0.0\n") + + # write any struct derivatives if there are struct derivatives + if num_struct_dvs > 0: + for idx, thick_var in enumerate(tacsAim.thickness_variables): + # assumes these are sorted in tacs aim wrapper + hdl.write(f"{thick_var.name}\n") + hdl.write("1\n") + hdl.write("0.0\n") + + return \ No newline at end of file