From 16ef2b96ae31b85f9556ab977d20f49837385035 Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Sat, 20 Feb 2021 18:31:41 +0000 Subject: [PATCH 01/33] Prep for v0.93.0 --- CHANGELOG.md | 8 ++++++++ defdap/_version.py | 2 +- setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3de07..68f85f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Current +### Added + +### Changed + +### Fixed + +## 0.93.0 (20-02-2020) + ### Added - Add EBSD file writing to CTF format. - Add cretation of EBSD maps from runtime data. diff --git a/defdap/_version.py b/defdap/_version.py index e1de385..08b7b92 100644 --- a/defdap/_version.py +++ b/defdap/_version.py @@ -1 +1 @@ -__version__ = '0.92.3.dev' +__version__ = '0.93.0' diff --git a/setup.py b/setup.py index f238bfc..35e5ed0 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ def get_version(): ], packages=find_packages(exclude=['tests']), package_data={'defdap': ['slip_systems/*.txt']}, - python_requires='>=3.5', + python_requires='>=3.6', install_requires=[ 'scipy', 'numpy', From c04bc46e5e4c6f4ca3c1bc784f60d80a5509e7da Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Sun, 21 Feb 2021 11:03:55 +0000 Subject: [PATCH 02/33] Update README.md --- README.md | 59 ++++++++++++++++--------------------------------------- 1 file changed, 17 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 6f628ea..8a9c8ae 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,38 @@ -

- -

- -

A python library for correlating EBSD and HRDIC data.

- -

- PyPI - PyPI - Python Version - License - - Documentation Status - - - Binder - - - DOI - -

- -

- How to install • - How To Use • - Documentation • - Credits • - Contributing • - License -

- +![DefDAP](https://defdap.readthedocs.io/en/latest/_images/logo.png) +#### A python library for correlating EBSD and HRDIC data. + +[![PyPI version](https://img.shields.io/pypi/v/defdap "PyPI version")](https://pypi.org/project/defdap) +[![Supported python versions](https://img.shields.io/pypi/pyversions/defdap "Supported python versions")](https://pypi.org/project/defdap) +[![License](https://img.shields.io/github/license/mechmicroman/defdap "License")](https://github.com/MechMicroMan/DefDAP/blob/master/LICENSE) +[![Documentation status](https://readthedocs.org/projects/defdap/badge/?version=latest "Documentation status")](https://defdap.readthedocs.io/en/latest/?badge=latest) +[![Binder](https://mybinder.org/badge_logo.svg "Try on binder")](https://mybinder.org/v2/gh/MechMicroMan/DefDAP/master?filepath=example_notebook.ipynb) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.3688096.svg "DOI")](https://doi.org/10.5281/zenodo.3688096) + +[How to install](#how-to-install "How to install") • +[How to use](#how-to-use "How to use") • +[Documentation](#documentation "Documentation") • +[Credits](#credits "Credits") • +[Contributing](#contributing "Contributing") • +[License](#license "License") ## How to install - - DefDAP can be installed from PyPI using the command `pip install defdap` - - If you use conda as your package manager (https://www.anaconda.com/), the prerequisite packages that are available from anaconda can be installed using the command `conda install scipy numpy matplotlib scikit-image scikit-learn pandas networkx jupyter ipython` (Anaconda3-2020.02 has been tested) - - For more information, see the [documentation](https://defdap.readthedocs.io/en/latest/installation.html) ## How to use - - To start the example notebook, use the command `jupyter lab` and click the notebook (.ipynb file) - - Try out the example notebook using the binder link above (Note: change the matplotlib plotting mode to inline, some of the interactive features will not work) - -- Here is a video demonstrating the basic functionality of the package: -[DefDAP demo](http://www.youtube.com/watch?v=JIbc7F-nFSQ "DefDAP demo") +- Here is a video demonstrating the basic functionality of the package: [DefDAP demo](http://www.youtube.com/watch?v=JIbc7F-nFSQ "DefDAP demo") ## Documentation - - For more help, see the online [documentation](https://defdap.readthedocs.io/). ## Contributing - - For information about contributing see the [guidelines](https://defdap.readthedocs.io/en/latest/contributing.html) - Any new functions or changes to arguments should be documented. ## Credits - The software uses the following open source packages: - [scipy](http://scipy.org/) @@ -72,5 +48,4 @@ The software uses the following open source packages: - [jupyter](https://jupyter.org/) ## License - [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0) From a37405e1542362a6e762a9fe053344902058ace7 Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Tue, 23 Feb 2021 18:03:44 +0000 Subject: [PATCH 03/33] Update docs URL in setup --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 35e5ed0..472ed87 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def get_version(): keywords='defdap, EBSD, HRDIC, deformation, crystal, correlative analysis', project_urls={ 'GitHub': 'https://github.com/MechMicroMan/DefDAP', - 'Documentation': 'http://mechmicroman.github.io/DefDAP' + 'Documentation': 'https://defdap.readthedocs.io/en/latest' }, classifiers=[ 'Intended Audience :: Science/Research', From 8a7d2510bd486a7d59a285c168054226b1067ff1 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Tue, 23 Feb 2021 18:34:26 +0000 Subject: [PATCH 04/33] Speed up EBSD map data rotation - Vectorise multiplication of a list of quats, by a single quat (10x faster) --- defdap/ebsd.py | 34 ++++++++++++++++++++-------------- defdap/quat.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/defdap/ebsd.py b/defdap/ebsd.py index 92702ad..bc60cff 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -234,25 +234,24 @@ def primaryPhase(self): def scale(self): return self.stepSize - @reportProgress("transforming EBSD data") - def transformData(self): + @reportProgress("rotating EBSD data") + def rotateData(self): """Rotate map by 180 degrees and transform quats accordingly. """ self.eulerAngleArray = self.eulerAngleArray[:, ::-1, ::-1] self.bandContrastArray = self.bandContrastArray[::-1, ::-1] self.phaseArray = self.phaseArray[::-1, ::-1] - self.buildQuatArray() + self.buildQuatArray(force=True) # Force rebuild quat array # Rotation from old coord system to new - transformQuat = Quat.fromAxisAngle(np.array([0, 0, 1]), np.pi) - transformQuat = transformQuat.conjugate - for i in range(self.xDim): - for j in range(self.yDim): - self.quatArray[j, i] = self.quatArray[j, i] * transformQuat + transformQuat = Quat.fromAxisAngle(np.array([0, 0, 1]), np.pi).conjugate - # report progress - yield i / self.xDim + # Perform vectorised multiplication + quats = Quat.multiplyManyQuats(self.quatArray.flatten(), transformQuat) + self.quatArray = np.array(quats).reshape(self.yDim, self.xDim) + + yield 1. def plotBandContrastMap(self, **kwargs): """Plot band contrast map @@ -376,7 +375,7 @@ def plotPhaseMap(self, **kwargs): Returns ------- - defdap.plotting.MapPlot. + defdap.plotting.MapPlot """ # Set default plot parameters then update with any input @@ -621,14 +620,21 @@ def checkDataLoaded(self): return True @reportProgress("building quaternion array") - def buildQuatArray(self): + def buildQuatArray(self, force = False): """Build quaternion array + Parameters + ---------- + force, optional + If true, re-build quaternion array """ self.checkDataLoaded() - if self.quatArray is None: - # create the array of quat objects + if force == False: + if self.quatArray is None: + # create the array of quat objects + self.quatArray = Quat.createManyQuats(self.eulerAngleArray) + if force == True: self.quatArray = Quat.createManyQuats(self.eulerAngleArray) yield 1. diff --git a/defdap/quat.py b/defdap/quat.py index c0d427c..70280cf 100755 --- a/defdap/quat.py +++ b/defdap/quat.py @@ -578,6 +578,34 @@ def createManyQuats(eulerArray): return quats + @staticmethod + def multiplyManyQuats(quats, right): + """ Multiply all quats in a list of quats, by a single quat. + + Parameters + ---------- + quats: list(defdap.quat.Quat) + List of quats to be operated on. + right: defdap.quat.Quat + Single quaternion to multiply with the list of quats. + + Returns + ------- + list(defdap.quat.Quat) + + """ + quatArray = np.array([q.quatCoef for q in quats]) + + tempArray = np.zeros((len(quatArray),4), dtype=float) + tempArray[...,0] = ((quatArray[...,0] * right.quatCoef[0]) - + np.dot(quatArray[...,1:4], right.quatCoef[1:4])) + + tempArray[...,1:4] = ((quatArray[...,0,None] * right.quatCoef[None,1:4]) + + (right.quatCoef[0] * quatArray[...,1:4]) + + np.cross(quatArray[...,1:4], right.quatCoef[1:4])) + + return [Quat(coefs) for coefs in tempArray] + @staticmethod def extract_quat_comps(quats): """Return a NumPy array of the provided quaternion components From 9b981d28cccdd59af38cd8ada4113bfdc2b6203e Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Tue, 23 Feb 2021 18:35:14 +0000 Subject: [PATCH 05/33] Ensure functions are hyperlinked in docs --- defdap/base.py | 12 ++++++------ defdap/quat.py | 22 +++++++++++----------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/defdap/base.py b/defdap/base.py index e709b43..6b741ff 100755 --- a/defdap/base.py +++ b/defdap/base.py @@ -565,7 +565,7 @@ def grainDataToMapData(self, grainData, grainIds=-1, bg=0): Parameters ---------- - grainData : list or np.ndarray + grainData : list or numpy.ndarray Grain values. This can be a single value per grain or RGB values. grainIds : list of int or int, optional @@ -575,7 +575,7 @@ def grainDataToMapData(self, grainData, grainIds=-1, bg=0): Returns ------- - grainMap: np.ndarray + grainMap: numpy.ndarray Array filled with grain data values """ @@ -616,7 +616,7 @@ def plotGrainDataMap( Parameters ---------- - mapData : np.array, optional + mapData : numpy.ndarray, optional Array of map data. This must be cropped! Either mapData or grainData must be supplied. grainData : list or np.array, optional @@ -627,7 +627,7 @@ def plotGrainDataMap( bg: int or real, optional Value to fill the background with. kwargs: - Other parameters are passed to defdap.plotting.MapPlot.create + Other parameters are passed to :func:`defdap.plotting.MapPlot.create` Returns ------- @@ -791,7 +791,7 @@ def grainOutline(self, bg=np.nan, fg=0): Returns ------- numpy.ndarray - Bounding box for grain with np.nan outside the grain and given number within. + Bounding box for grain with :obj:`~numpy.nan` outside the grain and given number within. """ x0, y0, xmax, ymax = self.extremeCoords @@ -814,7 +814,7 @@ def plotOutline(self, ax=None, plotScaleBar=False, **kwargs): plotScaleBar : bool plots the scale bar on the grain if true. kwargs : dict - keyword arguments to pass to plotting.GrainPlot.addMap. + keyword arguments to pass to :func:`defdap.plotting.GrainPlot.addMap`. Returns ------- diff --git a/defdap/quat.py b/defdap/quat.py index 70280cf..edfdd06 100755 --- a/defdap/quat.py +++ b/defdap/quat.py @@ -92,7 +92,7 @@ def fromAxisAngle(cls, axis, angle): Parameters ---------- - axis : np.ndarray + axis : numpy.ndarray Axis that the rotation is applied around. angle : float Magnitude of rotation in radians. @@ -119,7 +119,7 @@ def eulerAngles(self): Returns ------- - eulers : np.ndarray, shape 3 + eulers : numpy.ndarray, shape 3 Bunge euler angles (in radians). References @@ -176,7 +176,7 @@ def rotMatrix(self): Returns ------- - rotMatrix : np.ndarray, shape (3, 3) + rotMatrix : numpy.ndarray, shape (3, 3) Rotation matrix. References @@ -322,12 +322,12 @@ def transformVector(self, vector): Parameters ---------- - vector : numpy.ndarray of shape 3 or equivalent + vector : numpy.ndarray, shape 3 or equivalent Vector to transform. Returns ------- - numpy.ndarray of shape 3 + numpy.ndarray, shape 3 Transformed vector. """ @@ -364,7 +364,7 @@ def misOri(self, right, symGroup, returnQuat=0): float Minimum misorientation. defdap.quat.Quat - Symmetric equivalent orientation with minimum misorientation. + Symmetric equivalent orientation with minimum misorientation. """ if isinstance(right, type(self)): @@ -418,7 +418,7 @@ def plotIPF(self, direction, symGroup, projection=None, Parameters ---------- - direction : np.ndarray + direction : numpy.ndarray Sample reference direction for IPF. symGroup : str Crystal type (cubic, hexagonal). @@ -620,7 +620,7 @@ def extract_quat_comps(quats): Returns ------- - np.ndarray + numpy.ndarray Array of quaternion components, shape (4, ..) """ @@ -646,7 +646,7 @@ def calcSymEqvs(quats, symGroup, dtype=np.float): Returns ------- - quatComps: np.ndarray, shape: (numSym x 4 x numQuats) + quatComps: numpy.ndarray, shape: (numSym x 4 x numQuats) Array containing all symmetrically equivalent quaternion components of input quaternions. """ @@ -685,7 +685,7 @@ def calcAverageOri(quatComps): ---------- quatComps : numpy.ndarray Array containing all symmetrically equivalent quaternion components of given quaternions - (shape: numSym x 4 x numQuats), can be calculated with Quat.calcSymEqvs. + (shape: numSym x 4 x numQuats), can be calculated with :func:`Quat.calcSymEqvs`. Returns ------- @@ -722,7 +722,7 @@ def calcMisOri(quatComps, refOri): ---------- quatComps : numpy.ndarray Array containing all symmetrically equivalent quaternion components of given quaternions - (shape: numSym x 4 x numQuats), can be calculated from quats with Quat.calcSymEqvs. + (shape: numSym x 4 x numQuats), can be calculated from quats with :func:`Quat.calcSymEqvs` . refOri : defdap.quat.Quat Reference orientation. From 0258b997ecf798752efbe380ba048d1f8d2fd59e Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Tue, 23 Feb 2021 18:40:43 +0000 Subject: [PATCH 06/33] Add option to turn off draw for GBs --- defdap/plotting.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/defdap/plotting.py b/defdap/plotting.py index 0e873a9..93dbab1 100644 --- a/defdap/plotting.py +++ b/defdap/plotting.py @@ -310,7 +310,7 @@ def addScaleBar(self, scale=None): self.ax.add_artist(ScaleBar(scale)) def addGrainBoundaries(self, kind="pixel", boundaries=None, colour=None, - dilate=False, **kwargs): + dilate=False, draw=True, **kwargs): """Add grain boundaries to the plot. Parameters @@ -349,7 +349,8 @@ def addGrainBoundaries(self, kind="pixel", boundaries=None, colour=None, self.ax.add_collection(lc) # ax.autoscale() - self.draw() + if draw: + self.draw() else: boundariesImage = boundaries if boundariesImage is None: @@ -369,7 +370,8 @@ def addGrainBoundaries(self, kind="pixel", boundaries=None, colour=None, img = self.ax.imshow(boundariesImage, cmap=boundariesCmap, interpolation='None', vmin=0, vmax=1) - self.draw() + if draw: + self.draw() self.imgLayers.append(img) From 285dc8ae0694d7b58b5a2342840e1bf55439eaa9 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Tue, 23 Feb 2021 18:43:35 +0000 Subject: [PATCH 07/33] Speed up 'warp' grain finding - Replace loop --- defdap/hrdic.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/defdap/hrdic.py b/defdap/hrdic.py index 78603a6..f004a3d 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -789,18 +789,20 @@ def findGrains(self, algorithm=None, minGrainSize=10): index = np.digitize(self.grains.ravel(), old, right=True) self.grains = new[index].reshape(self.grains.shape) - # Make grain objects self.grainList = [] for i, (dicGrainId, ebsdGrainId) in enumerate(zip(dicGrainIds, self.ebsdGrainIds)): yield i / len(dicGrainIds) # Report progress - currentGrain = Grain(self) - coords = np.transpose(np.where(self.grains == dicGrainId)) # Find coordinates - for coord in coords: - currentGrain.addPoint((coord[1], coord[0]), - self.eMaxShear[coord[0] + self.cropDists[1, 0], - coord[1] + self.cropDists[0, 0]]) + # Make grain object + currentGrain = Grain(grainID=dicGrainId, dicMap=self) + # Find (x,y) coordinates and corresponding max shears of grain + coords = np.argwhere(self.grains == dicGrainId) # (y,x) + currentGrain.coordList = np.flip(coords, axis=1) # (x,y) + currentGrain.maxShearList = self.eMaxShear[coords[:,0]+ self.cropDists[1, 0], + coords[:,1]+ self.cropDists[0, 0]] + + # Assign EBSD grain ID to DIC grain and increment grain list currentGrain.ebsdGrainId = ebsdGrainId - 1 currentGrain.ebsdGrain = self.ebsdMap.grainList[ebsdGrainId - 1] currentGrain.ebsdMap = self.ebsdMap From 6b75a3b3e1b01a075d19e4ccf57d25082cf2e054 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Tue, 23 Feb 2021 18:45:42 +0000 Subject: [PATCH 08/33] Generalise threshold mask - add examples to docs --- defdap/hrdic.py | 54 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/defdap/hrdic.py b/defdap/hrdic.py index f004a3d..59ccbdc 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -570,19 +570,41 @@ def warpToDicFrame(self, mapData, cropImage=True, order=1, preserve_range=False) # return map return warpedMap - def generateThresholdMask(self, threshold, dilation=0, preview=False): - """ Generate a mask, based on thresholding of effective shear strain values. + def generateThresholdMask(self, mask, dilation=0, preview=True): + """ + Generate a dilated mask, based on a boolean array and previews the appication of + this mask to the max shear map. Parameters ---------- - threshold: float - Threshold effective shear strain value, above which pixels are masked. - dilation: int - Number of times to apply binary dilation. + mask: numpy.array(bool) + A boolean array where points to be removed are True + dilation: int, optional + Number of pixels to dilate the mask by. Useful to remove anomalous points + around masked values. No dilation applied if not specified. preview: bool - If true, show the mask and masked effective shear strain map. + If true, show the mask and preview the masked effective shear strain map. + + Examples + ---------- + To remove data points in dicMap where eMaxShear is above 0.8, use: + + >>> mask = dicMap.eMaxShear > 0.8 + + To remove data points in dicMap where e11 is above 1 or less than -1, use: + + >>> mask = (dicMap.e11 > 1) | (dicMap.e11 < -1) + + To remove data points in dicMap where corrVal is less than 0.4, use: + + >>> mask = dicMap.corrVal < 0.4 + + Note: correlation value data needs to be loaded seperately from the DIC map, + see :func:`defdap.hrdic.loadCorrValData` + """ - self.mask = self.eMaxShear > threshold + self.mask = mask + if dilation != 0: self.mask = binary_dilation(self.mask, iterations=dilation) @@ -592,21 +614,23 @@ def generateThresholdMask(self, threshold, dilation=0, preview=False): numRemovedCrop = np.sum(self.crop(self.mask)) numTotalCrop = self.xDim * self.yDim - print('Filtering removes {0} \ {1} ({2:.3f} %) datapoints in map'.format(numRemoved, numTotal, - (numRemoved / numTotal)*100)) - print('Filtering removes {0} \ {1} ({2:.3f} %) datapoints in cropped map'.format(numRemovedCrop, numTotalCrop, - (numRemovedCrop / numTotalCrop * 100))) + print('Filtering will remove {0} \ {1} ({2:.3f} %) datapoints in map' + .format(numRemoved, numTotal,(numRemoved / numTotal)*100)) + print('Filtering will remove {0} \ {1} ({2:.3f} %) datapoints in cropped map' + .format(numRemovedCrop, numTotalCrop,(numRemovedCrop / numTotalCrop * 100))) if preview == True: plot1 = MapPlot.create(self, self.crop(self.mask), cmap='binary') plot1.setTitle('Removed datapoints in black') - plot2 = MapPlot.create(self, self.crop(np.where(self.mask == True, np.nan, self.eMaxShear)), - plotColourBar='True', clabel="Effective shear strain") + plot2 = MapPlot.create(self, + self.crop(np.where(self.mask == True, np.nan, self.eMaxShear)), + plotColourBar='True', + clabel="Effective shear strain") plot2.setTitle('Effective shear strain preview') print('Use applyThresholdMask function to apply this filtering to data') def applyThresholdMask(self): - """ Apply mask to DIC data by setting masked values to nan. + """ Apply mask to all DIC map data by setting masked values to nan. """ self.eMaxShear = np.where(self.mask == True, np.nan, self.eMaxShear) From 2eb802a6565bdb4923a1e95abf65105dade845ad Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Wed, 24 Feb 2021 10:51:11 +0000 Subject: [PATCH 09/33] Support docstring generation from type hinting --- .readthedocs.yml | 1 + docs/source/conf.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 1d927ae..9b0fb26 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -10,3 +10,4 @@ python: path: . extra_requirements: - docs + - sphinx-autodoc-typehints diff --git a/docs/source/conf.py b/docs/source/conf.py index eb5dd2f..50a4805 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -54,9 +54,13 @@ def get_version(): 'sphinx.ext.githubpages', 'sphinx.ext.napoleon', 'sphinx.ext.intersphinx', + 'sphinx_autodoc_typehints', 'sphinx_rtd_theme' ] +napoleon_use_param = True +#set_type_checking_flag = True + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] From c69fdb11bb279fed674f12aaacecb7506ae8a22c Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Wed, 24 Feb 2021 16:00:07 +0000 Subject: [PATCH 10/33] Type hinting - inspector, file_readers, file_write and utils done --- defdap/file_readers.py | 115 +++++++++++++++++++++++++++-------------- defdap/file_writers.py | 17 +++++- defdap/inspector.py | 80 +++++++++++++++++++--------- defdap/utils.py | 4 +- 4 files changed, 149 insertions(+), 67 deletions(-) diff --git a/defdap/file_readers.py b/defdap/file_readers.py index 1db94df..9e8553b 100644 --- a/defdap/file_readers.py +++ b/defdap/file_readers.py @@ -18,6 +18,8 @@ import pathlib import re +from typing import TextIO, Tuple + from defdap.crystal import Phase from defdap.quat import Quat @@ -43,7 +45,7 @@ def __init__(self): self.dataFormat = None @staticmethod - def getLoader(dataType): + def getLoader(dataType: str): if dataType is None: dataType = "OxfordBinary" @@ -77,14 +79,17 @@ def checkData(self): class OxfordTextLoader(EBSDDataLoader): - def load(self, fileName, fileDir=""): + def load(self, + fileName: str, + fileDir: str = "" + ) -> Tuple[dict, dict]: """ Read an Oxford Instruments .ctf file, which is a HKL single orientation file. Parameters ---------- - fileName : str + fileName File name. - fileDir : str + fileDir Path to file. Returns @@ -198,29 +203,48 @@ def parsePhase(): self.checkData() + return self.loadedMetadata, self.loadedData + class OxfordBinaryLoader(EBSDDataLoader): - def load(self, fileName, fileDir=""): + def load(self, + fileName: str, + fileDir: str = "" + ) -> Tuple[dict, dict]: + """ Read Oxford Instruments .cpr/.crc file pair. + + Parameters + ---------- + fileName + File name. + fileDir + Path to file. + + Returns + ------- + dict, dict + EBSD metadata and EBSD data. + + """ self.loadOxfordCPR(fileName, fileDir=fileDir) self.loadOxfordCRC(fileName, fileDir=fileDir) - def loadOxfordCPR(self, fileName, fileDir=""): + return self.loadedMetadata, self.loadedData + + def loadOxfordCPR(self, + fileName: str, + fileDir: str = ""): """ Read an Oxford Instruments .cpr file, which is a metadata file describing EBSD data. Parameters ---------- - fileName : str + fileName File name. - fileDir : str + fileDir Path to file. - Returns - ------- - dict - Metadata for EBSD data, including dimensions and number of phases. - """ commentChar = ';' @@ -310,21 +334,18 @@ def parseLine(line): self.dataFormat = np.dtype(dataFormat) - def loadOxfordCRC(self, fileName, fileDir=""): + def loadOxfordCRC(self, + fileName: str, + fileDir: str = ""): """Read binary EBSD data from an Oxford Instruments .crc file Parameters ---------- - fileName : str + fileName File name. - fileDir : str + fileDir Path to file. - Returns - ------- - dict - EBSD data including phase, MAD and euler angle arrays. - """ xDim = self.loadedMetadata['xDim'] yDim = self.loadedMetadata['yDim'] @@ -353,14 +374,14 @@ def loadOxfordCRC(self, fileName, fileDir=""): self.checkData() - class PythonDictLoader(EBSDDataLoader): - def load(self, dataDict): + def load(self, + dataDict: dict): """Construct EBSD data from a python dictionary. Parameters ---------- - dataDict : dict + dataDict Dictionary with keys: 'stepSize' 'phases' @@ -425,14 +446,17 @@ def checkData(self): assert xdim == self.loadedMetadata['xDim'], "Dimensions of data and header do not match" assert ydim == self.loadedMetadata['yDim'], "Dimensions of data and header do not match" - def loadDavisMetadata(self, fileName, fileDir=""): + def loadDavisMetadata(self, + fileName: str, + fileDir: str = "" + ) -> dict: """ Load DaVis metadata from Davis .txt file. Parameters ---------- - fileName : str + fileName File name. - fileDir : str + fileDir Path to file. Returns @@ -462,15 +486,18 @@ def loadDavisMetadata(self, fileName, fileDir=""): return self.loadedMetadata - def loadDavisData(self, fileName, fileDir=""): + def loadDavisData(self, + fileName: str, + fileDir: str = "" + ) -> dict: """ Load displacement data from Davis .txt file containing x and y coordinates and x and y displacements for each coordinate. Parameters ---------- - fileName : str + fileName File name. - fileDir : str + fileDir Path to file. Returns @@ -495,16 +522,24 @@ def loadDavisData(self, fileName, fileDir=""): return self.loadedData - def loadDavisImageData(self, fileName, fileDir=""): + def loadDavisImageData(self, + fileName: str, + fileDir: str = "" + ) -> np.ndarray: """ A .txt file from DaVis containing a 2D image Parameters ---------- - fileName : str + fileName File name. - fileDir : str + fileDir Path to file. + Returns + ------- + np.ndarray + Array of data. + """ filePath = pathlib.Path(fileDir) / pathlib.Path(fileName) if not filePath.is_file(): @@ -518,20 +553,24 @@ def loadDavisImageData(self, fileName, fileDir=""): return loadedData -def readUntilString(file, termString, commentChar='*', lineProcess=None): +def readUntilString(file: TextIO, + termString: str, + commentChar: str = '*', + lineProcess: str = None + ) -> list: """Read lines in a file until a line starting with the `termString` is encounted. The file position is returned before the line starting with the `termString` when found. Comment and empty lines are ignored. Parameters ---------- - file : file + file An open python text file object. - termString : str + termString String to terminate reading. - commentChar : str + commentChar Character at start of a comment line to ignore. - lineProcess : function + lineProcess Function to apply to each line when loaded. Returns diff --git a/defdap/file_writers.py b/defdap/file_writers.py index f650f39..ebeecf0 100644 --- a/defdap/file_writers.py +++ b/defdap/file_writers.py @@ -35,7 +35,7 @@ def __init__(self): self.data_format = None @staticmethod - def get_writer(datatype): + def get_writer(datatype: str): if datatype is None: datatype = "OxfordText" @@ -46,7 +46,20 @@ def get_writer(datatype): class OxfordTextWriter(EBSDDataWriter): - def write(self, file_name, file_dir=""): + def write(self, + file_name: str, + file_dir: str = ""): + """ Write an Oxford Instruments .ctf file, which is a HKL single orientation file. + + Parameters + ---------- + fileName + File name. + file_dir + Path to file. + + """ + # check output file file_name = "{}.ctf".format(file_name) file_path = pathlib.Path(file_dir) / pathlib.Path(file_name) diff --git a/defdap/inspector.py b/defdap/inspector.py index 1b66a94..2bf3493 100644 --- a/defdap/inspector.py +++ b/defdap/inspector.py @@ -15,11 +15,15 @@ import numpy as np +from typing import List + from sklearn.cluster import MeanShift from scipy.stats import linregress +from scipy.stats._stats_mstats_common import LinregressResult import pandas as pd from defdap.plotting import Plot, GrainPlot, LineSlice +from defdap import hrdic class GrainInspector: @@ -28,7 +32,9 @@ class GrainInspector: and relative displacement ratio analysis. """ - def __init__(self, currMap, vmax=0.1): + def __init__(self, + currMap: 'hrdic.Map', + vmax: float = 0.1): # Initialise some values self.grainID = 0 self.currMap = currMap @@ -74,12 +80,14 @@ def draw(self): # Draw the stuff that will need to be redrawn often in a seperate function self.redraw() - def gotoGrain(self, event, plot): + def gotoGrain(self, + event: int, + plot): """ Go to a specified grain ID. Parameters ---------- - event: int + event Grain ID to go to. """ @@ -90,12 +98,14 @@ def gotoGrain(self, event, plot): self.currEBSDGrain = self.currDICGrain.ebsdGrain self.redraw() - def saveLine(self, event, plot): + def saveLine(self, + event: np.ndarray, + plot): """ Save the start point, end point and angle of drawn line into the grain. Parameters ---------- - event: numpy.ndarray + event Start x, start y, end x, end y point of line passed from drawn line. """ @@ -149,7 +159,9 @@ def groupLines(self): group[2] = activePlanes group[3] = deviation - def clearAllLines(self, event, plot): + def clearAllLines(self, + event, + plot): """ Clear all lines in a given grain. """ @@ -158,12 +170,14 @@ def clearAllLines(self, event, plot): self.currDICGrain.groupsList = [] self.redraw() - def removeLine(self, event, plot): + def removeLine(self, + event: int, + plot): """ Remove single line [runs after submitting a text box]. Parameters ---------- - event: int + event Line ID to remove. """ @@ -235,12 +249,14 @@ def redraw(self): self.groupsInfoAx.axis('off') self.plot.addText(self.groupsInfoAx, 0, 1, groupsTxt, va='top', fontsize=10) - def runRDRGroup(self, event, plot): + def runRDRGroup(self, + event: int, + plot): """ Run RDR on a specified group, upon submitting a text box. Parameters ---------- - event: int + event Group ID specified from text box. """ @@ -249,7 +265,9 @@ def runRDRGroup(self, event, plot): self.calcRDR(grain = self.currDICGrain, group=int(event)) self.RDRGroupBox.set_val('') - def batchRunSTA(self, event, plot): + def batchRunSTA(self, + event, + plot): """ Run slip trace analysis on all grains which hve slip trace lines drawn. """ @@ -269,18 +287,22 @@ def batchRunSTA(self, event, plot): group[0], group[1], group[2], np.round(group[3],3), group[4]) print(text) - def calcRDR(self, grain, group, showPlot=True, length=2.5): + 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: int + grain DIC grain ID to run RDR on. - group: int + group group ID to run RDR on. - showPlot: bool + showPlot if True, show plot window. - length: int + length length of perpendicular lines used for RDR. """ @@ -350,26 +372,34 @@ def calcRDR(self, grain, group, showPlot=True, length=2.5): if showPlot: self.plotRDR(grain, group, ulist, vlist, allxlist, allylist, linRegResults) - def plotRDR(self, grain, group, ulist, vlist, allxlist, allylist, linRegResults): + 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: int + grain DIC grain to plot. - group: int + group Group ID to plot. - ulist: list + ulist List of ucentered values. - vlist: list + vlist List of vcentered values. - allxlist: list + allxlist List of all x values. - allylist: list + allylist List of all y values. - linRegResults: numpy.ndarray, {slope, intercept, rvalue, pvalue, stderr} - Results from linear regression of ucentered vs vcentered. + linRegResults + Results from linear regression of ucentered vs vcentered + {slope, intercept, rvalue, pvalue, stderr}. """ diff --git a/defdap/utils.py b/defdap/utils.py index de4d494..c2565aa 100644 --- a/defdap/utils.py +++ b/defdap/utils.py @@ -17,12 +17,12 @@ from datetime import datetime -def reportProgress(message=""): +def reportProgress(message: str = ""): """Decorator for reporting progress of given function Parameters ---------- - message : str + message Message to display (prefixed by 'Starting ', progress percentage and then 'Finished ' From 2cca9b7908bd24a783357fdde7da5f2e913289e6 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Thu, 25 Feb 2021 11:23:13 +0000 Subject: [PATCH 11/33] Remove sklearn dependency Resolves #58 --- README.md | 1 - defdap/inspector.py | 46 ++++++++++++++++++++++++--------------------- setup.py | 1 - 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 8a9c8ae..b0222aa 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,6 @@ The software uses the following open source packages: - [numpy](http://numpy.org/) - [matplotlib](http://matplotlib.org/) - [scikit-image](http://scikit-image.org/) -- [scikit-learn](https://scikit-learn.org/stable/) - [networkx](https://networkx.github.io/) - [pandas](http://pandas.pydata.org) - [peakutils](https://peakutils.readthedocs.io/en/latest/) diff --git a/defdap/inspector.py b/defdap/inspector.py index 2bf3493..74542d8 100644 --- a/defdap/inspector.py +++ b/defdap/inspector.py @@ -17,7 +17,6 @@ from typing import List -from sklearn.cluster import MeanShift from scipy.stats import linregress from scipy.stats._stats_mstats_common import LinregressResult import pandas as pd @@ -114,7 +113,7 @@ def saveLine(self, self.drawnLine.points[2]-self.drawnLine.points[0])) if lineAngle > 180: lineAngle -= 180 elif lineAngle < 0: lineAngle += 180 - #lineAngle += self.currMap.ebsdTransform.rotation*-180/np.pi + #lineAngle -= np.rad2deg(self.currMap.ebsdTransform.rotation) # Save drawn line to the DIC grain self.currDICGrain.pointsList.append([self.drawnLine.points, lineAngle, -1]) @@ -126,27 +125,32 @@ def saveLine(self, def groupLines(self): """ Group the lines drawn in the current grain item using a mean shift algorithm, - save the average angle and detect the active slip planes. + save the average angle and then detect the active slip planes. + + pointsList: 0 = points, 1 = angle, 2 = group + groupsList: 0 = id, 1 = angle, 2 = active planes, 3 = angular deviation """ - angles = [x[1] for x in self.currDICGrain.pointsList] - # For single line, don't group - if len(angles) == 1: - self.currDICGrain.pointsList[0][2]=0 - self.currDICGrain.groupsList = [[0, angles[0], 0, 0, 0]] - else: - # Run clustering algorithm for >1 line - ms = MeanShift(bandwidth=10).fit(np.matrix([range(len(angles)), angles]).transpose()) - - # Add group ID for each line to the points list - for i, label in enumerate(ms.labels_): - self.currDICGrain.pointsList[i][2] = label - - # Make array of groups with mean angle - self.currDICGrain.groupsList = [] - for i in range(np.max(ms.labels_+1)): - self.currDICGrain.groupsList.append([i, ms.cluster_centers_[i][1], 0, 0, 0]) - + + for i, line in enumerate(self.currDICGrain.pointsList): + angle = line[1] + if i == 0: + line[2]=0 # Make group 0 for first detected angle + self.currDICGrain.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 self.currDICGrain.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 self.currDICGrain.groupsList])-angle)) + self.currDICGrain.pointsList[i][2]=group + newAv = np.average([x[1] for x in self.currDICGrain.pointsList if x[2]==group]) + self.currDICGrain.groupsList[group][1] = newAv + else: + # Make new group and set + self.currDICGrain.groupsList.append([nextGroup, angle, 0, 0, 0]) + line[2]=nextGroup + nextGroup += 1 + # Detect active slip systems in each group for group in self.currDICGrain.groupsList: activePlanes = [] diff --git a/setup.py b/setup.py index 472ed87..c5bb076 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,6 @@ def get_version(): 'numpy', 'matplotlib>=3.0.0', 'scikit-image', - 'scikit-learn', 'pandas', 'peakutils', 'matplotlib_scalebar', From 6df8d7efe34fe9c8b3d479387dd7171d0b9883b1 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Thu, 25 Feb 2021 12:41:15 +0000 Subject: [PATCH 12/33] Fix bug in 'warp' algorithm - All non-indexed points were being included in the first grain - Validated EBSD <-> DIC reference -> all correct as before --- defdap/hrdic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/defdap/hrdic.py b/defdap/hrdic.py index 59ccbdc..7517e84 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -806,8 +806,8 @@ def findGrains(self, algorithm=None, minGrainSize=10): # Make a new list of sequential IDs of same length as number of grains dicGrainIds = np.arange(1, len(self.ebsdGrainIds)+1) - # Map the old EBSD IDs to the new DIC IDs (keep the same mapping for negative values) - negVals = np.array([i for i in np.unique(self.grains) if i<0]) + # Map the EBSD IDs to the DIC IDs (keep the same mapping for values <= 0) + negVals = np.array([i for i in np.unique(self.grains) if i<=0]) old = np.concatenate((negVals, self.ebsdGrainIds)) new = np.concatenate((negVals, dicGrainIds)) index = np.digitize(self.grains.ravel(), old, right=True) From 02e5d268b7e88de7e391ebfc11f18cf8885db873 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Thu, 25 Feb 2021 12:50:22 +0000 Subject: [PATCH 13/33] Make sure hex unit cells fit in plotting window --- defdap/quat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/defdap/quat.py b/defdap/quat.py index edfdd06..479de16 100755 --- a/defdap/quat.py +++ b/defdap/quat.py @@ -515,7 +515,7 @@ def plotUnitCell(self, symGroup, OI=True, quat = self if symGroup == 'hexagonal': - szFac = 0.2 + szFac = 0.18 if OI: # Add 30 degrees to phi2 for OI eulerAngles = quat.eulerAngles() From 50b9d70b1e25a497bb5954cea329c5f566215d28 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Thu, 25 Feb 2021 15:59:25 +0000 Subject: [PATCH 14/33] Pass crystalStructure to plotUnitCell - Pass a grain's crystalStructure to plotUnitCell, so that the correct phase is plotted --- defdap/ebsd.py | 4 ++-- defdap/quat.py | 32 ++++++++++---------------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/defdap/ebsd.py b/defdap/ebsd.py index bc60cff..620a79d 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -1501,9 +1501,9 @@ def plotUnitCell(self, fig=None, ax=None, **kwargs): All other arguments are passed to :func:`defdap.quat.Quat.plotUnitCell`. """ - # TODO: Update to use phase + crystalStructure = self.ebsdMap.phases[self.phaseID].crystalStructure Quat.plotUnitCell(self.refOri, fig=fig, ax=ax, - symGroup=self.crystalSym, **kwargs) + crystalStructure=crystalStructure, **kwargs) def plotMisOri(self, component=0, **kwargs): """Plot misorientation map for a given grain. diff --git a/defdap/quat.py b/defdap/quat.py index 479de16..fbc80ed 100755 --- a/defdap/quat.py +++ b/defdap/quat.py @@ -470,25 +470,23 @@ def plotIPF(self, direction, symGroup, projection=None, return plot - def plotUnitCell(self, symGroup, OI=True, + def plotUnitCell(self, crystalStructure, OI=True, plot=None, fig=None, ax=None, makeInteractive=False, **kwargs): """Plots a unit cell. Parameters ---------- - symGroup : str - Crystal type, hexagonal or cubic. + crystalStructure : defdap.crystal.crystalStructure + Crystal structure. OI : bool True if using oxford instruments system. plot : defdap.plotting.CrystalPlot Plot object to plot to. fig : matplotlib.figure.Figure - Figure to plot on, if not provided the current - active axis is used. + Figure to plot on, if not provided the current active axis is used. ax : matplotlib.axes.Axes - Axis to plot on, if not provided the current - active axis is used. + Axis to plot on, if not provided the current active axis is used. makeInteractive : bool True to make the plot interactive. kwargs @@ -499,34 +497,24 @@ def plotUnitCell(self, symGroup, OI=True, plotParams = {} plotParams.update(kwargs) - # TODO: most of this should be moved to either the crystal or - # plotting module - # Dirty fix to stop circular dependency - from defdap.crystal import crystalStructures + # TODO: most of this should be moved to either the crystal or plotting module - try: - crystalStructure = crystalStructures[symGroup] - except KeyError: - raise ValueError("Invalid crystal type, only cubic or " - "hexagonal available.") vert = crystalStructure.vertices faces = crystalStructure.faces - quat = self - - if symGroup == 'hexagonal': + if crystalStructure.name == 'hexagonal': szFac = 0.18 if OI: # Add 30 degrees to phi2 for OI - eulerAngles = quat.eulerAngles() + eulerAngles = self.eulerAngles() eulerAngles[2] += np.pi / 6 quat = Quat.fromEulerAngles(*eulerAngles) - elif symGroup == 'cubic': + elif crystalStructure.name == 'cubic': szFac = 0.25 # Rotate the lattice cell points - gg = quat.rotMatrix().T + gg = self.rotMatrix().T pts = np.matmul(gg, vert.T).T * szFac # Plot unit cell From f7a05ed0d532244fa33bc95c3c8b4bf15522bf61 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Thu, 25 Feb 2021 19:09:37 +0000 Subject: [PATCH 15/33] Type hints in quat.py --- defdap/quat.py | 220 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 143 insertions(+), 77 deletions(-) diff --git a/defdap/quat.py b/defdap/quat.py index fbc80ed..cd6028b 100755 --- a/defdap/quat.py +++ b/defdap/quat.py @@ -17,6 +17,7 @@ from defdap import plotting +from typing import Union, Tuple, List class Quat(object): """Class used to define and perform operations on quaternions. These @@ -25,7 +26,9 @@ class Quat(object): """ __slots__ = ['quatCoef'] - def __init__(self, *args, allow_southern=False): + def __init__(self, + *args: Union[Tuple, list, np.ndarray], + allow_southern: bool=False): """ Construct a Quat object from 4 quat coefficients or an array of quat coefficients. @@ -34,6 +37,8 @@ def __init__(self, *args, allow_southern=False): ---------- *args Variable length argument list. + allow_southern + if False, move quat to northern hemisphere. """ # construct with array of quat coefficients @@ -56,16 +61,20 @@ def __init__(self, *args, allow_southern=False): self.quatCoef = self.quatCoef * -1 @classmethod - def fromEulerAngles(cls, ph1, phi, ph2): + def fromEulerAngles(cls, + ph1: float, + phi: float, + ph2: float + ) -> 'Quat': """Create a quat object from 3 Bunge euler angles. Parameters ---------- - ph1 : float + ph1 First Euler angle, rotation around Z in radians. - phi : float + phi Second Euler angle, rotation around new X in radians. - ph2 : float + ph2 Third Euler angle, rotation around new Z in radians. Returns @@ -86,15 +95,18 @@ def fromEulerAngles(cls, ph1, phi, ph2): return cls(quatCoef) @classmethod - def fromAxisAngle(cls, axis, angle): + def fromAxisAngle(cls, + axis: np.ndarray, + angle: float + ) -> 'Quat': """Create a quat object from a rotation around an axis. This creates a quaternion to represent the passive rotation (-ve axis). Parameters ---------- - axis : numpy.ndarray + axis Axis that the rotation is applied around. - angle : float + angle Magnitude of rotation in radians. Returns @@ -114,7 +126,8 @@ def fromAxisAngle(cls, axis, angle): # call constructor return cls(quatCoef) - def eulerAngles(self): + def eulerAngles(self + ) -> np.ndarray: """Calculate the Euler angle representation for this rotation. Returns @@ -171,7 +184,8 @@ def eulerAngles(self): return eulers - def rotMatrix(self): + def rotMatrix(self + ) -> np.ndarray: """Calculate the rotation matrix representation for this rotation. Returns @@ -216,11 +230,16 @@ def __repr__(self): def __str__(self): return self.__repr__() - def _plotIPF(self, direction, symGroup, **kwargs): + def _plotIPF(self, + direction: np.ndarray, + symGroup: str, + **kwargs): Quat.plotIPF([self], direction, symGroup, **kwargs) # overload * operator for quaternion product and vector product - def __mul__(self, right, allow_southern=False): + def __mul__(self, + right: 'Quat', + allow_southern: bool = False): if isinstance(right, type(self)): # another quat newQuatCoef = np.zeros(4, dtype=float) newQuatCoef[0] = ( @@ -236,12 +255,14 @@ def __mul__(self, right, allow_southern=False): raise TypeError("{:} - {:}".format(type(self), type(right))) - def dot(self, right): + def dot(self, + right: 'Quat' + ) -> float: """ Calculate dot product between two quaternions. Parameters ---------- - right : defdap.quat.Quat + right Right hand quaternion. Returns @@ -255,27 +276,33 @@ def dot(self, right): raise TypeError() # overload + operator - def __add__(self, right): + def __add__(self, + right: 'Quat'): if isinstance(right, type(self)): return Quat(self.quatCoef + right.quatCoef) raise TypeError() # overload += operator - def __iadd__(self, right): + def __iadd__(self, + right: 'Quat'): if isinstance(right, type(self)): self.quatCoef += right.quatCoef return self raise TypeError() # allow array like setting/getting of components - def __getitem__(self, key): + def __getitem__(self, + key: int): return self.quatCoef[key] - def __setitem__(self, key, value): + def __setitem__(self, + key: int, + value: float): self.quatCoef[key] = value return - def norm(self): + def norm(self + ) -> float: """Calculate the norm of the quaternion. Returns @@ -286,7 +313,8 @@ def norm(self): """ return np.sqrt(np.dot(self.quatCoef[0:4], self.quatCoef[0:4])) - def normalise(self): + def normalise(self + ) -> 'Quat': """ Normalise the quaternion (turn it into an unit quaternion). Returns @@ -300,7 +328,8 @@ def normalise(self): # also the inverse if this is a unit quaternion @property - def conjugate(self): + def conjugate(self + ) -> 'Quat': """Calculate the conjugate of the quaternion. Returns @@ -311,7 +340,9 @@ def conjugate(self): """ return Quat(self[0], -self[1], -self[2], -self[3]) - def transformVector(self, vector): + def transformVector(self, + vector: Union[Tuple, List, np.ndarray] + ) -> np.ndarray: """ Transforms vector by the quaternion. For passive EBSD quaterions this is a transformation from sample space to crystal space. @@ -343,7 +374,11 @@ def transformVector(self, vector): ) return vectorQuatTransformed.quatCoef[1:4] - def misOri(self, right, symGroup, returnQuat=0): + def misOri(self, + right: 'Quat', + symGroup: str, + returnQuat: int = 0 + ) -> Tuple[float, 'Quat']: """ Calculate misorientation angle between 2 orientations taking into account the symmetries of the crystal structure. @@ -351,11 +386,11 @@ def misOri(self, right, symGroup, returnQuat=0): Parameters ---------- - right : defdap.quat.Quat + right Orientation to find misorientation to. - symGroup : str + symGroup Crystal type (cubic, hexagonal). - returnQuat : int + returnQuat What to return: 0 for minimum misorientation, 1 for symmetric equivalent with minimum misorientation, 2 for both. @@ -386,7 +421,9 @@ def misOri(self, right, symGroup, returnQuat=0): return minMisOri raise TypeError("Input must be a quaternion.") - def misOriAxis(self, right): + def misOriAxis(self, + right: 'Quat' + ) -> np.ndarray: """ Calculate misorientation axis between 2 orientations. This does not consider symmetries of the crystal structure. @@ -409,38 +446,46 @@ def misOriAxis(self, right): return misOriAxis raise TypeError("Input must be a quaternion.") - def plotIPF(self, direction, symGroup, projection=None, - plot=None, fig=None, ax=None, makeInteractive=False, - plotColourBar=False, clabel="", - markerColour=None, markerSize=40, **kwargs): + def plotIPF(self, + direction: np.ndarray, + symGroup: str, + projection: str = None, + plot: 'plotting.Plot' = None, + fig: 'matplotlib.figure.Figure' = None, + ax: 'matplotlib.axes.Axes' = None, + makeInteractive: bool = False, + markerColour: Union[List[str], str] = None, + markerSize: float = 40, + **kwargs + ) -> 'plotting.PolePlot': """ Plot IPF of orientation, with relation to specified sample direction. Parameters ---------- - direction : numpy.ndarray + direction Sample reference direction for IPF. - symGroup : str + symGroup Crystal type (cubic, hexagonal). - projection : str + projection Projection to use. Either string (stereographic or lambert) or a function. - plot : defdap.plotting.Plot + plot Defdap plot to plot on. - fig : matplotlib.figure.Figure + fig Figure to plot on, if not provided the current active axis is used. - ax : matplotlib.axes.Axes + ax Axis to plot on, if not provided the current active axis is used. - makeInteractive : bool + makeInteractive If true, make the plot interactive. - markerColour : str + markerColour: str or list of str Colour of markers (only used for half and half colouring, - otherwise us argument c). - markerSize : int + otherwise use argument c). + markerSize Size of markers (only used for half and half colouring, - otherwise us argument s). + otherwise use argument s). kwargs All other arguments are passed to :func:`defdap.plotting.PolePlot.addPoints`. @@ -465,29 +510,31 @@ def plotIPF(self, direction, symGroup, projection=None, markerColour=markerColour, markerSize=markerSize, **plotParams) - if plotColourBar: - plot.addColourBar(clabel) - return plot - def plotUnitCell(self, crystalStructure, OI=True, - plot=None, fig=None, ax=None, makeInteractive=False, + def plotUnitCell(self, + crystalStructure: 'defdap.crystal.CrystalStructure', + OI: bool = True, + plot: 'plotting.CrystalPlot' = None, + fig: 'matplotlib.figure.Figure' = None, + ax: 'matplotlib.axes.Axes' = None, + makeInteractive: bool = False, **kwargs): """Plots a unit cell. Parameters ---------- - crystalStructure : defdap.crystal.crystalStructure + crystalStructure Crystal structure. - OI : bool + OI True if using oxford instruments system. - plot : defdap.plotting.CrystalPlot + plot Plot object to plot to. - fig : matplotlib.figure.Figure + fig Figure to plot on, if not provided the current active axis is used. - ax : matplotlib.axes.Axes + ax Axis to plot on, if not provided the current active axis is used. - makeInteractive : bool + makeInteractive True to make the plot interactive. kwargs All other arguments are passed to :func:`defdap.plotting.CrystalPlot.addVerts`. @@ -533,12 +580,13 @@ def plotUnitCell(self, crystalStructure, OI=True, # Static methods @staticmethod - def createManyQuats(eulerArray): + def createManyQuats(eulerArray: np.ndarray + ) -> np.ndarray: """Create a an array of quats from an array of Euler angles. Parameters ---------- - eulerArray : numpy.ndarray + eulerArray Array of Bunge Euler angles of shape 3 x n x ... x m. Returns @@ -567,14 +615,16 @@ def createManyQuats(eulerArray): return quats @staticmethod - def multiplyManyQuats(quats, right): + def multiplyManyQuats(quats: List['Quat'], + right: 'Quat' + ) -> List['Quat']: """ Multiply all quats in a list of quats, by a single quat. Parameters ---------- - quats: list(defdap.quat.Quat) + quats List of quats to be operated on. - right: defdap.quat.Quat + right Single quaternion to multiply with the list of quats. Returns @@ -595,7 +645,7 @@ def multiplyManyQuats(quats, right): return [Quat(coefs) for coefs in tempArray] @staticmethod - def extract_quat_comps(quats): + def extract_quat_comps(quats: np.ndarray): """Return a NumPy array of the provided quaternion components Input quaternions may be given as a list of Quat objects or any iterable @@ -620,16 +670,19 @@ def extract_quat_comps(quats): return quat_comps @staticmethod - def calcSymEqvs(quats, symGroup, dtype=np.float): + def calcSymEqvs(quats: np.ndarray, + symGroup: str, + dtype: type=np.float + ) -> np.ndarray: """Calculate all symmetrically equivalent quaternions of given quaternions. Parameters ---------- quats : numpy.ndarray(defdap.quat.Quat) Array of quat objects. - symGroup : str + symGroup Crystal type (cubic, hexagonal). - dtype : numpy.dtype + dtype Data type used for calculation, defaults to np.float. Returns @@ -666,7 +719,8 @@ def calcSymEqvs(quats, symGroup, dtype=np.float): return quatComps @staticmethod - def calcAverageOri(quatComps): + def calcAverageOri(quatComps: np.ndarray + ) -> 'Quat': """Calculate the average orientation of given quats. Parameters @@ -703,15 +757,17 @@ def calcAverageOri(quatComps): return avOri @staticmethod - def calcMisOri(quatComps, refOri): + def calcMisOri(quatComps: np.ndarray, + refOri: 'Quat' + ) -> Tuple[np.ndarray, 'Quat']: """Calculate the misorientation between the quaternions and a reference quaternion. Parameters ---------- - quatComps : numpy.ndarray + quatComps Array containing all symmetrically equivalent quaternion components of given quaternions (shape: numSym x 4 x numQuats), can be calculated from quats with :func:`Quat.calcSymEqvs` . - refOri : defdap.quat.Quat + refOri Reference orientation. Returns @@ -739,7 +795,9 @@ def calcMisOri(quatComps, refOri): return minMisOris, minQuatComps @staticmethod - def polarAngles(x, y, z): # spherical coordinates as per Wikipedia + def polarAngles(x: np.ndarray, + y: np.ndarray, + z: np.ndarray): # spherical coordinates as per Wikipedia """Convert Cartesian coordinates to polar coordinates, for an unit vector. Parameters @@ -768,7 +826,11 @@ def polarAngles(x, y, z): # spherical coordinates as per Wikipedia return alpha, beta @staticmethod - def calcIPFcolours(quats, direction, symGroup, dtype=np.float32): + def calcIPFcolours(quats: np.ndarray, + direction: np.ndarray, + symGroup: str, + dtype: type=np.float32 + ) -> np.ndarray: """ Calculate the RGB colours, based on the location of the given quats on the fundamental region of the IPF for the sample direction specified. @@ -777,11 +839,11 @@ def calcIPFcolours(quats, direction, symGroup, dtype=np.float32): ---------- quats : numpy.ndarray(defdap.quat.Quat) Array of quat objects. - direction : numpy.ndarray + direction Direction in sample space. - symGroup : str + symGroup Crystal type (cubic, hexagonal). - dtype: numpy.dtype + dtype Data type to use for calculation. Returns @@ -872,20 +934,23 @@ def calcIPFcolours(quats, direction, symGroup, dtype=np.float32): return rgb @staticmethod - def calcFundDirs(quats, direction, symGroup, dtype=np.float): + def calcFundDirs(quats: np.ndarray, + direction: np.ndarray, + symGroup: str, + dtype: type = np.float): """ Transform the sample direction to crystal coords based on the quats and find the ones in the fundamental sector of the IPF. Parameters ---------- - quats : array_like(defdap.quat.Quat) + quats: array_like(defdap.quat.Quat) Array of quat objects. - direction: numpy.ndarray + direction Direction in sample space. - symGroup : str + symGroup Crystal type (cubic, hexagonal). - dtype: numpy.dtype + dtype Data type to use for calculation. Returns @@ -997,7 +1062,8 @@ def calcFundDirs(quats, direction, symGroup, dtype=np.float): return alphaFund, betaFund @staticmethod - def symEqv(symGroup): + def symEqv(symGroup: str + ) -> List['Quat']: """Returns all symmetric equivalents for a given crystal type. LEGACY: move to use symmetries defined in crystal structures From 77eceb4c9b565007a04af3792bf9961d0a06babb Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Thu, 25 Feb 2021 19:14:49 +0000 Subject: [PATCH 16/33] locateGrainID works for EBSD now (shows unit cell) --- defdap/base.py | 2 +- defdap/ebsd.py | 14 ++++++++++++-- defdap/plotting.py | 7 ------- defdap/quat.py | 7 +++++++ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/defdap/base.py b/defdap/base.py index 6b741ff..39cbbfe 100755 --- a/defdap/base.py +++ b/defdap/base.py @@ -168,7 +168,6 @@ def clickGrainID(self, event, plot, displaySelected): # update the grain highlights layer in the plot plot.addGrainHighlights([self.currGrainId], alpha=self.highlightAlpha) - # TODO: Check display selected works for ebsd map if displaySelected: currGrain = self[self.currGrainId] if self.grainPlot is None or not self.grainPlot.exists: @@ -177,6 +176,7 @@ def clickGrainID(self, event, plot, displaySelected): self.grainPlot.clear() self.grainPlot.callingGrain = currGrain currGrain.plotDefault(plot=self.grainPlot) + self.grainPlot.draw() def setHomogPoint(self, binSize=1, points=None, **kwargs): """ diff --git a/defdap/ebsd.py b/defdap/ebsd.py index 620a79d..a97fe1b 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -1375,6 +1375,12 @@ def __init__(self, grainID, ebsdMap): self.slipTraceAngles = None # list of slip trace angles self.slipTraceInclinations = None + @property + def plotDefault(self): + return lambda *args, **kwargs: self.plotUnitCell( + *args, **kwargs + ) + def addPoint(self, coord, quat): """Append a coordinate and a quat to a grain. @@ -1488,7 +1494,7 @@ def plotOriSpread(self, direction=np.array([0, 0, 1]), **kwargs): return Quat.plotIPF(self.quatList, direction, self.crystalSym, **plotParams) - def plotUnitCell(self, fig=None, ax=None, **kwargs): + def plotUnitCell(self, fig=None, ax=None, plot=None, **kwargs): """Plot an unit cell of the average grain orientation. Parameters @@ -1497,14 +1503,18 @@ def plotUnitCell(self, fig=None, ax=None, **kwargs): Matplotlib figure to plot on ax : matplotlib.figure.Figure Matplotlib figure to plot on + plot : defdap.plotting.PolePlot + defdap plot to plot the figure to. kwargs All other arguments are passed to :func:`defdap.quat.Quat.plotUnitCell`. """ crystalStructure = self.ebsdMap.phases[self.phaseID].crystalStructure - Quat.plotUnitCell(self.refOri, fig=fig, ax=ax, + plot = Quat.plotUnitCell(self.refOri, fig=fig, ax=ax, plot=plot, crystalStructure=crystalStructure, **kwargs) + return plot + def plotMisOri(self, component=0, **kwargs): """Plot misorientation map for a given grain. diff --git a/defdap/plotting.py b/defdap/plotting.py index 93dbab1..aa3be2e 100644 --- a/defdap/plotting.py +++ b/defdap/plotting.py @@ -1448,13 +1448,6 @@ def __init__(self, fig=None, ax=None, axParams={}, **figParams ) - # Set plotting parameters - self.ax.set_xlim3d(-0.15, 0.15) - self.ax.set_ylim3d(-0.15, 0.15) - self.ax.set_zlim3d(-0.15, 0.15) - self.ax.view_init(azim=270, elev=90) - self.ax._axis3don = False - def addVerts(self, verts, **kwargs): """Plots planes, defined by the vertices provided. diff --git a/defdap/quat.py b/defdap/quat.py index cd6028b..568978b 100755 --- a/defdap/quat.py +++ b/defdap/quat.py @@ -573,6 +573,13 @@ def plotUnitCell(self, plot = plotting.CrystalPlot( ax=ax, fig=fig, makeInteractive=makeInteractive ) + + plot.ax.set_xlim3d(-0.15, 0.15) + plot.ax.set_ylim3d(-0.15, 0.15) + plot.ax.set_zlim3d(-0.15, 0.15) + plot.ax.view_init(azim=270, elev=90) + plot.ax._axis3don = False + plot.addVerts(planes, **plotParams) return plot From 4780edb4807f3d1b9889204142b65b0890f155c8 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Fri, 26 Feb 2021 16:43:15 +0000 Subject: [PATCH 17/33] Add save/load support to grainInspector - Add option to save drawn lines and detected slip planes to a file - Add option to load this file back in --- defdap/inspector.py | 208 ++++++++++++++++++++++++++++++++++---------- defdap/plotting.py | 7 +- 2 files changed, 169 insertions(+), 46 deletions(-) diff --git a/defdap/inspector.py b/defdap/inspector.py index 74542d8..0b2b48a 100644 --- a/defdap/inspector.py +++ b/defdap/inspector.py @@ -15,6 +15,8 @@ import numpy as np +import ast + from typing import List from scipy.stats import linregress @@ -41,6 +43,7 @@ def __init__(self, self.currDICGrain = self.currMap[self.grainID] self.currEBSDGrain = self.currDICGrain.ebsdGrain self.vmax = vmax + self.filename = str(self.currMap.retrieveName()) + '_RDR.txt' # Draw the figure self.draw() @@ -52,18 +55,32 @@ def draw(self): # 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.81, 0.02, 0.1, 0.04)) - self.plot.addButton('Clear\nAll Lines', self.clearAllLines, (0.89, 0.48, 0.05, 0.04)) + ######## 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='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\non group:', - loc=(0.78, 0.07, 0.05, 0.04), submitHandler=self.runRDRGroup) + 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)) @@ -102,19 +119,27 @@ def saveLine(self, plot): """ Save the start point, end point and angle of drawn line into the grain. + pointsList: + List of drawn lines: [(x0,y0, x1, y1), angle, group] + 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.drawnLine.points[3]-self.drawnLine.points[1], self.drawnLine.points[2]-self.drawnLine.points[0])) if lineAngle > 180: lineAngle -= 180 elif lineAngle < 0: lineAngle += 180 #lineAngle -= np.rad2deg(self.currMap.ebsdTransform.rotation) - + + # Two decimal places + self.drawnLine.points = [float("{:.2f}".format(point)) for point in self.drawnLine.points] + lineAngle = float("{:.2f}".format(lineAngle)) + # Save drawn line to the DIC grain self.currDICGrain.pointsList.append([self.drawnLine.points, lineAngle, -1]) @@ -122,46 +147,58 @@ def saveLine(self, self.groupLines() self.redraw() - def groupLines(self): + 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. - pointsList: 0 = points, 1 = angle, 2 = group - groupsList: 0 = id, 1 = angle, 2 = active planes, 3 = angular deviation + groupsList is a list of line groups: [id, angle, [slip plane id], [angular deviation] + + Parameters + ---------- + grain + Grain for which to group the slip lines. """ - for i, line in enumerate(self.currDICGrain.pointsList): - angle = line[1] - if i == 0: - line[2]=0 # Make group 0 for first detected angle - self.currDICGrain.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 self.currDICGrain.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 self.currDICGrain.groupsList])-angle)) - self.currDICGrain.pointsList[i][2]=group - newAv = np.average([x[1] for x in self.currDICGrain.pointsList if x[2]==group]) - self.currDICGrain.groupsList[group][1] = newAv - else: - # Make new group and set - self.currDICGrain.groupsList.append([nextGroup, angle, 0, 0, 0]) - line[2]=nextGroup - nextGroup += 1 - - # Detect active slip systems in each group - for group in self.currDICGrain.groupsList: - activePlanes = [] - deviation = [] - experimentalAngle = group[1] - for idx, theoreticalAngle in enumerate(np.rad2deg(self.currEBSDGrain.slipTraceAngles)): - if theoreticalAngle-5 < experimentalAngle < theoreticalAngle+5: - activePlanes.append(idx) - deviation.append(experimentalAngle-theoreticalAngle) - group[2] = activePlanes - group[3] = deviation + 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 = 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]) + 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(experimentalAngle-theoreticalAngle) + group[2] = activePlanes + group[3] = deviation def clearAllLines(self, event, @@ -187,6 +224,7 @@ def removeLine(self, """ ## Remove single line del self.currDICGrain.pointsList[int(event)] + self.groupLines() self.redraw() def redraw(self): @@ -499,3 +537,85 @@ def plotRDR(self, self.rdrPlot.numLineAx.set_ylim(slope-1, slope+1) self.rdrPlot.numLineAx.set_xlim(-0.01, 0.05) + + def updateFilename(self, + event: str, + plot): + """ Update class variable filename, based on text input from textbox handler. + + event: + Text in textbox. + + """ + + self.filename = event + + 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') + + 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]) + + print(indexlist) + + # 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() diff --git a/defdap/plotting.py b/defdap/plotting.py index aa3be2e..030dd84 100644 --- a/defdap/plotting.py +++ b/defdap/plotting.py @@ -120,7 +120,7 @@ def addButton(self, label, clickHandler, loc=(0.8, 0.0, 0.1, 0.07), **kwargs): self.btnStore.append(btn) - def addTextBox(self, label, submitHandler, loc=(0.8, 0.0, 0.1, 0.07), **kwargs): + def addTextBox(self, label, submitHandler=None, changeHandler=None, loc=(0.8, 0.0, 0.1, 0.07), **kwargs): """Add a text box to the plot. Parameters @@ -142,7 +142,10 @@ def addTextBox(self, label, submitHandler, loc=(0.8, 0.0, 0.1, 0.07), **kwargs): self.checkInteractive() txtBoxAx = self.fig.add_axes(loc) txtBox = TextBox(txtBoxAx, label, **kwargs) - txtBox.on_submit(lambda e: submitHandler(e, self)) + if submitHandler != None: + txtBox.on_submit(lambda e: submitHandler(e, self)) + if changeHandler != None: + txtBox.on_text_change(lambda e: changeHandler(e, self)) self.txtBoxStore.append(txtBox) From 051865865d2153f969fa1bca2d45dadca6dcab8f Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Fri, 26 Feb 2021 17:48:03 +0000 Subject: [PATCH 18/33] Keep band slope and MAD arrays from EBSD --- defdap/ebsd.py | 2 ++ defdap/file_readers.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/defdap/ebsd.py b/defdap/ebsd.py index a97fe1b..c5968b7 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -163,6 +163,8 @@ def loadData(self, fileName, dataType=None): dataDict = dataLoader.loadedData self.eulerAngleArray = dataDict['eulerAngle'] self.bandContrastArray = dataDict['bandContrast'] + self.bandSlopeArray = dataDict['bandSlope'] + self.meanAngularDeviationArray = dataDict['meanAngularDeviation'] self.phaseArray = dataDict['phase'] # write final status diff --git a/defdap/file_readers.py b/defdap/file_readers.py index 9e8553b..d3c62f0 100644 --- a/defdap/file_readers.py +++ b/defdap/file_readers.py @@ -169,7 +169,7 @@ def parsePhase(): 'BS': ('BS', 'uint8'), # Band Slope } - keepColNames = ('phase', 'ph1', 'phi', 'ph2', 'BC') + keepColNames = ('phase', 'ph1', 'phi', 'ph2', 'BC', 'BS', 'MAD') dataFormat = [] loadCols = [] try: @@ -190,6 +190,12 @@ def parsePhase(): self.loadedData['bandContrast'] = np.reshape( binData['BC'], (yDim, xDim) ) + self.loadedData['bandSlope'] = np.reshape( + binData['BS'], (yDim, xDim) + ) + self.loadedData['meanAngularDeviation'] = np.reshape( + binData['MAD'], (yDim, xDim) + ) self.loadedData['phase'] = np.reshape( binData['phase'], (yDim, xDim) ) @@ -361,6 +367,12 @@ def loadOxfordCRC(self, self.loadedData['bandContrast'] = np.reshape( binData['BC'], (yDim, xDim) ) + self.loadedData['bandSlope'] = np.reshape( + binData['BS'], (yDim, xDim) + ) + self.loadedData['meanAngularDeviation'] = np.reshape( + binData['MAD'], (yDim, xDim) + ) self.loadedData['phase'] = np.reshape( binData['phase'], (yDim, xDim) ) From 98d8fe407b3114e0a38f662e94aece33b83a2bc4 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Mon, 1 Mar 2021 12:49:37 +0000 Subject: [PATCH 19/33] Generate PB points and lines at same time as GB Generate phase boundary points and lines at same time as grain boundaries, in `findBoundaries` --- defdap/ebsd.py | 59 +++++++++++++++++++++++-------------------------- defdap/hrdic.py | 3 +++ 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/defdap/ebsd.py b/defdap/ebsd.py index c5968b7..f8a81ba 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -115,6 +115,9 @@ def __init__(self, fileName, dataType=None): self.boundariesX = None self.boundariesY = None self.boundaryLines = None + self.phaseBoundariesX = None + self.phaseBoundariesY = None + self.phaseBoundaryLines = None self.phaseBoundaries = None self.grains = None self.misOri = None @@ -787,25 +790,28 @@ def findBoundaries(self, boundDef=10): misOriX = 2 * np.arccos(misOriX) * 180 / np.pi misOriY = 2 * np.arccos(misOriY) * 180 / np.pi - # set boundary locations where misOriX or misOriY are greater + # GRAIN boundary POINTS where misOriX or misOriY are greater # than set value self.boundariesX = misOriX > boundDef self.boundariesY = misOriY > boundDef - # add in phase boundaries - phaseBoundariesX = np.not_equal(self.phaseArray, + # PHASE boundary POINTS + self.phaseBoundariesX = np.not_equal(self.phaseArray, np.roll(self.phaseArray, -1, axis=1)) - phaseBoundariesX[:, -1] = False - self.boundariesX = np.logical_or(self.boundariesX, phaseBoundariesX) - - phaseBoundariesY = np.not_equal(self.phaseArray, + self.phaseBoundariesX[:, -1] = False + self.phaseBoundariesY = np.not_equal(self.phaseArray, np.roll(self.phaseArray, -1, axis=0)) - phaseBoundariesY[-1, :] = False - self.boundariesY = np.logical_or(self.boundariesY, phaseBoundariesY) + self.phaseBoundariesY[-1, :] = False + self.phaseBoundaries = np.logical_or(self.phaseBoundariesX, self.phaseBoundariesY) + self.phaseBoundaries = -self.phaseBoundaries.astype(int) + # add PHASE boundary POINTS to GRAIN boundary POINTS + self.boundariesX = np.logical_or(self.boundariesX, self.phaseBoundariesX) + self.boundariesY = np.logical_or(self.boundariesY, self.phaseBoundariesY) self.boundaries = np.logical_or(self.boundariesX, self.boundariesY) self.boundaries = -self.boundaries.astype(int) + # generate GRAIN boundary LINES boundaryPoints = np.where(self.boundariesX) boundaryLinesX = [] for i, j in zip(*boundaryPoints): @@ -818,6 +824,19 @@ def findBoundaries(self, boundDef=10): self.boundaryLines = boundaryLinesX + boundaryLinesY + # generate PHASE boundary LINES + boundaryPoints = np.where(self.phaseBoundariesX) + phaseBoundaryLinesX = [] + for i, j in zip(*boundaryPoints): + phaseBoundaryLinesX.append(((j + 0.5, i - 0.5), (j + 0.5, i + 0.5))) + + boundaryPoints = np.where(self.phaseBoundariesY) + phaseBoundaryLinesY = [] + for i, j in zip(*boundaryPoints): + phaseBoundaryLinesY.append(((j - 0.5, i + 0.5), (j + 0.5, i + 0.5))) + + self.phaseBoundaryLines = phaseBoundaryLinesX + phaseBoundaryLinesY + yield 1. @reportProgress("constructing neighbour network") @@ -868,28 +887,6 @@ def buildNeighbourNetwork(self): self.neighbourNetwork = nn @reportProgress("finding phase boundaries") - def findPhaseBoundaries(self, treatNonIndexedAs=None): - """Finds boundaries in the phase map. - - Parameters - ---------- - treatNonIndexedAs : int - Value to assign to non-indexed points, defaults to -1. - - """ - # make new array shifted by one to left and up - phaseArrayShifted = np.full((self.yDim, self.xDim), -3) - phaseArrayShifted[:-1, :-1] = self.phaseArray[1:, 1:] - - if treatNonIndexedAs: - self.phaseArray[self.phaseArray == 0] = treatNonIndexedAs - phaseArrayShifted[phaseArrayShifted == 0] = treatNonIndexedAs - - # where shifted array not equal to starting array, set to -1 - self.phaseBoundaries = np.zeros((self.yDim, self.xDim)) - self.phaseBoundaries = np.where(np.not_equal(self.phaseArray, phaseArrayShifted), -1, 0) - - yield 1. def plotPhaseBoundaryMap(self, dilate=False, **kwargs): """Plot phase boundary map. diff --git a/defdap/hrdic.py b/defdap/hrdic.py index 7517e84..92b50d6 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -499,6 +499,9 @@ def linkEbsdMap(self, ebsdMap, transformType="affine", order=2): firstPointCoords = self.ebsdTransformInv(np.array(self.ebsdMap.boundaryLines)[:,0,:]) secondPointCoords = self.ebsdTransformInv(np.array(self.ebsdMap.boundaryLines)[:,1,:]) self.boundaryLines = np.dstack([firstPointCoords,secondPointCoords]).swapaxes(1,2) + firstPointCoords = np.round(self.ebsdTransformInv(np.array(self.ebsdMap.phaseBoundaryLines)[:, 0, :])+0.5)-0.5 + secondPointCoords = np.round(self.ebsdTransformInv(np.array(self.ebsdMap.phaseBoundaryLines)[:, 1, :])+0.5)-0.5 + self.phaseBoundaryLines = np.dstack([firstPointCoords, secondPointCoords]).swapaxes(1, 2) def checkEbsdLinked(self): """Check if an EBSD map has been linked. From 9a86e953b3cff5636a232f433c678455bdcaa579 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Mon, 1 Mar 2021 17:38:05 +0000 Subject: [PATCH 20/33] Refactor EBSD->DIC line warping for clarity --- defdap/hrdic.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/defdap/hrdic.py b/defdap/hrdic.py index 92b50d6..f7dd79d 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -496,12 +496,14 @@ def linkEbsdMap(self, ebsdMap, transformType="affine", order=2): ) # Transform the EBSD boundaryLines to DIC reference frame - firstPointCoords = self.ebsdTransformInv(np.array(self.ebsdMap.boundaryLines)[:,0,:]) - secondPointCoords = self.ebsdTransformInv(np.array(self.ebsdMap.boundaryLines)[:,1,:]) - self.boundaryLines = np.dstack([firstPointCoords,secondPointCoords]).swapaxes(1,2) - firstPointCoords = np.round(self.ebsdTransformInv(np.array(self.ebsdMap.phaseBoundaryLines)[:, 0, :])+0.5)-0.5 - secondPointCoords = np.round(self.ebsdTransformInv(np.array(self.ebsdMap.phaseBoundaryLines)[:, 1, :])+0.5)-0.5 - self.phaseBoundaryLines = np.dstack([firstPointCoords, secondPointCoords]).swapaxes(1, 2) + boundaryLineList = np.array(self.ebsdMap.boundaryLines).reshape(-1, 2) # Flatten to coord list + boundaryLines = self.ebsdTransformInv(boundaryLineList).reshape(-1, 2, 2) # Transform & reshape back + self.boundaryLines = np.round(boundaryLines - 0.5) + 0.5 # Round to nearest + + # Transform the EBSD phaseBoundaryLines to DIC reference frame + phaseBoundaryLineList = np.array(self.ebsdMap.phaseBoundaryLines).reshape(-1, 2) # Flatten to coord list + phaseBoundaryLines = self.ebsdTransformInv(phaseBoundaryLineList).reshape(-1, 2, 2) # Transform & reshape back + self.phaseBoundaryLines = np.round(phaseBoundaryLines - 0.5) + 0.5 # Round to nearest def checkEbsdLinked(self): """Check if an EBSD map has been linked. From fe5199e415b858b5500e0adc3d8c313af2c4aec8 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Thu, 4 Mar 2021 16:57:26 +0000 Subject: [PATCH 21/33] Improvements to inspector - Add option to add correction angle for slip trace analysis - Fix some minor bugs --- defdap/hrdic.py | 8 ++++++-- defdap/inspector.py | 18 ++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/defdap/hrdic.py b/defdap/hrdic.py index f7dd79d..dfe2ca3 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -977,16 +977,20 @@ def floodFill(self, x, y, grainIndex, points_left): return currentGrain - def runGrainInspector(self, vmax=0.1): + def runGrainInspector(self, vmax=0.1, corrAngle=None): """Run the grain inspector interactive tool. Parameters ---------- vmax : float Maximum value of the colour map. + corrAngle: float + Correction angle in degrees to subtract from measured angles to account + for small rotation between DIC and EBSD frames. Approximately the rotation + component of affine transform. """ - GrainInspector(currMap=self, vmax=vmax) + GrainInspector(currMap=self, vmax=vmax, corrAngle=corrAngle) class Grain(base.Grain): diff --git a/defdap/inspector.py b/defdap/inspector.py index 0b2b48a..d86ccdc 100644 --- a/defdap/inspector.py +++ b/defdap/inspector.py @@ -35,7 +35,8 @@ class GrainInspector: """ def __init__(self, currMap: 'hrdic.Map', - vmax: float = 0.1): + vmax: float = 0.1, + corrAngle = None): # Initialise some values self.grainID = 0 self.currMap = currMap @@ -48,6 +49,9 @@ def __init__(self, # Draw the figure self.draw() + if corrAngle is not None: + self.corrAngle = corrAngle + def draw(self): """ Draw the main window, buttons, text boxes and axes. @@ -134,7 +138,8 @@ def saveLine(self, self.drawnLine.points[2]-self.drawnLine.points[0])) if lineAngle > 180: lineAngle -= 180 elif lineAngle < 0: lineAngle += 180 - #lineAngle -= np.rad2deg(self.currMap.ebsdTransform.rotation) + if self.corrAngle is not None: + lineAngle -= self.corrAngle # Two decimal places self.drawnLine.points = [float("{:.2f}".format(point)) for point in self.drawnLine.points] @@ -179,12 +184,11 @@ def groupLines(self, # 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 = np.average([x[1] for x in grain.pointsList if x[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]) - grain.groupsList.append([nextGroup, angle, 0, 0, 0]) line[2]=nextGroup nextGroup += 1 @@ -196,7 +200,7 @@ def groupLines(self, for idx, theoreticalAngle in enumerate(np.rad2deg(grain.ebsdGrain.slipTraceAngles)): if theoreticalAngle-5 < experimentalAngle < theoreticalAngle+5: activePlanes.append(idx) - deviation.append(experimentalAngle-theoreticalAngle) + deviation.append(float('{0:.2f}'.format(experimentalAngle-theoreticalAngle))) group[2] = activePlanes group[3] = deviation @@ -257,7 +261,7 @@ def redraw(self): # Write grain info text self.grainInfoAx.clear() self.grainInfoAx.axis('off') - grainInfoText = 'Grain ID: {0} / {1}\n'.format(self.grainID, len(self.currMap.grainList)) + 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, @@ -604,8 +608,6 @@ def loadFile(self, numGroups = int(line.split(' ')[0]) indexlist.append([startIndex, grainID, numLines, numGroups]) - print(indexlist) - # Write data from file into grain for startIndex, grainID, numLines, numGroups in indexlist: startIndexLines = startIndex+2 From b62e5be418050790991404df37981b8fcc7eb491 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Thu, 4 Mar 2021 16:58:05 +0000 Subject: [PATCH 22/33] Improvements to histograms - Options for scatter (as before), step and bar - Options for logx, logy, loglog and linear --- defdap/plotting.py | 66 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/defdap/plotting.py b/defdap/plotting.py index 030dd84..26f6bea 100644 --- a/defdap/plotting.py +++ b/defdap/plotting.py @@ -22,6 +22,7 @@ from matplotlib_scalebar.scalebar import ScaleBar from mpl_toolkits.mplot3d.art3d import Poly3DCollection from mpl_toolkits.mplot3d import Axes3D +from matplotlib.ticker import FuncFormatter from skimage import morphology as mph @@ -1274,14 +1275,16 @@ class HistPlot(Plot): """ Class for creating a histogram. """ - def __init__(self, plotType="linear", density=True, fig=None, + def __init__(self, plotType = "scatter", axesType="linear", density=True, fig=None, ax=None, axParams={}, makeInteractive=False, **kwargs): """Initialise a histogram plot Parameters ---------- - plotType : str, {'log', 'None'}, optional - If 'log' is specified, logarithmic y scale is used. + plotType: str, {'scatter', 'bar', 'step'} + Type of plot to use + axesType : str, {'linear', 'logx', 'logy', 'loglog', 'None'}, optional + If 'log' is specified, logarithmic scale is used. density : If true, histogram is normalised such that the integral sums to 1. fig : matplotlib.figure.Figure @@ -1301,21 +1304,32 @@ def __init__(self, plotType="linear", density=True, fig=None, **kwargs ) - plotType = plotType.lower() - if plotType in ["linear", "log"]: - self.plotType = plotType + axesType = axesType.lower() + if axesType in ["linear", "logy", "logx", "loglog"]: + self.axesType = axesType else: raise ValueError("plotType must be linear or log.") + if plotType in ['scatter', 'bar', 'step']: + self.plotType = plotType + else: + raise ValueError("plotType must be scatter, bar or step.") + self.density = bool(density) # set y-axis label yLabel = "Normalised frequency" if self.density else "Frequency" - if self.plotType == "log": - yLabel = "ln({})".format(yLabel) self.ax.set_ylabel(yLabel) - def addHist(self, histData, bins=10, range=None, line='o', + # set axes to linear or log as appropriate and set to be numbers as opposed to scientific notation + if self.axesType == 'logx' or self.axesType == 'loglog': + self.ax.set_xscale("log") + self.ax.xaxis.set_major_formatter(FuncFormatter(lambda y, _: '{:.5g}'.format(y))) + if self.axesType == 'logy' or self.axesType == 'loglog': + self.ax.set_yscale("log") + self.ax.yaxis.set_major_formatter(FuncFormatter(lambda y, _: '{:.5g}'.format(y))) + + def addHist(self, histData, bins=100, range=None, line='o', label=None, **kwargs): """Add a histogram to the current plot @@ -1336,15 +1350,25 @@ def addHist(self, histData, bins=10, range=None, line='o', """ - hist = np.histogram(histData.flatten(), bins=bins, range=range, - density=self.density) + # Generate the x bins with appropriate spaceing for linear or log + if self.axesType == 'logx' or self.axesType == 'loglog': + binList = np.logspace(np.log10(range[0]), np.log10(range[1]), bins) + else: + binList = np.linspace(range[0], range[1], bins) + - yVals = hist[0] - if self.plotType == "log": - yVals = np.log(yVals) - xVals = 0.5 * (hist[1][1:] + hist[1][:-1]) + if self.plotType == 'scatter': + # Generate the histogram data and plot as a scatter plot + hist = np.histogram(histData.flatten(), bins=binList, density=self.density) + yVals = hist[0] + xVals = 0.5 * (hist[1][1:] + hist[1][:-1]) - self.ax.plot(xVals, yVals, line, label=label, **kwargs) + self.ax.plot(xVals, yVals, line, label=label, **kwargs) + + else: + # Plot as a matplotlib histogram + self.ax.hist(histData.flatten(),bins=binList, histtype=self.plotType, + density=self.density, label=label, **kwargs) def addLegend(self, **kwargs): """Add legend to histogram. @@ -1361,7 +1385,7 @@ def addLegend(self, **kwargs): def create( cls, histData, fig=None, figParams={}, ax=None, axParams={}, plot=None, makeInteractive=False, - plotType="linear", density=True, bins=10, range=None, + plotType = "scatter", axesType="linear", density=True, bins=10, range=None, line='o', label=None, **kwargs ): """Create a histogram plot. @@ -1382,8 +1406,10 @@ def create( Plot where histgram is created. If none, a new plot is created. makeInteractive : bool, optional If true, make plot interactive. - plotType : str, {'log', 'None'}, optional - If 'log' is specified, logarithmic y scale is used. + plotType: str, {'scatter', 'bar', 'barfilled', 'step'} + Type of plot to use + axesType : str, {'linear', 'logx', 'logy', 'loglog', 'None'}, optional + If 'log' is specified, logarithmic scale is used. density : If true, histogram is normalised such that the integral sums to 1. bins : int @@ -1403,7 +1429,7 @@ def create( """ if plot is None: - plot = cls(plotType=plotType, density=density, fig=fig, ax=ax, + plot = cls(axesType=axesType, plotType=plotType, density=density, fig=fig, ax=ax, axParams=axParams, makeInteractive=makeInteractive, **figParams) plot.addHist(histData, bins=bins, range=range, line=line, From 07625a54ac68151733cbcd235219e8b00d44d593 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Fri, 5 Mar 2021 16:25:46 +0000 Subject: [PATCH 23/33] Move addArrow so it works on any plot --- defdap/plotting.py | 93 +++++++++++++++++++++------------------------- 1 file changed, 43 insertions(+), 50 deletions(-) diff --git a/defdap/plotting.py b/defdap/plotting.py index 26f6bea..d46111e 100644 --- a/defdap/plotting.py +++ b/defdap/plotting.py @@ -58,6 +58,7 @@ def __init__(self, ax, axParams={}, fig=None, makeInteractive=False, else: self.ax = ax self.colourBar = None + self.arrow = None if title is not None: self.setTitle(title) @@ -173,6 +174,48 @@ def addText(self, ax, x, y, txt, **kwargs): txt = ax.text(x, y, txt, **kwargs) self.txtStore.append(txt) + def addArrow(self, startEnd, persistent=False, clearPrev=True, label=None): + """Add arrow to grain plot. + + Parameters + ---------- + startEnd: 4-tuple + Starting (x, y), Ending (x, y). + persistent : + If persistent, do not clear arrow with clearPrev. + clearPrev : + Clear all non-persistent arrows. + label + Label to place near arrow. + + """ + + arrowParams = { + 'xy': startEnd[0:2], # Arrow start coordinates + 'xycoords': 'data', + 'xytext': startEnd[2:4], # Arrow end coordinates + 'textcoords': 'data', + 'arrowprops': dict(arrowstyle="<-", connectionstyle="arc3", + color='red', alpha=0.7, linewidth=2, + shrinkA=0, shrinkB=0) + } + + # If persisent, add the arrow onto the plot directly + if persistent: + self.ax.annotate("", **arrowParams) + + # If not persistent, save a reference so that it can be removed later + if not persistent: + if clearPrev and (self.arrow is not None): self.arrow.remove() + if None not in startEnd: + self.arrow = self.ax.annotate("", **arrowParams) + + # Add a label if specified + if label is not None: + self.ax.annotate(label, xy=startEnd[2:4], xycoords='data', + xytext=(15, 15), textcoords='offset pixels', + c='red', fontsize=14, fontweight='bold') + def setSize(self, size): """Set size of plot. @@ -677,62 +720,12 @@ def addMap(self, mapData, vmin=None, vmax=None, cmap='viridis', **kwargs): img = self.ax.imshow(mapData, vmin=vmin, vmax=vmax, interpolation='None', cmap=cmap, **kwargs) self.draw() - self.arrow = None self.imgLayers.append(img) return img - def addArrow(self, startEnd, persistent=False, clearPrev=True, label=None): - """Add arrow to grain plot. - - Parameters - ---------- - startEnd: 4-tuple - Starting (x, y), Ending (x, y). - persistent : - If persistent, do not clear arrow with clearPrev. - clearPrev : - Clear all non-persistent arrows. - label - Label to place near arrow. - - """ - - x0 = startEnd[0] - y0 = startEnd[1] - x1 = startEnd[2] - y1 = startEnd[3] - - if persistent: - self.ax.annotate( - "", xy=(x0, y0), xycoords='data', xytext=(x1, y1), - textcoords='data', arrowprops=dict( - arrowstyle="<-", connectionstyle="arc3", - color='red', alpha=0.7, linewidth=2 - ) - ) - if not persistent: - if clearPrev: - if self.arrow is not None: - self.arrow.remove() - - if None in (x0, y0, x1, y1): - pass - else: - self.arrow = self.ax.annotate( - "", xy=(x0, y0), xycoords='data', xytext=(x1, y1), - textcoords='data', arrowprops=dict( - arrowstyle="<-", connectionstyle="arc3", - color='red',alpha=0.7,linewidth=2 - ) - ) - - if label is not None: - self.ax.annotate(label, xy=(x1, y1), xycoords='data', - xytext=(15, 15), textcoords='offset pixels', - c='red', fontsize=12) def addColourBar(self, label, layer=0, **kwargs): """Add colour bar to grain plot. From 361ba2b92872f01fd7408095647b76c1aab76c9f Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Fri, 5 Mar 2021 18:14:04 +0000 Subject: [PATCH 24/33] Simplify and move click/drag handler out of inspector - Turn class into function - Use built in `plot.addEventHandler` - Will be useful if other tools require click/drag --- defdap/inspector.py | 16 +++++++-------- defdap/plotting.py | 50 ++++++++++++++++++++------------------------- 2 files changed, 29 insertions(+), 37 deletions(-) diff --git a/defdap/inspector.py b/defdap/inspector.py index d86ccdc..d96477c 100644 --- a/defdap/inspector.py +++ b/defdap/inspector.py @@ -23,7 +23,7 @@ from scipy.stats._stats_mstats_common import LinregressResult import pandas as pd -from defdap.plotting import Plot, GrainPlot, LineSlice +from defdap.plotting import Plot, GrainPlot from defdap import hrdic @@ -123,9 +123,6 @@ def saveLine(self, plot): """ Save the start point, end point and angle of drawn line into the grain. - pointsList: - List of drawn lines: [(x0,y0, x1, y1), angle, group] - Parameters ---------- event @@ -134,19 +131,19 @@ def saveLine(self, """ # Get angle of lines - lineAngle = 90-np.rad2deg(np.arctan2(self.drawnLine.points[3]-self.drawnLine.points[1], - self.drawnLine.points[2]-self.drawnLine.points[0])) + 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 if self.corrAngle is not None: lineAngle -= self.corrAngle # Two decimal places - self.drawnLine.points = [float("{:.2f}".format(point)) for point in self.drawnLine.points] + 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([self.drawnLine.points, lineAngle, -1]) + self.currDICGrain.pointsList.append([points, lineAngle, -1]) # Group lines and redraw self.groupLines() @@ -269,7 +266,8 @@ def redraw(self): self.plot.addText(self.grainInfoAx, 0, 1, grainInfoText, va='top', ha='left', fontsize=10) # Detect lines - self.drawnLine = LineSlice(ax=self.maxShearAx, fig=self.plot.fig, action=self.grainPlot.addArrow) + 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)) # Write lines text and draw lines linesTxt = 'List of lines\n\nLineID x0 y0 x1 y1 Angle Group\n' diff --git a/defdap/plotting.py b/defdap/plotting.py index d46111e..9b39cc8 100644 --- a/defdap/plotting.py +++ b/defdap/plotting.py @@ -49,6 +49,8 @@ def __init__(self, ax, axParams={}, fig=None, makeInteractive=False, self.btnStore = [] self.txtStore = [] self.txtBoxStore = [] + self.p1=[];self.p2=[]; + else: self.fig = fig # TODO: flag for new figure @@ -238,6 +240,26 @@ def setTitle(self, txt): """ self.fig.canvas.set_window_title(txt) + def lineSlice(self, event, plot): + """ Catch click and drag then draw an arrow. + + Examples + ---------- + To use, add a click and release event handler to your plot, pointing to this function: + + >>> plot.addEventHandler('button_press_event',lambda e, p: lineSlice(e, p)) + >>> plot.addEventHandler('button_release_event', lambda e, p: lineSlice(e, p)) + + """ + + if event.inaxes is self.ax: + if event.name == 'button_press_event': + self.p1 = (event.xdata, event.ydata) # save 1st point + elif event.name == 'button_release_event': + self.p2 = (event.xdata, event.ydata) # save 2nd point + self.addArrow(startEnd=(self.p1[0], self.p1[1], self.p2[0], self.p2[1])) + self.fig.canvas.draw_idle() + @property def exists(self): self.checkInteractive() @@ -651,34 +673,6 @@ def create( return plot - -class LineSlice: - """ Class to catch click and drag and return start and end positions. - - """ - def __init__(self, fig, ax, action): - self.p1=[0,0] - self.p2=[0,0] - self.ax = ax - self.cidclick = plt.connect('button_press_event', self) - self.cidrelease = plt.connect('button_release_event', self) - self.action = action - self.fig=fig - - def __call__(self, event): - if event.name == 'button_press_event': - self.p1 = (event.xdata, event.ydata) # save 1st point - elif event.name == 'button_release_event': - self.p2 = (event.xdata, event.ydata) # save 2nd point - - self.action(startEnd=(self.p1[0], self.p1[1], self.p2[0], self.p2[1])) - self.fig.canvas.draw() - - self.points = (self.p1[0], self.p1[1], self.p2[0], self.p2[1]) - - return self.p1[0], self.p1[1], self.p2[0], self.p2[1] - - class GrainPlot(Plot): """ Class for creating a map for a grain. From 1d0b1635a5ced80f1cde891ba042ded477b9a44e Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Fri, 5 Mar 2021 18:40:51 +0000 Subject: [PATCH 25/33] Calculate misorientation if necessary when plotting --- defdap/ebsd.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/defdap/ebsd.py b/defdap/ebsd.py index f8a81ba..d322640 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -1169,6 +1169,9 @@ def plotMisOriMap(self, component=0, **kwargs): self.misOri = np.ones([self.yDim, self.xDim]) if component in [1, 2, 3]: + # Calculate misorientation axis if not calculated + if np.any([grain.misOriAxisList is None for grain in self.grainList]): + self.calcGrainMisOri(calcAxis = True) for grain in self.grainList: for coord, misOriAxis in zip(grain.coordList, np.array(grain.misOriAxisList)): self.misOri[coord[1], coord[0]] = misOriAxis[component - 1] @@ -1178,6 +1181,9 @@ def plotMisOriMap(self, component=0, **kwargs): ['X', 'Y', 'Z'][component-1] ) else: + # Calculate misorientation if not calculated + if np.any([grain.misOriList is None for grain in self.grainList]): + self.calcGrainMisOri(calcAxis = False) for grain in self.grainList: for coord, misOri in zip(grain.coordList, grain.misOriList): self.misOri[coord[1], coord[0]] = misOri @@ -1536,21 +1542,22 @@ def plotMisOri(self, component=0, **kwargs): 'plotColourBar': True } if component == 0: + if self.misOriList is None: self.buildMisOriList() plotParams['clabel'] = "Grain reference orientation " \ "deviation (GROD) ($^\circ$)" - plotData = 2 * np.arccos(self.misOriList) + plotData = np.rad2deg(2 * np.arccos(self.misOriList)) elif 0 < component < 4: + if self.misOriAxisList is None: self.buildMisOriList(calcAxis=True) plotParams['clabel'] = "Rotation around {:} ($^\circ$)".format( ['X', 'Y', 'Z'][component-1] ) - plotData = np.array(self.misOriAxisList)[:, component-1] + plotData = np.rad2deg(np.array(self.misOriAxisList)[:, component-1]) else: raise ValueError("Component must between 0 and 3") plotParams.update(kwargs) - plotData *= 180 / np.pi plot = self.plotGrainData(grainData=plotData, **plotParams) return plot From ece673b123871ab66560cfac6283172b97d08777 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Fri, 5 Mar 2021 18:41:10 +0000 Subject: [PATCH 26/33] Split redraw into two for speed --- defdap/inspector.py | 50 +++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/defdap/inspector.py b/defdap/inspector.py index d96477c..b75655f 100644 --- a/defdap/inspector.py +++ b/defdap/inspector.py @@ -147,7 +147,7 @@ def saveLine(self, # Group lines and redraw self.groupLines() - self.redraw() + self.redrawLine() def groupLines(self, grain: 'defdap.hrdic.Grain'=None): @@ -229,27 +229,14 @@ def removeLine(self, self.redraw() def redraw(self): - """ - Draw items which need to be redrawn often (i.e. when changing grain ID). + """Draw items which need to be redrawn when changing grain ID. """ # Plot max shear for grain self.maxShearAx.clear() - grainPlot = self.currMap[self.grainID].plotMaxShear( - fig=self.plot.fig,ax=self.maxShearAx, vmax=self.vmax, plotColourBar=False, plotScaleBar=True) - - # 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))) + 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() @@ -269,31 +256,50 @@ def redraw(self): 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() + + 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]) + 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]) + 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) - def runRDRGroup(self, + # 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))) + + def runRDRGroup(self, event: int, plot): """ Run RDR on a specified group, upon submitting a text box. From fa41086740a5b07a22c1918bacfe10b066dd950b Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Sat, 6 Mar 2021 13:01:09 +0000 Subject: [PATCH 27/33] Update example_notebook.ipynb - HistPlot has arguments: - `histType` which can be `scatter`, `bar` or `step` - `axesType` which can be `linear`, `logy`, `logx`, `loglog` --- example_notebook.ipynb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example_notebook.ipynb b/example_notebook.ipynb index ddf8b79..6eb0631 100644 --- a/example_notebook.ipynb +++ b/example_notebook.ipynb @@ -903,8 +903,8 @@ "# add histrogram of strain values\n", "plot2 = HistPlot.create(\n", " dicMap.crop(dicMap.eMaxShear),\n", - " ax=ax2, fig=fig,\n", - " plotType=\"log\", bins=50, range=(0,0.06)\n", + " ax=ax2, fig=fig, marker='o', markersize=2,\n", + " axesType=\"logy\", bins=50, range=(0,0.06)\n", ")\n", "plot2.ax.set_xlabel(\"Effective shear strain\")" ] @@ -963,7 +963,7 @@ "plot3 = HistPlot.create(\n", " dicMap.crop(dicMap.eMaxShear),\n", " ax=ax3, fig=fig,\n", - " plotType=\"log\", bins=50, range=(0,0.06))\n", + " axesType=\"logy\", bins=50, range=(0,0.06))\n", " \n", "plot3.ax.set_xlabel(\"Effective shear strain\")\n", "\n", @@ -995,7 +995,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.7" + "version": "3.7.4" } }, "nbformat": 4, From 31c4ce999707405a876bd37e1c06afe0f5e9a9f5 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Sat, 6 Mar 2021 17:16:05 +0000 Subject: [PATCH 28/33] Add component dictionary - Add general `plotMap` functions - Refactor `printStatsTable` --- defdap/hrdic.py | 96 +++++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/defdap/hrdic.py b/defdap/hrdic.py index dfe2ca3..43e0dab 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -175,6 +175,12 @@ def __init__(self, path, fname, dataType=None): # max shear component self.eMaxShear = np.sqrt(((self.e11 - self.e22) / 2.)**2 + self.e12**2) + # Dictionary references to all map types + self.component = {'f11': self.f11, 'f12': self.f12, 'f21': self.f21, 'f22': self.f22, + 'e11': self.e11, 'e12': self.e12, 'e22': self.e22, + 'eMaxShear': self.eMaxShear, + 'x_map': self.x_map, 'y_map': self.y_map} + # crop distances (default all zeros) self.cropDists = np.array(((0, 0), (0, 0)), dtype=int) @@ -296,57 +302,37 @@ def printStatsTable(self, percentiles, components): Parameters ---------- - percentiles : list - list of percentiles to print (number, Min, Mean or Max). + percentiles : list(float) + list of percentiles to print i.e. 0, 50, 99. components : list(str) - list of map components to print i.e. f11, mss. + list of map components to print i.e. e11, f11, eMaxShear, x_map. """ + # Check that components are valid + if set(components).issubset(self.component) is False: + strFormat = ('{}, ') * (len(self.component) - 1) + ('{}') + raise Exception("Components must be: " + strFormat.format(*self.component)) + # Print map info - print('\033[1m', end='') # START BOLD + print('\033[1m', end=''), # START BOLD print("{0} (dimensions: {1} x {2} pixels, sub-window size: {3} " "x {3} pixels, number of points: {4})\n".format( self.retrieveName(), self.xDim, self.yDim, self.binning, self.xDim * self.yDim )) - # Print table header - print("Component\t".format(), end="") - for x in percentiles: - if x == 'Min' or x == 'Mean' or x == 'Max': - print("{0}\t".format(x), end='') - else: - print("P{0}\t".format(x), end='') - print('\033[0m', end='') # END BOLD - print() + # Print header + strFormat = ('{:10} ') + (len(percentiles)) * '{:12}' + print(strFormat.format(*(['Component'] + percentiles))) + print('\033[0m', end='') # END BOLD # Print table + strFormat = ('{:10} ') + (len(percentiles)) * '{:12.4f}' for c in components: - selmap = [] - if c == 'mss': - selmap = self.crop(self.eMaxShear) * 100 - if c == 'e11': - selmap = self.crop(self.e11) * 100 - if c == 'e12': - selmap = self.crop(self.e12) * 100 - if c == 'e22': - selmap = self.crop(self.e22) * 100 - plist = [] - for p in percentiles: - if p == 'Min': - plist.append(np.nanmin(selmap)) - elif p == 'Mean': - plist.append(np.nanmean(selmap)) - elif p == 'Max': - plist.append(np.nanmax(selmap)) - else: - plist.append(np.nanpercentile(selmap, p)) - print("{0}\t\t".format(c), end="") - for l in plist: - print("{0:.2f}\t".format(l), end='') - print() - print() + # Get the values and print in table + per = [np.nanpercentile(self.crop(self.component[c]), p) for p in percentiles] + print(strFormat.format(*([c] + per))) def setCrop(self, xMin=None, xMax=None, yMin=None, yMax=None, updateHomogPoints=False): """Set a crop for the DIC map. @@ -735,28 +721,54 @@ def plotPattern(self, **kwargs): return plot - def plotMaxShear(self, **kwargs): - """Plot a map of maximum shear strain. + def plotMap(self, component, **kwargs): + """Plot a map from the DIC data. Parameters ---------- + component + Map component to plot i.e. e11, f11, eMaxShear. kwargs All arguments are passed to :func:`defdap.plotting.MapPlot.create`. Returns ------- defdap.plotting.MapPlot - Plot containing BSE image of map. + Plot containing map. """ # Set default plot parameters then update with any input plotParams = { 'plotColourBar': True, - 'clabel': "Effective shear strain" + 'clabel': component } plotParams.update(kwargs) - plot = MapPlot.create(self, self.crop(self.eMaxShear), **plotParams) + plot = MapPlot.create(self, self.crop(self.component[component]), **plotParams) + + return plot + + def plotMaxShear(self, **kwargs): + """Plot a map of maximum shear strain. + + Parameters + ---------- + kwargs + All arguments are passed to :func:`defdap.hrdic.plotMap`. + + Returns + ------- + defdap.plotting.MapPlot + Plot containing map. + + """ + + params = { + 'clabel': 'Effective Shear Strain' + } + params.update(kwargs) + + plot = self.plotMap('eMaxShear', **params) return plot From f871642fdf757010b4b6a160fa5bdaae8dbd4972 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Sat, 6 Mar 2021 17:17:09 +0000 Subject: [PATCH 29/33] Update copyright --- LICENSE | 2 +- defdap/base.py | 2 +- defdap/crystal.py | 2 +- defdap/ebsd.py | 2 +- defdap/file_readers.py | 2 +- defdap/file_writers.py | 2 +- defdap/hrdic.py | 2 +- defdap/inspector.py | 2 +- defdap/plotting.py | 2 +- defdap/quat.py | 2 +- defdap/utils.py | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/LICENSE b/LICENSE index 4349619..578dde6 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2020 Mechanics of Microstructures Group + Copyright 2021 Mechanics of Microstructures Group at The University of Manchester Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/defdap/base.py b/defdap/base.py index 39cbbfe..9de773b 100755 --- a/defdap/base.py +++ b/defdap/base.py @@ -1,4 +1,4 @@ -# Copyright 2020 Mechanics of Microstructures Group +# Copyright 2021 Mechanics of Microstructures Group # at The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/defdap/crystal.py b/defdap/crystal.py index e6a353f..354ba4d 100755 --- a/defdap/crystal.py +++ b/defdap/crystal.py @@ -1,4 +1,4 @@ -# Copyright 2020 Mechanics of Microstructures Group +# Copyright 2021 Mechanics of Microstructures Group # at The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/defdap/ebsd.py b/defdap/ebsd.py index d322640..f9e938e 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -1,4 +1,4 @@ -# Copyright 2020 Mechanics of Microstructures Group +# Copyright 2021 Mechanics of Microstructures Group # at The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/defdap/file_readers.py b/defdap/file_readers.py index d3c62f0..e852224 100644 --- a/defdap/file_readers.py +++ b/defdap/file_readers.py @@ -1,4 +1,4 @@ -# Copyright 2020 Mechanics of Microstructures Group +# Copyright 2021 Mechanics of Microstructures Group # at The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/defdap/file_writers.py b/defdap/file_writers.py index ebeecf0..987e3b7 100644 --- a/defdap/file_writers.py +++ b/defdap/file_writers.py @@ -1,4 +1,4 @@ -# Copyright 2020 Mechanics of Microstructures Group +# Copyright 2021 Mechanics of Microstructures Group # at The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/defdap/hrdic.py b/defdap/hrdic.py index 43e0dab..e58c686 100755 --- a/defdap/hrdic.py +++ b/defdap/hrdic.py @@ -1,4 +1,4 @@ -# Copyright 2020 Mechanics of Microstructures Group +# Copyright 2021 Mechanics of Microstructures Group # at The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/defdap/inspector.py b/defdap/inspector.py index b75655f..be6d67a 100644 --- a/defdap/inspector.py +++ b/defdap/inspector.py @@ -1,4 +1,4 @@ -# Copyright 2020 Mechanics of Microstructures Group +# Copyright 2021 Mechanics of Microstructures Group # at The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/defdap/plotting.py b/defdap/plotting.py index 9b39cc8..aeb3987 100644 --- a/defdap/plotting.py +++ b/defdap/plotting.py @@ -1,4 +1,4 @@ -# Copyright 2020 Mechanics of Microstructures Group +# Copyright 2021 Mechanics of Microstructures Group # at The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/defdap/quat.py b/defdap/quat.py index 568978b..42ea821 100755 --- a/defdap/quat.py +++ b/defdap/quat.py @@ -1,4 +1,4 @@ -# Copyright 2020 Mechanics of Microstructures Group +# Copyright 2021 Mechanics of Microstructures Group # at The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/defdap/utils.py b/defdap/utils.py index c2565aa..fda5a54 100644 --- a/defdap/utils.py +++ b/defdap/utils.py @@ -1,4 +1,4 @@ -# Copyright 2020 Mechanics of Microstructures Group +# Copyright 2021 Mechanics of Microstructures Group # at The University of Manchester # # Licensed under the Apache License, Version 2.0 (the "License"); From dd578f351722993828e8c81525bcd4ec2f8cb610 Mon Sep 17 00:00:00 2001 From: Rhys Thomas Date: Mon, 8 Mar 2021 12:38:52 +0000 Subject: [PATCH 30/33] Add `drawLineProfile` function Click and drag, then plot an intensity profile across the line --- defdap/base.py | 49 +++++++++++++++++++++++++++++++++++++++++++++- defdap/plotting.py | 16 +++++++++++++-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/defdap/base.py b/defdap/base.py index 9de773b..7e6fa68 100755 --- a/defdap/base.py +++ b/defdap/base.py @@ -18,7 +18,9 @@ from defdap.quat import Quat from defdap import plotting -from defdap.plotting import MapPlot, GrainPlot +from defdap.plotting import Plot, MapPlot, GrainPlot + +from skimage.measure import profile_line from defdap.utils import reportProgress @@ -48,6 +50,7 @@ def __init__(self): self.neighbourNetwork = None self.grainPlot = None + self.profilePlot = None def __len__(self): return len(self.grainList) @@ -178,6 +181,50 @@ def clickGrainID(self, event, plot, displaySelected): currGrain.plotDefault(plot=self.grainPlot) self.grainPlot.draw() + def drawLineProfile(self, **kwargs): + """Interactive plot for drawing a line profile of data. + + """ + plot = self.plotDefault(makeInteractive=True, **kwargs) + + plot.addEventHandler('button_press_event', plot.lineSlice) + plot.addEventHandler('button_release_event', lambda e, p: plot.lineSlice(e, p, + action=self.calcLineProfile)) + + return plot + + def calcLineProfile(self, plot, startEnd, **kwargs): + """Calculate and plot the line profile. + + Parameters + ---------- + plot : defdap.plotting.Plot + Plot to calculate the line profile for. + startEnd : array_like + Selected points (x0, y0, x1, y1). + + """ + + x0, y0 = startEnd[0:2] + x1, y1 = startEnd[2:4] + profile_length = np.sqrt((y1 - y0) ** 2 + (x1 - x0) ** 2) + + # Extract the values along the line + zi = profile_line(plot.imgLayers[0].get_array(), + (startEnd[1], startEnd[0]), (startEnd[3], startEnd[2]), + mode='nearest', **kwargs) + xi = np.linspace(0, profile_length, len(zi)) + + if self.profilePlot is None or not self.profilePlot.exists: + self.profilePlot = Plot(makeInteractive=True) + else: + self.profilePlot.clear() + + self.profilePlot.ax.plot(xi, zi, **kwargs) + self.profilePlot.ax.set_xlabel('Distance (pixels)') + self.profilePlot.ax.set_ylabel('Intensity') + self.profilePlot.draw() + def setHomogPoint(self, binSize=1, points=None, **kwargs): """ Interactive tool to set homologous points. Right-click on a point diff --git a/defdap/plotting.py b/defdap/plotting.py index aeb3987..3c66268 100644 --- a/defdap/plotting.py +++ b/defdap/plotting.py @@ -35,7 +35,7 @@ class Plot(object): """ Class used for creating and manipulating plots. """ - def __init__(self, ax, axParams={}, fig=None, makeInteractive=False, + def __init__(self, ax=None, axParams={}, fig=None, makeInteractive=False, title=None, **kwargs): self.interactive = makeInteractive if makeInteractive: @@ -240,9 +240,18 @@ def setTitle(self, txt): """ self.fig.canvas.set_window_title(txt) - def lineSlice(self, event, plot): + def lineSlice(self, event, plot, action=None): """ Catch click and drag then draw an arrow. + Parameters + ---------- + event : + Click event. + plot : defdap.plotting.Plot + Plot to capture clicks from. + action + Further action to perform. + Examples ---------- To use, add a click and release event handler to your plot, pointing to this function: @@ -260,6 +269,9 @@ def lineSlice(self, event, plot): self.addArrow(startEnd=(self.p1[0], self.p1[1], self.p2[0], self.p2[1])) self.fig.canvas.draw_idle() + if action is not None: + action(plot=self, startEnd=(self.p1[0], self.p1[1], self.p2[0], self.p2[1])) + @property def exists(self): self.checkInteractive() From e9631f7088b7a8c11bb5b1c81a5c46c3be9f2563 Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Thu, 11 Mar 2021 13:44:11 +0000 Subject: [PATCH 31/33] Remove explicit definition of number of phases when loading EBSD data and tidy up type hints --- defdap/ebsd.py | 6 ++- defdap/file_readers.py | 105 +++++++++++++++++++---------------------- defdap/file_writers.py | 15 +++--- 3 files changed, 61 insertions(+), 65 deletions(-) diff --git a/defdap/ebsd.py b/defdap/ebsd.py index f9e938e..d5129d0 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -108,7 +108,6 @@ def __init__(self, fileName, dataType=None): self.eulerAngleArray = None self.bandContrastArray = None self.quatArray = None - self.numPhases = None self.phaseArray = None self.phases = [] self.boundaries = None @@ -160,7 +159,6 @@ def loadData(self, fileName, dataType=None): self.xDim = metadataDict['xDim'] self.yDim = metadataDict['yDim'] self.stepSize = metadataDict['stepSize'] - self.numPhases = metadataDict['numPhases'] self.phases = metadataDict['phases'] dataDict = dataLoader.loadedData @@ -223,6 +221,10 @@ def cOverA(self): """ return self.primaryPhase.cOverA + @property + def numPhases(self): + return len(self.phases) or None + @property def primaryPhase(self): """Primary phase of the EBSD map. diff --git a/defdap/file_readers.py b/defdap/file_readers.py index e852224..d34f283 100644 --- a/defdap/file_readers.py +++ b/defdap/file_readers.py @@ -18,7 +18,7 @@ import pathlib import re -from typing import TextIO, Tuple +from typing import TextIO, Dict, List, Callable, Any, Type, Optional from defdap.crystal import Phase from defdap.quat import Quat @@ -28,13 +28,12 @@ class EBSDDataLoader(object): """Class containing methods for loading and checking EBSD data """ - def __init__(self): + def __init__(self) -> None: self.loadedMetadata = { 'xDim': 0, 'yDim': 0, 'stepSize': 0., 'acquisitionRotation': Quat(1.0, 0.0, 0.0, 0.0), - 'numPhases': 0, 'phases': [] } self.loadedData = { @@ -45,7 +44,7 @@ def __init__(self): self.dataFormat = None @staticmethod - def getLoader(dataType: str): + def getLoader(dataType: str) -> 'Type[EBSDDataLoader]': if dataType is None: dataType = "OxfordBinary" @@ -58,19 +57,16 @@ def getLoader(dataType: str): else: raise ValueError(f"No loader for EBSD data of type {dataType}.") - def checkMetadata(self): + def checkMetadata(self) -> None: """ Checks that the number of phases from metadata matches the amount of phases loaded. """ - if len(self.loadedMetadata['phases']) != self.loadedMetadata['numPhases']: - raise ValueError("Number of phases mismatch.") - for phase in self.loadedMetadata['phases']: assert type(phase) is Phase - def checkData(self): + def checkData(self) -> None: mapShape = (self.loadedMetadata['yDim'], self.loadedMetadata['xDim']) assert self.loadedData['phase'].shape == mapShape @@ -79,11 +75,13 @@ def checkData(self): class OxfordTextLoader(EBSDDataLoader): - def load(self, - fileName: str, + def load( + self, + fileName: str, fileDir: str = "" - ) -> Tuple[dict, dict]: - """ Read an Oxford Instruments .ctf file, which is a HKL single orientation file. + ) -> None: + """ Read an Oxford Instruments .ctf file, which is a HKL single + orientation file. Parameters ---------- @@ -104,7 +102,7 @@ def load(self, if not filePath.is_file(): raise FileNotFoundError("Cannot open file {}".format(filePath)) - def parsePhase(): + def parsePhase() -> Phase: lineSplit = line.split('\t') dims = lineSplit[0].split(';') dims = tuple(round(float(s), 3) for s in dims) @@ -138,7 +136,6 @@ def parsePhase(): acqEulers[2] = float(line.split()[-1]) elif 'Phases' in line: numPhases = int(line.split()[-1]) - self.loadedMetadata['numPhases'] = numPhases for j in range(numPhases): line = next(ctfFile) self.loadedMetadata['phases'].append(parsePhase()) @@ -209,15 +206,14 @@ def parsePhase(): self.checkData() - return self.loadedMetadata, self.loadedData - class OxfordBinaryLoader(EBSDDataLoader): - def load(self, - fileName: str, + def load( + self, + fileName: str, fileDir: str = "" - ) -> Tuple[dict, dict]: - """ Read Oxford Instruments .cpr/.crc file pair. + ) -> None: + """Read Oxford Instruments .cpr/.crc file pair. Parameters ---------- @@ -235,11 +231,7 @@ def load(self, self.loadOxfordCPR(fileName, fileDir=fileDir) self.loadOxfordCRC(fileName, fileDir=fileDir) - return self.loadedMetadata, self.loadedData - - def loadOxfordCPR(self, - fileName: str, - fileDir: str = ""): + def loadOxfordCPR(self, fileName: str, fileDir: str = "") -> None: """ Read an Oxford Instruments .cpr file, which is a metadata file describing EBSD data. @@ -265,7 +257,7 @@ def loadOxfordCPR(self, metadata = dict() groupPat = re.compile("\[(.+)\]") - def parseLine(line): + def parseLine(line: str) -> None: try: key, val = line.strip().split('=') groupDict[key] = val @@ -298,9 +290,9 @@ def parseLine(line): float(metadata['Acquisition Surface']['Euler2']) * np.pi / 180., float(metadata['Acquisition Surface']['Euler3']) * np.pi / 180. ) - self.loadedMetadata['numPhases'] = int(metadata['Phases']['Count']) + numPhases = int(metadata['Phases']['Count']) - for i in range(self.loadedMetadata['numPhases']): + for i in range(numPhases): phaseMetadata = metadata['Phase{:}'.format(i + 1)] self.loadedMetadata['phases'].append(Phase( phaseMetadata['StructureName'], @@ -340,9 +332,7 @@ def parseLine(line): self.dataFormat = np.dtype(dataFormat) - def loadOxfordCRC(self, - fileName: str, - fileDir: str = ""): + def loadOxfordCRC(self, fileName: str, fileDir: str = "") -> None: """Read binary EBSD data from an Oxford Instruments .crc file Parameters @@ -386,9 +376,9 @@ def loadOxfordCRC(self, self.checkData() + class PythonDictLoader(EBSDDataLoader): - def load(self, - dataDict: dict): + def load(self, dataDict: Dict[str, Any]) -> None: """Construct EBSD data from a python dictionary. Parameters @@ -407,7 +397,6 @@ def load(self, self.loadedMetadata['stepSize'] = dataDict['stepSize'] assert type(dataDict['phases']) is list self.loadedMetadata['phases'] = dataDict['phases'] - self.loadedMetadata['numPhases'] = len(dataDict['phases']) self.checkMetadata() @@ -422,7 +411,7 @@ class DICDataLoader(object): """Class containing methods for loading and checking HRDIC data """ - def __init__(self): + def __init__(self) -> None: self.loadedMetadata = { 'format': "", 'version': "", @@ -437,10 +426,10 @@ def __init__(self): 'yd': None } - def checkMetadata(self): + def checkMetadata(self) -> None: return - def checkData(self): + def checkData(self) -> None: """ Calculate size of map from loaded data and check it matches values from metadata. @@ -458,10 +447,11 @@ def checkData(self): assert xdim == self.loadedMetadata['xDim'], "Dimensions of data and header do not match" assert ydim == self.loadedMetadata['yDim'], "Dimensions of data and header do not match" - def loadDavisMetadata(self, - fileName: str, + def loadDavisMetadata( + self, + fileName: str, fileDir: str = "" - ) -> dict: + ) -> Dict[str, Any]: """ Load DaVis metadata from Davis .txt file. Parameters @@ -498,12 +488,13 @@ def loadDavisMetadata(self, return self.loadedMetadata - def loadDavisData(self, - fileName: str, + def loadDavisData( + self, + fileName: str, fileDir: str = "" - ) -> dict: - """ Load displacement data from Davis .txt file containing x and y coordinates - and x and y displacements for each coordinate. + ) -> Dict[str, Any]: + """Load displacement data from Davis .txt file containing x and + y coordinates and x and y displacements for each coordinate. Parameters ---------- @@ -533,11 +524,12 @@ def loadDavisData(self, self.checkData() return self.loadedData - - def loadDavisImageData(self, - fileName: str, + + @staticmethod + def loadDavisImageData( + fileName: str, fileDir: str = "" - ) -> np.ndarray: + ) -> np.ndarray: """ A .txt file from DaVis containing a 2D image Parameters @@ -559,17 +551,18 @@ def loadDavisImageData(self, data = pd.read_table(str(filePath), delimiter='\t', skiprows=1, header=None) - # x and y coordinates + # x and y coordinates loadedData = np.array(data) return loadedData -def readUntilString(file: TextIO, - termString: str, - commentChar: str = '*', - lineProcess: str = None - ) -> list: +def readUntilString( + file: TextIO, + termString: str, + commentChar: str = '*', + lineProcess: Optional[Callable[[str], Any]] = None +) -> List[Any]: """Read lines in a file until a line starting with the `termString` is encounted. The file position is returned before the line starting with the `termString` when found. Comment and empty lines are ignored. diff --git a/defdap/file_writers.py b/defdap/file_writers.py index 987e3b7..c1a46b5 100644 --- a/defdap/file_writers.py +++ b/defdap/file_writers.py @@ -16,11 +16,13 @@ import numpy as np import pathlib +from typing import Type + from defdap.quat import Quat class EBSDDataWriter(object): - def __init__(self): + def __init__(self) -> None: self.metadata = { 'shape': (0, 0), 'step_size': 0., @@ -35,7 +37,7 @@ def __init__(self): self.data_format = None @staticmethod - def get_writer(datatype: str): + def get_writer(datatype: str) -> "Type[EBSDDataLoader]": if datatype is None: datatype = "OxfordText" @@ -46,14 +48,13 @@ def get_writer(datatype: str): class OxfordTextWriter(EBSDDataWriter): - def write(self, - file_name: str, - file_dir: str = ""): - """ Write an Oxford Instruments .ctf file, which is a HKL single orientation file. + def write(self, file_name: str, file_dir: str = "") -> None: + """ Write an Oxford Instruments .ctf file, which is a HKL single + orientation file. Parameters ---------- - fileName + file_name File name. file_dir Path to file. From 14e97eaf10af5c9d9e66ac97079aa6ae85dcce9d Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Thu, 11 Mar 2021 13:49:34 +0000 Subject: [PATCH 32/33] Move creation of boundary lines to separate method --- defdap/ebsd.py | 79 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/defdap/ebsd.py b/defdap/ebsd.py index d5129d0..27ce8ea 100755 --- a/defdap/ebsd.py +++ b/defdap/ebsd.py @@ -730,7 +730,7 @@ def filterData(self, misOriTol=5): @reportProgress("finding grain boundaries") def findBoundaries(self, boundDef=10): - """Find grain boundaries + """Find grain and phase boundaries Parameters ---------- @@ -798,13 +798,16 @@ def findBoundaries(self, boundDef=10): self.boundariesY = misOriY > boundDef # PHASE boundary POINTS - self.phaseBoundariesX = np.not_equal(self.phaseArray, - np.roll(self.phaseArray, -1, axis=1)) + self.phaseBoundariesX = np.not_equal( + self.phaseArray, np.roll(self.phaseArray, -1, axis=1)) self.phaseBoundariesX[:, -1] = False - self.phaseBoundariesY = np.not_equal(self.phaseArray, - np.roll(self.phaseArray, -1, axis=0)) + + self.phaseBoundariesY = np.not_equal( + self.phaseArray, np.roll(self.phaseArray, -1, axis=0)) self.phaseBoundariesY[-1, :] = False - self.phaseBoundaries = np.logical_or(self.phaseBoundariesX, self.phaseBoundariesY) + + self.phaseBoundaries = np.logical_or( + self.phaseBoundariesX, self.phaseBoundariesY) self.phaseBoundaries = -self.phaseBoundaries.astype(int) # add PHASE boundary POINTS to GRAIN boundary POINTS @@ -813,34 +816,48 @@ def findBoundaries(self, boundDef=10): self.boundaries = np.logical_or(self.boundariesX, self.boundariesY) self.boundaries = -self.boundaries.astype(int) - # generate GRAIN boundary LINES - boundaryPoints = np.where(self.boundariesX) - boundaryLinesX = [] - for i, j in zip(*boundaryPoints): - boundaryLinesX.append(((j + 0.5, i - 0.5), (j + 0.5, i + 0.5))) - - boundaryPoints = np.where(self.boundariesY) - boundaryLinesY = [] - for i, j in zip(*boundaryPoints): - boundaryLinesY.append(((j - 0.5, i + 0.5), (j + 0.5, i + 0.5))) - - self.boundaryLines = boundaryLinesX + boundaryLinesY - - # generate PHASE boundary LINES - boundaryPoints = np.where(self.phaseBoundariesX) - phaseBoundaryLinesX = [] - for i, j in zip(*boundaryPoints): - phaseBoundaryLinesX.append(((j + 0.5, i - 0.5), (j + 0.5, i + 0.5))) - - boundaryPoints = np.where(self.phaseBoundariesY) - phaseBoundaryLinesY = [] - for i, j in zip(*boundaryPoints): - phaseBoundaryLinesY.append(((j - 0.5, i + 0.5), (j + 0.5, i + 0.5))) - - self.phaseBoundaryLines = phaseBoundaryLinesX + phaseBoundaryLinesY + _, _, self.boundaryLines = Map.create_boundary_lines( + boundaries_x=self.boundariesX, + boundaries_y=self.boundariesY + ) + _, _, self.phaseBoundaryLines = Map.create_boundary_lines( + boundaries_x=self.phaseBoundariesX, + boundaries_y=self.phaseBoundariesY + ) yield 1. + @staticmethod + def create_boundary_lines(*, boundaries_x=None, boundaries_y=None): + boundary_data = {} + if boundaries_x is not None: + boundary_data['x'] = boundaries_x + if boundaries_y is not None: + boundary_data['y'] = boundaries_y + if not boundary_data: + raise ValueError("No boundaries provided.") + + deltas = { + 'x': (0.5, -0.5, 0.5, 0.5), + 'y': (-0.5, 0.5, 0.5, 0.5) + } + all_lines = [] + for mode, boundaries in boundary_data.items(): + points = np.where(boundaries) + lines = [] + for i, j in zip(*points): + lines.append(( + (j + deltas[mode][0], i + deltas[mode][1]), + (j + deltas[mode][2], i + deltas[mode][3]) + )) + all_lines.append(lines) + + if len(all_lines) == 2: + all_lines.append(all_lines[0] + all_lines[1]) + return tuple(all_lines) + else: + return all_lines[0] + @reportProgress("constructing neighbour network") def buildNeighbourNetwork(self): # create network From 793f04009c69f41afcdd68bf78a1f86899bca047 Mon Sep 17 00:00:00 2001 From: Michael Atkinson Date: Thu, 11 Mar 2021 14:42:29 +0000 Subject: [PATCH 33/33] Fix tests --- tests/test_io.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_io.py b/tests/test_io.py index 9de068c..67dc85d 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -29,23 +29,24 @@ def test_init(data_loader_oxford_binary): @staticmethod def test_check_metadata_good(data_loader_oxford_binary): - """The check_metadata method should pass silently if phaseNames - and numPhases match.""" + """The check_metadata method should pass silently if each phase + is of `Phase` type.""" data_loader_oxford_binary.loadedMetadata["phases"] = [ Phase("test", 9, ()), Phase("tester", 11, ()), Phase("testist", 11, ()), ] - data_loader_oxford_binary.loadedMetadata["numPhases"] = 3 assert data_loader_oxford_binary.checkMetadata() is None @staticmethod def test_check_metadata_bad(data_loader_oxford_binary): - """The check_metadata method should fail if phaseNames and - numPhases do not match.""" - data_loader_oxford_binary.loadedMetadata["phases"] = ["1", "2"] - data_loader_oxford_binary.loadedMetadata["numPhases"] = 3 - with pytest.raises(ValueError): + """The check_metadata method should fail if a phase is is not of + `Phase` type.""" + data_loader_oxford_binary.loadedMetadata["phases"] = [ + Phase("test", 9, ()), + "2" + ] + with pytest.raises(AssertionError): data_loader_oxford_binary.checkMetadata() @staticmethod @@ -59,7 +60,6 @@ def test_load_oxford_cpr_good_file(data_loader_oxford_binary): assert metadata["acquisitionRotation"].quatCoef == \ pytest.approx((1., 0., 0., 0.)) - assert metadata["numPhases"] == 1 assert type(metadata["phases"]) is list assert len(metadata["phases"]) == 1 loaded_phase = metadata["phases"][0]