import numpy as np
import matplotlib.pyplot as plt
# import cv2
# from skimage.measure import label as skim_label
# from defdap.quat import Quat
import upxo._sup.gops as gops
import upxo._sup.dataTypeHandlers as dth
from upxo._sup.validation_values import _validation as val
# from upxo.xtalphy.orientation import grainoris as go
[docs]
class grain2d():
"""
pxt.gs[2].g[1]['grain']
------------------------------
ATTRIBUTES AND THEIR ACCESS
------------------------------
loc:
Grain pixel loca5tions.
pxt.gs[t].g[gid]['grain'].loc, pxt.gs[t].g[gid]['grain'].coords
npixels:
Number of pixels in the grain
pxt.gs[t].g[gid]['grain'].npixels
position:
Grain relative poistioning in the poly-crystal.
pxt.gs[t].g[gid]['grain'].position
initial 2 values provide x and y coord of the centroid
last string value provides the relative positioning of the grain
coords:
Explanation here
gbloc:
grain boundary pixel locations
bbox_bounds:
Bounds of the bounding box
pxt.gs[t].g[gid]['grain'].bbox_bounds
bbox_ex_bounds:
Bounds of the extended bounding box. Extended by unit pixels on all
sides. If grain is a boundary grain, only possible directions will
be extended by unit pixel.
pxt.gs[t].g[gid]['grain'].bbox_ex_bounds
bbox:
Mask on bounding box.
pxt.gs[t].g[gid]['grain'].bbox
bbox_ex:
Mask on extended bouning box.
pxt.gs[t].g[gid]['grain'].bbox_ex
skprop:
Scikit image property generator
pxt.gs[t].g[gid]['grain'].skprop
_px_area:
Area of a single pixel
pxt.gs[t].g[gid]['grain']._px_area
gid:
This grain ID. Same as the lgi number of this grain.
pxt.gs[t].g[gid]['grain'].gid
gind:
Indices of grain pixel in the parent state matrix
pxt.gs[t].g[gid]['grain'].gind
gbid:
Grain boundary ID
pxt.gs[t].g[gid]['grain'].gbid
gbind:
Indices of grain boundary pixel in the parent state matrix
pxt.gs[t].g[gid]['grain'].gbind
gbvert:
Grain boundary vertices
pxt.gs[t].g[gid]['grain'].gbvert
gbsegs:
grain boundary segments
pxt.gs[t].g[gid]['grain'].gbsegs
gbsegs_geo:
grain boundary segments: dict with below keys:
'info': OPTIONS:
'spg.menp': Single Pixel Grain. Multi-edge not possible
'slg.menp': Straight line Grain. Multi-edge not possible
'me': Multi-edge
pxt.gs[t].g[gid]['grain'].gbsegs_geo
s:
Monte-Car;o State value of the current grain
pxt.gs[t].g[gid]['grain'].s
sn:
pxt.gs[t].g[gid]['grain'].sn
neigh:
GIDs of the neighbouring grains
pxt.gs[t].g[gid]['grain'].neigh
grain_core:
Grain core region
pxt.gs[t].g[gid]['grain'].grain_core
gb_zone:
Grain boundary zone
pxt.gs[t].g[gid]['grain'].gb_zone
subgrains:
sub-grains inside grain. It shall also encompass any
island_grains
pxt.gs[t].g[gid]['grain'].subgrains
paps:
prior-austenite packats
pxt.gs[t].g[gid]['grain'].paps
blocks:
block structuers in paps
pxt.gs[t].g[gid]['grain'].blocks
laths:
Lath structures inside blocks
pxt.gs[t].g[gid]['grain'].laths
xstruc: Crystal Structure: 'fcc', 'bcc', 'hcp'
xmin, xmax, ymin, ymax:
pxt.gs[t].g[gid]['grain'].(xmin, xmax, ymin, ymax)
control_points_mesh:
Points places at strategic locations inside the grain to control
mesh density of a conformal mesh
pxt.gs[t].g[gid]['grain'].control_points_mesh
ea_pixels:
Euler snagles of all pixels
quats_pixels:
Quaternions of all the pixesl int he grain
ref_quat:
Reference quaternion value of the all pixes in the grain
ref_ea:
Referecne Euler angle of all the pixesl in the grain
texcomp:
T3exture component name to which the pixel would belong to.
Defined at each of the pixesl in the grain
glb_pert_min_ea1, glb_pert_max_ea1:
Global minimum and maximum allowed perturbation to euler angle 1
glb_pert_min_ea2, glb_pert_max_ea2:
Global minimum and maximum allowed perturbation to euler angle 2
glb_pert_min_ea3, glb_pert_max_ea3
Global minimum and maximum allowed perturbation to euler angle 3
lcl_pert_min_ea1, lcl_pert_max_ea1:
Local minimum and maximum allowed perturbation to euler angle 1
lcl_pert_min_ea2, lcl_pert_max_ea2:
Local minimum and maximum allowed perturbation to euler angle 2
lcl_pert_min_ea3, lcl_pert_max_ea3:
Local minimum and maximum allowed perturbation to euler angle 3
--------------------------------------------------------------------------
_xgr_min_: Minimum value of the xgr of the parent grain structure
_xgr_max_: Maximum value of the xgr of the parent grain structure
_xgr_incr_: Increment value of the xgr of the parent grain structure
_ygr_min_: Minimum value of the ygr of the parent grain structure
_ygr_max_: Maximum value of the ygr of the parent grain structure
_xgr_incr_: Increment value of the ygr of the parent grain structure
"""
__slots__ = ('loc', 'npixels', 'position', 'coords', 'gbloc', 'brec', 'm',
'bbox_bounds', 'bbox_ex_bounds', 'bbox', 'bbox_ex', 'skprop',
'_px_area', 'gid', 'gind', 'gbid', 'gbind', 'gbvert',
'gbsegs', 'gbsegs_geo', 's', 'sn', 'neigh',
'grain_core', 'gb_zone', 'xmin', 'xmax', 'ymin', 'ymax',
'loctree', 'coordtree', 'ea', 'xgid', 'bbox_bz', 'bbox_core',
'_lfi_gbseg_empties_'
)
__get_item_behaviour = 'locs_away_from_centroid'
rtol = 1e-6
def __init__(self):
"""Initialise the instance."""
# Set position/location related slots
self.loc, self.position = None, None
self.coords, self.gbloc = None, None
# set bounds related
self.xmin, self.xmax, self.ymin, self.ymax = None, None, None, None
self.brec, self.bbox_bounds, self.bbox_ex_bounds = None, None, None
self.loctree, self.coordtree = None, None
# set masks
self.bbox, self.bbox_ex = None, None
# Set local neighbourhood related slots
self.neigh = None
# set properties
self.npixels, self._px_area, self.skprop = None, None, None
# set state related slots
self.s, self.sn = None, None
# set grain indices related slots
self.gid, self.gind = None, None
# Set grain boundary indices related slots
self.gbid, self.gbind = None, None
# Set grain boundaryt points
# self.gbvert = None
# Set grain bounadryu segfments
# self.gbsegs, self.gbsegs_geo = None, None
# FEATURES
self.grain_core, self.gb_zone = None, None
# ------------------------------------------
# ORIENTATION RELATED
self.ea = None
# -----------------------------------------------------------------------
'''
lfi_gbseg_empties: Default values of non-grain boundary segment coordinates
inside the bounding box. You may change this value np.nan if necessary.
refer to demos/neighOps/neighOps-1.ipynb for usage example. use the
appropriate setter and getter methods to operate on this attribute.
'''
self._lfi_gbseg_empties_ = 0
# -----------------------------------------------------------------------
#def __repr__(self):
# _repr_ = f'UPXO [{self.position[2]}] grain2d. GID:{self.gid}'
# _repr_ += f'-S:{self.s}'
# _repr_ += f'-Sn:{self.sn}'
# _repr_ += f'-Centroid:[{self.position[0]:.4f},{self.position[1]:.4f}]'
# return _repr_
def __len__(self):
"""Return the number of items in this instance."""
return len(self.loc)
@property
def lfi_gbseg_empties(self):
"""Lfi gbseg empties."""
return self._lfi_gbseg_empties_
@lfi_gbseg_empties.setter
def lfi_gbseg_empties(self, value):
"""Lfi gbseg empties."""
self._lfi_gbseg_empties_ = value
@lfi_gbseg_empties.deleter
def lfi_gbseg_empties(self):
"""Lfi gbseg empties."""
del self._lfi_gbseg_empties_
@val.DEC_validate_samples
def __eq__(self, samples=None, types=None):
'''
Allowed input datatypes:
A number
A UPXO grain2D object
pxt.gs[tslice].xomap.map.grainList[0].__class__.__name__
'''
# VAL: See if sampls are numbers
if list(types)[0] in dth.dt.NUMBERS:
cmp = [self.npixels == _ for _ in samples]
elif samples[0].__class__.__name__ == 'grain2d': # Testing 1 is enough
'''UPXO grain object'''
cmp = [self.npixels == _.npixels for _ in samples]
elif samples[0].__class__.__name__ == 'Grain': # Testing 1 is enough
'''DefDap grain object'''
cmp = [self.npixels == len(_.coordList) for _ in samples]
return cmp
def __ne__(self, samples=None):
"""Check inequality with another object."""
return [not _ for _ in self.__eq__(samples=samples)]
@val.DEC_validate_samples
def __lt__(self, samples=None, types=None):
'''
Allowed input datatypes: number, A UPXO grain2D object
pxt.gs[tslice].xomap.map.grainList[0].__class__.__name__
'''
# VAL: See if samples are numbers
if list(types)[0] in dth.dt.NUMBERS:
cmp = [self.npixels < _ for _ in samples]
elif samples[0].__class__.__name__ == 'grain2d': # Testing 1 is enough
'''UPXO grain object'''
cmp = [self.npixels < _.npixels for _ in samples]
elif samples[0].__class__.__name__ == 'Grain': # Testing 1 is enough
'''DefDap grain object'''
cmp = [self.npixels < len(_.coordList) for _ in samples]
return cmp
@val.DEC_validate_samples
def __gt__(self, samples=None, types=None):
'''
Allowed input datatypes: number, A UPXO grain2D object
pxt.gs[tslice].xomap.map.grainList[0].__class__.__name__
'''
# VAL: See if samples are numbers
if list(types)[0] in dth.dt.NUMBERS:
cmp = [self.npixels > _ for _ in samples]
elif samples[0].__class__.__name__ == 'grain2d': # Testing 1 is enough
'''UPXO grain object'''
cmp = [self.npixels > _.npixels for _ in samples]
elif samples[0].__class__.__name__ == 'Grain': # Testing 1 is enough
'''DefDap grain object'''
cmp = [self.npixels > len(_.coordList) for _ in samples]
return cmp
def __le__(self, samples=None, types=None):
"""Less-than-or-equal comparison."""
return [lt or eq for lt, eq in zip(self.__lt__(samples=samples),
self.__eq__(samples=samples))]
def __ge__(self, samples=None, types=None):
"""Greater-than-or-equal comparison."""
return [gt or eq for gt, eq in zip(self.__gt__(samples=samples),
self.__eq__(samples=samples))]
def __getitem__(self, keys):
"""
Provides the location subset of all
keys --> start_percentage : end_percentage
"""
raise NotImplementedError("__getitem__ is not yet implemented.")
def __mul__(self, k):
"""Multiply this instance by a scalar."""
# INCLUDE VALIDATIONS
self._px_area *= k
def __att__(self):
"""Return a string listing of all attributes."""
return gops.att(self)
def __iter__(self):
"""Return an iterator over this instance."""
return iter(self.loc)
def __contains__(self, points):
'''
Currently accepted points data structure
points = [[1, 2, 3, 4, 5], [2, 3, 4, 5, 6]]
'''
if type(points[0]) != list:
raise TypeError(f"Points should be a list of lists. Currently provided {type(points)}")
if self.loctree is None:
self.make_loctree()
contained = [None]*len(points[0])
for i, xy in enumerate(zip(points[0], points[1])):
dist, idx = self.loctree.query(xy, k=1)
contained[i] = dist <= self.rtol
return contained
@property
def lfi_gbsegs(self):
'''Convert the sparse storage into a full grain boundary segment lfi array.'''
# Convert sparse storage into full gbseg lfi array
_gbseg_ = np.zeros(np.prod(self.gbsegs['shape']), dtype=self.gbsegs['dtype'])
_gbseg_[self.gbsegs['NZI']] = self.gbsegs['NZV']
_gbseg_ = np.reshape(_gbseg_, self.gbsegs['shape'])
# Handle empty values
condition1 = np.isnan(self.lfi_gbseg_empties)
condition2 = isinstance(self.lfi_gbseg_empties, np.floating)
condition3 = isinstance(self.lfi_gbseg_empties, float)
if condition1 or condition2 or condition3:
_gbseg_ = np.asarray(_gbseg_, dtype=np.float32)
_gbseg_[_gbseg_ == 0] = self.lfi_gbseg_empties
return _gbseg_
[docs]
def set_quat(self):
"""Set or update quat."""
self.quats_pixels
self.ref_quat
[docs]
def find_misori(self, angles):
"""Find misori."""
raise NotImplementedError("find_misori is not yet implemented.")
[docs]
def set_glb_ea_pert(self, pert_ea1, pert_ea2, pert_ea3):
"""Set or update glb ea pert."""
self.glb_pert_min_ea1, self.glb_pert_max_ea1 = pert_ea1[0], pert_ea1[1]
self.glb_pert_min_ea2, self.glb_pert_max_ea2 = pert_ea2[0], pert_ea2[1]
self.glb_pert_min_ea3, self.glb_pert_max_ea3 = pert_ea3[0], pert_ea3[1]
[docs]
def make_loctree(self):
"""Build and return loctree."""
from scipy.spatial import cKDTree as ckdt
self.loctree = ckdt(self.loc, copy_data=False, balanced_tree=True)
@property
def mean_ea(self):
"""Mean ea."""
# return the mean orientatipon: euler angle in degrees
raise NotImplementedError("mean_ea is not yet implemented.")
[docs]
def make_coordtree(self):
"""Build and return coordtree."""
from scipy.spatial import cKDTree as ckdt
self.loctree = ckdt(self.coords, copy_data=False, balanced_tree=True)
[docs]
def set_skprop(self):
"""Set or update skprop."""
from skimage.measure import regionprops as skim_regionprops
self.make_prop(skim_regionprops, skprop=True)
[docs]
def make_prop(self, generator, skprop=True):
"""Build and return prop."""
if skprop:
#print('=======================')
#print(generator)
#print('=======================')
self.skprop = generator(self.bbox_ex, cache=False)[0]
@property
def centroid(self):
"""Centroid."""
coords = self.coords.T
return (coords[0].mean(), coords[1].mean())
[docs]
def plot(self, hold_on=False):
"""Plot."""
if not hold_on:
plt.figure()
plt.imshow(self.bbox_ex)
plt.title(f"Grain plot \n Grain: {self.gid}. Area: {round(self.skprop.area*100)/100} mu m^2")
if not hold_on:
plt.xlabel(r"X-axis, $\mu m$", fontsize=12)
plt.ylabel(r"Y-axis, $\mu m$", fontsize=12)
plt.show()
[docs]
def plotgb(self, hold_on=False):
"""Plotgb."""
z = np.zeros_like(self.bbox_ex)
rmin = self.bbox_ex_bounds[0]
cmin = self.bbox_ex_bounds[2]
for rc in self.gbloc:
z[rc[0]-rmin, rc[1]-cmin] = 1
# ------------------------------------
if not hold_on:
plt.figure()
plt.imshow(z)
if not hold_on:
plt.title(f"Grain boundary plot \n Grain: {self.gid}. Area: {round(self.skprop.area*100)/100} mu m^2. Perimeter: {round(self.skprop.perimeter*10000)/10000} mu m")
plt.xlabel(r"X-axis, $\mu m$", fontsize=12)
plt.ylabel(r"Y-axis, $\mu m$", fontsize=12)
plt.show()
[docs]
def plotgbseg(self, hold_on=False):
"""Plotgbseg."""
if not hold_on:
plt.figure()
plt.imshow(self.gbsegs)
if not hold_on:
plt.title(f"Grain boundary segment plot \n Grain: {self.gid}. Area: {round(self.skprop.area*100)/100} mu m^2. Perimeter: {round(self.skprop.perimeter*10000)/10000} mu m")
plt.xlabel(r"X-axis, $\mu m$", fontsize=12)
plt.ylabel(r"Y-axis, $\mu m$", fontsize=12)
plt.show()