# Copyright 2021 Mechanics of Microstructures Group
# at The University of Manchester
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import numpy as np
import ast
from typing import List
from scipy.stats import linregress
from scipy.stats._stats_mstats_common import LinregressResult
import pandas as pd
from defdap.plotting import Plot, GrainPlot
from defdap import hrdic
[docs]class GrainInspector:
"""
Class containing the interactive grain inspector tool for slip trace analysis
and relative displacement ratio analysis.
"""
def __init__(self,
currMap: 'hrdic.Map',
vmax: float,
corrAngle: float):
# Initialise some values
self.grainID = 0
self.currMap = currMap
self.currEBSDMap = self.currMap.ebsdMap
self.currDICGrain = self.currMap[self.grainID]
self.currEBSDGrain = self.currDICGrain.ebsdGrain
self.vmax = vmax
self.corrAngle = corrAngle
self.filename = str(self.currMap.retrieveName()) + '_RDR.txt'
# Draw the figure
self.draw()
[docs] def draw(self):
""" Draw the main window, buttons, text boxes and axes.
"""
# Plot window
self.plot = Plot(ax=None, makeInteractive=True, figsize=(14,8), title='Grain Inspector')
######## Buttons
self.plot.addButton(
'Save\nLine', self.saveLine, (0.73, 0.48, 0.05, 0.04))
self.plot.addButton(
'Previous\nGrain', lambda e, p: self.gotoGrain(self.grainID-1, p), (0.73, 0.94, 0.05, 0.04))
self.plot.addButton(
'Next\nGrain', lambda e, p: self.gotoGrain(self.grainID+1, p), (0.79, 0.94, 0.05, 0.04))
self.plot.addButton(
'Run\nAll STA', self.batchRunSTA, (0.85, 0.07, 0.11, 0.04))
self.plot.addButton(
'Clear\nAll Lines', self.clearAllLines, (0.89, 0.48, 0.05, 0.04))
self.plot.addButton(
'Load\nFile', self.loadFile, (0.85, 0.02, 0.05, 0.04))
self.plot.addButton(
'Save\nFile', self.saveFile, (0.91, 0.02, 0.05, 0.04))
# Text boxes
self.plot.addTextBox(label='', loc=(0.7, 0.02, 0.13, 0.04),
changeHandler=self.updateFilename, initial = self.filename)
self.plot.addTextBox(label='Go to \ngrain ID:', loc=(0.9, 0.94, 0.05, 0.04),
submitHandler=self.gotoGrain)
self.plot.addTextBox(label='Remove\nID:', loc=(0.83, 0.48, 0.05, 0.04),
submitHandler=self.removeLine)
self.RDRGroupBox = self.plot.addTextBox(label='Run RDR only\non group:', loc=(0.78, 0.07, 0.05, 0.04),
submitHandler=self.runRDRGroup)
# Axes
self.maxShearAx = self.plot.addAxes((0.05, 0.4, 0.65, 0.55))
self.slipTraceAx = self.plot.addAxes((0.2, 0.05, 0.6, 0.3))
self.unitCellAx = self.plot.addAxes((0.05, 0.055, 0.15, 0.3), proj='3d')
self.grainInfoAx = self.plot.addAxes((0.73, 0.86, 0.25, 0.06))
self.lineInfoAx = self.plot.addAxes((0.73, 0.55, 0.25, 0.3))
self.groupsInfoAx = self.plot.addAxes((0.73, 0.15, 0.25, 0.3))
self.grainPlot = self.currMap[self.grainID].plotMaxShear(fig=self.plot.fig, ax=self.maxShearAx,
vmax=self.vmax, plotScaleBar=True, plotColourBar=True)
self.plot.ax.axis('off')
# Draw the stuff that will need to be redrawn often in a seperate function
self.redraw()
[docs] def gotoGrain(self,
event: int,
plot):
""" Go to a specified grain ID.
Parameters
----------
event
Grain ID to go to.
"""
## Go to grain ID specified in event
self.grainID=int(event)
self.grainPlot.arrow=None
self.currDICGrain = self.currMap[self.grainID]
self.currEBSDGrain = self.currDICGrain.ebsdGrain
self.redraw()
[docs] def saveLine(self,
event: np.ndarray,
plot):
""" Save the start point, end point and angle of drawn line into the grain.
Parameters
----------
event
Start x, start y, end x, end y point of line passed from drawn line.
"""
# Get angle of lines
lineAngle = 90-np.rad2deg(np.arctan2(self.grainPlot.p2[1]-self.grainPlot.p1[1],
self.grainPlot.p2[0]-self.grainPlot.p1[0]))
if lineAngle > 180: lineAngle -= 180
elif lineAngle < 0: lineAngle += 180
lineAngle -= self.corrAngle
# Two decimal places
points = [float("{:.2f}".format(point)) for point in self.grainPlot.p1+self.grainPlot.p2]
lineAngle = float("{:.2f}".format(lineAngle))
# Save drawn line to the DIC grain
self.currDICGrain.pointsList.append([points, lineAngle, -1])
# Group lines and redraw
self.groupLines()
self.redrawLine()
[docs] def groupLines(self,
grain: 'defdap.hrdic.Grain'=None):
"""
Group the lines drawn in the current grain item using a mean shift algorithm,
save the average angle and then detect the active slip planes.
groupsList is a list of line groups: [id, angle, [slip plane id], [angular deviation]
Parameters
----------
grain
Grain for which to group the slip lines.
"""
if grain == None:
grain = self.currDICGrain
if grain.pointsList == []:
grain.groupsList = []
else:
for i, line in enumerate(grain.pointsList):
angle = line[1]
if i == 0:
line[2]=0 # Make group 0 for first detected angle
grain.groupsList = [[0, angle, 0, 0, 0]]
nextGroup=1
else: # If there is more that one angle
if np.any(np.abs(np.array([x[1] for x in grain.groupsList])-angle)<10):
# If within +- 5 degrees of exisitng group, set that as the group
group = np.argmin(np.abs(np.array([x[1] for x in grain.groupsList])-angle))
grain.pointsList[i][2]=group
newAv = float('{0:.2f}'.format(np.average([x[1] for x in grain.pointsList if x[2]==group])))
grain.groupsList[group][1] = newAv
else:
# Make new group and set
grain.groupsList.append([nextGroup, angle, 0, 0, 0])
line[2]=nextGroup
nextGroup += 1
# Detect active slip systems in each group
for group in grain.groupsList:
activePlanes = []
deviation = []
experimentalAngle = group[1]
for idx, theoreticalAngle in enumerate(np.rad2deg(grain.ebsdGrain.slipTraceAngles)):
if theoreticalAngle-5 < experimentalAngle < theoreticalAngle+5:
activePlanes.append(idx)
deviation.append(float('{0:.2f}'.format(experimentalAngle-theoreticalAngle)))
group[2] = activePlanes
group[3] = deviation
[docs] def clearAllLines(self,
event,
plot):
""" Clear all lines in a given grain.
"""
self.currDICGrain.pointsList = []
self.currDICGrain.groupsList = []
self.redraw()
[docs] def removeLine(self,
event: int,
plot):
""" Remove single line [runs after submitting a text box].
Parameters
----------
event
Line ID to remove.
"""
## Remove single line
del self.currDICGrain.pointsList[int(event)]
self.groupLines()
self.redraw()
[docs] def redraw(self):
"""Draw items which need to be redrawn when changing grain ID.
"""
# Plot max shear for grain
self.maxShearAx.clear()
self.grainPlot = self.currMap[self.grainID].plotMaxShear(
fig=self.plot.fig, ax=self.maxShearAx, vmax=self.vmax, plotColourBar=False, plotScaleBar=True)
# Draw unit cell
self.unitCellAx.clear()
self.currEBSDGrain.plotUnitCell(fig=self.plot.fig, ax=self.unitCellAx)
# Write grain info text
self.grainInfoAx.clear()
self.grainInfoAx.axis('off')
grainInfoText = 'Grain ID: {0} / {1}\n'.format(self.grainID, len(self.currMap.grainList)-1)
grainInfoText += 'Min: {0:.1f} % Mean:{1:.1f} % Max: {2:.1f} %'.format(
np.min(self.currDICGrain.maxShearList)*100,
np.mean(self.currDICGrain.maxShearList)*100,
np.max(self.currDICGrain.maxShearList)*100)
self.plot.addText(self.grainInfoAx, 0, 1, grainInfoText, va='top', ha='left', fontsize=10)
# Detect lines
self.plot.addEventHandler('button_press_event',lambda e, p: self.grainPlot.lineSlice(e, p))
self.plot.addEventHandler('button_release_event', lambda e, p: self.grainPlot.lineSlice(e, p))
self.redrawLine()
[docs] def redrawLine(self):
"""
Draw items which need to be redrawn when adding a line.
"""
# Write lines text and draw lines
linesTxt = 'List of lines\n\nLineID x0 y0 x1 y1 Angle Group\n'
if self.currDICGrain.pointsList != []:
for idx, points in enumerate(self.currDICGrain.pointsList):
linesTxt += '{0} {1:.1f} {2:.1f} {3:.1f} {4:.1f} {5:.1f} {6}\n'.format(idx,
points[0][0],points[0][1],points[0][2],points[0][3],points[1],points[2])
self.grainPlot.addArrow(startEnd=points[0], clearPrev=False, persistent=True, label=idx)
self.lineInfoAx.clear()
self.lineInfoAx.axis('off')
self.plot.addText(self.lineInfoAx, 0, 1, linesTxt, va='top', fontsize=10)
# Write groups info text
groupsTxt = 'List of groups\n\nGroupID Angle System Dev RDR\n'
if self.currDICGrain.groupsList != []:
for idx, group in enumerate(self.currDICGrain.groupsList):
groupsTxt += '{0} {1:.1f} {2} {3} {4:.2f}\n'.format(
idx, group[1], group[2], np.round(group[3], 3), group[4])
self.groupsInfoAx.clear()
self.groupsInfoAx.axis('off')
self.plot.addText(self.groupsInfoAx, 0, 1, groupsTxt, va='top', fontsize=10)
# Draw slip traces
self.slipTraceAx.clear()
self.slipTraceAx.set_aspect('equal', 'box')
slipPlot = GrainPlot(fig=self.plot.fig, callingGrain=self.currMap[self.grainID], ax=self.slipTraceAx)
traces = slipPlot.addSlipTraces(topOnly=True)
self.slipTraceAx.axis('off')
# Draw slip bands
bands = [elem[1] for elem in self.currDICGrain.groupsList]
if self.currDICGrain.groupsList != None:
slipPlot.addSlipBands(topOnly=True, angles=list(np.deg2rad(bands)))
[docs] def runRDRGroup(self,
event: int,
plot):
""" Run RDR on a specified group, upon submitting a text box.
Parameters
----------
event
Group ID specified from text box.
"""
## Run RDR for group of lines
if event != '':
self.calcRDR(grain = self.currDICGrain, group=int(event))
self.RDRGroupBox.set_val('')
[docs] def batchRunSTA(self,
event,
plot):
""" Run slip trace analysis on all grains which hve slip trace lines drawn.
"""
# Print header
print("Grain\tEul1\tEul2\tEul3\tMaxSF\tGroup\tAngle\tSystem\tDev\RDR")
# Print information for each grain
for idx, grain in enumerate(self.currMap):
if grain.pointsList != []:
for group in grain.groupsList:
maxSF = np.max([item for sublist in grain.ebsdGrain.averageSchmidFactors for item in sublist])
eulers = self.currEBSDGrain.refOri.eulerAngles()*180/np.pi
text = '{0}\t{1:.1f}\t{2:.1f}\t{3:.1f}\t{4:.3f}\t'.format(
idx, eulers[0], eulers[1], eulers[2], maxSF)
text += '{0}\t{1:.1f}\t{2}\t{3}\t{4:.2f}'.format(
group[0], group[1], group[2], np.round(group[3],3), group[4])
print(text)
[docs] def calcRDR(self,
grain: int,
group: int,
showPlot: bool = True,
length: float = 2.5):
""" Calculates the relative displacement ratio for a given grain and group.
Parameters
----------
grain
DIC grain ID to run RDR on.
group
group ID to run RDR on.
showPlot
if True, show plot window.
length
length of perpendicular lines used for RDR.
"""
ulist=[]; vlist=[]; allxlist = []; allylist = [];
# Get all lines belonging to group
points = []
for point in grain.pointsList:
if point[2] == group:
points.append(point[0])
for point in points:
x0=point[0]; y0=point[1]; x1=point[2]; y1=point[3];
grad = (y1-y0)/(x1-x0)
invgrad = -1/grad
profile_length = np.sqrt((y1-y0)**2+(x1-x0)**2)
num = np.round(profile_length*2)
### Calculate positions for each point along slip trace line (x,y)
x, y = np.round(np.linspace(x0, x1, int(num))), np.round(np.linspace(y0, y1, int(num)))
df = pd.DataFrame({'x':x, 'y':y}).drop_duplicates()
x,y = df['x'].values.tolist(),df['y'].values.tolist()
## Calculate deviation from (0,0) for points along line with angle perpendicular to slip line (xnew,ynew)
x0new = np.sqrt(length/(invgrad**2+1))*np.sign(grad)
y0new = -np.sqrt(length/(1/invgrad**2+1))
x1new = -np.sqrt(length/(invgrad**2+1))*np.sign(grad)
y1new = np.sqrt(length/(1/invgrad**2+1))
profile_length=np.sqrt((y1new-y0new)**2+(x1new-x0new)**2)
num = np.round(profile_length)
xnew, ynew = np.linspace(x0new, x1new, int(num)), np.linspace(y0new, y1new, int(num))
xnew, ynew = np.around(xnew).astype(int), np.around(ynew).astype(int)
df = pd.DataFrame({'x':xnew, 'y':ynew}).drop_duplicates()
xnew,ynew = df['x'].values.tolist(), df['y'].values.tolist()
for x,y in zip(x,y):
xperp = []; yperp = [];
for xdiff, ydiff in zip(xnew, ynew):
xperp.append(int(x+xdiff))
yperp.append(int(y+ydiff))
allxlist.append(xperp)
allylist.append(yperp)
xmap = self.currDICGrain.extremeCoords[0] + xperp
ymap = self.currDICGrain.extremeCoords[1] + yperp
### For all points, append u and v to list
u = []; v = [];
for xmap, ymap in zip(xmap,ymap):
u.append((self.currMap.crop(self.currMap.x_map))[ymap, xmap])
v.append((self.currMap.crop(self.currMap.y_map))[ymap, xmap])
### Take away mean
u = u-np.mean(u); v = v-np.mean(v)
### Append to main lists (ulist,vlist)
ulist.extend(u)
vlist.extend(v)
### Linear regression of ucentered against vcentered
linRegResults = linregress(x=vlist,y=ulist)
# Save measured RDR
grain.groupsList[group][4] = linRegResults.slope
if showPlot: self.plotRDR(grain, group, ulist, vlist, allxlist, allylist, linRegResults)
[docs] def plotRDR(self,
grain: int,
group: int,
ulist: List[float],
vlist: List[float],
allxlist: List[float],
allylist: List[float],
linRegResults: 'LinregressResult'):
"""
Plot RDR figure, including location of perpendicular lines and scatter plot of ucentered vs vcentered.
Parameters
----------
grain
DIC grain to plot.
group
Group ID to plot.
ulist
List of ucentered values.
vlist
List of vcentered values.
allxlist
List of all x values.
allylist
List of all y values.
linRegResults
Results from linear regression of ucentered vs vcentered
{slope, intercept, rvalue, pvalue, stderr}.
"""
# Draw window and axes
self.rdrPlot = Plot(ax=None, makeInteractive=True, title='RDR Calculation', figsize=(21, 7))
self.rdrPlot.ax.axis('off')
self.rdrPlot.grainAx = self.rdrPlot.addAxes((0.05, 0.07, 0.20, 0.85))
self.rdrPlot.textAx = self.rdrPlot.addAxes((0.27, 0.07, 0.20, 0.85))
self.rdrPlot.textAx.axis('off')
self.rdrPlot.numLineAx = self.rdrPlot.addAxes((0.48, 0.07, 0.2, 0.85))
self.rdrPlot.numLineAx.axis('off')
self.rdrPlot.plotAx = self.rdrPlot.addAxes((0.75, 0.07, 0.2, 0.85))
## Draw grain plot
self.rdrPlot.grainPlot = self.currDICGrain.plotMaxShear(fig=self.rdrPlot.fig, ax=self.rdrPlot.grainAx,
plotColourBar=False, plotScaleBar = True)
self.rdrPlot.grainPlot.addColourBar(label='Effective Shear Strain', fraction=0.046, pad=0.04)
## Draw all points
self.rdrPlot.grainAx.plot(allxlist, allylist, 'rx',lw=0.5)
for xlist, ylist in zip(allxlist, allylist):
self.rdrPlot.grainAx.plot(xlist, ylist, '-',lw=1)
## Generate scatter plot
slope = linRegResults.slope
r_value = linRegResults.rvalue
intercept = linRegResults.intercept
std_err = linRegResults.stderr
self.rdrPlot.plotAx.scatter(x=vlist,y=ulist,marker='x', lw=1)
self.rdrPlot.plotAx.plot(
[np.min(vlist), np.max(vlist)],[slope*np.min(vlist)+intercept,slope*np.max(vlist)+intercept], '-')
self.rdrPlot.plotAx.set_xlabel('v-centered')
self.rdrPlot.plotAx.set_ylabel('u-centered')
self.rdrPlot.addText(self.rdrPlot.plotAx, 0.95, 0.01, 'Slope = {0:.3f} ± {1:.3f}\nR-squared = {2:.3f}\nn={3}'
.format(slope,std_err,r_value**2,len(ulist)), va='bottom', ha='right',
transform=self.rdrPlot.plotAx.transAxes, fontsize=10);
## Write grain info
ebsdGrain = grain.ebsdGrain
ebsdGrain.calcSlipTraces()
if ebsdGrain.averageSchmidFactors is None:
raise Exception("Run 'calcAverageGrainSchmidFactors' first")
eulers = np.rad2deg(ebsdGrain.refOri.eulerAngles())
text = 'Average angle: {0:.2f}\n'.format(grain.groupsList[group][1])
text += 'Eulers: {0:.1f} {1:.1f} {2:.1f}\n\n'.format(eulers[0], eulers[1], eulers[2])
self.rdrPlot.addText(self.rdrPlot.textAx, 0.15, 1, text, fontsize=10, va='top')
## Write slip system info
RDRs = []; offset = 0;
for idx, (ssGroup, sfGroup, slipTraceAngle) in enumerate(
zip(ebsdGrain.phase.slipSystems, ebsdGrain.averageSchmidFactors, np.rad2deg(ebsdGrain.slipTraceAngles))):
text = "{0:s} {1:.1f}\n".format(ssGroup[0].slipPlaneLabel, slipTraceAngle)
tempRDRs = [];
for ss, sf in zip(ssGroup, sfGroup):
slipDirSample = ebsdGrain.refOri.conjugate.transformVector(ss.slipDir)
text = text + " {0:s} SF: {1:.3f} RDR: {2:.3f}\n".format\
(ss.slipDirLabel, sf,-slipDirSample[0]/slipDirSample[1])
RDR = -slipDirSample[0]/slipDirSample[1]
tempRDRs.append(RDR)
RDRs.append(tempRDRs)
if idx in grain.groupsList[group][2]:
self.rdrPlot.addText(self.rdrPlot.textAx, 0.15, 0.9-offset, text, weight='bold', fontsize=10, va='top')
else:
self.rdrPlot.addText(self.rdrPlot.textAx, 0.15, 0.9-offset, text, fontsize=10, va='top')
offset += 0.0275 * text.count('\n')
# Plot RDR values on number line
uniqueRDRs = set()
for x in [item for sublist in RDRs for item in sublist]: uniqueRDRs.add(x)
self.rdrPlot.numLineAx.axvline(x=0, ymin=-20, ymax=20, c='k')
self.rdrPlot.numLineAx.plot(np.zeros(len(uniqueRDRs)), list(uniqueRDRs), 'bo', label='Theroretical RDR values')
self.rdrPlot.numLineAx.plot([0], slope, 'ro', label='Measured RDR value')
self.rdrPlot.addText(self.rdrPlot.numLineAx, -0.009, slope-0.01, '{0:.3f}'.format(float(slope)))
self.rdrPlot.numLineAx.legend(bbox_to_anchor=(1.15, 1.05))
# Label RDRs by slip system on number line
for RDR in list(uniqueRDRs):
self.rdrPlot.addText(self.rdrPlot.numLineAx, -0.009, RDR-0.01, '{0:.3f}'.format(float(RDR)))
txt = ''
for idx, ssGroup in enumerate(RDRs):
for idx2, rdr in enumerate(ssGroup):
if rdr == RDR:
txt += str('{0} {1} '.format(ebsdGrain.phase.slipSystems[idx][idx2].slipPlaneLabel,
ebsdGrain.phase.slipSystems[idx][idx2].slipDirLabel))
self.rdrPlot.addText(self.rdrPlot.numLineAx,0.002, RDR-0.01, txt)
self.rdrPlot.numLineAx.set_ylim(slope-1, slope+1)
self.rdrPlot.numLineAx.set_xlim(-0.01, 0.05)
[docs] def updateFilename(self,
event: str,
plot):
""" Update class variable filename, based on text input from textbox handler.
event:
Text in textbox.
"""
self.filename = event
[docs] def saveFile(self,
event,
plot):
""" Save a file which contains definitions of slip lines drawn in grains
[(x0, y0, x1, y1), angle, groupID]
and groups of lines, defined by an average angle and identified sip plane
[groupID, angle, [slip plane id(s)], [angular deviation(s)]]
"""
with open(self.currMap.path + str(self.filename), 'w') as file:
file.write('# This is a file generated by defdap which contains definitions of slip lines drawn in grains by grainInspector\n')
file.write('# [(x0, y0, x1, y1), angle, groupID]\n')
file.write('# and groups of lines, defined by an average angle and identified sip plane\n')
file.write('# [groupID, angle, [slip plane id], [angular deviation]\n\n')
for i, grain in enumerate(self.currMap):
if grain.pointsList != []:
file.write('Grain {0}\n'.format(i))
file.write('{0} Lines\n'.format(len(grain.pointsList)))
for point in grain.pointsList:
file.write(str(point)+'\n')
file.write('{0} Groups\n'.format(len(grain.groupsList)))
for group in grain.groupsList:
file.write(str(group)+'\n')
file.write('\n')
[docs] def loadFile(self,
event,
plot):
""" Load a file which contains definitions of slip lines drawn in grains
[(x0, y0, x1, y1), angle, groupID]
and groups of lines, defined by an average angle and identified sip plane
[groupID, angle, [slip plane id(s)], [angular deviation(s)]]
"""
with open(self.currMap.path + str(self.filename), 'r') as file:
lines = file.readlines()
# Parse file and make list of
# [start index, grain ID, number of lines, number of groups]
indexlist=[]
for i, line in enumerate(lines):
if line[0] != '#' and len(line) >1:
if ('Grain') in line:
grainID = int(line.split(' ')[-1])
startIndex = i
if ('Lines') in line:
numLines = int(line.split(' ')[0])
if ('Groups') in line:
numGroups = int(line.split(' ')[0])
indexlist.append([startIndex, grainID, numLines, numGroups])
# Write data from file into grain
for startIndex, grainID, numLines, numGroups in indexlist:
startIndexLines = startIndex+2
grainPoints = lines[startIndexLines:startIndexLines+numLines]
for point in grainPoints:
self.currMap[grainID].pointsList.append(ast.literal_eval(point.split('\\')[0]))
startIndexGroups = startIndex+3+numLines
grainGroups = lines[startIndexGroups:startIndexGroups+numGroups]
for group in grainGroups:
self.currMap[grainID].groupsList.append(ast.literal_eval(group.split('\\')[0]))
self.redraw()