You are currently viewing Custom Result Visualization in Simcenter 3D

Custom Result Visualization in Simcenter 3D

Learn how to visualize custom result data in Simcenter 3D by leveraging the I-deas Universal (UNV) file format. This post will focus on the step-by-step procedure needed to go from a Nastran BDF and OP2 file to a contour plot of margins within Simcenter 3D.

Challenges:

  • Generation of custom result data of interest within Simcenter 3D is complicated
  • Building a physical understanding of custom result data of interest is difficult without visualization
  • Communication of analysis is reduced; informed decision making is slowed

Values:

  • Learn how to leverage Simcenter 3D as an extension of internal visualization tools
  • Develop a more comprehensive physical understanding of custom data through visualization
  • Communicate and report results in a more effective and compelling way

Example Application

Consider the stiffened cylinder shown in Figure 1. The geometry consists of aluminum components (e.g. stiffeners, top plate) and steel components (e.g. top ribs.) The base of the cylinder structure is pinned and 500 lbf of load (in either the X or Z axes) is transferred into the cylinder through the top ring via a rigid element that connects the top ring bolt holes to the load origination point. The goal is to analyze the two static load scenarios in Nastran, compute margins outside of Simcenter 3D, and then import those margins into Simcenter 3D for visualization.

Figure 1: Cylinder FEM, Components, Boundary Conditions, and Load

Step 1: Create FEM and Execute Analysis

For this example, the Nastran input deck is assumed to already exist outside of Simcenter 3D. The model/analysis definition is imported into Simcenter 3D by selecting File -> Import -> Simulation…  and following the on-screen steps shown in Figure 2.

Figure 2: Procedure to Import Existing Nastran Input Deck into Simcenter 3D

Simcenter 3D creates a FEM and SIM file to store the model and analysis information, respectively. The imported analysis can be executed to generate results (using the Solve button,) and those results can be visualized as is shown in Figure 3. The contour plot is the absolute value of the worst principal stress at each shell element for one of the static load cases. The peak stress is observed in the top ribs, at the intersection between the top ribs and the top ring.

Figure 3: Procedure to Solve and Review Stress Contours

Step 2: Generate Custom Data

In the prior step, the execution of the Nastran analysis through Simcenter 3D generates a Nastran input deck (.dat) and result file (.op2)  We will leverage Python and the pyNastran package to read in the result data and compute margins at each element. A high-level overview of the script required to accomplish this task is shown in Figure 4. First, pyNastran is used to read in the model/result data. Second, the stress allowable is prescribed based on material ID and the margins are computed.

				
					import os

from pyNastran.bdf.bdf import BDF
from pyNastran.op2.op2 import OP2

class ResultsManager(object):

    def __init__(self, path_fem, path_op2):
    def _parse_fem_data(self):
    def set_stress_allowables_by_mid(self, mid_to_allowable):
    def compute_min_margins(self):
    def write_margin_unv(self, path_save):

def main():
    # Create result manager: read in model/result data
    path_dir_working = r"D:\PATH\TO\WORKING\DIR"
    fname = r"cylinder"
    path_fem = os.path.join(path_dir_working, fname + '.dat')
    path_op2 = os.path.join(path_dir_working, fname + '.op2')
    rm = ResultsManager(path_fem, path_op2)

    # Compute margins
    mid_to_allowable = {1: 30000, # Aluminum [psi]
                        2: 73000} # Steel [psi]
    rm.set_stress_allowables_by_mid(mid_to_allowable)
    rm.compute_min_margins()

    # Write out margins in UNV format
    path_save = os.path.join(path_dir_working, 'min_margin_by_element.unv')
    rm.write_margin_unv(path_save)

if __name__ == "__main__":
    main()

				
			

Figure 4: High-Level Overview of Python Script using pyNastran to Compute Margins

