Skip to content

Update to dislocationSystem and cleanUpPseudoSym#2530

Open
PhilKro wants to merge 3 commits intomtex-toolbox:developfrom
PhilKro:my-mtex-fix
Open

Update to dislocationSystem and cleanUpPseudoSym#2530
PhilKro wants to merge 3 commits intomtex-toolbox:developfrom
PhilKro:my-mtex-fix

Conversation

@PhilKro
Copy link
Copy Markdown

@PhilKro PhilKro commented Mar 19, 2026

dislocationSystem:
Previously, the dislocationSystem class properly scaled the Burgers vector lengths for edge dislocations based on the crystal lattice (e.g., 1/3 for hexagonal, 1/2 for cubic). However, this scaling logic was missing for screw dislocations, leaving their Burgers vector lengths ill-defined and different to those of the edge dislcoations in hexagonal lattices.

I updated the constructor in geometry/@dislocationSystem/dislocationSystem.m to apply the same lattice-specific scaling to screw dislocations:
Applied a 1/3 scaling factor for hexagonal lattices.
Applied a 1/2 scaling factor for cubic lattices.
Added a warning fallback for undefined lattice types prompting the user to adjust the length manually.

cleanUpPseudoSym:

Updates cleanUpPseudoSym function to implement arrays of pseudo-misorientation and overhaul to the orientation update logic for better performance on large EBSD maps.

Fixed multi-operator bug: Resolved an out-of-bounds logical indexing error (ori(swap) = ori(swap) * mori) that occurred when passing an array of pseudo-symmetry operators. The function now evaluates all operators (plus the identity) simultaneously and applies the one that minimizes the misorientation distance.

Vectorized pixel updates: Replaced the for loop over individual merged grains. The script now extracts all affected EBSD points (merged_pts) and calculates their pseudo-symmetry rotations in a single, fully vectorized matrix operation.

Added numChanged output: Added a third output argument to return the exact number of pixels that had their orientations modified during the cleanup (important parameter for scientific analysis).

Documentation updates: Added the supported varargin parameters (delta and threshold) to the function header for ease of use.

@rockdef
Copy link
Copy Markdown

rockdef commented Mar 20, 2026

Hi @PhilKro
I have been struggling for many days to fix the pseudosymmetry. I looked at your script; however, I was not able to fix it. Could you please have a quick look?

%% Specify Crystal and Specimen Symmetries
% crystal symmetry
CS = {...
    'notIndexed',...
    crystalSymmetry('2/m', [5.237 9.07 14.285], [90,97.90,90]*degree, 'X||a*', 'Y||b*', 'Z||c', 'mineral', 'Chlorite', 'color', 'light green'),...
    crystalSymmetry('2/m',  [6.5540 8.7080 7.0690],[90, 113.9, 90]*degree, 'X||a*','Y||b','Z||c','mineral', 'Titanite', 'color', 'magenta'),...
    crystalSymmetry('-3',  [5.0691 5.0691 13.9849],[90, 90, 120]*degree, 'X||a*', 'Z||c','mineral', 'Ilmenite', 'color', [0.8500 0.3250 0.0980]),...
    crystalSymmetry('4/mmm',  [4.5940 4.5940 2.9580],[90, 90, 90]*degree, 'X||a*','Y||b','Z||c','mineral', 'Rutile', 'color', 'yellow')};

%% Miller Indices

h_chl = [Miller(1,0,0,CS{2},'hkl'), Miller(0,1,0,CS{2},'hkl'),Miller(0,0,1,CS{2},'hkl')];
h_tit = [Miller(2,2,1,CS{3}), Miller(-1,-1,2,CS{3}), Miller(1,0,2,CS{3})];
h_ilm = [Miller(1,1,0,CS{4}), Miller(0,1,0,CS{4}), Miller(0,0,1,CS{4})];
h_rut = [Miller(1,1,0,CS{5}), Miller(0,1,0,CS{5}), Miller(0,0,1,CS{5})];

