Skip to main content

Granta MI Scripting Toolkit 4.1

Create scatter plots

Last update: 16.07.2025

Export multiple attributes from a table and use the pandas, matplotlib and seaborn packages to compare material properties using 2D, 3D, and 4D scatter plots.

This example uses data from the MaterialUniverse table in the MI Training database.

Get data from Granta MI

Import the granta libraries and connect to Granta MI via the Service Layer using Windows authentication, replacing my.server.name with the name of your Granta MI server. Specify a database and table.

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 = 'SI (Consistent)'
material_universe = training_db.get_table('MaterialUniverse')

Specify the records whose attributes you want to plot. In this case, all records in the table are used, but you can use other MI Scripting Toolkit methods to define a list of records (for example, search_criterion()).

records = material_universe.all_records(include_generics=False, include_folders=False)

Bulk export the attributes you want to plot. (The attribute names are stored in a list for later use.)

attributes = ['Designation', 'Base', 'Transparency', "Young's modulus", 'Density', 'Tensile strength',
              'Electrical resistivity']
material_universe.bulk_fetch(records, attributes=attributes)

Process the data using pandas

Build a DataFrame

A pandas DataFrame can be built from a List of Dicts, where each dictionary in the list corresponds to a separate row in the DataFrame. Each key-value pair in the Dict corresponds to a column and the cell value in that column, respectively.

First generate the list of dictionaries, and then use this list to create a DataFrame. Here, we are using the Record.record_guid property as the DataFrame index, but any column with a unique value for each record can be used. We also sort by the Base column to guarantee a repeatable row order.

import pandas as pd

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

data = [{'guid': r.record_guid, **get_attr_vals(r)} for r in records]
df = pd.DataFrame(data).set_index('guid').sort_values(by="Base")

Use the DataFrame .head() method to output the first few rows of the DataFrame and check the structure.

df.head()
Designation Base Transparency Young's modulus Density Tensile strength Electrical resistivity
guid
00000e41-000e-4fff-8fff-dd92ffff0000 7075 Al (Aluminum) Opaque {'low': 69000000000.0, 'high': 72538459777.83203} {'low': 2781.691551208496, 'high': 2809.648036... {'low': 386000000.0, 'high': 510000000.0} {'low': 4.199999809265137e-08, 'high': 4.40000...
00000e38-000e-4fff-8fff-dd92ffff0000 7075 Al (Aluminum) Opaque {'low': 69000000000.0, 'high': 76000000000.0} {'low': 2769.9999809265137, 'high': 2829.99992... {'low': 434000000.0, 'high': 580000000.0} {'low': 5.0999999046325684e-08, 'high': 5.3000...
02401a77-c3e7-4b68-875c-7be3b735b703 Tungsten Carbide Carbide Opaque {'low': 599999999999.9996, 'high': 67000000000... {'low': 15299.99999999999, 'high': 15900.00000... {'low': 372999999.9999999, 'high': 529999999.9... {'low': 6.309999847412099e-07, 'high': 1.00000...
000009cc-000e-4fff-8fff-dd92ffff0000 AISI 4130 Fe (Iron) Opaque {'low': 200000000000.0, 'high': 210256408691.4... {'low': 7790.849685668945, 'high': 7869.149684... {'low': 862000000.0, 'high': 1241000000.0} {'low': 2e-07, 'high': 2.5e-07}
000009cb-000e-4fff-8fff-dd92ffff0000 AISI 4130 Fe (Iron) Opaque {'low': 200000000000.0, 'high': 210256408691.4... {'low': 7794.244766235352, 'high': 7872.578620... {'low': 621000000.0, 'high': 686368408.203125} {'low': 2e-07, 'high': 2.5e-07}

Process any range data

A DataFrame cell can contain any data type we choose to store in it. However, plotting packages such as matplotlib require numerical values to be in a standard format. Some MI Scripting Toolkit data types, such as ranges represented as dictionaries, are not in that format and have to be processed before plotting.

Below, we define a granta_mean function which takes a single dict as input and calculates either the arithmetic or geometric mean (depending on the signs of the input values).

import math

def granta_mean(value):
    if value is None:
        return None
    if 'high' not in value:
        return value['low']
    if 'low' not in value:
        return value['high']
    product = value['low'] * value['high']
    if product > 0:
        gm = math.sqrt(product)
        if value['low'] > 0:
            return gm
        else:
            return -gm
    else:
        return sum(value.values())/2

Apply the granta_mean function to each cell in the Density, Young's modulus, Tensile strength, and Electrical resistivity columns. Store the results in a new DataFrame, and check the output to ensure the data processing worked as expected.

df_processed = df[['Density', "Young's modulus", 'Tensile strength', 'Electrical resistivity']].map(granta_mean)
df_processed.sort_values(by="Density").head()
Density Young's modulus Tensile strength Electrical resistivity
guid
0000132e-000e-4fff-8fff-dd92ffff0000 922.225549 2.236068e+08 3.162278e+07 1.000000e+01
0000157d-000e-4fff-8fff-dd92ffff0000 1049.571352 2.408319e+09 3.872983e+07 9.949874e+13
00001584-000e-4fff-8fff-dd92ffff0000 1079.953746 1.999375e+09 4.289522e+07 9.949874e+13
00001ca7-000e-4fff-8fff-dd92ffff0000 1095.415362 7.509658e+05 8.289964e+06 3.160000e+10
00001570-000e-4fff-8fff-dd92ffff0000 1134.988977 1.308128e+10 1.064425e+08 3.162278e-02

Since the columns now contain numeric data, we can use the .describe() method to provide some initial insights into the distribution of values in the table. (This also confirms that the granta_mean function worked as expected.)

df_processed.describe()
Density Young's modulus Tensile strength Electrical resistivity
count 29.000000 2.900000e+01 2.900000e+01 2.900000e+01
mean 3056.118133 7.441616e+10 3.107848e+08 2.184037e+15
std 3201.106148 1.256091e+11 4.631297e+08 4.056391e+15
min 922.225549 7.509658e+05 8.289964e+06 4.298837e-08
25% 1184.905061 2.408319e+09 3.863832e+07 1.689970e-06
50% 1369.963508 1.308128e+10 6.816744e+07 4.000000e+13
75% 2799.839234 8.692341e+10 4.446234e+08 1.000000e+15
max 15597.115118 6.340347e+11 1.775671e+09 1.000000e+16

Process any discrete or short text data

The Designation column is based on a short-text attribute, which is represented as a string and can be used as is.

The columns Transparency and Base are based on discrete attributes. Since they are single-valued, they contain strings and so can also be used as is.

df_processed[['Transparency', 'Base', 'Designation']] = df[['Transparency', 'Base', 'Designation']]
df_processed.sort_values(by="Density").head()
Density Young's modulus Tensile strength Electrical resistivity Transparency Base Designation
guid
0000132e-000e-4fff-8fff-dd92ffff0000 922.225549 2.236068e+08 3.162278e+07 1.000000e+01 Opaque Other Leather
0000157d-000e-4fff-8fff-dd92ffff0000 1049.571352 2.408319e+09 3.872983e+07 9.949874e+13 Opaque Polymer Acrylonitrile Butadiene Styrene (Extrusion)
00001584-000e-4fff-8fff-dd92ffff0000 1079.953746 1.999375e+09 4.289522e+07 9.949874e+13 Transparent Polymer Acrylonitrile Butadiene Styrene (Transparent, ...
00001ca7-000e-4fff-8fff-dd92ffff0000 1095.415362 7.509658e+05 8.289964e+06 3.160000e+10 Translucent Polymer Polyvinyl chloride elastomer (ultra high molec...
00001570-000e-4fff-8fff-dd92ffff0000 1134.988977 1.308128e+10 1.064425e+08 3.162278e-02 Opaque Polymer Acrylonitrile Butadiene Styrene/20% PAN Carbon...

Specify column units

DataFrame doesn't have a built-in method for specifying the units of a column. For this script, we will create a separate dict that provides a simple mapping between column headings and units.

units = {mi_attr: material_universe.attributes[mi_attr].unit for mi_attr in attributes
         if material_universe.attributes[mi_attr].unit}
units

Previous cell output:

{"Young's modulus": 'Pa',
 'Density': 'kg/m^3',
 'Tensile strength': 'Pa',
 'Electrical resistivity': 'ohm.m'}

Create scatter plots using pandas

This snippet of code 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

Use the built-in .plot() method to plot the data in your DataFrame.

By default, .plot() uses matplotlib to generate plots. Text wrapped in dollar signs is processed as LaTeX-style math text.

density_unit = units['Density']
ym_unit = units["Young's modulus"]
density_label = rf'Density / ${density_unit}$'
ym_label = rf'$E$ / ${ym_unit}$'

_ = df_processed.plot(x='Density',
                      y="Young's modulus",
                      kind='scatter',
                      s=50,
                      loglog=True,
                      xlabel=density_label,
                      ylabel=ym_label,
                      title = 'E vs Density using DataFrame.plot()')

png

Create scatter plots with matplotlib

Alternatively, you can use matplotlib directly with the DataFrame as an input. For comparison, here is the same plot using matplotlib. The result is the same, but the figure and axes are first created with plt.subplots(), the axis scales and labels modified, and then the ax.scatter() method is used to create the plot itself. Finally, the top-level plt object is used to add a title.

import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.set_yscale('log')
ax.set_xscale('log')
ax.set_ylabel(ym_label)
ax.set_xlabel(density_label)
ax.scatter(data=df_processed, x='Density', y="Young's modulus", s=50)
_ = ax.set_title('E vs Density using ax.scatter()')

png

Plotting data by category (discrete values)

Using matplotlib directly allows us to use the full power and flexibility of the package. For example, you can create a plot color-coded by the value of a discrete attribute by iteratively adding scatter plots to the same set of axes.

The simplest way to implement this is to use the .groupby() DataFrame method, which generates a set of groupby objects. These behave like individual DataFrames, and can be plotted similarly to the previous example. matplotlib will automatically increment the marker color for each new plot.

In the example below, the full DataFrame is separated into separate groupby objects that share the same Base value. These DataFrame rows are each plotted on their own scatter plot with a separate color. The marker is incremented manually through a pre-defined list, which ensures accessibility for users with color vision deficiencies.

fig, ax = plt.subplots()
ax.set_yscale('log')
ax.set_xscale('log')
ax.set_ylabel(ym_label)
ax.set_xlabel(density_label)

markers = ["o", "s", "v", "^", "<", ">", "X"]

for idx, (key, df) in enumerate(df_processed.groupby('Base')):
    marker = markers[idx]
    ax.scatter(data=df, x='Density', y="Young's modulus", label=key, s=50, marker=marker)
ax.set_title('E vs Density grouped by Base (using matplotlib)')
_ = plt.legend(title='Base')

png

Creating a 3D scatter plot

The set-up of a 3D scatter plot is slightly different to the 2D examples above, because the z-axis must be added as a subplot. Use figure() and add_subplot(projection='3d') to create a 3D figure, then use an alternative syntax to plot the data (in previous plots, the data was added using DataFrame column names, whereas this example plots the arrays themselves).

rho_unit = units['Electrical resistivity']
rho_label = rf'$\rho$ / ${rho_unit}$'

fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_ylabel(ym_label)
ax.set_xlabel(density_label)
ax.set_zlabel(rho_label)

ax.scatter(df_processed['Density'], df_processed["Young's modulus"], df_processed['Electrical resistivity'], s=50)
_ = ax.set_title(r'$\rho$ vs E vs Density')

png

Adding a continuous color axis to a 2D scatter plot

Add a continuous color axis using the c and cmap arguments to the plot.scatter() constructor.

This example uses the optional norm argument to specify an alternative method of mapping numeric values to a color in the colormap. The color_norm variable contains a logarithmic normalization which ensures color variation is used across all values of electrical resistivity.

The viridis colormap is perceptually uniform (monotonically increases in luminance) so is accessible to users with color vision deficiencies.

import matplotlib.colors as colors

rho_unit = units['Electrical resistivity']
rho_label = rf'$\rho$ / ${rho_unit}$'

fig, ax = plt.subplots()
ax.set_yscale('log')
ax.set_xscale('log')
ax.set_ylabel(ym_label)
ax.set_xlabel(density_label)

res_min = df_processed['Electrical resistivity'].min()
res_max = df_processed['Electrical resistivity'].max()
color_norm = colors.LogNorm(vmin=res_min, vmax=res_max)

sc = plt.scatter(data=df_processed,
                 x='Density',
                 y="Young's modulus",
                 c="Electrical resistivity",
                 s=50, cmap='viridis',
                 norm=color_norm)
cbar = plt.colorbar(sc)
cbar.set_label(rho_label, rotation=270, labelpad=20)
_ = ax.set_title(r'E vs Density and $\rho$ (continuous marker color)')

png

Plotting data by category (binned point or range values)

For this plot, we first use numpy.geomspace() to define 5 logarithmically-spaced bins and DataFrame.cut() to map the continuous Electrical resistivity values to those categories.

import numpy as np
min_value = df_processed['Electrical resistivity'].min()
max_value = df_processed['Electrical resistivity'].max()

# The number of values is calculated between the two values, and is one more than the required number of bins
spacing = np.geomspace(min_value, max_value, 6)

df_processed['Electrical resistivity (binned)'] = pd.cut(df_processed['Electrical resistivity'],
                                                         spacing,
                                                         labels=['Very low', 'Low', 'Medium', 'High', 'Very high'])
df_processed.head()
Density Young's modulus Tensile strength Electrical resistivity Transparency Base Designation Electrical resistivity (binned)
guid
00000e41-000e-4fff-8fff-dd92ffff0000 2795.634849 7.074711e+10 4.436891e+08 4.298837e-08 Opaque Al (Aluminum) 7075 NaN
00000e38-000e-4fff-8fff-dd92ffff0000 2799.839234 7.241547e+10 5.017171e+08 5.199038e-08 Opaque Al (Aluminum) 7075 Very low
02401a77-c3e7-4b68-875c-7be3b735b703 15597.115118 6.340347e+11 4.446234e+08 7.943551e-07 Opaque Carbide Tungsten Carbide Very low
000009cc-000e-4fff-8fff-dd92ffff0000 7829.901810 2.050641e+11 1.034283e+09 2.236068e-07 Opaque Fe (Iron) AISI 4130 Very low
000009cb-000e-4fff-8fff-dd92ffff0000 7833.313776 2.050641e+11 6.528666e+08 2.236068e-07 Opaque Fe (Iron) AISI 4130 Very low

Then, we can use .groupby() to plot each bin with a different color and shape of marker. A cycler.cycler object specifies which colors will be used for each separate scatter plot.

from cycler import cycler

fig, ax = plt.subplots()
ax.set_yscale('log')
ax.set_xscale('log')
ax.set_ylabel(ym_label)
ax.set_xlabel(density_label)

ax.set_prop_cycle(cycler('color', ['blue', 'green', 'yellow', 'orange', 'red']))

for idx, (key, df) in enumerate(df_processed.groupby('Electrical resistivity (binned)', observed=False)):
    marker = markers[idx]
    ax.scatter(data=df, x='Density', y="Young's modulus", label=key, s=50, marker=marker)
plt.legend(title='Electrical resistivity', loc='lower right')
_ = ax.set_title(r'E vs Density and $\rho$ (discrete marker color)')

png

Create scatter plots with seaborn

An alternative to using matplotlib directly is to use seaborn, a wrapper of matplotlib that aims to make it easier to create professional-looking plots.

For example, here is the E vs rho plot grouped by 'Base' in seaborn. Note that the plot itself is defined in one line, and we can specify DataFrame columns to use for the marker hue and style with no additional steps. The legend is created automatically.

from itertools import cycle, islice
import seaborn as sns

ax = sns.scatterplot(data=df_processed, x="Density", y="Young's modulus", hue="Base", style="Base", s=50)
ax.set_xscale('log')
ax.set_xlabel(density_label)
ax.set_yscale('log')
ax.set_ylabel(ym_label)
ax.set_title('E vs Density grouped by Base (using seaborn)')

Previous cell output:

Text(0.5, 1.0, 'E vs Density grouped by Base (using seaborn)')

png

Plotting in n dimensions (pair plots)

The seaborn package doesn't expose matplotlib's 3D scatter plotting functionality. However, it can produce stacked (paired) plots of any dimensionality as required. The example below shows a comparison across 4 continuous dimensions.

To avoid having to set the label of each axis, we've created a new DataFrame with the desired axis labels as column names. The labelling is then done automatically.

Although sns.pairplot doesn't have a style argument, you can provide a list of markers instead. This must be the same length as the number of categories. Here, we use itertools to cycle through the marker style list to get the correct number.

ftu_unit = units['Tensile strength']
ftu_label = rf'$F_{{tu}}$ / ${ftu_unit}$'

column_mapping = {'Density': density_label,
                  "Young's modulus": ym_label,
                  'Tensile strength': ftu_label,
                  'Electrical resistivity': rho_label}
df_pairplot = df_processed.rename(columns=column_mapping)

fig = plt.figure()
base_count = df_pairplot['Base'].nunique()
base_markers = list(islice(cycle(markers), 0, base_count))
g = sns.pairplot(vars=[density_label, ym_label, ftu_label, rho_label],
                 data=df_pairplot,
                 hue="Base",
                 markers=base_markers,
                 diag_kind='None')

for i in range(len(g.axes)):
    for j in range(len(g.axes[i])):
        g.axes[i,j].set_xscale('log')
        g.axes[i,j].set_yscale('log')
_ = g.fig.suptitle(r'Pair plot comparing Density, E, $F_{{{tu}}}$ and $\rho$, grouped by Base', y=1.03)

Previous cell output:

<Figure size 800x600 with 0 Axes>

png

Connect with Ansys