The reading of the model and result data is performed during the initialization of the ResultsManager class object, as shown in Figure 5. A pyNastran BDF object is used to load and access the Nastran FEM and a pyNastran OP2 object is used to load and access the Nastran result data. After loading the Nastran FEM data, the model information is interrogated in order to build Python dictionary mapping of the model element types (e.g. CQUAD4, CBEAM) to a list of the associated model element IDs. A similar mapping from element ID to material ID, and vice versa, is created.

				
					def __init__(self, path_fem, path_op2):
    # Inputs
    self.path_fem = path_fem
    self.path_op2 = path_op2

    # Read FEM
    self.fem_data = BDF()
    self.fem_data.read_bdf(self.path_fem, punch=False)
    self._parse_fem_data()

    # Read OP2
    self.op2_data = OP2(debug=False)
    self.op2_data.read_op2(self.path_op2, build_dataframe=False)

def _parse_fem_data(self):
    """
    Parse input fem data
    """
    self.elem_type_to_eids = {}
    self.mid_to_eids = {}
    self.eid_to_mid = {}
    for eid, elem_data in sorted(self.fem_data.elements.items()):

        # Note element type
        elem_type = elem_data.type
        if elem_type in self.elem_type_to_eids:
            self.elem_type_to_eids[elem_type].append(eid)
        else:
            self.elem_type_to_eids[elem_type] = [eid]

        # Note material ID
        if elem_type == 'CBEAM':
            mid = elem_data.pid_ref.mid
        elif elem_type in ['CQUAD4', 'CTRIA3']:
            mid = elem_data.material_ids[0]
        else:
            continue

        if mid in self.mid_to_eids:
            self.mid_to_eids[mid].append(eid)
        else:
            self.mid_to_eids[mid] = [eid]
        self.eid_to_mid[eid] = mid
				
			

Figure 5: Code Used to Read in Model and Result Data

The next step is to create a dictionary mapping the element ID to the stress allowable based on the material ID associated with the element. The code used to create this mapping is shown in Figure 6, which leverages the Python dictionaries made by interrogating the finite-element model. 

				
					def set_stress_allowables_by_mid(self, mid_to_allowable):
    """
    Creates dictionary mapping element id to element stress allowable
    given an input dictionary of element material ID to allowable.

    Parameters
    ----------
    mid_to_allowable : dict
        Dictionary mapping material ID to allowable
    """
    self.eid_to_stress_allowable = {}
    for mid, max_stress in mid_to_allowable.items():
        for eid in self.mid_to_eids[mid]:
            self.eid_to_stress_allowable[eid] = max_stress
				
			

Figure 6: Code Used to Map Element ID to Stress Allowable Based on Element Material ID