%% plotting convention
setMTEXpref('xAxisDirection','east');
setMTEXpref('zAxisDirection','outOfPlane');

%% Specify File Names
fname = 'XXX.crc';

%% Import the Data

% create an EBSD variable containing the data
ebsd = EBSD.load(fname,CS,'interface','crc',...
  'convertEuler2SpatialReferenceFrame','setting 2');

ebsd = rotate(ebsd,rotation('axis',vector3d.X,'angle',180*degree));

% if Chlorite_without_no_loop ==1; 
%% Simple grain construction and cleanup (probably want to modify)

% [grains,ebsd.grainId,ebsd.mis2mean] = calcGrains(ebsd,'angle',[1 10]*degree,'Boundary','tight','jcvoronoi');
[grains,ebsd.grainId,ebsd.mis2mean] = calcGrains(ebsd,'angle',[1 10]*degree,'Boundary','tight');

%% Remove potentially spurious "grains"
% remove grains that were constucted using less thn 3 pixels
grains = grains(grains.grainSize>3);

% reset the EBSD data to filtered grains
ebsd = ebsd(grains);

%% Recompute grain boundaries with the remaining EBSD data
% [grains,ebsd.grainId,ebsd.mis2mean] = calcGrains(ebsd,'angle',[1 10]*degree,'Boundary','tight','jcvoronoi');
[grains,ebsd.grainId,ebsd.mis2mean] = calcGrains(ebsd,'angle',[1 10]*degree,'Boundary','tight');

%% 
function [ebsd,grainsM,numChanged] = cleanUpPseudoSym(ebsd,grains,mori,varargin)
% cleanUpPseudoSym Corrects pseudo-symmetry artifacts using tortuosity.
%
% Syntax
%   [ebsd, grainsM, numChanged] = cleanUpPseudoSym(ebsd, grains, mori, varargin)
%
% Input
%   ebsd   - @EBSD object
%   grains - @grain2d object
%   mori   - @orientation (can be an array of pseudo-symmetries)
%
% Options
%   'delta'     - Tolerance for boundary misorientation angle to match pseudo-symmetry (default: 2*degree)
%   'threshold' - Minimum tortuosity (boundary length / straight distance) to trigger merge (default: 1.5)
%
% Output
%   ebsd       - updated @EBSD object
%   grainsM    - merged @grain2d object
%   numChanged - number of pixels that changed orientation

% initialize output counter
numChanged = 0;

% select grain boundaries with the correct symmetry
gB = grains.boundary(mori(1).CS, mori(1).CS);

% select grain boundaries with the mori
tol = get_option(varargin,'delta',2*degree);

% evaluate all moris and keep boundary if any match is within tolerance
isMatch = any(angle(gB.misorientation, mori) < tol, 2);
gB = gB(isMatch);

% extent of each component
xmin = accumarray(gB.componentId,gB.midPoint.x,[],@min);
ymin = accumarray(gB.componentId,gB.midPoint.y,[],@min);
xmax = accumarray(gB.componentId,gB.midPoint.x,[],@max);
ymax = accumarray(gB.componentId,gB.midPoint.y,[],@max);
d = sqrt((xmax-xmin).^2 + (ymax-ymin).^2);

% boundary length of each component
l = accumarray(gB.componentId,gB.segLength);

% tortuosity projected back to each segment
tortuosity = l(gB.componentId)./d(gB.componentId);
maxT = get_option(varargin,'threshold',1.5);
cond = tortuosity > maxT & gB.componentSize > 4;

% merge grains
[grainsM,parentId,newInd] = merge(grains,gB(cond),...
  'calcMeanOrientation','maxArea');

% update EBSD
ind = ebsd.grainId > 0;
ebsd.grainId(ind) = parentId(grains.id2ind(ebsd.grainId(ind)));

% array of all possible operators: [identity, mori_1, mori_2, ...]
id_op = orientation.id(mori(1).CS,mori(1).CS); 
all_ops = [id_op, transpose(mori(:))];

% identify ebsd points belonging to the newly merged grains
updated_grain_ids = grainsM.id(newInd);
is_merged_pt = ismember(ebsd.grainId, updated_grain_ids);
merged_pts = find(is_merged_pt);

if ~isempty(merged_pts)
  % extract orientations of merged points and their new grain mean orientations
  ori = ebsd.orientations(merged_pts);
  
  grain_inds = grainsM.id2ind(ebsd.grainId(merged_pts));
  mean_oris = grainsM.meanOrientation(grain_inds);
  
  % calculate distances for all operators simultaneously
  dists = angle(ori .* all_ops, mean_oris);
  
  % find the operator that minimizes the misorientation distance
  [~, best_op_idx] = min(dists, [], 2);
  
  % count pixels that will change (index 1 is the identity operator)
  numChanged = sum(best_op_idx > 1);
  
  % apply the best operator (skip index 1 since it is the identity)
  for m = 1:length(mori)
      swap = (best_op_idx == m + 1);
      if any(swap)
          ori(swap) = ori(swap) * mori(m);
      end
  end
  
  % update EBSD orientations
  ebsd.orientations(merged_pts) = ori;
end
end

Thanks in advance,
Bhupesh

@PhilKro
Copy link
Copy Markdown
Author

PhilKro commented Mar 20, 2026

Hi Bhupesh,
Its hard for me to find the problem without your dataset. However, I see that in your code you are not executing the pseudo-symmetry cleanup function (merely defining it). To run the function you have to define one or a set of misorientations that are present as pseudoSymmetries in your ebsd-dataset. I am not sure about possible pseudo-symmetries in your crystal systems, here is my guess (based on AI) and the command:

% --- Chlorite (CS{2}) ---
ps_chl_a180 = orientation.byAxisAngle(Miller(1,0,0,CS{2}), 180*degree);
ps_chl_b180 = orientation.byAxisAngle(Miller(0,1,0,CS{2}), 180*degree);
ps_chl_c180 = orientation.byAxisAngle(Miller(0,0,1,CS{2}), 180*degree);

% --- Titanite (CS{3}) ---
ps_ttn_a180 = orientation.byAxisAngle(Miller(1,0,0,CS{3}), 180*degree);
ps_ttn_c180 = orientation.byAxisAngle(Miller(0,0,1,CS{3}), 180*degree);

% --- Ilmenite (CS{4}) ---
ps_ilm_60 = orientation.byAxisAngle(Miller(0,0,0,1,CS{4}), 60*degree);
ps_ilm_30 = orientation.byAxisAngle(Miller(0,0,0,1,CS{4}), 30*degree);

% --- Rutile (CS{5}) ---
ps_rut_c90   = orientation.byAxisAngle(Miller(0,0,1,CS{5}), 90*degree);
ps_rut_diag  = orientation.byAxisAngle(Miller(1,1,0,CS{5}), 90*degree);
ps_rut_180   = orientation.byAxisAngle(Miller(1,0,0,CS{5}), 180*degree);

pseudoSym = [ ...
    ps_ilm_60, ps_ilm_30, ...                 % Ilmenite
    ps_rut_c90, ps_rut_diag, ps_rut_180, ... % Rutile
    ps_chl_a180, ps_chl_b180, ps_chl_c180, ... % Chlorite
    ps_ttn_a180, ps_ttn_c180 ...             % Titanite
];

[ebsd_cleaned,grains_cleaned, numChanged] = cleanUpPseudoSym(ebsd,grains,pseudoSym, 'threshold', 1.5);

Cheers
Philipp

@rockdef
Copy link
Copy Markdown

rockdef commented Mar 20, 2026

Hi @PhilKro again,
I tried something like that, but an error occurred.

%% Simple grain construction and cleanup (probably want to modify)
[grains,ebsd.grainId,ebsd.mis2mean] = calcGrains(ebsd,'angle',[1 10]*degree,'Boundary','tight');

%% Remove potentially spurious "grains"
grains = grains(grains.grainSize>5);

% reset the EBSD data to filtered grains
ebsd = ebsd(grains);

%% Recompute grain boundaries with the remaining EBSD data

[grains,ebsd.grainId,ebsd.mis2mean] = calcGrains(ebsd,'angle',[1 10]*degree,'Boundary','tight');
%% Clean-up
% --- Chlorite (CS{2}) ---
ps_chl_a120 = orientation.byAxisAngle(Miller(1,0,0,CS{2}), 120*degree);
ps_chl_b120 = orientation.byAxisAngle(Miller(0,1,0,CS{2}), 120*degree);
ps_chl_c120 = orientation.byAxisAngle(Miller(0,0,1,CS{2}), 120*degree);

pseudoSym = [ps_chl_a120, ps_chl_b120, ps_chl_c120];

[ebsd_cleaned,grains_cleaned, numChanged] = cleanUpPseudoSym(ebsd,grains,pseudoSym, 'threshold', 1.5);

Error:

Error using [grain2d/merge](matlab:matlab.lang.internal.introspective.errorDocCallback('grain2d/merge'))
Too many output arguments.

Error in [cleanUpPseudoSym](matlab:matlab.lang.internal.introspective.errorDocCallback('cleanUpPseudoSym', '/Users/bhupeshmeher/Documents/Software/mtex-6.0.0/EBSDAnalysis/cleanUpPseudoSym.m', 50)) ([line 50](matlab: opentoline('/Users/bhupeshmeher/Documents/Software/mtex-6.0.0/EBSDAnalysis/cleanUpPseudoSym.m',50,0)))
[grainsM,parentId,newInd] = merge(grains,gB(cond),...
                            ^^^^^^^^^^^^^^^^^^^^^^^^^
Error in [Trail_01](matlab:matlab.lang.internal.introspective.errorDocCallback('Trail_01', '/Users/bhupeshmeher/PhD Project/PROJECT_IV_Chlorite_BGU/5_Experiments_BGU/EBSD_Cambridge/EBSD_Cambridge/RD33Chl_500/RD33Chl_Day_Map01/Trail_01.m', 62)) ([line 62](matlab: opentoline('/Users/bhupeshmeher/PhD Project/PROJECT_IV_Chlorite_BGU/5_Experiments_BGU/EBSD_Cambridge/EBSD_Cambridge/RD33Chl_500/RD33Chl_Day_Map01/Trail_01.m',62,0)))
[ebsd_cleaned,grains_cleaned, numChanged] = cleanUpPseudoSym(ebsd,grains,pseudoSym, 'threshold', 1.5);

Have a look here about the pseudosymmetry of chlorite:
Pseudosymmetry_page-0001

Best,
Bhupesh

@PhilKro
Copy link
Copy Markdown
Author

PhilKro commented Mar 20, 2026

please run the newest version of MTEX (develop branch) and tell me if the error still occurs

@PhilKro
Copy link
Copy Markdown
Author

PhilKro commented Mar 20, 2026

Hey Bhupesh,

can you let me know if this worked for you? I had a limited dataset to test this function on :D

Cheers Philipp

@rockdef
Copy link
Copy Markdown

rockdef commented Mar 20, 2026

Hi Phil,
I will check with a different computer. Last time, it was very difficult to fix many MATLAB and MTEX syntaxes that were incompatible with macOS. That is why I do not want to upgrade suddenly to the latest version; otherwise, I might get stuck. I am trying with a different computer now and will let you know.

Cheers,
Bhupesh

@PhilKro
Copy link
Copy Markdown
Author

PhilKro commented Mar 20, 2026

I'm quite certain that you are running into this error because you don't have the newly updated grain2d/merge method.

@zmichels
Copy link
Copy Markdown

Hi Phil,

I will check with a different computer. Last time, it was very difficult to fix many MATLAB and MTEX syntaxes that were incompatible with macOS. That is why I do not want to upgrade suddenly to the latest version; otherwise, I might get stuck. I am trying with a different computer now and will let you know.

Cheers,

Bhupesh

FWIW, I previously avoided updating on Mac OS for similar reasons, but the latest version 6.1.1 (I think) installed well for me and without the same magnitude of headaches as previous updates.

@rockdef
Copy link
Copy Markdown

rockdef commented Mar 20, 2026

Hi @PhilKro
Still the error:

Warning: grains.grainSize is depreciated. Please use grains.numPixel instead
> In grain2d/get.grainSize (line 176)
In indexing (line 83)
In Trail_01 (line 46)
Error using grain2d/merge
Too many output arguments.

Error in EBSD/cleanUpPseudoSym (line 50)
[grainsM,parentId,newInd] = merge(grains,gB(cond),...
                            ^^^^^^^^^^^^^^^^^^^^^^^^^
Error in Trail_01 (line 63)
[ebsd_cleaned,grains_cleaned, numChanged] = cleanUpPseudoSym(ebsd,grains,pseudoSym, 'threshold', 1.5);

@PhilKro
Copy link
Copy Markdown
Author

PhilKro commented Mar 20, 2026

I don't understand why the error crops up. The new grain2d.merge method has 3 output arguments...
https://github.com/mtex-toolbox/mtex/blob/develop/EBSDAnalysis/%40grain2d/merge.m
And thats the size of the array left of the equals. Would you mind sharing you dataset with me?

@rockdef
Copy link
Copy Markdown

rockdef commented Mar 20, 2026

Hi @PhilKro
Surely, I will share my dataset. Would you mind sharing your email?

@PhilKro
Copy link
Copy Markdown
Author

PhilKro commented Mar 20, 2026

philipp.kroeker@empa.ch

@PhilKro
Copy link
Copy Markdown
Author

PhilKro commented Mar 23, 2026

Hey Bhupesh,

I just pushed an update to the PR! The cleanUpPseudoSym function now correctly handles EBSD datasets with multiple phases.

To test this out on your machine, you can clone this specific branch directly into a temporary demo folder. Just run this command in your terminal (make sure to replace the path with your actual MTEX directory):

git clone -b my-mtex-fix [https://github.com/PhilKro/mtex_twinSysDev.git](https://github.com/PhilKro/mtex_twinSysDev.git) "YOUR\MTEXPATH\pseudoSym_demo"

Regarding the initial error you ran into: I did not have any issues with the merge function on my end. This strongly suggests that your error was caused by running an older version of MTEX.

To verify this, could you do a quick check in MATLAB? Type the following into your command window:

edit grain2d.merge

Please make sure that the method that opens is exactly the same as the one currently on the develop branch here:
grain2d/merge.m

Let me know if it runs for you now! For me the function corrected ~2800 pseudoSymmetry datapoints. I attached a before and after. It does look quite messy still...

Before:

preCleanup

After:

postCleanup

@PhilKro
Copy link
Copy Markdown
Author

PhilKro commented Mar 23, 2026

Hey again Bhupesh,

To get better results with the cleanUpPseudoSym function in this PR, I recommend tweaking the threshold (which controls the tortuosity) and the delta parameters a bit. Playing around with those values might help, although i suspect it might not be sufficient.

If you aren't getting satisfactory results with that approach, you are more than welcome to check out a much more verbose graph-based clustering function I put together here:
pseudoSymmetryCorrection.m

I just implemented the multiphase handling into this version as well. Full disclosure: I haven't tested the multiphase logic on it yet, so there are no guarantees it works right out of the box. However, the benefit of this graph-based version is that you can define way more custom parameters and conditions to accurately characterize and isolate the pseudo-symmetry grains.

I'd be happy if you want to test it out and tweak it. If you manage to fix or improve anything, please feel free to push your changes back to the repo!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants