From a43061e90cc383ffdff02f49d86ee6ad21235d17 Mon Sep 17 00:00:00 2001
From: acrovato <a.crovato@uliege.be>
Date: Mon, 18 Nov 2024 12:40:47 +0100
Subject: [PATCH] Move TACS interface so that users can use the official TACS
 repo

---
 examples/tacs_sdpm_nipk_agard.py |   2 +-
 omflut/__init__.py               |   2 +-
 omflut/tacs_structure.py         | 202 +++++++++++++++++++++++++++++++
 3 files changed, 204 insertions(+), 2 deletions(-)
 create mode 100644 omflut/tacs_structure.py

diff --git a/examples/tacs_sdpm_nipk_agard.py b/examples/tacs_sdpm_nipk_agard.py
index 570f38f..72f18ac 100644
--- a/examples/tacs_sdpm_nipk_agard.py
+++ b/examples/tacs_sdpm_nipk_agard.py
@@ -16,7 +16,7 @@
 # limitations under the License.
 
 from omflut import FlutterGroup
-from tacs.omflut.om import TacsBuilder
+from omflut.tacs_structure import TacsBuilder
 from omflut import RbfXferBuilder
 from sdpm.api.om import SdpmBuilder
 from pypk.api_om import PypkBuilder
diff --git a/omflut/__init__.py b/omflut/__init__.py
index 219c897..5cfe94d 100644
--- a/omflut/__init__.py
+++ b/omflut/__init__.py
@@ -15,7 +15,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-__version__ = '1.0.0'
+__version__ = '1.1.0'
 
 from .builder import Builder
 from .coupling import FlutterGroup
diff --git a/omflut/tacs_structure.py b/omflut/tacs_structure.py
new file mode 100644
index 0000000..ddcb26b
--- /dev/null
+++ b/omflut/tacs_structure.py
@@ -0,0 +1,202 @@
+from omflut import Builder
+import openmdao.api as om
+import numpy as np
+
+class TacsMesh(om.IndepVarComp):
+    """Initial structural mesh
+    """
+    def initialize(self):
+        self.options.declare('fea_assembler', desc='pyTACS interface', recordable=False)
+        self.options.declare('x_mask', desc='array to recover true degrees of freedom (node coordinates)')
+
+    def setup(self):
+        asb = self.options['fea_assembler']
+        mask = self.options['x_mask']
+        # Store all the node coordinates and add as output
+        self.add_output('x_struct0', val=asb.getOrigNodes()[mask], desc='initial structural node coordinates')
+
+class TacsSolver(om.ExplicitComponent):
+    """OpenMDAO structural wrapper
+    
+    Attributes
+    ----------
+    n_modes : int
+        number of eigenvalue frequencies / eigenvector modes
+    abs : tacs.pyTACS object
+        pyTACS interface
+    pbl : tacs.problem.modal object
+        Modal problem definition
+    x_mask : np.array
+        Array to recover true degrees of freedom (node coordinates)
+    q_mask : np.array
+        Array to recover true degrees of freedom (mode shapes)
+    """
+    def initialize(self):
+        self.options.declare('fea_assembler', desc='pyTACS interface', recordable=False)
+        self.options.declare('fea_problem', desc='modal problem', recordable=False)
+        self.options.declare('write_solution', desc='flag to write solution')
+        self.options.declare('x_mask', desc='array to recover true degrees of freedom (node coordinates)')
+        self.options.declare('q_mask', desc='array to recover true degrees of freedom (mode shapes)')
+        self.options.declare('n_nodes', desc='number of retained nodes')
+        self.options.declare('retained_modes', desc='list of retained modes')
+
+    def setup(self):
+        self.asb = self.options['fea_assembler']
+        self.pbl = self.options['fea_problem']
+        self.x_mask = self.options['x_mask']
+        self.q_mask = self.options['q_mask']
+        self.retained_modes = self.options['retained_modes']
+        self.n_modes = len(self.retained_modes)
+        self.add_input('dv_struct', shape_by_conn=True, desc='structural design variables')
+        self.add_input('x_struct0', shape_by_conn=True, desc='structural coordinates')
+        self.add_output('q_struct', val=np.zeros((3 * self.options['n_nodes'], self.n_modes)), desc='modal displacements')
+        self.add_output('M', val=np.identity(self.n_modes), desc='mass matrix')
+        self.add_output('K', val=np.zeros((self.n_modes, self.n_modes)), desc='stiffness matrix')
+        # Partials
+        # self.declare_partials(of=['q_struct', 'M', 'K'], wrt=['x_struct0'], method='exact')
+        self.declare_partials(of=['K'], wrt=['dv_struct'], method='exact')
+
+    def compute(self, inputs, outputs):
+        # Update nodes and design variables
+        x_s = self.asb.getOrigNodes()
+        x_s[self.x_mask] = inputs['x_struct0']
+        self.pbl.setNodes(x_s)
+        self.pbl.setDesignVars(inputs['dv_struct'])
+        # Perform modal analysis
+        self.pbl.solve()
+        if self.options['write_solution']:
+            self.pbl.writeSolution()
+        # Mask and set modes
+        for i in range(self.n_modes):
+            _, q_s = self.pbl.getVariables(self.retained_modes[i])
+            outputs['q_struct'][:, i] = self._extract_data(q_s[self.q_mask])
+        # Get eigenvalues and set modal stiffness matrix
+        funcs = {}
+        self.pbl.evalFunctions(funcs)
+        outputs['K'] = np.diag(list(funcs.values()))[np.ix_(self.retained_modes, self.retained_modes)]
+
+    def compute_partials(self, inputs, partials):
+        # Approximate derivative of stiffness matrix diagonal entries by derivative of eigenvalues
+        func_sens = {}
+        self.pbl.evalFunctionsSens(func_sens, evalVars='dv')
+        for i in range(self.n_modes):
+            d_lambda = func_sens[f'{self.pbl.name}_eigsm.{self.retained_modes[i]}']
+            partials['K', 'dv_struct'][np.ravel_multi_index(([i], [i]), (self.n_modes, self.n_modes)), :] = d_lambda['struct']
+
+    def _extract_data(self, q):
+        """Extract modal displacements along z and rotations about x and y
+        """
+        # Constants
+        vars_node = 6 # number of variables per node
+        v_idx = [2, 3, 4] # indices of dz, rx and ry
+        n_var = len(v_idx) # number of variables to extract
+        n_nod = len(q) // vars_node # number of nodes
+        # Extract
+        q_red = np.zeros(n_nod * n_var)
+        for i in range(n_nod):
+            for j in range(n_var):
+                q_red[i * n_var + j] = q[i * vars_node + v_idx[j]]
+        return q_red
+
+class TacsBuilder(Builder):
+    """TACS builder for OpenMDAO
+
+    Attributes
+    ----------
+    fea_assembler : tacs.pyTACS object
+        pyTACS interface
+    fea_problem : tacs.problem.modal object
+        Modal problem definition
+    mask : np.array
+        Array to recover true degrees of freedom
+    write_solution : bool
+        Flag indicating whether to write solution
+    """
+    def __init__(self, mesh_file, num_modes, sigma=1., element_callback=None, exclude_modes=None, components_tag=None, pytacs_options=None, write_solution=True):
+        """Instantiate and initialize TACS components
+
+        Parameters
+        ----------
+        mesh_file : str or pyNastran.bdf.bdf.BDF
+            The BDF file or a pyNastran BDF object to load
+        num_modes : int
+            Number of modes
+        sigma : float
+            Guess for the lowest eigenvalue (default: 1.0)
+        element_callback : collections.abc.Callable, optional
+            User-defined callback function for setting up TACS elements and element DVs (default: None)
+        exclude_modes : list[int]
+            List of indices of modes to exclude from analysis (default: None)
+        components_tag : list[str]
+            List of tag names for which nodes will be included (default: None)
+        pytacs_options : dict, optional
+            Options dictionary passed to pyTACS assembler (default: None)
+        write_solution : bool, optional
+            Flag to determine whether to write out TACS solutions to f5 file each design iteration (default: True)
+        """
+        from tacs.pytacs import pyTACS
+        from mpi4py import MPI
+        # Initialize assembler
+        self.fea_assembler = pyTACS(mesh_file, options=pytacs_options, comm=MPI.COMM_WORLD)
+        self.fea_assembler.initialize(element_callback)
+        # Set up the problem
+        self.fea_problem = self.fea_assembler.createModalProblem('modal', sigma, num_modes)
+        self.retained_modes = list(range(num_modes))
+        if exclude_modes is not None: self.retained_modes = list(np.delete(self.retained_modes, exclude_modes))
+        # Select nodes from components
+        nodes_id = self._select_nodes(components_tag)
+        self.n_nodes = len(nodes_id)
+        # Create masks
+        self.x_mask = self._create_mask(nodes_id, 3)
+        self.q_mask = self._create_mask(nodes_id, 6)
+        # Save other parameters
+        self.write_solution = write_solution
+
+    def get_mesh(self):
+        """Return OpenMDAO component to get the initial mesh coordinates
+        """
+        return TacsMesh(fea_assembler=self.fea_assembler, x_mask=self.x_mask)
+
+    def get_solver(self, scenario_name=''):
+        """Return OpenMDAO component containing the solver
+        """
+        return TacsSolver(fea_assembler=self.fea_assembler, fea_problem=self.fea_problem, write_solution=self.write_solution, x_mask=self.x_mask, q_mask=self.q_mask, n_nodes=self.n_nodes, retained_modes=self.retained_modes)
+
+    def get_number_of_nodes(self):
+        """Return the number of (true) nodes
+        """
+        return self.n_nodes
+
+    def get_number_of_dv(self):
+        """Return the number of degrees of freedom
+        """
+        return self.fea_assembler.getTotalNumDesignVars()
+
+    def _select_nodes(self, components):
+        """Select nodes from given components
+        
+        Parameters
+        ----------
+        components : list[str]
+            List of tag names for which nodes will be included
+        """
+        if components is None:
+            nodes_id = np.arange(self.fea_assembler.getNumOwnedNodes())
+        else:
+            nodes_id = self.fea_assembler.getLocalNodeIDsForComps(self.fea_assembler.selectCompIDs(components))
+        mults_id = self.fea_assembler.getLocalMultiplierNodeIDs()
+        return list(set(nodes_id) - set(mults_id))
+
+    def _create_mask(self, nodes_id, vars_node):
+        """Create a mask to recover array indices associated to retained true degrees of freedom
+        
+        Parameters
+        ----------
+        nodes_id : list[int]
+            index of retained nodes
+        vars_node : int
+            Number of variables per node
+        """
+        mask = np.full((self.fea_assembler.getNumOwnedNodes(), vars_node), False)
+        mask[nodes_id, :] = True
+        return mask.flatten()
-- 
GitLab