Material Phases¶
The physical and mechanical properties of a polycrystalline materials depend not only on its chemical composition but also on the phases from which it is composed. This tutorial will review the objects and tools available in Pymicro to store, load and process the informations related to the various constitutive phases of a material.
In Pymicro, a phase is defined by a set of crystallographic and physical properties. The CrystallinePhase class of the pymicro.crystal.lattice module allows to store and manipulate phase data. In addition, the data model of the Microstructure class includes a specific Group to store phase data in datasets.
To begin with, we will look at the phase data stored in one of Pymicro’s example datasets. This file is zipped in the package to reduce its size. If you are reading through this tutorial as a Notebook, you will first have to unzip the file:
[1]:
from pymicro import get_examples_data_dir # import file directory path
PYMICRO_EXAMPLES_DATA_DIR = get_examples_data_dir() # get the file directory path
import os
dataset_file = os.path.join(PYMICRO_EXAMPLES_DATA_DIR, 'example_microstructure') # test dataset desired file path
tar_file = os.path.join(PYMICRO_EXAMPLES_DATA_DIR, 'example_microstructure.tar.gz') # dataset zipped archive path
# Save current directory
cwd = os.getcwd()
# move to example data directory
os.chdir(PYMICRO_EXAMPLES_DATA_DIR)
# unarchive the dataset
os.system(f'tar -xvf {tar_file}')
# get back to UserGuide directory
os.chdir(cwd)
example_microstructure.h5
example_microstructure.xdmf
Phase Groups¶
Let us now open the dataset and display its content, using the Microstructure class:
[2]:
# import SampleData class
from pymicro.crystal.microstructure import Microstructure
# Open Microstructure dataset
micro = Microstructure(filename=dataset_file)
(111,)
/GrainData/GrainDataTable (Table(111,)) ''
end of _init_phase, fixing phase array
[1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1]
[3]:
# display content of dataset
micro.print_dataset_content(max_depth=2, short=True)
Printing dataset content with max depth 2
|--GROUP Amitex_Results: /Amitex_Results (Group)
--NODE mean_strain: /Amitex_Results/mean_strain (data_array) ( 63.984 Kb)
--NODE mean_stress: /Amitex_Results/mean_stress (data_array) ( 63.984 Kb)
--NODE rms_strain: /Amitex_Results/rms_strain (data_array) ( 63.984 Kb)
--NODE rms_stress: /Amitex_Results/rms_stress (data_array) ( 63.984 Kb)
--NODE simulation_iterations: /Amitex_Results/simulation_iterations (data_array) ( 64.000 Kb)
--NODE simulation_time: /Amitex_Results/simulation_time (data_array) ( 64.000 Kb)
|--GROUP CellData: /CellData (3DImage)
|--GROUP Amitex_output_fields: /CellData/Amitex_output_fields (Group)
--NODE Field_index: /CellData/Field_index (string_array) ( 63.999 Kb)
--NODE grain_map: /CellData/grain_map (field_array) ( 1.945 Mb)
--NODE grain_map_raw: /CellData/grain_map_raw (field_array) ( 1.945 Mb)
--NODE mask: /CellData/mask (field_array) ( 996.094 Kb)
--NODE phase_map: /CellData/phase_map (field_array - empty) ( 64.000 Kb)
--NODE uncertainty_map: /CellData/uncertainty_map (field_array) ( 996.094 Kb)
|--GROUP GrainData: /GrainData (Group)
--NODE GrainDataTable: /GrainData/GrainDataTable (structured array) ( 65.127 Kb)
|--GROUP MeshData: /MeshData (emptyMesh)
|--GROUP grains_mesh: /MeshData/grains_mesh (3DMesh)
|--GROUP PhaseData: /PhaseData (Group)
|--GROUP phase_01: /PhaseData/phase_01 (Group)
The dataset contains a PhaseData group, that is used to store all phase data relative to the sample associated to the dataset. It has one Group children for each phase in the dataset, that stores all relevant information for this phase.
Here there only one phase in the microstructure, whose data is stored into the phase_01 group. The content of this group can be easily printed:
[4]:
micro.print_node_info('phase_01')
GROUP phase_01
=====================
-- Parent Group : PhaseData
-- Group attributes :
* description : Polycrystalline alpha grade 2 Titanium phase
* elastic_constants : [162000.0, 92000.0, 69000.0, 180000.0, 46700.0]
* elastic_constants_unit : MPa
* formula : Ti
* group_type : Group
* lattice_parameters : [1.0, 1.0]
* lattice_parameters_unit : nm
* name : Ti grade 2
* phase_id : 1
* symmetry : hexagonal
-- Childrens :
----------------
As shown by the print, this group contains only metadata (node attributes, see here) providing information on the phase crystallographic (symmetry, lattice parameters) and physical (elasticity) properties, but also some identification metadata (phase name, formula, Id number).
The CrystallinePhase class of the pymicro.crystal.lattice module is the object that allows to manipulate interactively all the data stored into the PhaseData group. A CrystallinePhase can be directly retrieved from a phase description group with the get_phase method:
[5]:
phase_01 = micro.get_phase(phase_id=1)
print(phase_01)
Phase 1 (Ti grade 2)
-- Lattice (Symmetry.hexagonal) a=1.000, b=1.000, c=1.000 alpha=90.0, beta=90.0, gamma=120.0
-- elastic constants: [162000.0, 92000.0, 69000.0, 180000.0, 46700.0]
This object contains a Lattice object, defining the crystal lattice of the phase. It also stores the elastic constants of the phase. Currently, those are the only information that are handled by the CrystallinePhase object. As many attributes as desired can be stored in a phase_XX group, but only those will be loaded into the CrystallinePhase object with the get_phase method.
The data associated to the CrystallinePhase class are used by other tools of the Pymicro package, related to X-ray diffration or mechanical behavior simulations.
We will now see how to create phase objects, and store them into microstructure datasets.
[6]:
# close the opened dataset and remove the phase object
del phase_01
del micro
Phase objects¶
As shown above, Phase objects are instances of the class CrystallinePhase. To create one, the class constructor can be used. Three arguments can be passed to the constructor: * a phase id number * a name for the phase * a crystal lattice object
A lattice object must hence be created to be associated to the phase object.
Lattice objects¶
The Lattice class of the pymicro.crystal.lattice module allows to create and manipulate lattice objects. A Lattice is defined by a by a symmetry group and three vectors.
Most lattice systems encountered in real materials have a lot of symetries. In those cases, the complete description of the lattice vectors is redundant. Specific lattice constructors for each type of the 7 Bravais lattice systems are available in Pymicro, and allow to simplify the declaration of lattice objects.
For instance, you can create a cubic lattice (completely defined by one lattice parameter) as follows:
[7]:
from pymicro.crystal.lattice import Lattice
a = 0.352 # lattice parameter for FCC Nickel (nm)
l = Lattice.face_centered_cubic(a)
print(l)
Lattice (Symmetry.cubic) a=0.352, b=0.352, c=0.352 alpha=90.0, beta=90.0, gamma=90.0
Note
The unit for lattice parameters in Pymicro is the nanometer (nm).
The class allows to get usefull properties of the crystal lattice, such as: * the director vectors of the lattice and the reciprocal lattice * the metric tensor and the volume of the lattice * crystallographic planes of the lattice * slip systems of the lattice
[8]:
# get lattice directors : each column of the lattice matrix is a lattice director vector
print(f' Crystal lattice directors. \n - D1 : {l.matrix.round(decimals=3)[0,:]} '
f'\n - D2 : {l.matrix.round(decimals=3)[1,:]} '
f'\n - D3 : {l.matrix.round(decimals=3)[2,:]} \n')
# same for reciprocal lattice
Rmat = l.reciprocal_lattice()
print(f' Crystal reciprocal lattice directors. \n - D1 : {Rmat[0].round(decimals=3)} '
f'\n - D2 : {Rmat[1].round(decimals=3)} '
f'\n - D3 : {Rmat[2].round(decimals=3)} \n')
# get lattice volume
print(f'Volume of the crystal lattice: {l.volume()} \n')
# get lattice metric tensor
g = l.metric_tensor()
print(f'Lattice Metric tensor: \n {g} \n')
# get 111 planes
print(f'1,1,1 planes : \n {l.get_hkl_family([1,1,1])} \n')
# slip systems
slip_list = l.get_slip_systems(slip_type='111')
print(f'List of octahedral slip systems: \n {slip_list}')
Crystal lattice directors.
- D1 : [0.352 0. 0. ]
- D2 : [0. 0.352 0. ]
- D3 : [0. 0. 0.352]
Crystal reciprocal lattice directors.
- D1 : [ 2.841 -0. -0. ]
- D2 : [ 0. 2.841 -0. ]
- D3 : [0. 0. 2.841]
Volume of the crystal lattice: 0.043614207999999995
Lattice Metric tensor:
[[1.23904000e-01 7.58693185e-18 7.58693185e-18]
[7.58693185e-18 1.23904000e-01 7.58693185e-18]
[7.58693185e-18 7.58693185e-18 1.23904000e-01]]
1,1,1 planes :
[HKL Plane
Miller indices:
h : 1.0
k : 1.0
l : 1.0
plane normal : [0.57735027 0.57735027 0.57735027]
crystal lattice : Lattice (Symmetry.cubic) a=0.352, b=0.352, c=0.352 alpha=90.0, beta=90.0, gamma=90.0, HKL Plane
Miller indices:
h : -1.0
k : 1.0
l : 1.0
plane normal : [-0.57735027 0.57735027 0.57735027]
crystal lattice : Lattice (Symmetry.cubic) a=0.352, b=0.352, c=0.352 alpha=90.0, beta=90.0, gamma=90.0, HKL Plane
Miller indices:
h : 1.0
k : -1.0
l : 1.0
plane normal : [ 0.57735027 -0.57735027 0.57735027]
crystal lattice : Lattice (Symmetry.cubic) a=0.352, b=0.352, c=0.352 alpha=90.0, beta=90.0, gamma=90.0, HKL Plane
Miller indices:
h : 1.0
k : 1.0
l : -1.0
plane normal : [ 0.57735027 0.57735027 -0.57735027]
crystal lattice : Lattice (Symmetry.cubic) a=0.352, b=0.352, c=0.352 alpha=90.0, beta=90.0, gamma=90.0]
List of octahedral slip systems:
[(111)[-101], (111)[0-11], (111)[-110], (1-11)[-101], (1-11)[011], (1-11)[110], (-111)[0-11], (-111)[110], (-111)[101], (11-1)[-110], (11-1)[101], (11-1)[011]]
Phase object creation¶
Now that the lattice object has been created, it is time to create a phase object, using the CrystallinePhase class constructor:
[9]:
# import CrystallinePhase class
from pymicro.crystal.lattice import CrystallinePhase
# create empty phase object
phase = CrystallinePhase(phase_id=1)
print(phase)
Phase 1 (unknown)
-- Lattice (Symmetry.cubic) a=1.000, b=1.000, c=1.000 alpha=90.0, beta=90.0, gamma=90.0
The default lattice set to created Phase objects is a cubic lattice with a unitary lattice parameter. To change that and set the lattice object that we created above to our phase, the set_lattice method must be used:
[10]:
phase.set_lattice(l)
print(phase)
symmetry was changed to Symmetry.cubic
Phase 1 (unknown)
-- Lattice (Symmetry.cubic) a=0.352, b=0.352, c=0.352 alpha=90.0, beta=90.0, gamma=90.0
The name of the phase, by default “unkown”, can be easily set:
[11]:
phase.set_name('FCC Ni-16Cr')
print(phase)
Phase 1 (FCC Ni-16Cr)
-- Lattice (Symmetry.cubic) a=0.352, b=0.352, c=0.352 alpha=90.0, beta=90.0, gamma=90.0
Elastic constants¶
The definition of elastic constants in Pymicro complies with the classical expression of the generalized Hooke’s law with Voigt’s notation. In this framework, the elastic constants are the coefficients of the symmetric stiffness matrix \(C\) which linearly links stress to strain:
The single indices used in Voigt’s notation, that define the \(C_{IJ}\) coefficients in second expression above, correspond to the following convention:
Depending on the symmetry of the crystal lattice, the number of independent coefficients of the matrix can vary from 3 (cubic symmetry) to 21 (triclinic symmetry). The set_elastic_constants method allows to set this constants for the phase, from the list of independent coefficients. The list of symmetries implemented in Pymicro with the associated list of coefficients to use as input is provided hereafter:
cubic : 3 independant constants \([C_{11}, C_{12}, C_{44}]\)
\[\begin{split}\begin{bmatrix} C_{11} & C_{12} & C_{12} & 0 & 0 & 0 \\ C_{12} & C_{11} & C_{12} & 0 & 0 & 0 \\ C_{12} & C_{12} & C_{11} & 0 & 0 & 0 \\ 0 & 0 & 0 & C_{44} & 0 & 0 \\ 0 & 0 & 0 & C_{44} & 0 & 0 \\ 0 & 0 & 0 & C_{44} & 0 & 0 \end{bmatrix}\end{split}\]hexagonal : 5 independant constants \([C_{11}, C_{12}, C_{13}, C_{33}, C_{44}]\):
\[\begin{split}\begin{bmatrix} C_{11} & C_{12} & C_{13} & 0 & 0 & 0 \\ C_{12} & C_{11} & C_{13} & 0 & 0 & 0 \\ C_{13} & C_{13} & C_{33} & 0 & 0 & 0 \\ 0 & 0 & 0 & C_{44} & 0 & 0 \\ 0 & 0 & 0 & 0 & C_{44} & 0 \\ 0 & 0 & 0 & 0 & 0 & \frac{C_{11} - C_{12}}{2} \end{bmatrix}\end{split}\]tetragonal : 6 independant constants \([C_{11}, C_{12}, C_{13}, C_{33}, C_{44}, C_{66}]\):
\[\begin{split}\begin{bmatrix} C_{11} & C_{12} & C_{13} & 0 & 0 & 0 \\ C_{12} & C_{11} & C_{13} & 0 & 0 & 0 \\ C_{13} & C_{13} & C_{33} & 0 & 0 & 0 \\ 0 & 0 & 0 & C_{44} & 0 & 0 \\ 0 & 0 & 0 & 0 & C_{44} & 0 \\ 0 & 0 & 0 & 0 & 0 & C_{66} \end{bmatrix}\end{split}\]orthorhombic : 9 independant constants \([C_{11}, C_{12}, C_{13}, C_{22}, C_{23}, C_{33}, C_{44}, C_{55}, C_{66}]\):
\[\begin{split}\begin{bmatrix} C_{11} & C_{12} & C_{13} & 0 & 0 & 0 \\ C_{12} & C_{22} & C_{23} & 0 & 0 & 0 \\ C_{13} & C_{23} & C_{33} & 0 & 0 & 0 \\ 0 & 0 & 0 & C_{44} & 0 & 0 \\ 0 & 0 & 0 & 0 & C_{55} & 0 \\ 0 & 0 & 0 & 0 & 0 & C_{66} \end{bmatrix}\end{split}\]monoclinic : 13 independant constants \([C_{11}, C_{12}, C_{13}, C_{16}, C_{22}, C_{23}, C_{26}, C_{33}, C_{36}, C_{44}, C_{45}, C_{55}, C_{66}]\):
\[\begin{split}\begin{bmatrix} C_{11} & C_{12} & C_{13} & 0 & 0 & C_{16} \\ C_{12} & C_{22} & C_{23} & 0 & 0 & C_{26} \\ C_{13} & C_{23} & C_{33} & 0 & 0 & C_{36} \\ 0 & 0 & 0 & C_{44} & C_{45} & 0 \\ 0 & 0 & 0 & C_{45} & C_{55} & 0 \\ C_{16} & C_{26} & C_{36} & 0 & 0 & C_{66} \end{bmatrix}\end{split}\]triclinic : all 21 constants \(C_{IJ}\)
Note
The unit for elastic constants in Pymicro is the mega Pascal (MPa).
For the cubic phase created above, the three \([C_{11}, C_{12}, C_{44}]\) coefficients must be provided:
[12]:
# create list to store C11, C12, C44 for Ni-16Cr (MPa):
elastic_constants = [300000., 180000.,140000.]
# set elastic constants:
phase.set_elastic_constants(elastic_constants)
print(phase)
Phase 1 (FCC Ni-16Cr)
-- Lattice (Symmetry.cubic) a=0.352, b=0.352, c=0.352 alpha=90.0, beta=90.0, gamma=90.0
-- elastic constants: [300000.0, 180000.0, 140000.0]
Once set in the phase object, the 9 orthotropic constants (\(E_1, E_2, E_3, \nu_{12}, \nu_{13}, \nu_{23}, G_{12}, G_{13}, G_{23},\)) or the stiffness matrix can be easily obtained:
[13]:
# get the orthotropic constants
print(f"The orthotropic elastic constants of the phase {phase.name} are : \n {phase.orthotropic_constants()}")
The orthotropic elastic constants of the phase FCC Ni-16Cr are :
{'E1': 165000.0, 'E2': 165000.0, 'E3': 165000.0, 'Nu12': 0.37499999999999994, 'Nu13': 0.375, 'Nu23': 0.375, 'G12': 140000.0, 'G13': 140000.0, 'G23': 140000.0}
[14]:
# get the stiffness matrix
print(f"The complete stiffness matrix of the phase {phase.name} is : \n {phase.stiffness_matrix()}")
The complete stiffness matrix of the phase FCC Ni-16Cr is :
[[300000. 180000. 180000. 0. 0. 0.]
[180000. 300000. 180000. 0. 0. 0.]
[180000. 180000. 300000. 0. 0. 0.]
[ 0. 0. 0. 140000. 0. 0.]
[ 0. 0. 0. 0. 140000. 0.]
[ 0. 0. 0. 0. 0. 140000.]]
Include Phase data in datasets¶
The final step of this tutorial will show how to add a phase object into a polycrystalline dataset, with the Microstructure class. The first way to do it is to use the add_phase method:
[15]:
# create microstructure
micro = Microstructure(filename='micro_test', autodelete=True)
# print content of phase data group
micro.print_node_info('PhaseData')
# add new phase
micro.add_phase(phase)
# print content of phase data group
micro.print_group_content('PhaseData')
0 phases found in the data set
new phase added: unknown
GROUP PhaseData
=====================
-- Parent Group : /
-- Group attributes :
* group_type : Group
-- Childrens : phase_01,
----------------
warning, adding phase with phase_id = 2 (was 1)
new phase added: FCC Ni-16Cr
****** Group PhaseData CONTENT ******
GROUP phase_01
=====================
-- Parent Group : PhaseData
-- Group attributes :
* description :
* elastic_constants : []
* elastic_constants_unit : MPa
* formula :
* group_type : Group
* lattice_parameters : [1.0]
* lattice_parameters_unit : nm
* name : unknown
* phase_id : 1
* symmetry : cubic
-- Childrens :
----------------
GROUP phase_02
=====================
-- Parent Group : PhaseData
-- Group attributes :
* description :
* elastic_constants : [300000.0, 180000.0, 140000.0]
* elastic_constants_unit : MPa
* formula :
* group_type : Group
* lattice_parameters : [0.352]
* lattice_parameters_unit : nm
* name : FCC Ni-16Cr
* phase_id : 2
* symmetry : cubic
-- Childrens :
----------------
As shown above, the microstructure is created by default with an unknown phase that has a cubic structure with a default lattice parameter of 1 nm, and no elastic constants. The add_phase method creates a new phase in the dataset, and attributes to it the next available phase id number, which is why the dataset has now two phases.
To change the definition of an already existing phase in the dataset, use the set_phase method:
[16]:
# create a new phase
phase_2 = CrystallinePhase(phase_id=1, name='Hcp Ti', lattice=Lattice.hexagonal(a=0.295, c=0.468))
# set first dataset phase
micro.set_phase(phase_2, id_number=1)
# print content of phase data group
micro.print_group_content('PhaseData')
setting phase 1 with Hcp Ti
2 phases found in the data set
****** Group PhaseData CONTENT ******
GROUP phase_01
=====================
-- Parent Group : PhaseData
-- Group attributes :
* description :
* elastic_constants : []
* elastic_constants_unit : MPa
* formula :
* group_type : Group
* lattice_parameters : [0.295, 0.468]
* lattice_parameters_unit : nm
* name : Hcp Ti
* phase_id : 1
* symmetry : hexagonal
-- Childrens :
----------------
GROUP phase_02
=====================
-- Parent Group : PhaseData
-- Group attributes :
* description :
* elastic_constants : [300000.0, 180000.0, 140000.0]
* elastic_constants_unit : MPa
* formula :
* group_type : Group
* lattice_parameters : [0.352]
* lattice_parameters_unit : nm
* name : FCC Ni-16Cr
* phase_id : 2
* symmetry : cubic
-- Childrens :
----------------
[17]:
# close microstructure dataset
del micro
Microstructure Autodelete:
Removing hdf5 file micro_test.h5
The Microstructure class constructor allows to pass a list of phases as argument, to initialize the dataset with the desired phase data in it:
[18]:
# create microstructure with 2 phase objects
micro = Microstructure(filename='micro_test', autodelete=True, phase=[phase, phase_2])
# print content of phase data group
micro.print_group_content('PhaseData')
# close microstructure dataset
del micro
0 phases found in the data set
warning, adding phase with phase_id = 1 (was 2)
new phase added: FCC Ni-16Cr
warning, adding phase with phase_id = 2 (was 1)
new phase added: Hcp Ti
****** Group PhaseData CONTENT ******
GROUP phase_01
=====================
-- Parent Group : PhaseData
-- Group attributes :
* description :
* elastic_constants : [300000.0, 180000.0, 140000.0]
* elastic_constants_unit : MPa
* formula :
* group_type : Group
* lattice_parameters : [0.352]
* lattice_parameters_unit : nm
* name : FCC Ni-16Cr
* phase_id : 1
* symmetry : cubic
-- Childrens :
----------------
GROUP phase_02
=====================
-- Parent Group : PhaseData
-- Group attributes :
* description :
* elastic_constants : []
* elastic_constants_unit : MPa
* formula :
* group_type : Group
* lattice_parameters : [0.295, 0.468]
* lattice_parameters_unit : nm
* name : Hcp Ti
* phase_id : 2
* symmetry : hexagonal
-- Childrens :
----------------
Microstructure Autodelete:
Removing hdf5 file micro_test.h5
With this method, the order of the phases in the created datasets is determined by the order of the phase objects in the list passed to the class constructor.
This concludes the tutorial on Pymicro’s material phase objects.
[19]:
os.remove(dataset_file+'.h5')
os.remove(dataset_file+'.xdmf')