From 3be235e134be497bc42ed6405c68105fa11e7bf6 Mon Sep 17 00:00:00 2001 From: IMBalENce Date: Tue, 16 May 2023 18:06:04 +1000 Subject: [PATCH 01/33] Add ctf file reader and interactive IPF example Signed-off-by: IMBalENce --- examples/plotting/interactive_IPF.py | 72 ++++++ orix/io/__init__.py | 8 +- orix/io/plugins/__init__.py | 3 +- orix/io/plugins/ctf.py | 357 +++++++++++++++++++++++++++ 4 files changed, 438 insertions(+), 2 deletions(-) create mode 100644 examples/plotting/interactive_IPF.py create mode 100644 orix/io/plugins/ctf.py diff --git a/examples/plotting/interactive_IPF.py b/examples/plotting/interactive_IPF.py new file mode 100644 index 00000000..31e9b94b --- /dev/null +++ b/examples/plotting/interactive_IPF.py @@ -0,0 +1,72 @@ +""" +======== +Interactive IPF map with Euler angle +======== + +This example shows how to use [`matplotlib event connections`](https://matplotlib.org/stable/users/explain/event_handling.html) +to add an interactive click function to the IPF plot to retrieve the phase +name and corresponding Euler angles from the location of click. +""" +import matplotlib.pyplot as plt +import numpy as np + +from orix import data, plot +from orix.quaternion import Rotation +from orix.vector import Miller + +xmap = data.sdss_ferrite_austenite(allow_download=True) +print(xmap) + +pg_laue = xmap.phases[1].point_group.laue +ori_au = xmap["austenite"].orientations +ori_fe = xmap["ferrite"].orientations + +# Orientation colors +ipf_key = plot.IPFColorKeyTSL(pg_laue) +rgb_au = ipf_key.orientation2color(ori_au) +rgb_fe = ipf_key.orientation2color(ori_fe) + +rgb_all = np.zeros((xmap.size, 3)) +rgb_all[xmap.phase_id == 1] = rgb_au +rgb_all[xmap.phase_id == 2] = rgb_fe +xmap_gb = rgb_all.reshape(xmap.shape + (3,)) + + +# An interactive function for getting the phase name and euler angles from the clicking position +def select_point(image): + """Return location of interactive user click on image.""" + fig, ax = plt.subplots(subplot_kw=dict(projection="plot_map"), figsize=(12, 8)) + ax.imshow(image) + ax.set_title("Click position") + coords = [] + + def on_click(event): + print(event.xdata, event.ydata) + coords.append(event.xdata) + coords.append(event.ydata) + plt.clf() + plt.imshow(image) + try: + x_pos = coords[-2] + y_pos = coords[-1] + except: + x_pos = 0 + y_pos = 0 + + phase = xmap.phases[xmap[int(y_pos), int(x_pos)].phase_id[0]].name + [Eu1, Eu2, Eu3] = np.rad2deg( + Rotation.to_euler(xmap[int(y_pos), int(x_pos)].orientations) + )[0] + plt.plot(x_pos, y_pos, "+", c="black", markersize=15, markeredgewidth=3) + plt.title( + f"Phase: {phase}, Euler angles: {np.round(Eu1, 2)}, {np.round(Eu2, 2)}, {np.round(Eu3, 2)}" + ) + plt.draw() + + fig.canvas.mpl_connect("button_press_event", on_click) + plt.show() + plt.draw() + return coords # click point coordintes in [x, y] format + + +result = select_point(xmap_gb) diff --git a/orix/io/__init__.py b/orix/io/__init__.py index 825a7362..f854b813 100644 --- a/orix/io/__init__.py +++ b/orix/io/__init__.py @@ -81,13 +81,19 @@ def loadctf(file_string: str) -> Rotation: file_string Path to the ``.ctf`` file. This file is assumed to list the Euler angles in the Bunge convention in the columns 5, 6, and 7. + The starting row for the data that contains Euler angles is relevant + to the number of inlcuded phases. Returns ------- rotation Rotations in the file. """ - data = np.loadtxt(file_string, skiprows=17)[:, 5:8] + with open(file_string, "r") as file: + all_data = [line.strip() for line in file.readlines()] + phase_num = int(all_data[12].split("\t")[1]) + + data = np.loadtxt(file_string, skiprows=(14 + phase_num))[:, 5:8] euler = np.radians(data) return Rotation.from_euler(euler) diff --git a/orix/io/plugins/__init__.py b/orix/io/plugins/__init__.py index b3b500fe..b22753c4 100644 --- a/orix/io/plugins/__init__.py +++ b/orix/io/plugins/__init__.py @@ -32,10 +32,11 @@ orix_hdf5 """ -from orix.io.plugins import ang, bruker_h5ebsd, emsoft_h5ebsd, orix_hdf5 +from orix.io.plugins import ang, bruker_h5ebsd, ctf, emsoft_h5ebsd, orix_hdf5 plugin_list = [ ang, + ctf, bruker_h5ebsd, emsoft_h5ebsd, orix_hdf5, diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py new file mode 100644 index 00000000..7e4658b8 --- /dev/null +++ b/orix/io/plugins/ctf.py @@ -0,0 +1,357 @@ +# -*- coding: utf-8 -*- +# Copyright 2018-2023 the orix developers +# +# This file is part of orix. +# +# orix is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# orix is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with orix. If not, see . + +"""Reader of a crystal map from an .ctf file in formats produced by Oxford AZtec +, EMsoft's EMdpmerge program. +""" + +from io import TextIOWrapper +import re +from typing import List, Optional, Tuple, Union +import warnings + +from diffpy.structure import Lattice, Structure +import numpy as np + +from orix import __version__ +from orix.crystal_map import CrystalMap, PhaseList, create_coordinate_arrays +from orix.quaternion import Rotation +from orix.quaternion.symmetry import point_group_aliases + +__all__ = ["file_reader", "file_writer"] + +# Plugin description +format_name = "ctf" +file_extensions = ["ctf"] +writes = True +writes_this = CrystalMap + + +def file_reader(filename: str) -> CrystalMap: + """Return a crystal map from a file in Oxford AZtec HKL's .ctf format. The + map in the input is assumed to be 2D. + + Many vendors produce an .ctf file. Supported vendors are: + + * Oxford AZtec HKL + * EMsoft (from program `EMdpmerge`) + * orix + + All points satisfying the following criteria are classified as not + indexed: + + * Oxford AZtec HKL: Phase == 0 + + Parameters + ---------- + filename + Path and file name. + + Returns + ------- + xmap + Crystal map. + """ + # Get file header + with open(filename, "r") as f: + [header, data_starting_row] = _get_header(f) + + # Get phase names and crystal symmetries from header (potentially empty) + phase_ids, phase_names, symmetries, lattice_constants = _get_phases_from_header( + header + ) + structures = [] + for name, abcABG in zip(phase_names, lattice_constants): + structures.append(Structure(title=name, lattice=Lattice(*abcABG))) + + # Read all file data + file_data = np.loadtxt(filename, skiprows=data_starting_row) + + # Get vendor and column names + n_rows, n_cols = file_data.shape + vendor, column_names = _get_vendor_columns(header, n_cols) + + # Data needed to create a CrystalMap object + data_dict = { + "euler1": None, + "euler2": None, + "euler3": None, + "x": None, + "y": None, + "phase_id": None, + "prop": {}, + } + for column, name in enumerate(column_names): + if name in data_dict.keys(): + data_dict[name] = file_data[:, column] + else: + data_dict["prop"][name] = file_data[:, column] + + # Add phase list to dictionary + data_dict["phase_list"] = PhaseList( + names=phase_names, + space_groups=symmetries, + structures=structures, + ids=phase_ids, + ) + + # Set which data points are not indexed + if vendor in ["orix", "hkl"]: + not_indexed = data_dict["phase_id"] == 0 + data_dict["phase_id"][not_indexed] = -1 + + # Set scan unit + scan_unit = "um" + data_dict["scan_unit"] = scan_unit + + # Create rotations + data_dict["rotations"] = Rotation.from_euler( + np.column_stack( + (data_dict.pop("euler1"), data_dict.pop("euler2"), data_dict.pop("euler3")) + ), + degrees=True, + ) + + return CrystalMap(**data_dict) + + +def _get_header(file: TextIOWrapper) -> List[str]: + """Return the first lines above the mapping data and the data starting row number + in an .ctf file. + + Parameters + ---------- + file + File object. + + Returns + ------- + header + List with header lines as individual elements. + data_starting_row + The starting row number for the data lines + """ + all_data = [line.rstrip() for line in file.readlines()] + + phase_num_row = 0 + for line in all_data: + if "Phases" in line: + phases_num_line = line + break + phase_num_row += 1 + + phase_num = int(phases_num_line.split("\t")[1]) + header = all_data[: (phase_num_row + phase_num + 1)] + data_starting_row = phase_num_row + phase_num + 2 + return header, data_starting_row + + +def _get_vendor_columns(header: List[str], n_cols_file: int) -> Tuple[str, List[str]]: + """Return the .ctf file column names and vendor, determined from the + header. + + Parameters + ---------- + header + List with header lines as individual elements. + n_cols_file + Number of file columns. + + Returns + ------- + vendor + Determined vendor (``"hkl"``, ``"emsoft"`` or ``"orix"``). + column_names + List of column names. + """ + # Assume Oxford TSL by default + vendor = "hkl" + + # Determine vendor by searching for the vendor footprint in the header + vendor_footprint = { + "emsoft": "EMsoft", + "orix": "Column names: phi1, Phi, phi2", + } + footprint_line = None + for name, footprint in vendor_footprint.items(): + for line in header: + if footprint in line: + vendor = name + footprint_line = line + break + + # Variants of vendor column names encountered in real data sets + column_names = { + "hkl": { + 0: [ + "phase_id", + "x", + "y", + "bands", + "error", + "euler1", + "euler2", + "euler3", + "MAD", # Mean angular deviation + "BC", # Band contrast + "BS", # Band Slope + ], + }, + "emsoft": { + 0: [ + "phase_id", + "x", + "y", + "bands", + "error", + "euler1", + "euler2", + "euler3", + "MAD", # Mean angular deviation + "BC", # Band contrast + "BS", # Band Slope + ] + }, + "orix": { + 0: [ + "phase_id", + "x", + "y", + "bands", + "error", + "euler1", + "euler2", + "euler3", + "MAD", # Mean angular deviation + "BC", # Band contrast + "BS", # Band Slope + ], + }, + "unknown": { + 0: [ + "phase_id", + "x", + "y", + "bands", + "error", + "euler1", + "euler2", + "euler3", + "MAD", # Mean angular deviation + "BC", # Band contrast + "BS", # Band Slope + ] + }, + } + + n_variants = len(column_names[vendor]) + n_cols_expected = [len(column_names[vendor][k]) for k in range(n_variants)] + if vendor == "orix" and "Column names" in footprint_line: + # Append names of extra properties found, if any, in the orix + # .ang file header + vendor_column_names = column_names[vendor][0] + n_cols = n_cols_expected[0] + extra_props = footprint_line.split(":")[1].split(",")[n_cols:] + vendor_column_names += [i.lstrip(" ").replace(" ", "_") for i in extra_props] + elif n_cols_file not in n_cols_expected: + warnings.warn( + f"Number of columns, {n_cols_file}, in the file is not equal to " + f"the expected number of columns, {n_cols_expected}, for the \n" + f"assumed vendor '{vendor}'. Will therefore assume the following " + "columns: phase_id, x, y, bands, error, euler1, euler2, euler3" + "MAD, BC, BS, etc." + ) + vendor = "unknown" + vendor_column_names = column_names[vendor][0] + n_cols = len(vendor_column_names) + if n_cols_file > n_cols: + # Add any extra columns as properties + for i in range(n_cols_file - n_cols): + vendor_column_names.append("unknown" + str(i + 3)) + else: + idx = np.where(np.equal(n_cols_file, n_cols_expected))[0][0] + vendor_column_names = column_names[vendor][idx] + + return vendor, vendor_column_names + + +def _get_phases_from_header( + header: List[str], +) -> Tuple[List[int], List[str], List[str], List[List[float]]]: + """Return phase names and symmetries detected in an .ctf file + header. + + Parameters + ---------- + header + List with header lines as individual elements. + + Returns + ------- + ids + Phase IDs. + phase_names + List of names of detected phases. + phase_point_groups + List of point groups of detected phase. + lattice_constants + List of list of lattice parameters of detected phases. + + Notes + ----- + Regular expressions are used to collect phase name, formula and + point group. This function have been tested with files from the + following vendor's formats: Oxford AZtec HKL, and EMsoft v4/v5. + """ + + phases = { + "name": [], + "space_group": [], + "lattice_constants": [], + "id": [], + } + phase_num_row = 0 + for line in header: + if "Phases" in line: + phases_num_line = line + break + phase_num_row += 1 + phase_num = int(phases_num_line.split("\t")[1]) + + for num in range(phase_num): + phase_data = header[phase_num_row + num + 1].split("\t") + phases["name"].append(phase_data[2]) + phases["space_group"].append(int(phase_data[4])) + phases["lattice_constants"].append( + [float(i) for i in phase_data[0].split(";") + phase_data[1].split(";")] + ) + phases["id"].append(num + 1) + + names = phases["name"] + + # Ensure each phase has an ID (hopefully found in the header) + phase_ids = [int(i) for i in phases["id"]] + n_phases = len(phases["name"]) + if len(phase_ids) == 0: + phase_ids += [i for i in range(n_phases)] + elif n_phases - len(phase_ids) > 0 and len(phase_ids) != 0: + next_id = max(phase_ids) + 1 + n_left = n_phases - len(phase_ids) + phase_ids += [i for i in range(next_id, next_id + n_left)] + + return phase_ids, names, phases["space_group"], phases["lattice_constants"] From 069bfbc8e69b98c5b4524020c54a1ee729d83fd2 Mon Sep 17 00:00:00 2001 From: IMBalENce Date: Mon, 29 May 2023 11:38:36 +1000 Subject: [PATCH 02/33] Update recommended review changes --- examples/plotting/interactive_IPF.py | 25 +++++---- orix/io/plugins/ctf.py | 77 +++++++++++++--------------- 2 files changed, 49 insertions(+), 53 deletions(-) diff --git a/examples/plotting/interactive_IPF.py b/examples/plotting/interactive_IPF.py index 31e9b94b..6e019827 100644 --- a/examples/plotting/interactive_IPF.py +++ b/examples/plotting/interactive_IPF.py @@ -1,18 +1,18 @@ """ -======== -Interactive IPF map with Euler angle -======== +==================================== +Interactive IPF map with Euler angle +==================================== -This example shows how to use [`matplotlib event connections`](https://matplotlib.org/stable/users/explain/event_handling.html) -to add an interactive click function to the IPF plot to retrieve the phase -name and corresponding Euler angles from the location of click. +This example shows how to use +:doc:`matplotlib event connections ` +to add an interactive click function to the inverse pole figure (IPF) plot to +retrieve the phase name and corresponding Euler angles from the location of +click. """ import matplotlib.pyplot as plt import numpy as np from orix import data, plot -from orix.quaternion import Rotation -from orix.vector import Miller xmap = data.sdss_ferrite_austenite(allow_download=True) print(xmap) @@ -53,13 +53,12 @@ def on_click(event): x_pos = 0 y_pos = 0 - phase = xmap.phases[xmap[int(y_pos), int(x_pos)].phase_id[0]].name - [Eu1, Eu2, Eu3] = np.rad2deg( - Rotation.to_euler(xmap[int(y_pos), int(x_pos)].orientations) - )[0] + xmap_yx = xmap[int(y_pos), int(x_pos)] + eu = xmap_yx.rotations.to_euler(degrees=True)[0] + phase_name = xmap_yx.phases_in_data[:].name plt.plot(x_pos, y_pos, "+", c="black", markersize=15, markeredgewidth=3) plt.title( - f"Phase: {phase}, Euler angles: {np.round(Eu1, 2)}, {np.round(Eu2, 2)}, {np.round(Eu3, 2)}" + f"Phase: {phase_name}, Euler angles: {np.array_str(eu, precision=2)[1:-1]}" ) plt.draw() diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index 7e4658b8..a563a67f 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -16,30 +16,22 @@ # You should have received a copy of the GNU General Public License # along with orix. If not, see . -"""Reader of a crystal map from an .ctf file in formats produced by Oxford AZtec -, EMsoft's EMdpmerge program. +"""Reader of a crystal map from an .ctf file in formats produced by +Oxford AZtec and EMsoft's EMdpmerge program. """ from io import TextIOWrapper -import re -from typing import List, Optional, Tuple, Union +from typing import List, Tuple import warnings from diffpy.structure import Lattice, Structure import numpy as np -from orix import __version__ -from orix.crystal_map import CrystalMap, PhaseList, create_coordinate_arrays +# from orix import __version__ +from orix.crystal_map import CrystalMap, PhaseList from orix.quaternion import Rotation -from orix.quaternion.symmetry import point_group_aliases -__all__ = ["file_reader", "file_writer"] - -# Plugin description -format_name = "ctf" -file_extensions = ["ctf"] -writes = True -writes_this = CrystalMap +__all__ = ["file_reader"] def file_reader(filename: str) -> CrystalMap: @@ -111,9 +103,8 @@ def file_reader(filename: str) -> CrystalMap: ) # Set which data points are not indexed - if vendor in ["orix", "hkl"]: - not_indexed = data_dict["phase_id"] == 0 - data_dict["phase_id"][not_indexed] = -1 + not_indexed = data_dict["phase_id"] == 0 + data_dict["phase_id"][not_indexed] = -1 # Set scan unit scan_unit = "um" @@ -149,15 +140,32 @@ def _get_header(file: TextIOWrapper) -> List[str]: all_data = [line.rstrip() for line in file.readlines()] phase_num_row = 0 + phases_num_line = str() for line in all_data: if "Phases" in line: phases_num_line = line break phase_num_row += 1 + if phases_num_line: + try: + phase_num = int(phases_num_line.split("\t")[1]) + header = all_data[: (phase_num_row + phase_num + 1)] + data_starting_row = phase_num_row + phase_num + 2 + except: + header = None + data_starting_row = None + warnings.warn( + f"Total number of phases has to be defined in the .ctf file." + f"No such information can be found. Incompatible file format." + ) + else: + header = None + data_starting_row = None + warnings.warn( + f"Total number of phases has to be defined in the .ctf file." + f"No such information can be found. Incompatible file format." + ) - phase_num = int(phases_num_line.split("\t")[1]) - header = all_data[: (phase_num_row + phase_num + 1)] - data_starting_row = phase_num_row + phase_num + 2 return header, data_starting_row @@ -316,42 +324,31 @@ def _get_phases_from_header( ----- Regular expressions are used to collect phase name, formula and point group. This function have been tested with files from the - following vendor's formats: Oxford AZtec HKL, and EMsoft v4/v5. + following vendor's formats: Oxford AZtec HKL v5/v6, and EMsoft v4/v5. """ - phases = { "name": [], "space_group": [], "lattice_constants": [], "id": [], } - phase_num_row = 0 - for line in header: - if "Phases" in line: - phases_num_line = line + + for i, line in enumerate(header): + if line.startswith("Phases"): break - phase_num_row += 1 - phase_num = int(phases_num_line.split("\t")[1]) - for num in range(phase_num): - phase_data = header[phase_num_row + num + 1].split("\t") + n_phases = int(line.split("\t")[1]) + + for j in range(n_phases): + phase_data = header[i + 1 + j].split("\t") phases["name"].append(phase_data[2]) phases["space_group"].append(int(phase_data[4])) phases["lattice_constants"].append( [float(i) for i in phase_data[0].split(";") + phase_data[1].split(";")] ) - phases["id"].append(num + 1) + phases["id"].append(j + 1) names = phases["name"] - - # Ensure each phase has an ID (hopefully found in the header) phase_ids = [int(i) for i in phases["id"]] - n_phases = len(phases["name"]) - if len(phase_ids) == 0: - phase_ids += [i for i in range(n_phases)] - elif n_phases - len(phase_ids) > 0 and len(phase_ids) != 0: - next_id = max(phase_ids) + 1 - n_left = n_phases - len(phase_ids) - phase_ids += [i for i in range(next_id, next_id + n_left)] return phase_ids, names, phases["space_group"], phases["lattice_constants"] From d792fdfb341d6633da15b9507e963e9220719415 Mon Sep 17 00:00:00 2001 From: Zhou Xu Date: Mon, 29 May 2023 10:32:38 +1000 Subject: [PATCH 03/33] Update examples/plotting/interactive_IPF.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Håkon Wiik Ånes --- examples/plotting/interactive_IPF.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/plotting/interactive_IPF.py b/examples/plotting/interactive_IPF.py index 6e019827..5786709e 100644 --- a/examples/plotting/interactive_IPF.py +++ b/examples/plotting/interactive_IPF.py @@ -57,9 +57,7 @@ def on_click(event): eu = xmap_yx.rotations.to_euler(degrees=True)[0] phase_name = xmap_yx.phases_in_data[:].name plt.plot(x_pos, y_pos, "+", c="black", markersize=15, markeredgewidth=3) - plt.title( - f"Phase: {phase_name}, Euler angles: {np.array_str(eu, precision=2)[1:-1]}" - ) + plt.title(f"Phase: {phase_name}, Euler angles: {np.array_str(eu, precision=2)[1:-1]}") plt.draw() fig.canvas.mpl_connect("button_press_event", on_click) From 1e34a016d3eaf4dd5c96a45aa38ae349389b15f2 Mon Sep 17 00:00:00 2001 From: Zhou Xu Date: Mon, 29 May 2023 10:48:45 +1000 Subject: [PATCH 04/33] Update orix/io/plugins/ctf.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Håkon Wiik Ånes --- orix/io/plugins/ctf.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index a563a67f..8738d183 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -44,10 +44,7 @@ def file_reader(filename: str) -> CrystalMap: * EMsoft (from program `EMdpmerge`) * orix - All points satisfying the following criteria are classified as not - indexed: - - * Oxford AZtec HKL: Phase == 0 + All points with a phase of 0 are classified as not indexed. Parameters ---------- From d8f40057410d7a2c49541465b9440f155379dd79 Mon Sep 17 00:00:00 2001 From: Zhou Xu Date: Mon, 29 May 2023 11:01:38 +1000 Subject: [PATCH 05/33] Update orix/io/plugins/ctf.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Håkon Wiik Ånes --- orix/io/plugins/ctf.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index 8738d183..0df5f7ee 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -104,8 +104,7 @@ def file_reader(filename: str) -> CrystalMap: data_dict["phase_id"][not_indexed] = -1 # Set scan unit - scan_unit = "um" - data_dict["scan_unit"] = scan_unit + data_dict["scan_unit"] = "um" # Create rotations data_dict["rotations"] = Rotation.from_euler( From 209f74c7e26cc7cc50525f021565e96c4fc7c12d Mon Sep 17 00:00:00 2001 From: Zhou Xu Date: Mon, 29 May 2023 11:08:40 +1000 Subject: [PATCH 06/33] Update orix/io/plugins/ctf.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Håkon Wiik Ånes --- orix/io/plugins/ctf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index 0df5f7ee..e6c3ff85 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -328,7 +328,6 @@ def _get_phases_from_header( "lattice_constants": [], "id": [], } - for i, line in enumerate(header): if line.startswith("Phases"): break From d3651fad4bae341ce593d338ee55c2fc72cfcad1 Mon Sep 17 00:00:00 2001 From: IMBalENce Date: Mon, 29 May 2023 11:54:53 +1000 Subject: [PATCH 07/33] Refine the ctf reader for more efficient header reading --- orix/io/plugins/ctf.py | 41 ++++++++++------------------------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index e6c3ff85..4aa06d5e 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -135,34 +135,14 @@ def _get_header(file: TextIOWrapper) -> List[str]: """ all_data = [line.rstrip() for line in file.readlines()] - phase_num_row = 0 - phases_num_line = str() - for line in all_data: - if "Phases" in line: - phases_num_line = line - break - phase_num_row += 1 - if phases_num_line: - try: - phase_num = int(phases_num_line.split("\t")[1]) - header = all_data[: (phase_num_row + phase_num + 1)] - data_starting_row = phase_num_row + phase_num + 2 - except: - header = None - data_starting_row = None - warnings.warn( - f"Total number of phases has to be defined in the .ctf file." - f"No such information can be found. Incompatible file format." - ) - else: - header = None - data_starting_row = None - warnings.warn( - f"Total number of phases has to be defined in the .ctf file." - f"No such information can be found. Incompatible file format." - ) - - return header, data_starting_row + header = [] + line = file.readline() + i = 0 + while not line.startswith("Phase\tX\tY"): + header.append(line.rstrip()) + i += 1 + line = file.readline() + return header, i + 1 def _get_vendor_columns(header: List[str], n_cols_file: int) -> Tuple[str, List[str]]: @@ -179,17 +159,16 @@ def _get_vendor_columns(header: List[str], n_cols_file: int) -> Tuple[str, List[ Returns ------- vendor - Determined vendor (``"hkl"``, ``"emsoft"`` or ``"orix"``). + Determined vendor (``"hkl"`` or ``"emsoft"``). column_names List of column names. """ - # Assume Oxford TSL by default + # Assume Oxford HKL by default vendor = "hkl" # Determine vendor by searching for the vendor footprint in the header vendor_footprint = { "emsoft": "EMsoft", - "orix": "Column names: phi1, Phi, phi2", } footprint_line = None for name, footprint in vendor_footprint.items(): From 0db22b9e4ca08528fed1f2ca6432044435bcd182 Mon Sep 17 00:00:00 2001 From: IMBalENce Date: Mon, 29 May 2023 12:45:07 +1000 Subject: [PATCH 08/33] Add back Plugin description section to avoid io.load error --- orix/io/plugins/ctf.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index 4aa06d5e..a3b833f3 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -33,6 +33,12 @@ __all__ = ["file_reader"] +# Plugin description +format_name = "ctf" +file_extensions = ["ctf"] +writes = False +writes_this = CrystalMap + def file_reader(filename: str) -> CrystalMap: """Return a crystal map from a file in Oxford AZtec HKL's .ctf format. The @@ -133,8 +139,6 @@ def _get_header(file: TextIOWrapper) -> List[str]: data_starting_row The starting row number for the data lines """ - all_data = [line.rstrip() for line in file.readlines()] - header = [] line = file.readline() i = 0 From e489eb5292ee4d57ffc3bfc59215a8bac88355f4 Mon Sep 17 00:00:00 2001 From: Zhou Xu Date: Tue, 30 May 2023 10:16:42 +1000 Subject: [PATCH 09/33] Update orix/io/plugins/ctf.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Håkon Wiik Ånes --- orix/io/plugins/ctf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index a3b833f3..1e07cdc6 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -37,7 +37,7 @@ format_name = "ctf" file_extensions = ["ctf"] writes = False -writes_this = CrystalMap +writes_this = None def file_reader(filename: str) -> CrystalMap: From 25f975309ac9b9d521ed14b01ae4d5916fa96607 Mon Sep 17 00:00:00 2001 From: Zhou Xu Date: Tue, 30 May 2023 10:25:59 +1000 Subject: [PATCH 10/33] Update examples/plotting/interactive_IPF.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Håkon Wiik Ånes --- examples/plotting/interactive_IPF.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/plotting/interactive_IPF.py b/examples/plotting/interactive_IPF.py index 5786709e..80d5346f 100644 --- a/examples/plotting/interactive_IPF.py +++ b/examples/plotting/interactive_IPF.py @@ -5,9 +5,17 @@ This example shows how to use :doc:`matplotlib event connections ` -to add an interactive click function to the inverse pole figure (IPF) plot to +to add an interactive click function to the inverse pole figure (IPF) map to retrieve the phase name and corresponding Euler angles from the location of click. + +.. note:: + This example shows the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import matplotlib.pyplot as plt import numpy as np From a9cd4425cd7340aec462975f6167b70d6e812f61 Mon Sep 17 00:00:00 2001 From: IMBalENce Date: Tue, 5 Dec 2023 12:06:06 +1100 Subject: [PATCH 11/33] Add depreciation warning for loadctf() --- orix/io/__init__.py | 11 ++++------- orix/tests/io/test_ctf.py | 0 orix/tests/io/test_io.py | 9 ++++++++- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 orix/tests/io/test_ctf.py diff --git a/orix/io/__init__.py b/orix/io/__init__.py index f854b813..fe852b26 100644 --- a/orix/io/__init__.py +++ b/orix/io/__init__.py @@ -37,6 +37,7 @@ from h5py import File, is_hdf5 import numpy as np +from orix._util import deprecated from orix.crystal_map import CrystalMap from orix.io.plugins import plugin_list from orix.io.plugins._h5ebsd import hdf5group2dict @@ -73,6 +74,8 @@ def loadang(file_string: str) -> Rotation: return Rotation.from_euler(euler) +# TODO: Remove in 0.13 +@deprecated(since="0.12", removal="0.13", alternative="ctf") def loadctf(file_string: str) -> Rotation: """Load ``.ctf`` files. @@ -81,19 +84,13 @@ def loadctf(file_string: str) -> Rotation: file_string Path to the ``.ctf`` file. This file is assumed to list the Euler angles in the Bunge convention in the columns 5, 6, and 7. - The starting row for the data that contains Euler angles is relevant - to the number of inlcuded phases. Returns ------- rotation Rotations in the file. """ - with open(file_string, "r") as file: - all_data = [line.strip() for line in file.readlines()] - phase_num = int(all_data[12].split("\t")[1]) - - data = np.loadtxt(file_string, skiprows=(14 + phase_num))[:, 5:8] + data = np.loadtxt(file_string, skiprows=17)[:, 5:8] euler = np.radians(data) return Rotation.from_euler(euler) diff --git a/orix/tests/io/test_ctf.py b/orix/tests/io/test_ctf.py new file mode 100644 index 00000000..e69de29b diff --git a/orix/tests/io/test_io.py b/orix/tests/io/test_io.py index 5b040b9d..7a749463 100644 --- a/orix/tests/io/test_io.py +++ b/orix/tests/io/test_io.py @@ -152,5 +152,12 @@ def test_loadctf(): fname = "temp.ctf" np.savetxt(fname, z) - _ = loadctf(fname) + msg = msg = ( + r"Function `loadctf()` is deprecated and will be removed in version 0.13. " + r"Use `ctf()` instead. " + r"def loadctf(file_string: str) -> Rotation: " + ) + + with pytest.warns(np.VisibleDeprecationWarning, match=msg): + _ = loadctf(fname) os.remove(fname) From 9c283a5fa7f71217e0182a50e9f226593a732ccd Mon Sep 17 00:00:00 2001 From: IMBalENce Date: Tue, 5 Dec 2023 16:21:59 +1100 Subject: [PATCH 12/33] Fix test_io timeout issue, and simplify ctf reader --- orix/io/plugins/ctf.py | 147 +++++--------------------------------- orix/tests/io/test_ctf.py | 0 orix/tests/io/test_io.py | 18 ++--- 3 files changed, 23 insertions(+), 142 deletions(-) delete mode 100644 orix/tests/io/test_ctf.py diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index 1e07cdc6..17e1a477 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -48,7 +48,7 @@ def file_reader(filename: str) -> CrystalMap: * Oxford AZtec HKL * EMsoft (from program `EMdpmerge`) - * orix + All points with a phase of 0 are classified as not indexed. @@ -79,7 +79,22 @@ def file_reader(filename: str) -> CrystalMap: # Get vendor and column names n_rows, n_cols = file_data.shape - vendor, column_names = _get_vendor_columns(header, n_cols) + + column_names = ( + [ + "phase_id", + "x", + "y", + "bands", + "error", + "euler1", + "euler2", + "euler3", + "MAD", # Mean angular deviation + "BC", # Band contrast + "BS", # Band Slope + ], + ) # Data needed to create a CrystalMap object data_dict = { @@ -149,134 +164,6 @@ def _get_header(file: TextIOWrapper) -> List[str]: return header, i + 1 -def _get_vendor_columns(header: List[str], n_cols_file: int) -> Tuple[str, List[str]]: - """Return the .ctf file column names and vendor, determined from the - header. - - Parameters - ---------- - header - List with header lines as individual elements. - n_cols_file - Number of file columns. - - Returns - ------- - vendor - Determined vendor (``"hkl"`` or ``"emsoft"``). - column_names - List of column names. - """ - # Assume Oxford HKL by default - vendor = "hkl" - - # Determine vendor by searching for the vendor footprint in the header - vendor_footprint = { - "emsoft": "EMsoft", - } - footprint_line = None - for name, footprint in vendor_footprint.items(): - for line in header: - if footprint in line: - vendor = name - footprint_line = line - break - - # Variants of vendor column names encountered in real data sets - column_names = { - "hkl": { - 0: [ - "phase_id", - "x", - "y", - "bands", - "error", - "euler1", - "euler2", - "euler3", - "MAD", # Mean angular deviation - "BC", # Band contrast - "BS", # Band Slope - ], - }, - "emsoft": { - 0: [ - "phase_id", - "x", - "y", - "bands", - "error", - "euler1", - "euler2", - "euler3", - "MAD", # Mean angular deviation - "BC", # Band contrast - "BS", # Band Slope - ] - }, - "orix": { - 0: [ - "phase_id", - "x", - "y", - "bands", - "error", - "euler1", - "euler2", - "euler3", - "MAD", # Mean angular deviation - "BC", # Band contrast - "BS", # Band Slope - ], - }, - "unknown": { - 0: [ - "phase_id", - "x", - "y", - "bands", - "error", - "euler1", - "euler2", - "euler3", - "MAD", # Mean angular deviation - "BC", # Band contrast - "BS", # Band Slope - ] - }, - } - - n_variants = len(column_names[vendor]) - n_cols_expected = [len(column_names[vendor][k]) for k in range(n_variants)] - if vendor == "orix" and "Column names" in footprint_line: - # Append names of extra properties found, if any, in the orix - # .ang file header - vendor_column_names = column_names[vendor][0] - n_cols = n_cols_expected[0] - extra_props = footprint_line.split(":")[1].split(",")[n_cols:] - vendor_column_names += [i.lstrip(" ").replace(" ", "_") for i in extra_props] - elif n_cols_file not in n_cols_expected: - warnings.warn( - f"Number of columns, {n_cols_file}, in the file is not equal to " - f"the expected number of columns, {n_cols_expected}, for the \n" - f"assumed vendor '{vendor}'. Will therefore assume the following " - "columns: phase_id, x, y, bands, error, euler1, euler2, euler3" - "MAD, BC, BS, etc." - ) - vendor = "unknown" - vendor_column_names = column_names[vendor][0] - n_cols = len(vendor_column_names) - if n_cols_file > n_cols: - # Add any extra columns as properties - for i in range(n_cols_file - n_cols): - vendor_column_names.append("unknown" + str(i + 3)) - else: - idx = np.where(np.equal(n_cols_file, n_cols_expected))[0][0] - vendor_column_names = column_names[vendor][idx] - - return vendor, vendor_column_names - - def _get_phases_from_header( header: List[str], ) -> Tuple[List[int], List[str], List[str], List[List[float]]]: diff --git a/orix/tests/io/test_ctf.py b/orix/tests/io/test_ctf.py deleted file mode 100644 index e69de29b..00000000 diff --git a/orix/tests/io/test_io.py b/orix/tests/io/test_io.py index 7a749463..9235366a 100644 --- a/orix/tests/io/test_io.py +++ b/orix/tests/io/test_io.py @@ -73,11 +73,11 @@ def test_load_no_filename_match(self): with pytest.raises(IOError, match=f"No filename matches '{fname}'."): _ = load(fname) - @pytest.mark.parametrize("temp_file_path", ["ctf"], indirect=["temp_file_path"]) - def test_load_unsupported_format(self, temp_file_path): - np.savetxt(temp_file_path, X=np.random.rand(100, 8)) - with pytest.raises(IOError, match=f"Could not read "): - _ = load(temp_file_path) + # @pytest.mark.parametrize("temp_file_path", ["ctf"], indirect=["temp_file_path"]) + # def test_load_unsupported_format(self, temp_file_path): + # np.savetxt(temp_file_path, X=np.random.rand(100, 8)) + # with pytest.raises(IOError, match=f"Could not read "): + # _ = load(temp_file_path) @pytest.mark.parametrize( "manufacturer, expected_plugin", @@ -152,12 +152,6 @@ def test_loadctf(): fname = "temp.ctf" np.savetxt(fname, z) - msg = msg = ( - r"Function `loadctf()` is deprecated and will be removed in version 0.13. " - r"Use `ctf()` instead. " - r"def loadctf(file_string: str) -> Rotation: " - ) - - with pytest.warns(np.VisibleDeprecationWarning, match=msg): + with pytest.warns(np.VisibleDeprecationWarning): _ = loadctf(fname) os.remove(fname) From e4769606e30637dab10437f2b5be45e497851aef Mon Sep 17 00:00:00 2001 From: IMBalENce Date: Tue, 5 Dec 2023 16:31:04 +1100 Subject: [PATCH 13/33] Enhance the interactive IPF plot example --- examples/plotting/interactive_IPF.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/examples/plotting/interactive_IPF.py b/examples/plotting/interactive_IPF.py index 80d5346f..d86ec9f5 100644 --- a/examples/plotting/interactive_IPF.py +++ b/examples/plotting/interactive_IPF.py @@ -39,6 +39,18 @@ rgb_all[xmap.phase_id == 2] = rgb_fe xmap_gb = rgb_all.reshape(xmap.shape + (3,)) +# Add an overlay of dot product to the orientation color map to enhance grain boundary contrast= +xmap_overlay = rgb_all.reshape(xmap.shape + (3,)) +overlay_1dim = (xmap.prop["dp"]).reshape(xmap.shape) +overlay_min = np.nanmin(overlay_1dim) +rescaled_overlay = (overlay_1dim - overlay_min) / ( + np.nanmax(overlay_1dim) - overlay_min +) +n_channels = 3 +for i in range(n_channels): + xmap_overlay[:, :, i] *= rescaled_overlay +xmap_image = xmap_overlay + # An interactive function for getting the phase name and euler angles from the clicking position def select_point(image): @@ -69,6 +81,7 @@ def on_click(event): plt.draw() fig.canvas.mpl_connect("button_press_event", on_click) + plt.axis("off") plt.show() plt.draw() return coords # click point coordintes in [x, y] format From 6ba09f31f9afe7fc71d8e6742cb00ade0e1dee3f Mon Sep 17 00:00:00 2001 From: Zhou Xu Date: Tue, 5 Dec 2023 16:33:44 +1100 Subject: [PATCH 14/33] Update orix/io/plugins/ctf.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Håkon Wiik Ånes --- orix/io/plugins/ctf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index 17e1a477..f5d7b628 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -27,7 +27,6 @@ from diffpy.structure import Lattice, Structure import numpy as np -# from orix import __version__ from orix.crystal_map import CrystalMap, PhaseList from orix.quaternion import Rotation From 6b8053a0a9d7bdaea8df83cb77e3ef530bc80866 Mon Sep 17 00:00:00 2001 From: IMBalENce Date: Tue, 5 Dec 2023 16:44:59 +1100 Subject: [PATCH 15/33] minor fix in io.plugins.ctf --- orix/io/plugins/ctf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index f5d7b628..b36379f8 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -27,6 +27,7 @@ from diffpy.structure import Lattice, Structure import numpy as np +from orix import __version__ from orix.crystal_map import CrystalMap, PhaseList from orix.quaternion import Rotation From 4671b73af324a2904b002908af8d5d8ab9d5be6c Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Tue, 26 Mar 2024 10:33:13 -0500 Subject: [PATCH 16/33] Testing: Update test to pass with new unknown file type. --- orix/tests/io/test_io.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/orix/tests/io/test_io.py b/orix/tests/io/test_io.py index 9235366a..48d65441 100644 --- a/orix/tests/io/test_io.py +++ b/orix/tests/io/test_io.py @@ -73,11 +73,11 @@ def test_load_no_filename_match(self): with pytest.raises(IOError, match=f"No filename matches '{fname}'."): _ = load(fname) - # @pytest.mark.parametrize("temp_file_path", ["ctf"], indirect=["temp_file_path"]) - # def test_load_unsupported_format(self, temp_file_path): - # np.savetxt(temp_file_path, X=np.random.rand(100, 8)) - # with pytest.raises(IOError, match=f"Could not read "): - # _ = load(temp_file_path) + @pytest.mark.parametrize("temp_file_path", ["ktf"], indirect=["temp_file_path"]) + def test_load_unsupported_format(self, temp_file_path): + np.savetxt(temp_file_path, X=np.random.rand(100, 8)) + with pytest.raises(IOError, match=f"Could not read "): + _ = load(temp_file_path) @pytest.mark.parametrize( "manufacturer, expected_plugin", From 4164f3a44ba5a966f49f8844a574f737e2d30792 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 Mar 2024 17:12:37 +0000 Subject: [PATCH 17/33] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- examples/plotting/interactive_IPF.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/plotting/interactive_IPF.py b/examples/plotting/interactive_IPF.py index d86ec9f5..22529f55 100644 --- a/examples/plotting/interactive_IPF.py +++ b/examples/plotting/interactive_IPF.py @@ -17,6 +17,7 @@ You can copy and paste individual parts, or download the entire example using the link at the bottom of the page. """ + import matplotlib.pyplot as plt import numpy as np @@ -77,7 +78,9 @@ def on_click(event): eu = xmap_yx.rotations.to_euler(degrees=True)[0] phase_name = xmap_yx.phases_in_data[:].name plt.plot(x_pos, y_pos, "+", c="black", markersize=15, markeredgewidth=3) - plt.title(f"Phase: {phase_name}, Euler angles: {np.array_str(eu, precision=2)[1:-1]}") + plt.title( + f"Phase: {phase_name}, Euler angles: {np.array_str(eu, precision=2)[1:-1]}" + ) plt.draw() fig.canvas.mpl_connect("button_press_event", on_click) From f25089861b931f96c741b50c1969f395aa8a7481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Thu, 25 Apr 2024 22:34:50 +0200 Subject: [PATCH 18/33] Rename interactive xmap plot, simplify and use CrystalMap.plot() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- examples/plotting/interactive_IPF.py | 93 ------------------------- examples/plotting/interactive_xmap.py | 98 +++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 93 deletions(-) delete mode 100644 examples/plotting/interactive_IPF.py create mode 100644 examples/plotting/interactive_xmap.py diff --git a/examples/plotting/interactive_IPF.py b/examples/plotting/interactive_IPF.py deleted file mode 100644 index 22529f55..00000000 --- a/examples/plotting/interactive_IPF.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -==================================== -Interactive IPF map with Euler angle -==================================== - -This example shows how to use -:doc:`matplotlib event connections ` -to add an interactive click function to the inverse pole figure (IPF) map to -retrieve the phase name and corresponding Euler angles from the location of -click. - -.. note:: - This example shows the interactive capabilities of Matplotlib, and this - will not appear in the static documentation. Please run this code on your - machine to see the interactivity. - - You can copy and paste individual parts, or download the entire example - using the link at the bottom of the page. -""" - -import matplotlib.pyplot as plt -import numpy as np - -from orix import data, plot - -xmap = data.sdss_ferrite_austenite(allow_download=True) -print(xmap) - -pg_laue = xmap.phases[1].point_group.laue -ori_au = xmap["austenite"].orientations -ori_fe = xmap["ferrite"].orientations - -# Orientation colors -ipf_key = plot.IPFColorKeyTSL(pg_laue) -rgb_au = ipf_key.orientation2color(ori_au) -rgb_fe = ipf_key.orientation2color(ori_fe) - -rgb_all = np.zeros((xmap.size, 3)) -rgb_all[xmap.phase_id == 1] = rgb_au -rgb_all[xmap.phase_id == 2] = rgb_fe -xmap_gb = rgb_all.reshape(xmap.shape + (3,)) - -# Add an overlay of dot product to the orientation color map to enhance grain boundary contrast= -xmap_overlay = rgb_all.reshape(xmap.shape + (3,)) -overlay_1dim = (xmap.prop["dp"]).reshape(xmap.shape) -overlay_min = np.nanmin(overlay_1dim) -rescaled_overlay = (overlay_1dim - overlay_min) / ( - np.nanmax(overlay_1dim) - overlay_min -) -n_channels = 3 -for i in range(n_channels): - xmap_overlay[:, :, i] *= rescaled_overlay -xmap_image = xmap_overlay - - -# An interactive function for getting the phase name and euler angles from the clicking position -def select_point(image): - """Return location of interactive user click on image.""" - fig, ax = plt.subplots(subplot_kw=dict(projection="plot_map"), figsize=(12, 8)) - ax.imshow(image) - ax.set_title("Click position") - coords = [] - - def on_click(event): - print(event.xdata, event.ydata) - coords.append(event.xdata) - coords.append(event.ydata) - plt.clf() - plt.imshow(image) - try: - x_pos = coords[-2] - y_pos = coords[-1] - except: - x_pos = 0 - y_pos = 0 - - xmap_yx = xmap[int(y_pos), int(x_pos)] - eu = xmap_yx.rotations.to_euler(degrees=True)[0] - phase_name = xmap_yx.phases_in_data[:].name - plt.plot(x_pos, y_pos, "+", c="black", markersize=15, markeredgewidth=3) - plt.title( - f"Phase: {phase_name}, Euler angles: {np.array_str(eu, precision=2)[1:-1]}" - ) - plt.draw() - - fig.canvas.mpl_connect("button_press_event", on_click) - plt.axis("off") - plt.show() - plt.draw() - return coords # click point coordintes in [x, y] format - - -result = select_point(xmap_gb) diff --git a/examples/plotting/interactive_xmap.py b/examples/plotting/interactive_xmap.py new file mode 100644 index 00000000..d3925535 --- /dev/null +++ b/examples/plotting/interactive_xmap.py @@ -0,0 +1,98 @@ +""" +============================ +Interactive crystal map plot +============================ + +This example shows how to use +:doc:`matplotlib event connections ` to +add an interactive click function to a :class:`~orix.crystal_map.CrystalMap` plot. +Here, we navigate an inverse pole figure (IPF) map and retreive the phase name and +corresponding Euler angles from the location of the click. + +.. note:: + + This example uses the interactive capabilities of Matplotlib, and this will not + appear in the static documentation. + Please run this code on your machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example using the + link at the bottom of the page. +""" + +import matplotlib.pyplot as plt +import numpy as np + +from orix import data, plot +from orix.crystal_map import CrystalMap + +xmap = data.sdss_ferrite_austenite(allow_download=True) +print(xmap) + +pg_laue = xmap.phases[1].point_group.laue +O_au = xmap["austenite"].orientations +O_fe = xmap["ferrite"].orientations + +# Get IPF colors +ipf_key = plot.IPFColorKeyTSL(pg_laue) +rgb_au = ipf_key.orientation2color(O_au) +rgb_fe = ipf_key.orientation2color(O_fe) + +# Combine IPF color arrays +rgb_all = np.zeros((xmap.size, 3)) +phase_id_au = xmap.phases.id_from_name("austenite") +phase_id_fe = xmap.phases.id_from_name("ferrite") +rgb_all[xmap.phase_id == phase_id_au] = rgb_au +rgb_all[xmap.phase_id == phase_id_fe] = rgb_fe + + +def select_point(xmap: CrystalMap, rgb_all: np.ndarray) -> tuple[int, int]: + """Return location of interactive user click on image. + + Interactive function for showing the phase name and Euler angles + from the click-position. + """ + fig = xmap.plot( + rgb_all, + overlay="dp", + return_figure=True, + figure_kwargs={"figsize": (12, 8)}, + ) + ax = fig.axes[0] + ax.set_title("Click position") + + # Extract array in the plot with IPF colors + dot product overlay + rgb_dp_2d = ax.images[0].get_array() + + x = y = 0 + + def on_click(event): + x, y = (event.xdata, event.ydata) + if x is None: + print("Please click inside the IPF map") + return + print(x, y) + + # Extract location in crystal map and extract phase name and + # Euler angles + xmap_yx = xmap[int(np.round(y)), int(np.round(x))] + phase_name = xmap_yx.phases_in_data[:].name + eu = xmap_yx.rotations.to_euler(degrees=True)[0].round(2) + + # Format Euler angles + eu_str = "(" + ", ".join(np.array_str(eu)[1:-1].split()) + ")" + + plt.clf() + plt.imshow(rgb_dp_2d) + plt.plot(x, y, "+", c="k", markersize=15, markeredgewidth=3) + plt.title( + f"Phase: {phase_name}, Euler angles: $(\phi_1, \Phi, \phi_2)$ = {eu_str}" + ) + plt.draw() + + fig.canvas.mpl_connect("button_press_event", on_click) + + return x, y + + +x, y = select_point(xmap, rgb_all) +plt.show() From cba8ad19d85c2d21884d504fdc61b2fb83d19ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sat, 27 Apr 2024 18:45:54 +0200 Subject: [PATCH 19/33] Deprecate loadang (not just loadctf) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- CHANGELOG.rst | 3 +++ orix/io/__init__.py | 7 ++++--- orix/tests/io/test_ang.py | 3 ++- orix/tests/io/test_io.py | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 103fa4dd..a1f86042 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,7 @@ Unreleased Added ----- +- We can now read 2D crystal maps from Channel Text Files (CTFs) using ``io.load()``. Changed ------- @@ -20,6 +21,8 @@ Removed Deprecated ---------- +- ``loadang()`` and ``loadctf()`` are deprecated and will be removed in the next minor + release. Please use ``io.load()`` instead. Fixed ----- diff --git a/orix/io/__init__.py b/orix/io/__init__.py index a07d4813..c1434776 100644 --- a/orix/io/__init__.py +++ b/orix/io/__init__.py @@ -46,7 +46,6 @@ extensions = [plugin.file_extensions for plugin in plugin_list if plugin.writes] -# Lists what will be imported when calling "from orix.io import *" __all__ = [ "loadang", "loadctf", @@ -55,6 +54,8 @@ ] +# TODO: Remove after 0.13.0 +@deprecated(since="0.13", removal="0.14", alternative="io.load") def loadang(file_string: str) -> Rotation: """Load ``.ang`` files. @@ -74,8 +75,8 @@ def loadang(file_string: str) -> Rotation: return Rotation.from_euler(euler) -# TODO: Remove in 0.13 -@deprecated(since="0.12", removal="0.13", alternative="ctf") +# TODO: Remove after 0.13.0 +@deprecated(since="0.13", removal="0.14", alternative="io.load") def loadctf(file_string: str) -> Rotation: """Load ``.ctf`` files. diff --git a/orix/tests/io/test_ang.py b/orix/tests/io/test_ang.py index 082c4f70..8cf74340 100644 --- a/orix/tests/io/test_ang.py +++ b/orix/tests/io/test_ang.py @@ -77,7 +77,8 @@ indirect=["angfile_astar"], ) def test_loadang(angfile_astar, expected_data): - loaded_data = loadang(angfile_astar) + with pytest.warns(np.VisibleDeprecationWarning): + loaded_data = loadang(angfile_astar) assert np.allclose(loaded_data.data, expected_data) diff --git a/orix/tests/io/test_io.py b/orix/tests/io/test_io.py index f14d7408..12719c47 100644 --- a/orix/tests/io/test_io.py +++ b/orix/tests/io/test_io.py @@ -146,8 +146,8 @@ def test_save_overwrite( assert crystal_map2.phases[0].name == expected_phase_name +# TODO: Remove after 0.13.0 def test_loadctf(): - """Crude test of the ctf loader""" z = np.random.rand(100, 8) fname = "temp.ctf" np.savetxt(fname, z) From e62b06831e1fc278970251d4ee64ff0bbe54b6bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 28 Apr 2024 22:57:55 +0200 Subject: [PATCH 20/33] Prefer Formula over MaterialName for phase names from .ang files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- CHANGELOG.rst | 2 + orix/io/plugins/ang.py | 151 ++++++++++++++++++-------------------- orix/tests/io/test_ang.py | 12 +-- 3 files changed, 79 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a1f86042..bf9b41a8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,8 @@ Added Changed ------- +- Phase names in crystal maps read from .ang files with ``io.load()`` now prefer to use + the abbreviated "Formula" instead of "MaterialName" in the file header. Removed ------- diff --git a/orix/io/plugins/ang.py b/orix/io/plugins/ang.py index e82232d5..dc1481b2 100644 --- a/orix/io/plugins/ang.py +++ b/orix/io/plugins/ang.py @@ -22,7 +22,7 @@ from io import TextIOWrapper import re -from typing import List, Optional, Tuple, Union +from typing import Optional, Union import warnings from diffpy.structure import Lattice, Structure @@ -43,10 +43,12 @@ def file_reader(filename: str) -> CrystalMap: - """Return a crystal map from a file in EDAX TLS's .ang format. The - map in the input is assumed to be 2D. + """Return a crystal map from a file in EDAX TLS's .ang format. - Many vendors produce an .ang file. Supported vendors are: + The map in the input is assumed to be 2D. + + Many vendors/programs produce an .ang file. Files from the following + vendors/programs are tested: * EDAX TSL * NanoMegas ASTAR Index @@ -72,20 +74,19 @@ def file_reader(filename: str) -> CrystalMap: with open(filename) as f: header = _get_header(f) - # Get phase names and crystal symmetries from header (potentially empty) - phase_ids, phase_names, symmetries, lattice_constants = _get_phases_from_header( - header - ) - structures = [] - for name, abcABG in zip(phase_names, lattice_constants): - structures.append(Structure(title=name, lattice=Lattice(*abcABG))) + # Phase information, potentially empty + phases = _get_phases_from_header(header) + phases["structures"] = [] + lattice_constants = phases.pop("lattice_constants") + for name, abcABG in zip(phases["names"], lattice_constants): + structure = Structure(title=name, lattice=Lattice(*abcABG)) + phases["structures"].append(structure) # Read all file data file_data = np.loadtxt(filename) # Get vendor and column names - n_rows, n_cols = file_data.shape - vendor, column_names = _get_vendor_columns(header, n_cols) + vendor, column_names = _get_vendor_columns(header, file_data.shape[1]) # Data needed to create a CrystalMap object data_dict = { @@ -98,18 +99,13 @@ def file_reader(filename: str) -> CrystalMap: "prop": {}, } for column, name in enumerate(column_names): - if name in data_dict.keys(): + if name in data_dict: data_dict[name] = file_data[:, column] else: data_dict["prop"][name] = file_data[:, column] # Add phase list to dictionary - data_dict["phase_list"] = PhaseList( - names=phase_names, - point_groups=symmetries, - structures=structures, - ids=phase_ids, - ) + data_dict["phase_list"] = PhaseList(**phases) # Set which data points are not indexed # TODO: Add not-indexed convention for INDEX ASTAR @@ -134,7 +130,7 @@ def file_reader(filename: str) -> CrystalMap: return CrystalMap(**data_dict) -def _get_header(file: TextIOWrapper) -> List[str]: +def _get_header(file: TextIOWrapper) -> list[str]: """Return the first lines starting with '#' in an .ang file. Parameters @@ -149,13 +145,16 @@ def _get_header(file: TextIOWrapper) -> List[str]: """ header = [] line = file.readline() - while line.startswith("#"): + i = 0 + # Prevent endless loop by not reading past 1 000 lines + while line.startswith("#") and i < 1_000: header.append(line.rstrip()) line = file.readline() + i += 1 return header -def _get_vendor_columns(header: List[str], n_cols_file: int) -> Tuple[str, List[str]]: +def _get_vendor_columns(header: list[str], n_cols_file: int) -> tuple[str, list[str]]: """Return the .ang file column names and vendor, determined from the header. @@ -174,15 +173,13 @@ def _get_vendor_columns(header: List[str], n_cols_file: int) -> Tuple[str, List[ column_names List of column names. """ - # Assume EDAX TSL by default - vendor = "tsl" - - # Determine vendor by searching for the vendor footprint in the header + # Determine vendor by searching for vendor footprint in header vendor_footprint = { "emsoft": "EMsoft", "astar": "ACOM", "orix": "Column names: phi1, Phi, phi2", } + vendor = "tsl" # Default guess footprint_line = None for name, footprint in vendor_footprint.items(): for line in header: @@ -307,9 +304,7 @@ def _get_vendor_columns(header: List[str], n_cols_file: int) -> Tuple[str, List[ return vendor, vendor_column_names -def _get_phases_from_header( - header: List[str], -) -> Tuple[List[int], List[str], List[str], List[List[float]]]: +def _get_phases_from_header(header: list[str]) -> dict: """Return phase names and symmetries detected in an .ang file header. @@ -320,43 +315,38 @@ def _get_phases_from_header( Returns ------- - ids - Phase IDs. - phase_names - List of names of detected phases. - phase_point_groups - List of point groups of detected phase. - lattice_constants - List of list of lattice parameters of detected phases. + phase_dict + Dictionary with the following keys (and types): "ids" (int), + "names" (str), "point_groups" (str), "lattice_constants" (list + of floats). Notes ----- - Regular expressions are used to collect phase name, formula and - point group. This function have been tested with files from the - following vendor's formats: EDAX TSL OIM Data Collection v7, ASTAR - Index, and EMsoft v4/v5. + This function has been tested with files from the following vendor's + formats: EDAX TSL OIM Data Collection v7, ASTAR Index, and EMsoft + v4/v5. """ - regexps = { - "id": "# Phase([ \t]+)([0-9 ]+)", - "name": "# MaterialName([ \t]+)([A-z0-9 ]+)", - "formula": "# Formula([ \t]+)([A-z0-9 ]+)", - "point_group": "# Symmetry([ \t]+)([A-z0-9 ]+)", + str_patterns = { + "ids": "# Phase([ \t]+)([0-9 ]+)", + "names": "# MaterialName([ \t]+)([A-z0-9 ]+)", + "formulas": "# Formula([ \t]+)([A-z0-9 ]+)", + "point_groups": "# Symmetry([ \t]+)([A-z0-9 ]+)", "lattice_constants": r"# LatticeConstants([ \t+])(.*)", } phases = { - "name": [], - "formula": [], - "point_group": [], + "ids": [], + "names": [], + "formulas": [], + "point_groups": [], "lattice_constants": [], - "id": [], } for line in header: - for key, exp in regexps.items(): + for key, exp in str_patterns.items(): match = re.search(exp, line) if match: group = re.split("[ \t]", line.lstrip("# ").rstrip(" ")) group = list(filter(None, group)) - if key == "name": + if key == "names": group = " ".join(group[1:]) # Drop "MaterialName" elif key == "lattice_constants": group = [float(i) for i in group[1:]] @@ -364,22 +354,24 @@ def _get_phases_from_header( group = group[-1] phases[key].append(group) - # Check if formula is empty (sometimes the case for ASTAR Index) - names = phases["formula"] - if len(names) == 0 or any([i != "" for i in names]): - names = phases["name"] + n_phases = len(phases["names"]) + + # Use formulas in place of material names if they are all valid + formulas = phases.pop("formulas") + if len(formulas) == n_phases and all([len(name) for name in formulas]): + phases["names"] = formulas # Ensure each phase has an ID (hopefully found in the header) - phase_ids = [int(i) for i in phases["id"]] - n_phases = len(phases["name"]) - if len(phase_ids) == 0: + phase_ids = [int(i) for i in phases["ids"]] + if not len(phase_ids): phase_ids += [i for i in range(n_phases)] elif n_phases - len(phase_ids) > 0 and len(phase_ids) != 0: next_id = max(phase_ids) + 1 n_left = n_phases - len(phase_ids) phase_ids += [i for i in range(next_id, next_id + n_left)] + phases["ids"] = phase_ids - return phase_ids, names, phases["point_group"], phases["lattice_constants"] + return phases def file_writer( @@ -390,7 +382,7 @@ def file_writer( confidence_index_prop: Optional[str] = None, detector_signal_prop: Optional[str] = None, pattern_fit_prop: Optional[str] = None, - extra_prop: Union[str, List[str], None] = None, + extra_prop: Union[str, list[str], None] = None, ): """Write a crystal map to an .ang file readable by MTEX and EDAX TSL OIM Analysis v7. @@ -424,31 +416,31 @@ def file_writer( Which map property to use as the image quality. If not given (default), ``"iq"`` or ``"imagequality"``, if present, is used, otherwise just zeros. If the property has more than one value - per point and ``index`` is not given, only the first value is + per point and *index* is not given, only the first value is used. confidence_index_prop Which map property to use as the confidence index. If not given (default), ``"ci"``, ``"confidenceindex"``, ``"scores"``, or ``"correlation"``, if present, is used, otherwise just zeros. If - the property has more than one value per point and ``index`` is + the property has more than one value per point and *index* is not given, only the first value is used. detector_signal_prop Which map property to use as the detector signal. If not given (default), ``"ds"``, or ``"detector_signal"``, if present, is used, otherwise just zeros. If the property has more than one - value per point and ``index`` is not given, only the first value + value per point and *index* is not given, only the first value is used. pattern_fit_prop Which map property to use as the pattern fit. If not given (default), ``"fit"`` or ``"patternfit"``, if present, is used, otherwise just zeros. If the property has more than one value - per point and ``index`` is not given, only the first value is + per point and *index* is not given, only the first value is used. extra_prop One or multiple properties to add as extra columns in the .ang file, as a string or a list of strings. If not given (default), no extra properties are added. If a property has more than one - value per point and ``index`` is not given, only the first value + value per point and *index* is not given, only the first value is used. """ header = _get_header_from_phases(xmap) @@ -598,7 +590,7 @@ def _get_header_from_phases(xmap: CrystalMap) -> str: phase_name = phase.name if phase_name == "": phase_name = f"phase{phase_id}" - if phase.point_group is None: + if not phase.point_group: point_group_name = "1" else: proper_point_group = phase.point_group.proper_subgroup @@ -634,7 +626,7 @@ def _get_header_from_phases(xmap: CrystalMap) -> str: return header -def _get_nrows_ncols_step_sizes(xmap: CrystalMap) -> Tuple[int, int, float, float]: +def _get_nrows_ncols_step_sizes(xmap: CrystalMap) -> tuple[int, int, float, float]: """Get crystal map shape and step sizes. Parameters @@ -649,7 +641,7 @@ def _get_nrows_ncols_step_sizes(xmap: CrystalMap) -> Tuple[int, int, float, floa dy dx """ - nrows, ncols = (1, 1) + nrows = ncols = 1 dy, dx = xmap.dy, xmap.dx if xmap.ndim == 1: ncols = xmap.shape[0] @@ -677,8 +669,8 @@ def _get_column_width(max_value: int, decimals: int = 5) -> int: def _get_prop_arrays( xmap: CrystalMap, - prop_names: List[str], - desired_prop_names: List[str], + prop_names: list[str], + desired_prop_names: list[str], map_size: int, index: Union[int, None], decimals: int = 5, @@ -733,8 +725,8 @@ def _get_prop_arrays( def _get_prop_array( xmap: CrystalMap, prop_name: str, - expected_prop_names: List[str], - prop_names: List[str], + expected_prop_names: list[str], + prop_names: list[str], prop_names_lower_arr: np.ndarray, index: Union[int, None], decimals: int = 5, @@ -745,9 +737,8 @@ def _get_prop_array( Reasons for why the property cannot be read: - * Property name isn't among the crystal map properties - * Property has only one value per point, but ``index`` is not - ``None`` + * Property name isn't among the crystal map properties + * Property has only one value per point, but *index* is not ``None`` Parameters ---------- @@ -766,10 +757,10 @@ def _get_prop_array( Property array or none if none found. """ kwargs = dict(decimals=decimals, fill_value=fill_value) - if len(prop_names_lower_arr) == 0 and prop_name is None: + if not len(prop_names_lower_arr) and not prop_name: return else: - if prop_name is None: + if not prop_name: # Search for a suitable property for k in expected_prop_names: is_equal = k == prop_names_lower_arr @@ -783,6 +774,6 @@ def _get_prop_array( # Return the single array even if `index` is given return xmap.get_map_data(prop_name, **kwargs) else: - if index is None: + if not index: index = 0 return xmap.get_map_data(xmap.prop[prop_name][:, index], **kwargs) diff --git a/orix/tests/io/test_ang.py b/orix/tests/io/test_ang.py index 8cf74340..4e091f93 100644 --- a/orix/tests/io/test_ang.py +++ b/orix/tests/io/test_ang.py @@ -176,7 +176,7 @@ def test_load_ang_tsl( assert xmap.phases.size == 2 # Including non-indexed assert xmap.phases.ids == [-1, 0] phase = xmap.phases[0] - assert phase.name == "Aluminum" + assert phase.name == "Al" assert phase.point_group.name == "432" @pytest.mark.parametrize( @@ -501,12 +501,12 @@ def test_get_phases_from_header( "#", "# GRID: SqrGrid#", ] - ids, names, point_groups, lattice_constants = _get_phases_from_header(header) + phases = _get_phases_from_header(header) - assert names == expected_names - assert point_groups == expected_point_groups - assert np.allclose(lattice_constants, expected_lattice_constants) - assert np.allclose(ids, expected_phase_id) + assert phases["names"] == expected_names + assert phases["point_groups"] == expected_point_groups + assert np.allclose(phases["lattice_constants"], expected_lattice_constants) + assert np.allclose(phases["ids"], expected_phase_id) class TestAngWriter: From bc70df195ae0e37ec2128b5481946df337809609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 28 Apr 2024 22:58:45 +0200 Subject: [PATCH 21/33] Move private crystal map functions to allow calls from other files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/crystal_map/crystal_map.py | 122 ++++++++++++++++++++------------ 1 file changed, 75 insertions(+), 47 deletions(-) diff --git a/orix/crystal_map/crystal_map.py b/orix/crystal_map/crystal_map.py index aae07db9..ced9681d 100644 --- a/orix/crystal_map/crystal_map.py +++ b/orix/crystal_map/crystal_map.py @@ -17,7 +17,7 @@ # along with orix. If not, see . import copy -from typing import Optional, Tuple, Union +from typing import Optional, Union import matplotlib.pyplot as plt import numpy as np @@ -350,12 +350,12 @@ def y(self) -> Union[None, np.ndarray]: @property def dx(self) -> float: """Return the x coordinate step size.""" - return self._step_size_from_coordinates(self._x) + return _step_size_from_coordinates(self._x) @property def dy(self) -> float: """Return the y coordinate step size.""" - return self._step_size_from_coordinates(self._y) + return _step_size_from_coordinates(self._y) @property def row(self) -> Union[None, np.ndarray]: @@ -1039,29 +1039,9 @@ def plot( if return_figure: return fig - @staticmethod - def _step_size_from_coordinates(coordinates: np.ndarray) -> float: - """Return step size in input ``coordinates`` array. - - Parameters - ---------- - coordinates - Linear coordinate array. - - Returns - ------- - step_size - Step size in ``coordinates`` array. - """ - unique_sorted = np.sort(np.unique(coordinates)) - step_size = 0 - if unique_sorted.size != 1: - step_size = unique_sorted[1] - unique_sorted[0] - return step_size - def _data_slices_from_coordinates(self, only_is_in_data: bool = True) -> tuple: - """Return a tuple of slices defining the current data extent in - all directions. + """Return a slices defining the current data extent in all + directions. Parameters ---------- @@ -1072,23 +1052,14 @@ def _data_slices_from_coordinates(self, only_is_in_data: bool = True) -> tuple: Returns ------- slices - Data slice in each existing dimension, in (z, y, x) order. + Data slice in each existing direction in (y, x) order. """ if only_is_in_data: coordinates = self._coordinates else: coordinates = self._all_coordinates - - # Loop over dimension coordinates and step sizes - slices = [] - for coords, step in zip(coordinates.values(), self._step_sizes.values()): - if coords is not None and step != 0: - c_min, c_max = np.min(coords), np.max(coords) - i_min = int(np.around(c_min / step)) - i_max = int(np.around((c_max / step) + 1)) - slices.append(slice(i_min, i_max)) - - return tuple(slices) + slices = _data_slices_from_coordinates(coordinates, self._step_sizes) + return slices def _data_shape_from_coordinates(self, only_is_in_data: bool = True) -> tuple: """Return data shape based upon coordinate arrays. @@ -1102,7 +1073,7 @@ def _data_shape_from_coordinates(self, only_is_in_data: bool = True) -> tuple: Returns ------- data_shape - Shape of data in all existing dimensions, in (z, y, x) order. + Shape of data in each existing direction in (y, x) order. """ data_shape = [] for dim_slice in self._data_slices_from_coordinates(only_is_in_data): @@ -1110,13 +1081,70 @@ def _data_shape_from_coordinates(self, only_is_in_data: bool = True) -> tuple: return tuple(data_shape) +def _data_slices_from_coordinates( + coords: dict[str, np.ndarray], steps: Union[dict[str, float], None] = None +) -> tuple[slice]: + """Return a list of slices defining the current data extent in all + directions. + + Parameters + ---------- + coords + Dictionary with coordinate arrays. + steps + Dictionary with step sizes in each direction. If not given, they + are computed from *coords*. + + Returns + ------- + slices + Data slice in each direction. + """ + if steps is None: + steps = { + "x": _step_size_from_coordinates(coords["x"]), + "y": _step_size_from_coordinates(coords["y"]), + } + slices = [] + for coords, step in zip(coords.values(), steps.values()): + if coords is not None and step != 0: + c_min, c_max = np.min(coords), np.max(coords) + i_min = int(np.around(c_min / step)) + i_max = int(np.around((c_max / step) + 1)) + slices.append(slice(i_min, i_max)) + slices = tuple(slices) + return slices + + +def _step_size_from_coordinates(coordinates: np.ndarray) -> float: + """Return step size in input *coordinates* array. + + Parameters + ---------- + coordinates + Linear coordinate array. + + Returns + ------- + step_size + Step size in *coordinates* array. + """ + unique_sorted = np.sort(np.unique(coordinates)) + if unique_sorted.size != 1: + step_size = unique_sorted[1] - unique_sorted[0] + else: + step_size = 0 + return step_size + + def create_coordinate_arrays( shape: Optional[tuple] = None, step_sizes: Optional[tuple] = None -) -> Tuple[dict, int]: - """Create flattened coordinate arrays from a given map shape and +) -> tuple[dict, int]: + """Return flattened coordinate arrays from a given map shape and step sizes, suitable for initializing a - :class:`~orix.crystal_map.CrystalMap`. Arrays for 1D or 2D maps can - be returned. + :class:`~orix.crystal_map.CrystalMap`. + + Arrays for 1D or 2D maps can be returned. Parameters ---------- @@ -1125,13 +1153,13 @@ def create_coordinate_arrays( and ten columns. step_sizes Map step sizes. If not given, it is set to 1 px in each map - direction given by ``shape``. + direction given by *shape*. Returns ------- d - Dictionary with keys ``"y"`` and ``"x"``, depending on the - length of ``shape``, with coordinate arrays. + Dictionary with keys ``"x"`` and ``"y"``, depending on the + length of *shape*, with coordinate arrays. map_size Number of map points. @@ -1145,10 +1173,10 @@ def create_coordinate_arrays( >>> create_coordinate_arrays((2, 3), (1.5, 1.5)) ({'x': array([0. , 1.5, 3. , 0. , 1.5, 3. ]), 'y': array([0. , 0. , 0. , 1.5, 1.5, 1.5])}, 6) """ - if shape is None: + if not shape: shape = (5, 10) ndim = len(shape) - if step_sizes is None: + if not step_sizes: step_sizes = (1,) * ndim if ndim == 3 or len(step_sizes) > 2: From 0a8740be50e33db97d771ae1308653b1b2b0aa8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 28 Apr 2024 23:01:21 +0200 Subject: [PATCH 22/33] Update CTF to allow reading from ASTAR, EMsoft and MTEX files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/io/plugins/ctf.py | 279 +++++++++++++++++++++++++++++------------ 1 file changed, 196 insertions(+), 83 deletions(-) diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index b36379f8..aa2b5918 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2018-2023 the orix developers +# Copyright 2018-2024 the orix developers # # This file is part of orix. # @@ -16,19 +16,18 @@ # You should have received a copy of the GNU General Public License # along with orix. If not, see . -"""Reader of a crystal map from an .ctf file in formats produced by -Oxford AZtec and EMsoft's EMdpmerge program. +"""Reader of a crystal map from a file in the Channel Text File (CTF) +format. """ from io import TextIOWrapper -from typing import List, Tuple -import warnings +import re from diffpy.structure import Lattice, Structure import numpy as np -from orix import __version__ from orix.crystal_map import CrystalMap, PhaseList +from orix.crystal_map.crystal_map import _data_slices_from_coordinates from orix.quaternion import Rotation __all__ = ["file_reader"] @@ -41,62 +40,55 @@ def file_reader(filename: str) -> CrystalMap: - """Return a crystal map from a file in Oxford AZtec HKL's .ctf format. The - map in the input is assumed to be 2D. + """Return a crystal map from a file in the Channel Text File (CTF) + format. - Many vendors produce an .ctf file. Supported vendors are: + The map in the input is assumed to be 2D. - * Oxford AZtec HKL - * EMsoft (from program `EMdpmerge`) + Many vendors/programs produce a .ctf files. Files from the following + vendors/programs are tested: + * Oxford Instruments AZtec + * Bruker Esprit + * NanoMegas ASTAR Index + * EMsoft (from program `EMdpmerge`) + * MTEX All points with a phase of 0 are classified as not indexed. Parameters ---------- filename - Path and file name. + Path to file. Returns ------- xmap Crystal map. + + Notes + ----- + Files written by MTEX do not contain information of the space group. + + Files written by EMsoft have the column names for mean angular + deviation (MAD), band contrast (BC), and band slope (BS) renamed to + DP (dot product), OSM (orientation similarity metric), and IQ (image + quality), respectively. """ - # Get file header with open(filename, "r") as f: - [header, data_starting_row] = _get_header(f) + header, data_starting_row, vendor = _get_header(f) - # Get phase names and crystal symmetries from header (potentially empty) - phase_ids, phase_names, symmetries, lattice_constants = _get_phases_from_header( - header - ) - structures = [] - for name, abcABG in zip(phase_names, lattice_constants): - structures.append(Structure(title=name, lattice=Lattice(*abcABG))) + # Phase information, potentially empty + phases = _get_phases_from_header(header) + phases["structures"] = [] + lattice_constants = phases.pop("lattice_constants") + for name, abcABG in zip(phases["names"], lattice_constants): + structure = Structure(title=name, lattice=Lattice(*abcABG)) + phases["structures"].append(structure) - # Read all file data file_data = np.loadtxt(filename, skiprows=data_starting_row) - # Get vendor and column names - n_rows, n_cols = file_data.shape - - column_names = ( - [ - "phase_id", - "x", - "y", - "bands", - "error", - "euler1", - "euler2", - "euler3", - "MAD", # Mean angular deviation - "BC", # Band contrast - "BS", # Band Slope - ], - ) - - # Data needed to create a CrystalMap object + # Data needed to create a crystal map data_dict = { "euler1": None, "euler2": None, @@ -106,19 +98,32 @@ def file_reader(filename: str) -> CrystalMap: "phase_id": None, "prop": {}, } + column_names = [ + "phase_id", + "x", + "y", + "bands", + "error", + "euler1", + "euler2", + "euler3", + "MAD", # Mean angular deviation + "BC", # Band contrast + "BS", # Band slope + ] + emsoft_mapping = {"MAD": "DP", "BC": "OSM", "BS": "IQ"} for column, name in enumerate(column_names): - if name in data_dict.keys(): + if name in data_dict: data_dict[name] = file_data[:, column] else: + if vendor == "emsoft" and name in emsoft_mapping: + name = emsoft_mapping[name] data_dict["prop"][name] = file_data[:, column] - # Add phase list to dictionary - data_dict["phase_list"] = PhaseList( - names=phase_names, - space_groups=symmetries, - structures=structures, - ids=phase_ids, - ) + if vendor == "astar": + data_dict = _fix_astar_coords(header, data_dict) + + data_dict["phase_list"] = PhaseList(**phases) # Set which data points are not indexed not_indexed = data_dict["phase_id"] == 0 @@ -138,9 +143,9 @@ def file_reader(filename: str) -> CrystalMap: return CrystalMap(**data_dict) -def _get_header(file: TextIOWrapper) -> List[str]: - """Return the first lines above the mapping data and the data starting row number - in an .ctf file. +def _get_header(file: TextIOWrapper) -> tuple[list[str], int, list[str]]: + """Return file header, row number of start of data in file, and the + detected vendor(s). Parameters ---------- @@ -153,50 +158,72 @@ def _get_header(file: TextIOWrapper) -> List[str]: List with header lines as individual elements. data_starting_row The starting row number for the data lines + vendor + Vendor detected based on some header pattern. Default is to + assume Oxford/Bruker, ``"oxford_or_bruker"`` (assuming no + difference between the two vendor's CTF formats). Other options + are ``"emsoft"``, ``"astar"``, and ``"mtex"``. """ + vendor = [] + vendor_pattern = { + "emsoft": re.compile( + ( + r"EMsoft v\. ([A-Za-z0-9]+(_[A-Za-z0-9]+)+); BANDS=pattern index, " + r"MAD=CI, BC=OSM, BS=IQ" + ), + ), + "astar": re.compile(r"Author[\t\s]File created from ACOM RES results"), + "mtex": re.compile("(?<=)Created from mtex"), + } + header = [] line = file.readline() i = 0 - while not line.startswith("Phase\tX\tY"): + # Prevent endless loop by not reading past 1 000 lines + while not line.startswith("Phase\tX\tY") and i < 1_000: + for k, v in vendor_pattern.items(): + if v.search(line): + vendor.append(k) header.append(line.rstrip()) i += 1 line = file.readline() - return header, i + 1 + if not vendor: + vendor = "oxford_or_bruker" + else: + vendor = vendor[0] # Assume only one vendor + + return header, i + 1, vendor -def _get_phases_from_header( - header: List[str], -) -> Tuple[List[int], List[str], List[str], List[List[float]]]: - """Return phase names and symmetries detected in an .ctf file - header. + +def _get_phases_from_header(header: list[str]) -> dict: + """Return phase names and symmetries detected in a .ctf file header. Parameters ---------- header List with header lines as individual elements. + vendor + Vendor of the file. Returns ------- - ids - Phase IDs. - phase_names - List of names of detected phases. - phase_point_groups - List of point groups of detected phase. - lattice_constants - List of list of lattice parameters of detected phases. + phase_dict + Dictionary with the following keys (and types): "ids" (int), + "names" (str), "space_groups" (int), "point_groups" (str), + "lattice_constants" (list of floats). Notes ----- - Regular expressions are used to collect phase name, formula and - point group. This function have been tested with files from the - following vendor's formats: Oxford AZtec HKL v5/v6, and EMsoft v4/v5. + This function has been tested with files from the following vendor's + formats: Oxford AZtec HKL v5/v6 and EMsoft v4/v5. """ phases = { - "name": [], - "space_group": [], + "ids": [], + "names": [], + "point_groups": [], + "space_groups": [], "lattice_constants": [], - "id": [], } for i, line in enumerate(header): if line.startswith("Phases"): @@ -204,16 +231,102 @@ def _get_phases_from_header( n_phases = int(line.split("\t")[1]) + # Laue classes + laue_ids = [ + "-1", + "2/m", + "mmm", + "4/m", + "4/mmm", + "-3", + "-3m", + "6/m", + "6/mmm", + "m3", + "m-3m", + ] + for j in range(n_phases): phase_data = header[i + 1 + j].split("\t") - phases["name"].append(phase_data[2]) - phases["space_group"].append(int(phase_data[4])) - phases["lattice_constants"].append( - [float(i) for i in phase_data[0].split(";") + phase_data[1].split(";")] - ) - phases["id"].append(j + 1) + phases["ids"].append(j + 1) + abcABG = ";".join(phase_data[:2]) + abcABG = abcABG.split(";") + abcABG = [float(i.replace(",", ".")) for i in abcABG] + phases["lattice_constants"].append(abcABG) + phases["names"].append(phase_data[2]) + laue_id = int(phase_data[3]) + phases["point_groups"].append(laue_ids[laue_id - 1]) + sg = int(phase_data[4]) + if sg == 0: + sg = None + phases["space_groups"].append(sg) + + return phases + + +def _fix_astar_coords(header: list[str], data_dict: dict) -> dict: + """Return the data dictionary with coordinate arrays possibly fixed + for ASTAR Index files. - names = phases["name"] - phase_ids = [int(i) for i in phases["id"]] + Parameters + ---------- + header + List with header lines. + data_dict + Dictionary for creating a crystal map. - return phase_ids, names, phases["space_group"], phases["lattice_constants"] + Returns + ------- + data_dict + Dictionary with possibly fixed coordinate arrays. + + Notes + ----- + ASTAR Index files may have fewer decimals in the coordinate columns + than in the X/YSteps header values (e.g. X_1 = 0.0019 vs. + XStep = 0.00191999995708466). This may cause our crystal map + algorithm for finding the map shape to fail. We therefore run this + algorithm and compare the found shape to the shape given in the + file. If they are different, we use our own coordinate arrays. + """ + coords = {k: data_dict[k] for k in ["x", "y"]} + slices = _data_slices_from_coordinates(coords) + found_shape = (slices[0].stop + 1, slices[1].stop + 1) + cells = _get_xy_cells(header) + shape = (cells["y"], cells["x"]) + if found_shape != shape: + steps = _get_xy_step(header) + y, x = np.indices(shape, dtype=np.float64) + y *= steps["y"] + x *= steps["x"] + data_dict["y"] = y.ravel() + data_dict["x"] = x.ravel() + return data_dict + + +def _get_xy_step(header: list[str]) -> dict[str, float]: + pattern_step = re.compile(r"(?<=[XY]Step[\t\s])(.*)") + steps = {"x": None, "y": None} + for line in header: + match = pattern_step.search(line) + if match: + step = float(match.group(0).replace(",", ".")) + if line.startswith("XStep"): + steps["x"] = step + elif line.startswith("YStep"): + steps["y"] = step + return steps + + +def _get_xy_cells(header: list[str]) -> dict[str, int]: + pattern_cells = re.compile(r"(?<=[XY]Cells[\t\s])(.*)") + cells = {"x": None, "y": None} + for line in header: + match = pattern_cells.search(line) + if match: + step = int(match.group(0)) + if line.startswith("XCells"): + cells["x"] = step + elif line.startswith("YCells"): + cells["y"] = step + return cells From de711238e4472c3859a0c95681bd05a0863040b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 28 Apr 2024 23:01:54 +0200 Subject: [PATCH 23/33] List CTF reader in IO plugins in docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/io/plugins/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/orix/io/plugins/__init__.py b/orix/io/plugins/__init__.py index 57841d61..6485b624 100644 --- a/orix/io/plugins/__init__.py +++ b/orix/io/plugins/__init__.py @@ -28,6 +28,7 @@ ang bruker_h5ebsd + ctf emsoft_h5ebsd orix_hdf5 """ @@ -36,8 +37,8 @@ plugin_list = [ ang, - ctf, bruker_h5ebsd, + ctf, emsoft_h5ebsd, orix_hdf5, ] From 0d4429bad7766e05733db8ab511fcf3d8789cb85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sat, 11 May 2024 13:32:44 +0200 Subject: [PATCH 24/33] Test private function in crystal map module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/tests/test_crystal_map.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/orix/tests/test_crystal_map.py b/orix/tests/test_crystal_map.py index 6acf9f02..a04c65d8 100644 --- a/orix/tests/test_crystal_map.py +++ b/orix/tests/test_crystal_map.py @@ -22,6 +22,7 @@ import pytest from orix.crystal_map import CrystalMap, Phase, PhaseList, create_coordinate_arrays +from orix.crystal_map.crystal_map import _data_slices_from_coordinates from orix.plot import CrystalMapPlot from orix.quaternion import Orientation, Rotation from orix.quaternion.symmetry import C2, C3, C4, O @@ -1090,6 +1091,11 @@ def test_coordinate_axes(self, crystal_map_input, expected_coordinate_axes): xmap = CrystalMap(**crystal_map_input) assert xmap._coordinate_axes == expected_coordinate_axes + def test_data_slices_from_coordinates_no_steps(self): + d, _ = create_coordinate_arrays((3, 4), step_sizes=(0.1, 0.2)) + slices = _data_slices_from_coordinates(d) + assert slices == (slice(0, 4, None), slice(0, 3, None)) + class TestCrystalMapPlotMethod: def test_plot(self, crystal_map): From 8ff2eed2c9637cd6790095bc12db8d8a8a562de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sat, 11 May 2024 13:33:18 +0200 Subject: [PATCH 25/33] Silence some test warnings from setuptools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- setup.cfg | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 27a67e7c..d3a1bb39 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,6 +8,10 @@ addopts = # Examples --ignore=examples/*/*.py doctest_optionflags = NORMALIZE_WHITESPACE +filterwarnings = + # From setuptools + ignore:Deprecated call to \`pkg_resources:DeprecationWarning + ignore:pkg_resources is deprecated as an API:DeprecationWarning [coverage:run] source = orix @@ -22,7 +26,6 @@ precision = 2 [manifix] known_excludes = .* - .*/** .git/** *.code-workspace **/*.pyc From 48c650543919689d893c2f9f0a87b2a403e7cca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sat, 11 May 2024 13:33:38 +0200 Subject: [PATCH 26/33] Add test fixtures for CTF files in various formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/tests/conftest.py | 552 +++++++++++++++++++++++++++++------------ 1 file changed, 390 insertions(+), 162 deletions(-) diff --git a/orix/tests/conftest.py b/orix/tests/conftest.py index 22d8a961..fb9c9a64 100644 --- a/orix/tests/conftest.py +++ b/orix/tests/conftest.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with orix. If not, see . -import gc import os from tempfile import TemporaryDirectory @@ -40,102 +39,9 @@ def eu(): return np.random.rand(10, 3) -ANGFILE_TSL_HEADER = ( - "# TEM_PIXperUM 1.000000\n" - "# x-star 0.413900\n" - "# y-star 0.729100\n" - "# z-star 0.514900\n" - "# WorkingDistance 27.100000\n" - "#\n" - "# Phase 2\n" - "# MaterialName Aluminum\n" - "# Formula Al\n" - "# Info \n" - "# Symmetry 43\n" - "# LatticeConstants 4.040 4.040 4.040 90.000 90.000 90.000\n" - "# NumberFamilies 69\n" - "# hklFamilies 1 -1 -1 1 8.469246 1\n" - "# ElasticConstants -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000\n" - "# Categories0 0 0 0 0 \n" - "# Phase 3\n" - "# MaterialName Iron Titanium Oxide\n" - "# Formula FeTiO3\n" - "# Info \n" - "# Symmetry 32\n" - "# LatticeConstants 5.123 5.123 13.760 90.000 90.000 120.000\n" - "# NumberFamilies 60\n" - "# hklFamilies 3 0 0 1 100.000000 1\n" - "# ElasticConstants -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000\n" - "# Categories0 0 0 0 0\n" - "#\n" - "# GRID: SqrGrid\n" - "# XSTEP: 0.100000\n" - "# YSTEP: 0.100000\n" - "# NCOLS_ODD: 42\n" - "# NCOLS_EVEN: 42\n" - "# NROWS: 13\n" - "#\n" - "# OPERATOR: sem\n" - "#\n" - "# SAMPLEID: \n" - "#\n" - "# SCANID: \n" - "#\n" -) - -ANGFILE_ASTAR_HEADER = ( - "# File created from ACOM RES results\n" - "# ni-dislocations.res\n" - "# \n" - "# \n" - "# MaterialName Nickel\n" - "# Formula\n" - "# Symmetry 43\n" - "# LatticeConstants 3.520 3.520 3.520 90.000 90.000 90.000\n" - "# NumberFamilies 4\n" - "# hklFamilies 1 1 1 1 0.000000\n" - "# hklFamilies 2 0 0 1 0.000000\n" - "# hklFamilies 2 2 0 1 0.000000\n" - "# hklFamilies 3 1 1 1 0.000000\n" - "#\n" - "# GRID: SqrGrid#\n" -) +# ---------------------------- IO fixtures --------------------------- # -ANGFILE_EMSOFT_HEADER = ( - "# TEM_PIXperUM 1.000000\n" - "# x-star 0.446667\n" - "# y-star 0.586875\n" - "# z-star 0.713450\n" - "# WorkingDistance 0.000000\n" - "#\n" - "# Phase 1\n" - "# MaterialName austenite\n" - "# Formula austenite\n" - "# Info patterns indexed using EMsoft::EMEBSDDI\n" - "# Symmetry 43\n" - "# LatticeConstants 3.595 3.595 3.595 90.000 90.000 90.000\n" - "# NumberFamilies 0\n" - "# Phase 2\n" - "# MaterialName ferrite/ferrite\n" - "# Formula ferrite/ferrite\n" - "# Info patterns indexed using EMsoft::EMEBSDDI\n" - "# Symmetry 43\n" - "# LatticeConstants 2.867 2.867 2.867 90.000 90.000 90.000\n" - "# NumberFamilies 0\n" - "# GRID: SqrGrid\n" - "# XSTEP: 1.500000\n" - "# YSTEP: 1.500000\n" - "# NCOLS_ODD: 13\n" - "# NCOLS_EVEN: 13\n" - "# NROWS: 42\n" - "#\n" - "# OPERATOR: Håkon Wiik Ånes\n" - "#\n" - "# SAMPLEID:\n" - "#\n" - "# SCANID:\n" - "#\n" -) +# ----------------------------- .ang file ---------------------------- # @pytest.fixture() @@ -143,7 +49,6 @@ def temp_ang_file(): with TemporaryDirectory() as tempdir: f = open(os.path.join(tempdir, "temp_ang_file.ang"), mode="w+") yield f - gc.collect() # Garbage collection so that file can be used by multiple tests @pytest.fixture(params=["h5"]) @@ -155,7 +60,48 @@ def temp_file_path(request): with TemporaryDirectory() as tmp: file_path = os.path.join(tmp, "data_temp." + ext) yield file_path - gc.collect() + + +ANGFILE_TSL_HEADER = r"""# TEM_PIXperUM 1.000000 +# x-star 0.413900 +# y-star 0.729100 +# z-star 0.514900 +# WorkingDistance 27.100000 +# +# Phase 2 +# MaterialName Aluminum +# Formula Al +# Info +# Symmetry 43 +# LatticeConstants 4.040 4.040 4.040 90.000 90.000 90.000 +# NumberFamilies 69 +# hklFamilies 1 -1 -1 1 8.469246 1 +# ElasticConstants -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 +# Categories0 0 0 0 0 +# Phase 3 +# MaterialName Iron Titanium Oxide +# Formula FeTiO3 +# Info +# Symmetry 32 +# LatticeConstants 5.123 5.123 13.760 90.000 90.000 120.000 +# NumberFamilies 60 +# hklFamilies 3 0 0 1 100.000000 1 +# ElasticConstants -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 -1.000000 +# Categories0 0 0 0 0 +# +# GRID: SqrGrid +# XSTEP: 0.100000 +# YSTEP: 0.100000 +# NCOLS_ODD: 42 +# NCOLS_EVEN: 42 +# NROWS: 13 +# +# OPERATOR: sem +# +# SAMPLEID: +# +# SCANID: +#""" @pytest.fixture( @@ -237,7 +183,23 @@ def angfile_tsl(tmpdir, request): ) yield f - gc.collect() + + +ANGFILE_ASTAR_HEADER = r"""# File created from ACOM RES results +# ni-dislocations.res +# +# +# MaterialName Nickel +# Formula +# Symmetry 43 +# LatticeConstants 3.520 3.520 3.520 90.000 90.000 90.000 +# NumberFamilies 4 +# hklFamilies 1 1 1 1 0.000000 +# hklFamilies 2 0 0 1 0.000000 +# hklFamilies 2 2 0 1 0.000000 +# hklFamilies 3 1 1 1 0.000000 +# +# GRID: SqrGrid#""" @pytest.fixture( @@ -302,7 +264,41 @@ def angfile_astar(tmpdir, request): ) yield f - gc.collect() + + +ANGFILE_EMSOFT_HEADER = r"""# TEM_PIXperUM 1.000000 +# x-star 0.446667 +# y-star 0.586875 +# z-star 0.713450 +# WorkingDistance 0.000000 +# +# Phase 1 +# MaterialName austenite +# Formula austenite +# Info patterns indexed using EMsoft::EMEBSDDI +# Symmetry 43 +# LatticeConstants 3.595 3.595 3.595 90.000 90.000 90.000 +# NumberFamilies 0 +# Phase 2 +# MaterialName ferrite/ferrite +# Formula ferrite/ferrite +# Info patterns indexed using EMsoft::EMEBSDDI +# Symmetry 43 +# LatticeConstants 2.867 2.867 2.867 90.000 90.000 90.000 +# NumberFamilies 0 +# GRID: SqrGrid +# XSTEP: 1.500000 +# YSTEP: 1.500000 +# NCOLS_ODD: 13 +# NCOLS_EVEN: 13 +# NROWS: 42 +# +# OPERATOR: Håkon Wiik Ånes +# +# SAMPLEID: +# +# SCANID: +#""" @pytest.fixture( @@ -359,7 +355,238 @@ def angfile_emsoft(tmpdir, request): ) yield f - gc.collect() + + +# ----------------------------- .ctf file ---------------------------- # + +# Variable map shape and step sizes +CTF_OXFORD_HEADER = r"""Channel Text File +Prj standard steel sample +Author +JobMode Grid +XCells %i +YCells %i +XStep %.4f +YStep %.4f +AcqE1 0.0000 +AcqE2 0.0000 +AcqE3 0.0000 +Euler angles refer to Sample Coordinate system (CS0)! Mag 180.0000 Coverage 97 Device 0 KV 20.0000 TiltAngle 70.0010 TiltAxis 0 DetectorOrientationE1 0.9743 DetectorOrientationE2 89.4698 DetectorOrientationE3 2.7906 WorkingDistance 14.9080 InsertionDistance 185.0 +Phases 2 +3.660;3.660;3.660 90.000;90.000;90.000 Iron fcc 11 225 Some reference +2.867;2.867;2.867 90.000;90.000;90.000 Iron bcc 11 229 Some other reference +Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS""" + + +@pytest.fixture( + params=[ + ( + (7, 13), # map_shape + (0.1, 0.1), # step_sizes + np.random.choice([1, 2], 7 * 13), # phase_id + np.array([[4.48549, 0.95242, 0.79150], [1.34390, 0.27611, 0.82589]]), # R + ) + ] +) +def ctf_oxford(tmpdir, request): + """Create a dummy CTF file in Oxford Instrument's format from input. + + 10% of points are non-indexed (phase ID of 0 and MAD = 0). + + Parameters expected in `request` + -------------------------------- + map_shape : tuple of ints + Map shape to create. + step_sizes : tuple of floats + Step sizes in x and y coordinates in microns. + phase_id : numpy.ndarray + Array of map size with phase IDs in header. + rotations : numpy.ndarray + A sample, smaller than the map size, of Euler angle triplets. + """ + # Unpack parameters + (ny, nx), (dy, dx), phase_id, R_example = request.param + + # File columns + d, map_size = create_coordinate_arrays((ny, nx), (dy, dx)) + x, y = d["x"], d["y"] + rng = np.random.default_rng() + bands = rng.integers(8, size=map_size, dtype=np.uint8) + err = np.zeros(map_size, dtype=np.uint8) + mad = rng.random(map_size) + bc = rng.integers(150, 200, map_size) + bs = rng.integers(190, 255, map_size) + R_idx = np.random.choice(np.arange(len(R_example)), map_size) + R = R_example[R_idx] + R = np.rad2deg(R) + + # Insert 10% non-indexed points + non_indexed_points = np.random.choice( + np.arange(map_size), replace=False, size=int(map_size * 0.1) + ) + phase_id[non_indexed_points] = 0 + R[non_indexed_points] = 0.0 + bands[non_indexed_points] = 0 + err[non_indexed_points] = 3 + mad[non_indexed_points] = 0.0 + bc[non_indexed_points] = 0 + bs[non_indexed_points] = 0 + + CTF_OXFORD_HEADER2 = CTF_OXFORD_HEADER % (nx, ny, dx, dy) + + f = tmpdir.join("file.ctf") + np.savetxt( + fname=f, + X=np.column_stack( + (phase_id, x, y, bands, err, R[:, 0], R[:, 1], R[:, 2], mad, bc, bs) + ), + fmt="%-4i%-8.4f%-8.4f%-4i%-4i%-11.4f%-11.4f%-11.4f%-8.4f%-4i%-i", + header=CTF_OXFORD_HEADER2, + comments="", + ) + + yield f + + +# Variable map shape, comma as decimal separator in fixed step size +CTF_BRUKER_HEADER = r"""Channel Text File +Prj unnamed +Author [Unknown] +JobMode Grid +XCells %i +YCells %i +XStep 0,001998 +YStep 0,001998 +AcqE1 0 +AcqE2 0 +AcqE3 0 +Euler angles refer to Sample Coordinate system (CS0)! Mag 150000,000000 Coverage 100 Device 0 KV 30,000000 TiltAngle 0 TiltAxis 0 +Phases 1 +4,079000;4,079000;4,079000 90,000000;90,000000;90,000000 Gold 11 225 +Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS""" + + +@pytest.fixture( + params=[ + ( + (7, 13), # map_shape + np.array([[4.48549, 0.95242, 0.79150], [1.34390, 0.27611, 0.82589]]), # R + ) + ] +) +def ctf_bruker(tmpdir, request): + """Create a dummy CTF file in Bruker's format from input. + + Identical to Oxford files except for the following: + + * All band slopes (BS) may be set to 255 + * Decimal separators in header may be with comma + + Parameters expected in `request` + -------------------------------- + map_shape : tuple of ints + Map shape to create. + rotations : numpy.ndarray + A sample, smaller than the map size, of Euler angle triplets. + """ + # Unpack parameters + (ny, nx), R_example = request.param + dy = dx = 0.001998 + + # File columns + d, map_size = create_coordinate_arrays((ny, nx), (dy, dx)) + x, y = d["x"], d["y"] + rng = np.random.default_rng() + bands = rng.integers(8, size=map_size, dtype=np.uint8) + err = np.zeros(map_size, dtype=np.uint8) + mad = rng.random(map_size) + bc = rng.integers(50, 105, map_size) + bs = np.full(map_size, 255, dtype=np.uint8) + R_idx = np.random.choice(np.arange(len(R_example)), map_size) + R = R_example[R_idx] + R = np.rad2deg(R) + + # Insert 10% non-indexed points + phase_id = np.ones(map_size, dtype=np.uint8) + non_indexed_points = np.random.choice( + np.arange(map_size), replace=False, size=int(map_size * 0.1) + ) + phase_id[non_indexed_points] = 0 + R[non_indexed_points] = 0.0 + bands[non_indexed_points] = 0 + err[non_indexed_points] = 3 + mad[non_indexed_points] = 0.0 + bc[non_indexed_points] = 0 + bs[non_indexed_points] = 0 + + CTF_BRUKER_HEADER2 = CTF_BRUKER_HEADER % (nx, ny) + + f = tmpdir.join("file.ctf") + np.savetxt( + fname=f, + X=np.column_stack( + (phase_id, x, y, bands, err, R[:, 0], R[:, 1], R[:, 2], mad, bc, bs) + ), + fmt="%-4i%-8.4f%-8.4f%-4i%-4i%-11.4f%-11.4f%-11.4f%-8.4f%-4i%-i", + header=CTF_BRUKER_HEADER2, + comments="", + ) + + yield f + + +# Variable map shape, small fixed step size +CTF_ASTAR_HEADER = r"""Channel Text File +Prj C:\some\where\scan.res +Author File created from ACOM RES results +JobMode Grid +XCells %i +YCells %i +XStep 0.00191999995708466 +YStep 0.00191999995708466 +AcqE1 0 +AcqE2 0 +AcqE3 0 +Euler angles refer to Sample Coordinate system (CS0)! Mag 200 Coverage 100 Device 0 KV 20 TiltAngle 70 TiltAxis 0 +Phases 1 +4.0780;4.0780;4.0780 90;90;90 _mineral 'Gold' 'Gold' 11 225 +Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS""" + +# Variable map shape and step sizes +CTF_EMSOFT_HEADER = r"""Channel Text File +EMsoft v. 4_1_1_9d5269a; BANDS=pattern index, MAD=CI, BC=OSM, BS=IQ +Author Me +JobMode Grid +XCells %i +YCells %i +XStep %.2f +YStep %.2f +AcqE1 0 +AcqE2 0 +AcqE3 0 +Euler angles refer to Sample Coordinate system (CS0)! Mag 30 Coverage 100 Device 0 KV 0.0 TiltAngle 0.00 TiltAxis 0 +Phases 1 +3.524;3.524;3.524 90.000;90.000;90.000 Ni 11 225 +Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS""" + +# Variable map shape and step sizes +CTF_MTEX_HEADER = r"""Channel Text File +Prj /some/where/mtex.ctf +Author Me Again +JobMode Grid +XCells %i +YCells %i +XStep %.4f +YStep %.4f +AcqE1 0.0000 +AcqE2 0.0000 +AcqE3 0.0000 +Euler angles refer to Sample Coordinate system (CS0)! Mag 0.0000 Coverage 0 Device 0 KV 0.0000 TiltAngle 0.0000 TiltAxis 0 DetectorOrientationE1 0.0000 DetectorOrientationE2 0.0000 DetectorOrientationE3 0.0000 WorkingDistance 0.0000 InsertionDistance 0.0000 +Phases 1 +4.079;4.079;4.079 90.000;90.000;90.000 Gold 11 0 Created from mtex +Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS""" + +# ---------------------------- HDF5 files ---------------------------- # @pytest.fixture( @@ -486,7 +713,6 @@ def temp_emsoft_h5ebsd_file(tmpdir, request): phase_group.create_dataset(name, data=np.array([data], dtype=np.dtype("S"))) yield f - gc.collect() @pytest.fixture( @@ -602,7 +828,68 @@ def temp_bruker_h5ebsd_file(tmpdir, request): data_group.create_dataset("phi2", data=rot[:, 2]) yield f - gc.collect() + + +# --------------------------- Other files ---------------------------- # + + +@pytest.fixture +def cif_file(tmpdir): + """Actual CIF file of beta double prime phase often seen in Al-Mg-Si + alloys. + """ + file_contents = """#====================================================================== + +# CRYSTAL DATA + +#---------------------------------------------------------------------- + +data_VESTA_phase_1 + + +_chemical_name_common '' +_cell_length_a 15.50000 +_cell_length_b 4.05000 +_cell_length_c 6.74000 +_cell_angle_alpha 90 +_cell_angle_beta 105.30000 +_cell_angle_gamma 90 +_space_group_name_H-M_alt 'C 2/m' +_space_group_IT_number 12 + +loop_ +_space_group_symop_operation_xyz + 'x, y, z' + '-x, -y, -z' + '-x, y, -z' + 'x, -y, z' + 'x+1/2, y+1/2, z' + '-x+1/2, -y+1/2, -z' + '-x+1/2, y+1/2, -z' + 'x+1/2, -y+1/2, z' + +loop_ + _atom_site_label + _atom_site_occupancy + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z + _atom_site_adp_type + _atom_site_B_iso_or_equiv + _atom_site_type_symbol + Mg(1) 1.0 0.000000 0.000000 0.000000 Biso 1.000000 Mg + Mg(2) 1.0 0.347000 0.000000 0.089000 Biso 1.000000 Mg + Mg(3) 1.0 0.423000 0.000000 0.652000 Biso 1.000000 Mg + Si(1) 1.0 0.054000 0.000000 0.649000 Biso 1.000000 Si + Si(2) 1.0 0.190000 0.000000 0.224000 Biso 1.000000 Si + Al 1.0 0.211000 0.000000 0.626000 Biso 1.000000 Al""" + f = open(tmpdir.join("betapp.cif"), mode="w") + f.write(file_contents) + f.close() + yield f.name + + +# ----------------------- Crystal map fixtures ----------------------- # @pytest.fixture( @@ -665,65 +952,6 @@ def crystal_map(crystal_map_input): return CrystalMap(**crystal_map_input) -@pytest.fixture -def cif_file(tmpdir): - """Actual CIF file of beta double prime phase often seen in Al-Mg-Si - alloys. - """ - file_contents = """ -#====================================================================== - -# CRYSTAL DATA - -#---------------------------------------------------------------------- - -data_VESTA_phase_1 - - -_chemical_name_common '' -_cell_length_a 15.50000 -_cell_length_b 4.05000 -_cell_length_c 6.74000 -_cell_angle_alpha 90 -_cell_angle_beta 105.30000 -_cell_angle_gamma 90 -_space_group_name_H-M_alt 'C 2/m' -_space_group_IT_number 12 - -loop_ -_space_group_symop_operation_xyz - 'x, y, z' - '-x, -y, -z' - '-x, y, -z' - 'x, -y, z' - 'x+1/2, y+1/2, z' - '-x+1/2, -y+1/2, -z' - '-x+1/2, y+1/2, -z' - 'x+1/2, -y+1/2, z' - -loop_ - _atom_site_label - _atom_site_occupancy - _atom_site_fract_x - _atom_site_fract_y - _atom_site_fract_z - _atom_site_adp_type - _atom_site_B_iso_or_equiv - _atom_site_type_symbol - Mg(1) 1.0 0.000000 0.000000 0.000000 Biso 1.000000 Mg - Mg(2) 1.0 0.347000 0.000000 0.089000 Biso 1.000000 Mg - Mg(3) 1.0 0.423000 0.000000 0.652000 Biso 1.000000 Mg - Si(1) 1.0 0.054000 0.000000 0.649000 Biso 1.000000 Si - Si(2) 1.0 0.190000 0.000000 0.224000 Biso 1.000000 Si - Al 1.0 0.211000 0.000000 0.626000 Biso 1.000000 Al" -""" - f = open(tmpdir.join("betapp.cif"), mode="w") - f.write(file_contents) - f.close() - yield f.name - gc.collect() - - # ---------- Rotation representations for conversion tests ----------- # # NOTE to future test writers on unittest data: # All the data below can be recreated using 3Drotations, which is From 0170f7b1166a54c9d66e7268146eed11513a90f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sat, 11 May 2024 13:34:03 +0200 Subject: [PATCH 27/33] Test Oxford Instruments and Bruker CTF files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/tests/io/test_ctf.py | 193 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 orix/tests/io/test_ctf.py diff --git a/orix/tests/io/test_ctf.py b/orix/tests/io/test_ctf.py new file mode 100644 index 00000000..eb18571c --- /dev/null +++ b/orix/tests/io/test_ctf.py @@ -0,0 +1,193 @@ +# Copyright 2018-2024 the orix developers +# +# This file is part of orix. +# +# orix is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# orix is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with orix. If not, see . + +from diffpy.structure import Atom, Lattice, Structure +import numpy as np +import pytest + +from orix import io +from orix.crystal_map import Phase, PhaseList + + +class TestCTFReader: + @pytest.mark.parametrize( + "ctf_oxford, map_shape, step_sizes, R_example", + [ + ( + ( + (5, 3), + (0.1, 0.1), + np.random.choice([1, 2], 5 * 3), + np.array( + [[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]] + ), + ), + (5, 3), + (0.1, 0.1), + np.array([[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]), + ), + ( + ( + (8, 8), + (1.0, 1.5), + np.random.choice([1, 2], 8 * 8), + np.array( + [[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]] + ), + ), + (8, 8), + (1.0, 1.5), + np.array([[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]), + ), + ], + indirect=["ctf_oxford"], + ) + def test_load_ctf_oxford( + self, + ctf_oxford, + map_shape, + step_sizes, + R_example, + ): + xmap = io.load(ctf_oxford) + + # Fraction of non-indexed points + non_indexed_fraction = int(np.prod(map_shape) * 0.1) + assert non_indexed_fraction == np.sum(~xmap.is_indexed) + + # Properties + prop_names = ["bands", "error", "MAD", "BC", "BS"] + assert list(xmap.prop.keys()) == prop_names + + # Coordinates + ny, nx = map_shape + dy, dx = step_sizes + assert np.allclose(xmap.x, np.tile(np.arange(nx) * dx, ny)) + assert np.allclose(xmap.y, np.sort(np.tile(np.arange(ny) * dy, nx))) + assert xmap.scan_unit == "um" + + # Map shape and size + assert xmap.shape == map_shape + assert xmap.size == np.prod(map_shape) + + # Attributes are within expected ranges or have a certain value + assert xmap.bands.min() >= 0 + assert xmap.error.min() >= 0 + assert np.allclose(xmap["not_indexed"].bands, 0) + assert not any(np.isclose(xmap["not_indexed"].error, 0)) + assert np.allclose(xmap["not_indexed"].MAD, 0) + assert np.allclose(xmap["not_indexed"].BC, 0) + assert np.allclose(xmap["not_indexed"].BS, 0) + + # Rotations + R_unique = np.unique(xmap["indexed"].rotations.to_euler(), axis=0) + assert np.allclose( + np.sort(R_unique, axis=0), np.sort(R_example, axis=0), atol=1e-5 + ) + assert np.allclose(xmap["not_indexed"].rotations.to_euler()[0], 0) + + # Phases + phases = PhaseList( + names=["Iron fcc", "Iron bcc"], + space_groups=[225, 229], + structures=[ + Structure(lattice=Lattice(3.66, 3.66, 3.66, 90, 90, 90)), + Structure(lattice=Lattice(2.867, 2.867, 2.867, 90, 90, 90)), + ], + ) + + assert all(np.isin(xmap.phase_id, [-1, 1, 2])) + assert np.allclose(xmap["not_indexed"].phase_id, -1) + assert xmap.phases.ids == [-1, 1, 2] + for (_, phase), (_, phase_test) in zip(xmap["indexed"].phases_in_data, phases): + assert phase.name == phase_test.name + assert phase.space_group.number == phase_test.space_group.number + assert np.allclose( + phase.structure.lattice.abcABG(), phase_test.structure.lattice.abcABG() + ) + + @pytest.mark.parametrize( + "ctf_bruker, map_shape, R_example", + [ + ( + ( + (5, 3), + np.array( + [[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]] + ), + ), + (5, 3), + np.array([[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]), + ), + ( + ( + (8, 8), + np.array( + [[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]] + ), + ), + (8, 8), + np.array([[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]), + ), + ], + indirect=["ctf_bruker"], + ) + def test_load_ctf_bruker(self, ctf_bruker, map_shape, R_example): + xmap = io.load(ctf_bruker) + + # Fraction of non-indexed points + non_indexed_fraction = int(np.prod(map_shape) * 0.1) + assert non_indexed_fraction == np.sum(~xmap.is_indexed) + + # Properties + prop_names = ["bands", "error", "MAD", "BC", "BS"] + assert list(xmap.prop.keys()) == prop_names + + # Coordinates + ny, nx = map_shape + dy = dx = 0.001998 + assert np.allclose(xmap.x, np.tile(np.arange(nx) * dx, ny), atol=1e-4) + assert np.allclose(xmap.y, np.sort(np.tile(np.arange(ny) * dy, nx)), atol=1e-4) + assert xmap.scan_unit == "um" + + # Map shape and size + assert xmap.shape == map_shape + assert xmap.size == np.prod(map_shape) + + # Attributes are within expected ranges or have a certain value + assert xmap.bands.min() >= 0 + assert xmap.error.min() >= 0 + assert np.allclose(xmap["not_indexed"].bands, 0) + assert not any(np.isclose(xmap["not_indexed"].error, 0)) + assert np.allclose(xmap["not_indexed"].MAD, 0) + assert np.allclose(xmap["not_indexed"].BC, 0) + assert np.allclose(xmap["not_indexed"].BS, 0) + + # Rotations + R_unique = np.unique(xmap["indexed"].rotations.to_euler(), axis=0) + assert np.allclose( + np.sort(R_unique, axis=0), np.sort(R_example, axis=0), atol=1e-5 + ) + assert np.allclose(xmap["not_indexed"].rotations.to_euler()[0], 0) + + # Phases + assert all(np.isin(xmap.phase_id, [-1, 1])) + assert np.allclose(xmap["not_indexed"].phase_id, -1) + assert xmap.phases.ids == [-1, 1] + phase = xmap.phases[1] + assert phase.name == "Gold" + assert phase.space_group.number == 225 From 70e252b1cdc7203b2facb56f34499f848e1bdd4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sat, 11 May 2024 14:20:50 +0200 Subject: [PATCH 28/33] Fix manifest exclude patterns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index d3a1bb39..8957643a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,7 +26,7 @@ precision = 2 [manifix] known_excludes = .* - .git/** + .*/** *.code-workspace **/*.pyc **/*.nbi From 1a39e98c5dd72a01dc388f0cfadea1e185323589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sat, 11 May 2024 14:22:06 +0200 Subject: [PATCH 29/33] Use type hint classes for tuple and dict instead of types, valid for 3.8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/crystal_map/crystal_map.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/orix/crystal_map/crystal_map.py b/orix/crystal_map/crystal_map.py index ced9681d..d74feb82 100644 --- a/orix/crystal_map/crystal_map.py +++ b/orix/crystal_map/crystal_map.py @@ -17,7 +17,7 @@ # along with orix. If not, see . import copy -from typing import Optional, Union +from typing import Dict, Optional, Tuple, Union import matplotlib.pyplot as plt import numpy as np @@ -1082,8 +1082,8 @@ def _data_shape_from_coordinates(self, only_is_in_data: bool = True) -> tuple: def _data_slices_from_coordinates( - coords: dict[str, np.ndarray], steps: Union[dict[str, float], None] = None -) -> tuple[slice]: + coords: Dict[str, np.ndarray], steps: Union[Dict[str, float], None] = None +) -> Tuple[slice]: """Return a list of slices defining the current data extent in all directions. @@ -1139,7 +1139,7 @@ def _step_size_from_coordinates(coordinates: np.ndarray) -> float: def create_coordinate_arrays( shape: Optional[tuple] = None, step_sizes: Optional[tuple] = None -) -> tuple[dict, int]: +) -> Tuple[dict, int]: """Return flattened coordinate arrays from a given map shape and step sizes, suitable for initializing a :class:`~orix.crystal_map.CrystalMap`. From 0b8f46b182631eeaf296c3d584c82e68f0767521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sat, 11 May 2024 15:53:03 +0200 Subject: [PATCH 30/33] Complete testing of CTF reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/io/plugins/ctf.py | 5 +- orix/tests/conftest.py | 215 ++++++++++++++++++++++++++++++- orix/tests/io/test_ctf.py | 262 +++++++++++++++++++++++++++++++++++++- 3 files changed, 471 insertions(+), 11 deletions(-) diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index aa2b5918..0daede78 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -188,10 +188,7 @@ def _get_header(file: TextIOWrapper) -> tuple[list[str], int, list[str]]: i += 1 line = file.readline() - if not vendor: - vendor = "oxford_or_bruker" - else: - vendor = vendor[0] # Assume only one vendor + vendor = vendor[0] if len(vendor) == 1 else "oxford_or_bruker" return header, i + 1, vendor diff --git a/orix/tests/conftest.py b/orix/tests/conftest.py index fb9c9a64..da5b7a40 100644 --- a/orix/tests/conftest.py +++ b/orix/tests/conftest.py @@ -29,6 +29,10 @@ from orix.quaternion import Rotation +def pytest_sessionstart(session): # pragma: no cover + plt.rcParams["backend"] = "agg" + + @pytest.fixture def rotations(): return Rotation([(2, 4, 6, 8), (-1, -3, -5, -7)]) @@ -434,7 +438,7 @@ def ctf_oxford(tmpdir, request): CTF_OXFORD_HEADER2 = CTF_OXFORD_HEADER % (nx, ny, dx, dy) - f = tmpdir.join("file.ctf") + f = tmpdir.join("oxford.ctf") np.savetxt( fname=f, X=np.column_stack( @@ -521,7 +525,7 @@ def ctf_bruker(tmpdir, request): CTF_BRUKER_HEADER2 = CTF_BRUKER_HEADER % (nx, ny) - f = tmpdir.join("file.ctf") + f = tmpdir.join("bruker.ctf") np.savetxt( fname=f, X=np.column_stack( @@ -552,6 +556,77 @@ def ctf_bruker(tmpdir, request): 4.0780;4.0780;4.0780 90;90;90 _mineral 'Gold' 'Gold' 11 225 Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS""" + +@pytest.fixture( + params=[ + ( + (7, 13), # map_shape + np.array([[4.48549, 0.95242, 0.79150], [1.34390, 0.27611, 0.82589]]), # R + ) + ] +) +def ctf_astar(tmpdir, request): + """Create a dummy CTF file in NanoMegas ASTAR's format from input. + + Identical to Oxford files except for the following: + + * Bands = 6 (always?) + * Error = 0 (always?) + * Only two decimals in Euler angles + + Parameters expected in `request` + -------------------------------- + map_shape : tuple of ints + Map shape to create. + rotations : numpy.ndarray + A sample, smaller than the map size, of Euler angle triplets. + """ + # Unpack parameters + (ny, nx), R_example = request.param + dy = dx = 0.00191999995708466 + + # File columns + d, map_size = create_coordinate_arrays((ny, nx), (dy, dx)) + x, y = d["x"], d["y"] + rng = np.random.default_rng() + bands = np.full(map_size, 6, dtype=np.uint8) + err = np.zeros(map_size, dtype=np.uint8) + mad = rng.random(map_size) + bc = rng.integers(0, 60, map_size) + bs = rng.integers(35, 42, map_size) + R_idx = np.random.choice(np.arange(len(R_example)), map_size) + R = R_example[R_idx] + R = np.rad2deg(R) + + # Insert 10% non-indexed points + phase_id = np.ones(map_size, dtype=np.uint8) + non_indexed_points = np.random.choice( + np.arange(map_size), replace=False, size=int(map_size * 0.1) + ) + phase_id[non_indexed_points] = 0 + R[non_indexed_points] = 0.0 + bands[non_indexed_points] = 0 + err[non_indexed_points] = 3 + mad[non_indexed_points] = 0.0 + bc[non_indexed_points] = 0 + bs[non_indexed_points] = 0 + + CTF_ASTAR_HEADER2 = CTF_ASTAR_HEADER % (nx, ny) + + f = tmpdir.join("astar.ctf") + np.savetxt( + fname=f, + X=np.column_stack( + (phase_id, x, y, bands, err, R[:, 0], R[:, 1], R[:, 2], mad, bc, bs) + ), + fmt="%-4i%-8.4f%-8.4f%-4i%-4i%-9.2f%-9.2f%-9.2f%-8.4f%-4i%-i", + header=CTF_ASTAR_HEADER2, + comments="", + ) + + yield f + + # Variable map shape and step sizes CTF_EMSOFT_HEADER = r"""Channel Text File EMsoft v. 4_1_1_9d5269a; BANDS=pattern index, MAD=CI, BC=OSM, BS=IQ @@ -569,6 +644,78 @@ def ctf_bruker(tmpdir, request): 3.524;3.524;3.524 90.000;90.000;90.000 Ni 11 225 Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS""" + +@pytest.fixture( + params=[ + ( + (7, 13), # map_shape + (1, 2), # step_sizes + np.array([[4.48549, 0.95242, 0.79150], [1.34390, 0.27611, 0.82589]]), # R + ) + ] +) +def ctf_emsoft(tmpdir, request): + """Create a dummy CTF file in EMsoft's format from input. + + Identical to Oxford files except for the following: + + * Bands = dictionary index + * Error = 0 + * Only three decimals in Euler angles + + Parameters expected in `request` + -------------------------------- + map_shape : tuple of ints + Map shape to create. + step_sizes : tuple of floats + Step sizes in x and y coordinates in microns. + rotations : numpy.ndarray + A sample, smaller than the map size, of Euler angle triplets. + """ + # Unpack parameters + (ny, nx), (dy, dx), R_example = request.param + + # File columns + d, map_size = create_coordinate_arrays((ny, nx), (dy, dx)) + x, y = d["x"], d["y"] + rng = np.random.default_rng() + bands = rng.integers(0, 333_000, map_size) + err = np.zeros(map_size, dtype=np.uint8) + mad = rng.random(map_size) + bc = rng.integers(60, 140, map_size) + bs = rng.integers(60, 120, map_size) + R_idx = np.random.choice(np.arange(len(R_example)), map_size) + R = R_example[R_idx] + R = np.rad2deg(R) + + # Insert 10% non-indexed points + phase_id = np.ones(map_size, dtype=np.uint8) + non_indexed_points = np.random.choice( + np.arange(map_size), replace=False, size=int(map_size * 0.1) + ) + phase_id[non_indexed_points] = 0 + R[non_indexed_points] = 0.0 + bands[non_indexed_points] = 0 + mad[non_indexed_points] = 0.0 + bc[non_indexed_points] = 0 + bs[non_indexed_points] = 0 + + CTF_EMSOFT_HEADER2 = CTF_EMSOFT_HEADER % (nx, ny, dx, dy) + + f = tmpdir.join("emsoft.ctf") + np.savetxt( + fname=f, + X=np.column_stack( + (phase_id, x, y, bands, err, R[:, 0], R[:, 1], R[:, 2], mad, bc, bs) + ), + fmt="%-4i%-8.4f%-8.4f%-7i%-4i%-10.3f%-10.3f%-10.3f%-8.4f%-4i%-i", + header=CTF_EMSOFT_HEADER2, + comments="", + ) + + yield f + + # Variable map shape and step sizes CTF_MTEX_HEADER = r"""Channel Text File Prj /some/where/mtex.ctf @@ -586,6 +733,70 @@ def ctf_bruker(tmpdir, request): 4.079;4.079;4.079 90.000;90.000;90.000 Gold 11 0 Created from mtex Phase X Y Bands Error Euler1 Euler2 Euler3 MAD BC BS""" + +@pytest.fixture( + params=[ + ( + (7, 13), # map_shape + (1, 2), # step_sizes + np.array([[4.48549, 0.95242, 0.79150], [1.34390, 0.27611, 0.82589]]), # R + ) + ] +) +def ctf_mtex(tmpdir, request): + """Create a dummy CTF file in MTEX's format from input. + + Identical to Oxford files except for the properties Bands, Error, + MAD, BC, and BS are all equal to 0. + + Parameters expected in `request` + -------------------------------- + map_shape : tuple of ints + Map shape to create. + step_sizes : tuple of floats + Step sizes in x and y coordinates in microns. + rotations : numpy.ndarray + A sample, smaller than the map size, of Euler angle triplets. + """ + # Unpack parameters + (ny, nx), (dy, dx), R_example = request.param + + # File columns + d, map_size = create_coordinate_arrays((ny, nx), (dy, dx)) + x, y = d["x"], d["y"] + bands = np.zeros(map_size) + err = np.zeros(map_size) + mad = np.zeros(map_size) + bc = np.zeros(map_size) + bs = np.zeros(map_size) + R_idx = np.random.choice(np.arange(len(R_example)), map_size) + R = R_example[R_idx] + R = np.rad2deg(R) + + # Insert 10% non-indexed points + phase_id = np.ones(map_size, dtype=np.uint8) + non_indexed_points = np.random.choice( + np.arange(map_size), replace=False, size=int(map_size * 0.1) + ) + phase_id[non_indexed_points] = 0 + R[non_indexed_points] = 0.0 + + CTF_MTEX_HEADER2 = CTF_MTEX_HEADER % (nx, ny, dx, dy) + + f = tmpdir.join("mtex.ctf") + np.savetxt( + fname=f, + X=np.column_stack( + (phase_id, x, y, bands, err, R[:, 0], R[:, 1], R[:, 2], mad, bc, bs) + ), + fmt="%-4i%-8.4f%-8.4f%-4i%-4i%-11.4f%-11.4f%-11.4f%-8.4f%-4i%-i", + header=CTF_MTEX_HEADER2, + comments="", + ) + + yield f + + # ---------------------------- HDF5 files ---------------------------- # diff --git a/orix/tests/io/test_ctf.py b/orix/tests/io/test_ctf.py index eb18571c..62a11239 100644 --- a/orix/tests/io/test_ctf.py +++ b/orix/tests/io/test_ctf.py @@ -20,7 +20,7 @@ import pytest from orix import io -from orix.crystal_map import Phase, PhaseList +from orix.crystal_map import PhaseList class TestCTFReader: @@ -70,8 +70,7 @@ def test_load_ctf_oxford( assert non_indexed_fraction == np.sum(~xmap.is_indexed) # Properties - prop_names = ["bands", "error", "MAD", "BC", "BS"] - assert list(xmap.prop.keys()) == prop_names + assert list(xmap.prop.keys()) == ["bands", "error", "MAD", "BC", "BS"] # Coordinates ny, nx = map_shape @@ -154,8 +153,7 @@ def test_load_ctf_bruker(self, ctf_bruker, map_shape, R_example): assert non_indexed_fraction == np.sum(~xmap.is_indexed) # Properties - prop_names = ["bands", "error", "MAD", "BC", "BS"] - assert list(xmap.prop.keys()) == prop_names + assert list(xmap.prop.keys()) == ["bands", "error", "MAD", "BC", "BS"] # Coordinates ny, nx = map_shape @@ -191,3 +189,257 @@ def test_load_ctf_bruker(self, ctf_bruker, map_shape, R_example): phase = xmap.phases[1] assert phase.name == "Gold" assert phase.space_group.number == 225 + assert phase.structure.lattice.abcABG() == (4.079, 4.079, 4.079, 90, 90, 90) + + @pytest.mark.parametrize( + "ctf_astar, map_shape, R_example", + [ + ( + ( + (5, 3), + np.array( + [[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]] + ), + ), + (5, 3), + np.array([[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]), + ), + ( + ( + (8, 8), + np.array( + [[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]] + ), + ), + (8, 8), + np.array([[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]), + ), + ], + indirect=["ctf_astar"], + ) + def test_load_ctf_astar(self, ctf_astar, map_shape, R_example): + xmap = io.load(ctf_astar) + + # Fraction of non-indexed points + non_indexed_fraction = int(np.prod(map_shape) * 0.1) + assert non_indexed_fraction == np.sum(~xmap.is_indexed) + + # Properties + assert list(xmap.prop.keys()) == ["bands", "error", "MAD", "BC", "BS"] + + # Coordinates + ny, nx = map_shape + dy = dx = 0.00191999995708466 + assert np.allclose(xmap.x, np.tile(np.arange(nx) * dx, ny), atol=1e-4) + assert np.allclose(xmap.y, np.sort(np.tile(np.arange(ny) * dy, nx)), atol=1e-4) + assert xmap.scan_unit == "um" + + # Map shape and size + assert xmap.shape == map_shape + assert xmap.size == np.prod(map_shape) + + # Attributes are within expected ranges or have a certain value + assert np.allclose(xmap["indexed"].bands, 6) + assert np.allclose(xmap["indexed"].error, 0) + assert np.allclose(xmap["not_indexed"].bands, 0) + assert not any(np.isclose(xmap["not_indexed"].error, 0)) + assert np.allclose(xmap["not_indexed"].MAD, 0) + assert np.allclose(xmap["not_indexed"].BC, 0) + assert np.allclose(xmap["not_indexed"].BS, 0) + + # Rotations + R_unique = np.unique(xmap["indexed"].rotations.to_euler(), axis=0) + assert np.allclose( + np.sort(R_unique, axis=0), np.sort(R_example, axis=0), atol=1e-5 + ) + assert np.allclose(xmap["not_indexed"].rotations.to_euler()[0], 0) + + # Phases + assert all(np.isin(xmap.phase_id, [-1, 1])) + assert np.allclose(xmap["not_indexed"].phase_id, -1) + assert xmap.phases.ids == [-1, 1] + phase = xmap.phases[1] + assert phase.name == "_mineral 'Gold' 'Gold'" + assert phase.space_group.number == 225 + assert phase.structure.lattice.abcABG() == (4.078, 4.078, 4.078, 90, 90, 90) + + @pytest.mark.parametrize( + "ctf_emsoft, map_shape, step_sizes, R_example", + [ + ( + ( + (5, 3), + (0.1, 0.2), + np.array( + [[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]] + ), + ), + (5, 3), + (0.1, 0.2), + np.array([[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]), + ), + ( + ( + (8, 8), + (1.0, 1.5), + np.array( + [[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]] + ), + ), + (8, 8), + (1.0, 1.5), + np.array([[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]), + ), + ], + indirect=["ctf_emsoft"], + ) + def test_load_ctf_emsoft( + self, + ctf_emsoft, + map_shape, + step_sizes, + R_example, + ): + xmap = io.load(ctf_emsoft) + + # Fraction of non-indexed points + non_indexed_fraction = int(np.prod(map_shape) * 0.1) + assert non_indexed_fraction == np.sum(~xmap.is_indexed) + + # Properties + assert list(xmap.prop.keys()) == ["bands", "error", "DP", "OSM", "IQ"] + + # Coordinates + ny, nx = map_shape + dy, dx = step_sizes + assert np.allclose(xmap.x, np.tile(np.arange(nx) * dx, ny)) + assert np.allclose(xmap.y, np.sort(np.tile(np.arange(ny) * dy, nx))) + assert xmap.scan_unit == "um" + + # Map shape and size + assert xmap.shape == map_shape + assert xmap.size == np.prod(map_shape) + + # Attributes are within expected ranges or have a certain value + assert xmap.bands.max() <= 333_000 + assert np.allclose(xmap.error, 0) + assert np.allclose(xmap["not_indexed"].bands, 0) + assert np.allclose(xmap["not_indexed"].DP, 0) + assert np.allclose(xmap["not_indexed"].OSM, 0) + assert np.allclose(xmap["not_indexed"].IQ, 0) + + # Rotations + R_unique = np.unique(xmap["indexed"].rotations.to_euler(), axis=0) + assert np.allclose( + np.sort(R_unique, axis=0), np.sort(R_example, axis=0), atol=1e-3 + ) + assert np.allclose(xmap["not_indexed"].rotations.to_euler()[0], 0) + + # Phases + phases = PhaseList( + names=["Ni"], + space_groups=[225], + structures=[ + Structure(lattice=Lattice(3.524, 3.524, 3.524, 90, 90, 90)), + ], + ) + + assert all(np.isin(xmap.phase_id, [-1, 1])) + assert np.allclose(xmap["not_indexed"].phase_id, -1) + assert xmap.phases.ids == [-1, 1] + for (_, phase), (_, phase_test) in zip(xmap["indexed"].phases_in_data, phases): + assert phase.name == phase_test.name + assert phase.space_group.number == phase_test.space_group.number + assert np.allclose( + phase.structure.lattice.abcABG(), phase_test.structure.lattice.abcABG() + ) + + @pytest.mark.parametrize( + "ctf_mtex, map_shape, step_sizes, R_example", + [ + ( + ( + (5, 3), + (0.1, 0.2), + np.array( + [[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]] + ), + ), + (5, 3), + (0.1, 0.2), + np.array([[1.59942, 2.37748, 4.53419], [1.59331, 2.37417, 4.53628]]), + ), + ( + ( + (8, 8), + (1.0, 1.5), + np.array( + [[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]] + ), + ), + (8, 8), + (1.0, 1.5), + np.array([[5.81107, 2.34188, 4.47345], [6.16205, 0.79936, 1.31702]]), + ), + ], + indirect=["ctf_mtex"], + ) + def test_load_ctf_mtex( + self, + ctf_mtex, + map_shape, + step_sizes, + R_example, + ): + xmap = io.load(ctf_mtex) + + # Fraction of non-indexed points + non_indexed_fraction = int(np.prod(map_shape) * 0.1) + assert non_indexed_fraction == np.sum(~xmap.is_indexed) + + # Properties + assert list(xmap.prop.keys()) == ["bands", "error", "MAD", "BC", "BS"] + + # Coordinates + ny, nx = map_shape + dy, dx = step_sizes + assert np.allclose(xmap.x, np.tile(np.arange(nx) * dx, ny)) + assert np.allclose(xmap.y, np.sort(np.tile(np.arange(ny) * dy, nx))) + assert xmap.scan_unit == "um" + + # Map shape and size + assert xmap.shape == map_shape + assert xmap.size == np.prod(map_shape) + + # Attributes are within expected ranges or have a certain value + assert np.allclose(xmap.bands, 0) + assert np.allclose(xmap.error, 0) + assert np.allclose(xmap.MAD, 0) + assert np.allclose(xmap.BC, 0) + assert np.allclose(xmap.BS, 0) + + # Rotations + R_unique = np.unique(xmap["indexed"].rotations.to_euler(), axis=0) + assert np.allclose( + np.sort(R_unique, axis=0), np.sort(R_example, axis=0), atol=1e-5 + ) + assert np.allclose(xmap["not_indexed"].rotations.to_euler()[0], 0) + + # Phases + phases = PhaseList( + names=["Gold"], + point_groups=["m-3m"], + structures=[ + Structure(lattice=Lattice(4.079, 4.079, 4.079, 90, 90, 90)), + ], + ) + + assert all(np.isin(xmap.phase_id, [-1, 1])) + assert np.allclose(xmap["not_indexed"].phase_id, -1) + assert xmap.phases.ids == [-1, 1] + for (_, phase), (_, phase_test) in zip(xmap["indexed"].phases_in_data, phases): + assert phase.name == phase_test.name + assert phase.point_group.name == phase_test.point_group.name + assert np.allclose( + phase.structure.lattice.abcABG(), phase_test.structure.lattice.abcABG() + ) From 2e2c8d95d76b00ced7d2a714d80e28a32f914050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sat, 11 May 2024 15:53:16 +0200 Subject: [PATCH 31/33] Exclude orix/tests directory from test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 8957643a..f59f4c06 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,7 @@ source = orix omit = setup.py orix/__init__.py + orix/tests/**/*.py relative_files = True [coverage:report] From 0b34bc254dbf224784953b9888fb6e15f439196a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sat, 11 May 2024 18:30:40 +0200 Subject: [PATCH 32/33] Remove a few more type hints using types (not supported on 3.8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/io/plugins/ang.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/orix/io/plugins/ang.py b/orix/io/plugins/ang.py index dc1481b2..521b27bb 100644 --- a/orix/io/plugins/ang.py +++ b/orix/io/plugins/ang.py @@ -22,7 +22,7 @@ from io import TextIOWrapper import re -from typing import Optional, Union +from typing import List, Optional, Tuple, Union import warnings from diffpy.structure import Lattice, Structure @@ -130,7 +130,7 @@ def file_reader(filename: str) -> CrystalMap: return CrystalMap(**data_dict) -def _get_header(file: TextIOWrapper) -> list[str]: +def _get_header(file: TextIOWrapper) -> List[str]: """Return the first lines starting with '#' in an .ang file. Parameters @@ -154,7 +154,7 @@ def _get_header(file: TextIOWrapper) -> list[str]: return header -def _get_vendor_columns(header: list[str], n_cols_file: int) -> tuple[str, list[str]]: +def _get_vendor_columns(header: List[str], n_cols_file: int) -> Tuple[str, List[str]]: """Return the .ang file column names and vendor, determined from the header. @@ -304,7 +304,7 @@ def _get_vendor_columns(header: list[str], n_cols_file: int) -> tuple[str, list[ return vendor, vendor_column_names -def _get_phases_from_header(header: list[str]) -> dict: +def _get_phases_from_header(header: List[str]) -> dict: """Return phase names and symmetries detected in an .ang file header. @@ -382,7 +382,7 @@ def file_writer( confidence_index_prop: Optional[str] = None, detector_signal_prop: Optional[str] = None, pattern_fit_prop: Optional[str] = None, - extra_prop: Union[str, list[str], None] = None, + extra_prop: Union[str, List[str], None] = None, ): """Write a crystal map to an .ang file readable by MTEX and EDAX TSL OIM Analysis v7. @@ -626,7 +626,7 @@ def _get_header_from_phases(xmap: CrystalMap) -> str: return header -def _get_nrows_ncols_step_sizes(xmap: CrystalMap) -> tuple[int, int, float, float]: +def _get_nrows_ncols_step_sizes(xmap: CrystalMap) -> Tuple[int, int, float, float]: """Get crystal map shape and step sizes. Parameters @@ -669,10 +669,10 @@ def _get_column_width(max_value: int, decimals: int = 5) -> int: def _get_prop_arrays( xmap: CrystalMap, - prop_names: list[str], - desired_prop_names: list[str], + prop_names: List[str], + desired_prop_names: List[str], map_size: int, - index: Union[int, None], + index: Optional[int], decimals: int = 5, ) -> np.ndarray: """Return a 2D array (n_points, n_properties) with desired property @@ -725,10 +725,10 @@ def _get_prop_arrays( def _get_prop_array( xmap: CrystalMap, prop_name: str, - expected_prop_names: list[str], - prop_names: list[str], + expected_prop_names: List[str], + prop_names: List[str], prop_names_lower_arr: np.ndarray, - index: Union[int, None], + index: Optional[int], decimals: int = 5, fill_value: Union[int, float, bool] = 0, ) -> Union[np.ndarray, None]: From a68d37e3e3c50063ba3fb918e560db6f11b20c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sat, 11 May 2024 19:40:15 +0200 Subject: [PATCH 33/33] Remove type hints using types also in CTF reader file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/io/plugins/ctf.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/orix/io/plugins/ctf.py b/orix/io/plugins/ctf.py index 0daede78..e271a5e7 100644 --- a/orix/io/plugins/ctf.py +++ b/orix/io/plugins/ctf.py @@ -22,6 +22,7 @@ from io import TextIOWrapper import re +from typing import Dict, List, Tuple from diffpy.structure import Lattice, Structure import numpy as np @@ -143,7 +144,7 @@ def file_reader(filename: str) -> CrystalMap: return CrystalMap(**data_dict) -def _get_header(file: TextIOWrapper) -> tuple[list[str], int, list[str]]: +def _get_header(file: TextIOWrapper) -> Tuple[List[str], int, List[str]]: """Return file header, row number of start of data in file, and the detected vendor(s). @@ -193,7 +194,7 @@ def _get_header(file: TextIOWrapper) -> tuple[list[str], int, list[str]]: return header, i + 1, vendor -def _get_phases_from_header(header: list[str]) -> dict: +def _get_phases_from_header(header: List[str]) -> dict: """Return phase names and symmetries detected in a .ctf file header. Parameters @@ -261,7 +262,7 @@ def _get_phases_from_header(header: list[str]) -> dict: return phases -def _fix_astar_coords(header: list[str], data_dict: dict) -> dict: +def _fix_astar_coords(header: List[str], data_dict: dict) -> dict: """Return the data dictionary with coordinate arrays possibly fixed for ASTAR Index files. @@ -301,7 +302,7 @@ def _fix_astar_coords(header: list[str], data_dict: dict) -> dict: return data_dict -def _get_xy_step(header: list[str]) -> dict[str, float]: +def _get_xy_step(header: List[str]) -> Dict[str, float]: pattern_step = re.compile(r"(?<=[XY]Step[\t\s])(.*)") steps = {"x": None, "y": None} for line in header: @@ -315,7 +316,7 @@ def _get_xy_step(header: list[str]) -> dict[str, float]: return steps -def _get_xy_cells(header: list[str]) -> dict[str, int]: +def _get_xy_cells(header: List[str]) -> Dict[str, int]: pattern_cells = re.compile(r"(?<=[XY]Cells[\t\s])(.*)") cells = {"x": None, "y": None} for line in header: