Skip to main content

Granta MI Scripting Toolkit 4.1

Plot data by category

Last update: 16.07.2025

Export pedigree data and tensile test results for composite materials, process the data in a pandas DataFrame, and plot summaries of the data (including layup information) using matplotlib and seaborn.

Get data from Granta MI

Connect to Granta MI and specify a database.

from GRANTA_MIScriptingToolkit import granta as mpy

mi = mpy.connect("http://my.server.name/mi_servicelayer", autologon=True)
training_db = mi.get_db('MI Training')
training_db.unit_system = 'Metric'

Specify the records and attributes you want to export from the first table. We want to use attributes describing the layup for all records in the Composite Pedigree table.

comp_pedigree = training_db.get_table('Composite Pedigree')
pedigree_records = comp_pedigree.all_records()
pedigree_attributes = ['Panel number', 'Ply count', 'Panel thickness, average (measured)',
                       'Ply thickness, average (measured)']
comp_pedigree.bulk_fetch(pedigree_records, attributes=pedigree_attributes)

Specify the records you want to export from the second table. Here, a link group is used to export only records linked to records in the first table.

Alternatively, you can specify a table with no link groups to the original table, as long as you can join the two datasets later on. For example, we could search for all records created after a certain date, or tested on a certain machine.

link_group = 'Tensile test data'

# Use a set comprehension to ensure we only select each record once
tensile_test_records = {tr for pr in pedigree_records for tr in pr.links[link_group]}
tensile_test_records = list(tensile_test_records)

Specify the attributes you want to export from the second table. Panel number will be used to relate the tensile tests to the layup data.

tensile_test_data = training_db.get_table('Tensile Test Data')
tensile_attributes = ['Panel number', 'Specimen ID', "Young's Modulus (11-axis)", 'Ultimate Tensile Strength',
                      'Test Type']
tensile_test_data.bulk_fetch(tensile_test_records, attributes=tensile_attributes)

Use DataFrames to pre-process and append the data

Build a separate DataFrame for each table

Create a DataFrame for each Granta MI table. The DataFrames are built from two separate lists of dictionaries:

import pandas as pd
from functools import partial

def get_attr_vals(attributes, r):
    return {mi_attr: r.attributes[mi_attr].value for mi_attr in attributes}

get_pedigree_vals = partial(get_attr_vals, pedigree_attributes)
get_tensile_vals = partial(get_attr_vals, tensile_attributes)

pedigree_data = [get_pedigree_vals(r) for r in pedigree_records]
tensile_data = [get_tensile_vals(r) for r in tensile_test_records]

Create a DataFrame pedigree_df that contains the pedigree records. Set the DataFrame index to be the Panel number attribute value and sort by the index.

pedigree_df = pd.DataFrame(pedigree_data).set_index('Panel number').sort_index()
pedigree_df.head()
Ply count Panel thickness, average (measured) Ply thickness, average (measured)
Panel number
LBJ13 6 1.397000 0.232029
LBJ14 6 1.431636 0.238044
LBJ42 6 1.438564 0.240007
LBJ53 6 1.385455 0.231279
LBJ62 6 1.410677 0.234618

Create a second DataFrame tensile_df that contains the tensile test results. Again, set the index to be the Panel number attribute value and sort by both the index and the Specimen ID.

tensile_df = pd.DataFrame(tensile_data).set_index('Panel number').sort_values(by=["Panel number", "Specimen ID"])
tensile_df.head()
Specimen ID Young's Modulus (11-axis) Ultimate Tensile Strength Test Type
Panel number
LBJ13 LBJ1311A 47.594514 1582.870937 0° tension
LBJ13 LBJ1312A 47.353197 1542.122918 0° tension
LBJ13 LBJ1313A 47.387671 1612.952766 0° tension
LBJ14 LBJ1413A [] 1613.614663 0° tension
LBJ14 LBJ1415A [] 1611.201497 0° tension

Join the dataframes

Use the dataframe.join() method to create a DataFrame with both sets of data. The join type will default to a left-join if not specified; this is fine in our case because the records were linked via a smart link in Granta MI, and so the set of indices is the same for both DataFrames.

df = pedigree_df.join(tensile_df)
df.head()
Ply count Panel thickness, average (measured) Ply thickness, average (measured) Specimen ID Young's Modulus (11-axis) Ultimate Tensile Strength Test Type
Panel number
LBJ13 6 1.397000 0.232029 LBJ1311A 47.594514 1582.870937 0° tension
LBJ13 6 1.397000 0.232029 LBJ1312A 47.353197 1542.122918 0° tension
LBJ13 6 1.397000 0.232029 LBJ1313A 47.387671 1612.952766 0° tension
LBJ14 6 1.431636 0.238044 LBJ1413A [] 1613.614663 0° tension
LBJ14 6 1.431636 0.238044 LBJ1415A [] 1611.201497 0° tension

Finally, create a units dict so you can create plots with meaningful axis labels.

attr_defs = [comp_pedigree.attributes[attr] for attr in pedigree_attributes] + \
            [tensile_test_data.attributes[attr] for attr in tensile_attributes]
units = {attr.name: attr.unit for attr in attr_defs if attr.unit}
units

Previous cell output:

{'Panel thickness, average (measured)': 'mm',
 'Ply thickness, average (measured)': 'mm',
 "Young's Modulus (11-axis)": 'GPa',
 'Ultimate Tensile Strength': 'MPa'}

Plot the data

This code snippet is optional, and is included to increase the size of all plots generated in this notebook.

import matplotlib.pyplot as plt
plt.rcParams['figure.dpi'] = 125

Investigate the relationship between two properties

Create a scatter plot with matplotlib comparing Ultimate Tensile Strength of the panels to Ply thickness, average.

import matplotlib.pyplot as plt

ply_thickness_unit = units['Ply thickness, average (measured)']
ftu_unit = units["Ultimate Tensile Strength"]
ply_thickness_label = rf'Average ply thickness / ${ply_thickness_unit}$'
ftu_label = rf'$F_{{tu}}$ / ${ftu_unit}$'

fig, ax = plt.subplots()
ax.scatter(data=df, x="Ply thickness, average (measured)", y="Ultimate Tensile Strength", alpha=0.75, s=100)
ax.set_xlabel(ply_thickness_label)
ax.set_ylabel(ftu_label)
_ = ax.set_title(r'$F_{tu}$ vs Average ply thickness')

png

There is a significant difference between the results at 1200 - 1600 MPa and the group below 100 MPa. We need to identify what causes this difference, and then represent the data in a clearer way.

Investigate the impact of other properties

First, examine the underlying data; use the DataFrame.describe() method to view a statistical summary of the data in each column.

df.describe(include='all')
Ply count Panel thickness, average (measured) Ply thickness, average (measured) Specimen ID Young's Modulus (11-axis) Ultimate Tensile Strength Test Type
count 67.000000 67.000000 67.000000 67 67 67.000000 67
unique NaN NaN NaN 67 19 NaN 2
top NaN NaN NaN LBJ1311A [] NaN 0° tension
freq NaN NaN NaN 1 49 NaN 56
mean 6.492537 1.534167 0.235604 NaN NaN 1288.110261 NaN
std 1.119701 0.294450 0.005039 NaN NaN 557.743977 NaN
min 6.000000 1.379415 0.229948 NaN NaN 44.629768 NaN
25% 6.000000 1.385455 0.231279 NaN NaN 1353.613343 NaN
50% 6.000000 1.410677 0.234618 NaN NaN 1545.653034 NaN
75% 6.000000 1.438564 0.240007 NaN NaN 1603.610369 NaN
max 9.000000 2.213429 0.245727 NaN NaN 1639.559637 NaN

The Test Type column summary shows that the most frequent value is '0° tension' (as expected), but there are 9 occurrences of a second test type.

To identify all possible values in the Test Type column, use DataFrame['Test Type'].unique().

df['Test Type'].unique()

Previous cell output:

array(['0° tension', '90° tension'], dtype=object)

Although there is only one populated attribute for Ultimate Tensile Strength and Young's modulus, the Test Type column shows that the tensile tests were performed in two different directions.

Since composite materials are often highly orthotropic, the direction of the test must be taken into account when plotting the results. We can filter the DataFrame based on the Test Type column when plotting:

ftu_0_label = rf'$F_{{tu}}, 0^{{\circ}}$ / ${ftu_unit}$'
df_0_tension = df[df['Test Type'] == '0° tension']

fig, ax = plt.subplots()
ax.scatter(data=df_0_tension,
           x="Ply thickness, average (measured)",
           y="Ultimate Tensile Strength",
           alpha=0.75,
           s=100)
ax.set_xlabel(ply_thickness_label)
ax.set_ylabel(ftu_0_label)
_ = ax.set_title(r'$F_{tu}, 0^{{\circ}}$ vs Average ply thickness')

png

We can also overlay the 90° Ultimate Tensile Strength results, using .twinx() to plot them on a separate y-axis.

ftu_90_label = rf'$F_{{tu}}, 90^{{\circ}}$ / ${ftu_unit}$'
df_90_tension = df[df['Test Type'] == '90° tension']

fig, ax1 = plt.subplots()
ax1.scatter(data=df_0_tension,
            x="Ply thickness, average (measured)",
            y="Ultimate Tensile Strength",
            color="blue",
            marker="o",
            alpha=0.75,
            s=100)
ax2 = plt.twinx()
ax2.scatter(data=df_90_tension,
            x="Ply thickness, average (measured)",
            y="Ultimate Tensile Strength",
            color="orange",
            marker="x",
            alpha=0.75,
            s=100)

ax1.set_xlabel(ply_thickness_label)
ax1.set_ylabel(ftu_0_label)
ax2.set_ylabel(ftu_90_label)

h1, l1 = ax1.get_legend_handles_labels()
h2, l2 = ax2.get_legend_handles_labels()

ax1.legend(h1+h2, [ftu_0_label, ftu_90_label], loc='lower right')
_ = ax1.set_title(r'$F_{tu}, 0^{{\circ}}$ and $90^{{\circ}}$ vs Average ply thickness')

png

Connect with Ansys