Skip to content

Commit

Permalink
Updates in RVE generation from EBSD maps
Browse files Browse the repository at this point in the history
  • Loading branch information
AHartmaier committed Mar 29, 2024
1 parent ef7c5a5 commit 2c97a1b
Show file tree
Hide file tree
Showing 12 changed files with 392 additions and 205 deletions.
7 changes: 7 additions & 0 deletions src/kanapy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from kanapy.plotting import plot_voxels_3D, plot_polygons_3D
from kanapy.input_output import writeAbaqusMat, pickle2microstructure, import_voxels, \
import_stats, write_stats
from kanapy.rve_stats import find_rot_axis
from kanapy.util import ROOT_DIR, MAIN_DIR, MTEX_DIR, poly_scale

log_level = 20 # Levels for logging: 10: DEBUG, 20: INFO, 30: WARNING, 40: ERROR
Expand All @@ -21,6 +22,12 @@
logging.error('Inconsistent installation of Kanapy package. MTEX tools will not be available.')
MTEX_AVAIL = False

try:
from kanapy.triple_surface import create_ref_ell
triple_surf = True
except:
triple_surf = False

__author__ = 'Mahesh R.G Prasad, Abhishek Biswas, Golsa Tolooei Eshlaghi, Alexander Hartmaier'
__email__ = '[email protected]'
__version__ = version('kanapy')
26 changes: 16 additions & 10 deletions src/kanapy/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ class Microstructure(object):
rve_stats : list
List of dictionaries containing statistical information on different RVE types: particles, voxels,
polyhedral grains
rve_stats_labels : list
List of strings containing the labels for the RVE types, i.e. Partcls, Voxels, Grains
"""

def __init__(self, descriptor=None, file=None, name='Microstructure'):
Expand All @@ -84,6 +86,7 @@ def __init__(self, descriptor=None, file=None, name='Microstructure'):
self.simbox = None
self.mesh = None
self.rve_stats = None
self.rve_stats_labels = None
self.from_voxels = False
self.ialloy = None

Expand Down Expand Up @@ -451,10 +454,11 @@ def plot_stats(self, data=None,
labels = []
if dual_phase:
iphase = ip
print(f'Plotting input & output statistics for phase {ip}')
if data is None or \
(type(data) is str and 'p' in data.lower()):
# analyse particle statistics
print(f'Plotting statistical information for phase {ip}')

# Analyze and plot particles statistics
if (data is None and self.particles is not None) or \
(type(data) is str and 'p' in data.lower()):
if self.particles is None:
logging.error('Particle statistics requested, but no particles defined. '
'Run "pack()" first.')
Expand All @@ -465,9 +469,9 @@ def plot_stats(self, data=None,
stats_list.append(part_stats)
labels.append("Partcls")

if data is None or \
(type(data) is str and 'v' in data.lower()):
# analyse voxel statistics
# Analyze and plot statistics of voxel structure in RVE
if (data is None and self.mesh is not None) or \
(type(data) is str and 'v' in data.lower()):
if self.mesh is None:
logging.error('Voxel statistics requested, but no voxel mesh defined. '
'Run "voxelize()" first.')
Expand All @@ -477,9 +481,10 @@ def plot_stats(self, data=None,
verbose=verbose, save_files=save_files)
stats_list.append(vox_stats)
labels.append('Voxels')
if data is None or \
(type(data) is str and 'g' in data.lower()):
# analyse voxel statistics

# Analyze and plot statistics of polyhedral grains in RVE
if (data is None and self.geometry is not None) or \
(type(data) is str and 'g' in data.lower()):
if self.geometry is None:
logging.error('Geometry statistics requested, but no polyhedral grains defined. '
'Run "generate_grains()" first.')
Expand All @@ -489,6 +494,7 @@ def plot_stats(self, data=None,
verbose=verbose, save_files=save_files)
stats_list.append(grain_stats)
labels.append('Grains')

if dual_phase:
print(f'\nStatistical microstructure parameters of phase {iphase} in RVE')
print('-------------------------------------------------------')
Expand Down
13 changes: 6 additions & 7 deletions src/kanapy/entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def __init__(self, iden, x, y, z, a, b, c, quat, phasenum=0, dup=None, points=No
self.speedx = 0.
self.speedy = 0.
self.speedz = 0.
self.rotationMatrixGen() # Initialize roatation matrix for the ellipsoid
self.rotation_matrix = self.rotationMatrixGen() # Initialize roatation matrix for the ellipsoid
self.surface_points = self.surfacePointsGen() # Initialize surface points for the ellipsoid
self.inside_voxels = [] # List that stores voxels belonging to the ellipsoid
self.set_cub() # sets particle cuboid for collision testing with octree boxes
Expand Down Expand Up @@ -151,6 +151,8 @@ def rotationMatrixGen(self):

w, x, y, z = self.quat
Nq = w * w + x * x + y * y + z * z
if Nq < FLOAT_EPS:
return np.eye(3)

s = 2.0 / Nq
X = x * s
Expand All @@ -166,12 +168,9 @@ def rotationMatrixGen(self):
yZ = y * Z
zZ = z * Z

if Nq < FLOAT_EPS:
self.rotation_matrix = np.eye(3)
else:
self.rotation_matrix = np.array([[1.0 - (yY + zZ), xY - wZ, xZ + wY],
[xY + wZ, 1.0 - (xX + zZ), yZ - wX],
[xZ - wY, yZ + wX, 1.0 - (xX + yY)]])
return np.array([[1.0 - (yY + zZ), xY - wZ, xZ + wY],
[xY + wZ, 1.0 - (xX + zZ), yZ - wX],
[xZ - wY, yZ + wX, 1.0 - (xX + yY)]])

def surfacePointsGen(self, nang=20):
"""
Expand Down
100 changes: 68 additions & 32 deletions src/kanapy/initializations.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
"""
Classes for RVE and mesh initialization
Authors: Mahesh Prassad, Abhishek Biswas, Alexander Hartmaier
Ruhr University Bochum, Germany
March 2024
"""
import json
import os
import numpy as np
Expand Down Expand Up @@ -80,31 +88,33 @@ def __init__(self, stats_dicts, nsteps=1000, from_voxels=False, poly=None):
Attributes
----------
self.packing_steps = nsteps # max number of steps for packing simulation
self.size = None # tuple of lengths along Cartesian axes
self.dim = None # tuple of number of voxels along Cartesian axes
self.periodic = None # Boolean for periodicity of RVE
self.units = None # Units of RVE dimensions, either "mm" or "um" (micron)
self.nparticles = [] # list of particle numbers for each phase
packing_steps = nsteps # max number of steps for packing simulation
size = None # tuple of lengths along Cartesian axes
dim = None # tuple of number of voxels along Cartesian axes
periodic = None # Boolean for periodicity of RVE
units = None # Units of RVE dimensions, either "mm" or "um" (micron)
nparticles = [] # list of particle numbers for each phase
particle_data = [] # list of cits for statistical particle data for each grains
phase_names = [] # list of names of phases
phase_vf = [] # list of volume fractions of phases
ialloy : int
Number of alloy in ICAMS CP-UMAT
"""

def init_particles(particle_data):
def init_particles():
"""
Extract statistical microstructure information from data dictionary
and initalize particles for packing accordingly
"""

def gen_data_basic(pdict):
# Compute the Log-normal PDF & CDF.
frozen_lognorm = lognorm(s=sig_ED, loc=loc_ED, scale=scale_ED)

"""Computes number of particles according to equivalent diameter distribution
and initializes particle ensemble with individual diameters approximating
this lognormal distribution.
"""
# Compute the Log-normal CDF according to parameters for equiv. diameter distrib.
xaxis = np.linspace(0.1, 200, 1000)
ycdf = frozen_lognorm.cdf(xaxis)
ycdf = lognorm.cdf(xaxis, s=sig_ED, loc=loc_ED, scale=scale_ED)

# Get the mean value for each pair of neighboring points as centers of bins
xaxis = np.vstack([xaxis[1:], xaxis[:-1]]).mean(axis=0)
Expand Down Expand Up @@ -145,7 +155,7 @@ def gen_data_basic(pdict):
totalEllipsoids = int(np.sum(num))

# Duplicate the diameter values
eq_Dia = np.repeat(eq_Dia, num) # better calculate num first
eq_Dia = np.repeat(eq_Dia, num)

# Raise value error in case the RVE side length is too small to fit grains inside.
if len(eq_Dia) == 0:
Expand Down Expand Up @@ -189,24 +199,34 @@ def gen_data_elong(pdict):
# Sample from Normal distribution: It takes mean and std of normal distribution
tilt_angle = []
num = pdict['Number']
iter = 0
while num > 0:
tilt = vonmises.rvs(kappa, loc=loc_ori, size=num)
index_array = np.where((tilt >= ori_cutoff_min) & (tilt <= ori_cutoff_max))
TA = tilt[index_array].tolist()
tilt_angle.extend(TA)
num = pdict['Number'] - len(tilt_angle)
iter += 1
if iter > 10000:
raise StopIteration('Iteration for tilt angles did not converge in 10000 iterations.'
'Increase cutoff range to assure proper generation of particles.')

# Aspect ratio statistics
# Sample from lognormal or gamma distribution:
# it takes mean, std and scale of the underlying normal distribution
finalAR = []
num = pdict['Number']
iter = 0
while num > 0:
ar = lognorm.rvs(sig_AR, loc=loc_AR, scale=scale_AR, size=num)
index_array = np.where((ar >= ar_cutoff_min) & (ar <= ar_cutoff_max))
AR = ar[index_array].tolist()
finalAR.extend(AR)
num = pdict['Number'] - len(finalAR)
iter += 1
if iter > 10000:
raise StopIteration('Iteration for aspect ratios did not converge in 10000 iterations.'
'Increase cutoff range to assure proper generation of particles.')
finalAR = np.array(finalAR)

# Calculate the major, minor axes lengths for particles using:
Expand All @@ -222,10 +242,9 @@ def gen_data_elong(pdict):
pdict['Tilt angle'] = list(tilt_angle)
return pdict

if stats["Grain type"] not in ["Elongated", "Equiaxed"]:
raise ValueError('The value for "Grain type" must be either "Equiaxed" or "Elongated".')

# Attributes for equivalent diameter
# start of particle initialization
# analyze information on grains in "stats" dictionary from outer scope
# 1. Attributes for equivalent diameter
if 'mean' in stats['Equivalent diameter'].keys():
# load legacy names to ensure compatibility with previous versions
sig, loc, scale, kappa = stat_names(legacy=True)
Expand All @@ -235,22 +254,28 @@ def gen_data_elong(pdict):
sig, loc, scale, kappa = stat_names(legacy=False)
sig_ED = stats["Equivalent diameter"][sig]
scale_ED = stats["Equivalent diameter"][scale]
loc_ED = stats["Equivalent diameter"][loc]
if loc in stats["Equivalent diameter"].keys():
loc_ED = stats["Equivalent diameter"][loc]
else:
loc_ED = 0.0

dia_cutoff_min = stats["Equivalent diameter"]["cutoff_min"]
dia_cutoff_max = stats["Equivalent diameter"]["cutoff_max"]
if dia_cutoff_min / dia_cutoff_max > 0.75:
raise ValueError('Min/Max values for cutoffs of equiavalent diameter are too close: ' +
f'Max: {dia_cutoff_max}, Min: {dia_cutoff_min}')
# generate dict for particle data
# generate dict for basic particle data: number of particles and equiv. diameters
pdict = gen_data_basic(dict({'Type': stats["Grain type"], 'Phase': ip}))

# Additional attributes for elongated grains
# 2. Additional attributes for elongated or freely-defined grains
if stats["Grain type"] == "Elongated":
# Extract descriptors for aspect ratio distrib. from dict
sig_AR = stats["Aspect ratio"][sig]
scale_AR = stats["Aspect ratio"][scale]
loc_AR = stats["Aspect ratio"][loc]
if loc in stats["Aspect ratio"].keys():
loc_AR = stats["Aspect ratio"][loc]
else:
loc_AR = 0.0
ar_cutoff_min = stats["Aspect ratio"]["cutoff_min"]
ar_cutoff_max = stats["Aspect ratio"]["cutoff_max"]
if ar_cutoff_min / ar_cutoff_max > 0.75:
Expand All @@ -265,29 +290,40 @@ def gen_data_elong(pdict):
if ori_cutoff_min / ori_cutoff_max > 0.75:
raise ValueError('Min/Max values for cutoffs of orientation of tilt axis are too close: ' +
f'Max: {ori_cutoff_max}, Min: {ori_cutoff_min}')

# Add attributes for elongated particle to dictionary
pdict = gen_data_elong(pdict)
particle_data.append(pdict)
self.nparticles.append(pdict['Number'])
return particle_data
elif stats["Grain type"] == "Free":
try:
from kanapy.triple_surface import gen_data_free
except:
raise ModuleNotFoundError('Free grain definitions are not available '
'in the public version of Kanapy.')
# Add attributes for elongated particle to dictionary
pdict = gen_data_free(pdict, stats)
return pdict
return pdict

# Start RVE generation
if from_voxels:
print('Creating an RVE from voxel input')
else:
print('Creating an RVE based on user defined statistics')
# Extract grain diameter statistics info
# declare attributes of RVE object
self.packing_steps = nsteps # max number of steps for packing simulation
self.size = None # tuple of lengths along Cartesian axes
self.dim = None # tuple of number of voxels along Cartesian axes
self.periodic = None # Boolean for periodicity of RVE
self.units = None # Units of RVE dimensions, either "mm" or "um" (micron)
self.ialloy = None # Number of alloy in ICAMS CP-UMAT
self.nparticles = [] # List of article numbers for each phase
particle_data = [] # list of cits for statistical particle data for each grains
phase_names = [] # list of names of phases
phase_vf = [] # list of volume fractions of phases
ialloy = []
if from_voxels:
self.particle_data = None
self.nparticles = None
else:
self.particle_data = [] # list of cits for statistical particle data for each grains
self.nparticles = [] # List of article numbers for each phase

# extract data from descriptors of individual phases
for ip, stats in enumerate(stats_dicts):
Expand Down Expand Up @@ -351,10 +387,11 @@ def gen_data_elong(pdict):

# Extract grains shape attributes to initialize particles
if not from_voxels:
particle_data = init_particles(particle_data)
else:
particle_data = None
self.nparticles = None
if stats["Grain type"] not in ["Elongated", "Equiaxed", "Free"]:
raise ValueError('The value for "Grain type" must be either "Equiaxed" or "Elongated".')
part_dict = init_particles()
self.particle_data.append(part_dict)
self.nparticles.append(part_dict['Number'])
print(' RVE characteristics:')
print(f' RVE side lengths (X, Y, Z) = {self.size} ({self.units})')
print(f' Number of voxels (X, Y, Z) = {self.dim}')
Expand All @@ -367,7 +404,6 @@ def gen_data_elong(pdict):
print('\n')
self.phase_names = phase_names
self.phase_vf = phase_vf
self.particle_data = particle_data
nall = len(ialloy)
if nall > 0:
if nall != len(phase_vf):
Expand Down
Loading

0 comments on commit 2c97a1b

Please sign in to comment.