Finally, the margins can be computed based on element type as shown in Figure 7. For the shell elements (CQUAD4, CTRIA3), the minimum margin across all load cases in both tension and compression is determined across the two faces of the shell element. Similarly for the beam elements (CBEAM), the minimum margin across all load cases in both tension and compression is determined across the two ends of the beam element. The final result is a dictionary that maps the element ID to the minimum margin at that element. 

				
					def compute_min_margins(self):
    """
    Compute min margins at each element across all load cases
    """
    # Loop over subcases
    self.eid_to_min_margin = {}
    for i_case, subcase_id in self.op2_data.subcase_key.items():
        subcase_id = subcase_id[0]

        # Loop over CQUAD4 shell elements
        assert self.op2_data.cquad4_stress[subcase_id].nnodes_per_element == 1
        for i_row in range(int(self.op2_data.cquad4_stress[subcase_id].data.shape[1]/2)):
            # [fiber_distance, oxx, oyy, txy, angle, omax, omin, von_mises]
            z1_data = self.op2_data.cquad4_stress[subcase_id].data[0,2*i_row,:] 
            z2_data = self.op2_data.cquad4_stress[subcase_id].data[0,2*i_row+1,:]
            eid, nid = self.op2_data.cquad4_stress[subcase_id].element_node[2*i_row]
            z1_margin = self.eid_to_stress_allowable[eid]/max(z1_data[5],-z1_data[6]) - 1
            z2_margin = self.eid_to_stress_allowable[eid]/max(z2_data[5],-z2_data[6]) - 1
            min_margin = min(z1_margin, z2_margin)
            if eid in self.eid_to_min_margin:
                self.eid_to_min_margin[eid] = min(self.eid_to_min_margin[eid],min_margin)
            else:
                self.eid_to_min_margin[eid] = min_margin


        # Loop over CQUAD3 shell elements
        assert self.op2_data.ctria3_stress[subcase_id].nnodes_per_element == 1
        for i_row in range(int(self.op2_data.ctria3_stress[subcase_id].data.shape[1]/2)):
            # [fiber_distance, oxx, oyy, txy, angle, omax, omin, von_mises]
            z1_data = self.op2_data.ctria3_stress[subcase_id].data[0,2*i_row,:] 
            z2_data = self.op2_data.ctria3_stress[subcase_id].data[0,2*i_row+1,:]
            eid, nid = self.op2_data.ctria3_stress[subcase_id].element_node[2*i_row]
            z1_margin = self.eid_to_stress_allowable[eid]/max(z1_data[5],-z1_data[6]) - 1
            z2_margin = self.eid_to_stress_allowable[eid]/max(z2_data[5],-z2_data[6]) - 1
            min_margin = min(z1_margin, z2_margin)
            if eid in self.eid_to_min_margin:
                self.eid_to_min_margin[eid] = min(self.eid_to_min_margin[eid],min_margin)
            else:
                self.eid_to_min_margin[eid] = min_margin

        # Loop over CBEAM elements
        for i_row in range(int(self.op2_data.cbeam_stress[subcase_id].data.shape[1]/2)):
            # [sxc, sxd, sxe, sxf, smax, smin, MS_tension, MS_compression]
            endA_data = self.op2_data.cbeam_stress[subcase_id].data[0,2*i_row,:] 
            endB_data = self.op2_data.cbeam_stress[subcase_id].data[0,2*i_row+1,:]
            eid, nid = self.op2_data.cbeam_stress[subcase_id].element_node[2*i_row]
            endA_margin=self.eid_to_stress_allowable[eid]/max(endA_data[4],-endA_data[5])-1
            endB_margin=self.eid_to_stress_allowable[eid]/max(endB_data[4],-endB_data[5])-1
            min_margin = min(endA_margin, endB_margin)
            if eid in self.eid_to_min_margin:
                self.eid_to_min_margin[eid] = min(self.eid_to_min_margin[eid],min_margin)
            else:
                self.eid_to_min_margin[eid] = min_margin

				
			

Figure 7: Code Used to Compute Margins

Step 3: Format Custom Data in I-deas File Format

With the margins computed, the next step is to write out the margin data in the I-deas Universal file format (UNV) readable by Simcenter 3D. In particular, we will look into creating a companion result file, meaning that we only need to prescribe the margin data according to Universal File Dataset 2414. In doing this, Simcenter 3D will assume that the companion result file originates from the geometry and results currently loaded in the active Simcenter 3D session. The user saves time by eliminating the need to redefine the nodes, element, unit system, and coordinate systems making up the finite-element model. The segment of code in Figure 8 shows the beginning of the UNV output file: a specification of the dataset number and multiple lines of header information. The -1 line entry is a delimiter used to denote the beginning of a new dataset.

				
					def write_margin_unv(self, path_save):
    """
    Write margin data in universal file format (dataset 2414).

    Parameters
    ----------
    path_save : str
        Path to UNV file to be written
    """
    with open(path_save, mode='w') as outfile:

        # Specify Dataset type
        outfile.write(f"{-1:6}\n") # Dataset start/end delimeter
        outfile.write(f"{2414:6}\n")

        # Record 1:        FORMAT(1I10)
        #                  Field 1       -- Analysis dataset label
        analysis_label = 1
        outfile.write(f"{analysis_label:10}\n")

        # Record 2:        FORMAT(40A2)
        #                  Field 1       -- Analysis dataset name
        loadcase_name = 'EnvelopedLoads'
        outfile.write(f"{'LOADCASE_NAME_KEY ' + loadcase_name:80}\n")

        # Record 3:        FORMAT (1I10)
        #                  Field 1:      -- Dataset location
        dataset_location = 2 # Data on elements
        outfile.write(f"{dataset_location:10}\n")

        # Record 4:        FORMAT (40A2)
        #                  Field 1:      -- ID line 1
        result_name = 'MinMargins'
        outfile.write(f"{'RESULT_NAME_KEY ' + result_name:80}\n")

        # Record 5:        FORMAT (40A2)
        #                  Field 1:      -- ID line 2
        outfile.write(f"{'None':80}\n")

        # Record 6:        FORMAT (40A2)
        #                  Field 1:      -- ID line 3
        outfile.write(f"{'None':80}\n")

        # Record 7:        FORMAT (40A2)
        #                  Field 1:      -- ID line 4
        outfile.write(f"{'None':80}\n")

        # Record 8:        FORMAT (40A2)
        #                  Field 1:      -- ID line 5
        outfile.write(f"{'None':80}\n")

				
			

Figure 8: Code to Write out Margins in Universal File Format (Part 1)

Following the header definition is the overall model/analysis description information shown in Figure 9. We specify that the results are consistent with a static structural solution result and is a scalar quantity, with a unique value for each element. Note that the comments shown in the code are consistent with the Universal File Dataset 2414 definition, where record 9 is of format 6I10, meaning “6 entries of type Integer in a single line, each specified within a block 10 characters long.” Python f-Strings are leveraged to achieve the proper string formatting.

				
					        # Record 9:        FORMAT (6I10)
        #                  Field 1:      -- Model type
        #                  Field 2:      -- Analysis type
        #                  Field 3:      -- Data characteristic
        #                  Field 4:      -- Result type
        #                  Field 5:      -- Data type
        #                  Field 6:      -- Number of data values for the data
        model_type = 1 # Structural
        analysis_type = 1 # Static
        data_characteristic = 1 # Scalar
        result_type = 94 # Unknown Scalar --> Margin not listed, so use general scalar
        data_type = 2 # Single precision floating point
        ndata_vals = 1 # Single value per element
        outfile.write(f"{model_type:10}")
        outfile.write(f"{analysis_type:10}")
        outfile.write(f"{data_characteristic:10}")
        outfile.write(f"{result_type:10}")
        outfile.write(f"{data_type:10}")
        outfile.write(f"{ndata_vals:10}\n")

				
			

Figure 9: Code to Write out Margins in Universal File Format (Part 2)

Next, Figure 10 shows various data descriptor parameters related to integer analysis types. All need to be specified, but not all apply to the margin results for a static structural solution. Make sure to reference the Dataset 2414 Interpretation Tables to understand what parameters apply to which solution types. When a parameter does not apply to the solution type of interest, enter “0” as the value. 

				
					        # Record 10:       FORMAT (8I10)
        #                  Field 1:      -- Integer analysis type specific data (1-8)
        design_set_id = 1
        iteration_number = 0
        solution_set_id = 1
        boundary_condition = 0
        load_set = 1
        mode_number = 0
        time_step_number = 0
        frequency_number = 0
        outfile.write(f"{design_set_id:10}")
        outfile.write(f"{iteration_number:10}")
        outfile.write(f"{solution_set_id:10}")
        outfile.write(f"{boundary_condition:10}")
        outfile.write(f"{load_set:10}")
        outfile.write(f"{mode_number:10}")
        outfile.write(f"{time_step_number:10}")
        outfile.write(f"{frequency_number:10}\n")

        # Record 11:       FORMAT (8I10)
        #                  Field 1:      -- Integer analysis type specific data (9,10)
        creation_option = 0
        number_retained = 0
        outfile.write(f"{creation_option:10}")
        outfile.write(f"{number_retained:10}\n")

				
			

Figure 10: Code to Write out Margins in Universal File Format (Part 3)

Similarly, the Dataset 2414 Interpretation Tables will need to be used for the various data descriptor parameters related to real analysis types. None of these will apply to the static structural solution, and therefore all parameters are set to zero in Figure 11.

				
					        # Record 12:       FORMAT (6E13.5)
        #                  Field 1:      -- Real analysis type specific data (1-6)
        time = 0
        frequency = 0
        eigenvalue = 0
        modal_mass = 0
        viscous_damping_ratio = 0
        hysteretic_damping_ratio = 0
        outfile.write(f"{time:13.5e}")
        outfile.write(f"{frequency:13.5e}")
        outfile.write(f"{eigenvalue:13.5e}")
        outfile.write(f"{modal_mass:13.5e}")
        outfile.write(f"{viscous_damping_ratio:13.5e}")
        outfile.write(f"{hysteretic_damping_ratio:13.5e}\n")

        # Record 13:       FORMAT (6E13.5)
        #                  Field 1:      -- Real analysis type specific data (7-12)
        real_part_eigenvalue = 0
        imaginary_part_eigenvalue = 0
        real_part_of_modalA_or_real_part_of_mass = 0
        imaginary_part_of_modalA_or_imaginary_part_of_mass = 0
        real_part_of_modalB_or_real_part_of_stiffness = 0
        imaginary_part_of_modalB_or_imaginary_part_of_stiffness = 0
        outfile.write(f"{real_part_eigenvalue:13.5e}{imaginary_part_eigenvalue:13.5e}")
        outfile.write(f"{real_part_of_modalA_or_real_part_of_mass:13.5e}")
        outfile.write(f"{imaginary_part_of_modalA_or_imaginary_part_of_mass:13.5e}")
        outfile.write(f"{real_part_of_modalB_or_real_part_of_stiffness:13.5e}")
        outfile.write(f"{imaginary_part_of_modalB_or_imaginary_part_of_stiffness:13.5e}\n")

				
			

Figure 11: Code to Write out Margins in Universal File Format (Part 4)

Finally, we can output the minimum margins at each element. This is as simple as looping through our Python dictionary mapping element ID to minimum margin and writing the value pairs to the UNV file (see Figure 12.) The NDVAL parameter is set to 1, meaning that a single result value is associated with each element. The -1 delimiter is again used to denote the end of the dataset input. 

				
					        # Dataset class: Data at elements
        # Record 14:       FORMAT (2I10)
        #                  Field 1:      -- Element number
        #                  Field 2:      -- Number Of data values For this element(NDVAL)
        # Record 15:       FORMAT (6E13.5)
        #                  Fields 1-N:   -- Data on element(NDVAL Real Or Complex Values)
        #                  Note: Records 14 and 15 are repeated for all elements.
        NDVAL = 1
        for eid, min_margin in self.eid_to_min_margin.items():
            outfile.write(f"{eid:10}{NDVAL:10}\n")
            outfile.write(f"{min_margin:13.5e}\n")

        # End current dataset
        outfile.write(f"{-1:6}\n") # Dataset start/end delimeter

				
			

Figure 12: Code to Write out Margins in Universal File Format (Part 5)

Step 4: Add Custom Data as Companion Result

With the ASCII-based UNV file now generated, Figure 13 shows the set of on-screen steps needed to load the margin summary as a companion result into the existing StaticLoads analysis results. As seen in the sub-image furthest to the right, the MinMarginsElemental scalar result is now listed in the Simcenter 3D result viewer. 

Figure 13: Procedure to Load Companion Result

At this point we can visualize the minimum margin values as we would any other result. The leftmost image shown in Figure 14 gives an overview of the full range of minimum margins observed across the FEM. The color-bar is in log-scale and inverted so that the lowest margin values are shown in red. We see that there is a drastic variation in the margin value, with the largest margins found in the stiffeners lining the cylinder, and the lowest margins concentrated near the center of the top plate. The margin contour image on the right has been updated to cap the upper value of the legend to 8, meaning all margins above 8 are now shown as blue. This gives a clearer illustration of the variation in the margin at and around the top plate, where the minimum margin is observed in the top plate at the intersection with the top ring. It is interesting to note that although the largest stresses were earlier observed in the steel top ribs (see Figure 3,) due to the lower material stress allowable, the lower stress levels in the aluminum top plate resulted in lower margins.

Figure 14: Visualization of Minimum Margin at Each Element Across the Two Static Cases

summary

We looked at using a custom Python script to compute margins for a Nastran analysis, wrote out the margin results in the I-deas Universal file format, and used Simcenter 3D to visualize the results. The procedure shown here is an introduction to general custom data visualization within Simcenter 3D.

José Márquez

Written by José Márquez

José Márquez is an Aerospace Engineer with a background in loads analysis, multi-disciplinary optimization, and software development. Outside of work, he enjoys gardening and rock climbing.