From 3be235e134be497bc42ed6405c68105fa11e7bf6 Mon Sep 17 00:00:00 2001 From: IMBalENce Date: Tue, 16 May 2023 18:06:04 +1000 Subject: [PATCH 01/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] 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/49] [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 9420cd02dfef8331bf0ad4ec69d1f68c8a1af2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 21 Apr 2024 13:30:13 +0200 Subject: [PATCH 18/49] Add unreleased section to changelog after 0.12.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- CHANGELOG.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5368fb05..103fa4dd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,25 @@ All user facing changes to this project are documented in this file. The format on `Keep a Changelog `__, and this project tries its best to adhere to `Semantic Versioning `__. +Unreleased +========== + +Added +----- + +Changed +------- + +Removed +------- + +Deprecated +---------- + +Fixed +----- + + 2024-04-21 - version 0.12.1 =========================== From 804300852c9a2d5ee2a36d04938a617592c61bf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sun, 21 Apr 2024 13:30:32 +0200 Subject: [PATCH 19/49] Change version from 0.12.1 to 0.13.dev1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orix/__init__.py b/orix/__init__.py index c24b46a4..c3bb483d 100644 --- a/orix/__init__.py +++ b/orix/__init__.py @@ -1,5 +1,5 @@ __name__ = "orix" -__version__ = "0.12.1" +__version__ = "0.13.dev1" __author__ = "orix developers" __author_email__ = "pyxem.team@gmail.com" __description__ = "orix is an open-source Python library for handling crystal orientation mapping data." 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 20/49] 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 21/49] 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 22/49] 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 23/49] 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 24/49] 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 25/49] 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 8bdf4b2fce8c3da0c20684fa2f01ea52d7244789 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 21:58:11 +0000 Subject: [PATCH 26/49] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 24.4.0 → 24.4.2](https://github.com/psf/black/compare/24.4.0...24.4.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0676a6fd..b4cd29ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 24.4.0 + rev: 24.4.2 hooks: - id: black - id: black-jupyter 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 27/49] 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 28/49] 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 29/49] 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 30/49] 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 31/49] 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 32/49] 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 33/49] 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 34/49] 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 35/49] 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 b475758b5a872f55bd3e3fa17bb1479dc4cfa973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Sat, 11 May 2024 18:58:52 +0200 Subject: [PATCH 36/49] Update valid nbconvert versions to < 7.14, >= 7.16.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index e5a3f54e..fd889bde 100644 --- a/setup.py +++ b/setup.py @@ -12,9 +12,7 @@ "doc": [ "ipykernel", # Used by nbsphinx to execute notebooks "memory_profiler", - # TODO: Remove nbconvert pin once - # https://github.com/pyxem/orix/issues/494 is resolved - "nbconvert < 7.14", + "nbconvert >= 7.16.4", "nbsphinx >= 0.7", "numpydoc", "pydata-sphinx-theme", 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 37/49] 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: From 96e64fc6e0a6fce1b1d72d399a980785b59c0269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 13 May 2024 21:15:50 +0200 Subject: [PATCH 38/49] Replace assert-or-raise with better check-or-raise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/vector/vector3d.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/orix/vector/vector3d.py b/orix/vector/vector3d.py index a565ba71..ea62b36d 100644 --- a/orix/vector/vector3d.py +++ b/orix/vector/vector3d.py @@ -676,7 +676,8 @@ def get_nearest( Vector3d (1,) [[0.6 0. 0. ]] """ - assert self.size == 1, "`get_nearest` only works for single vectors." + if self.size != 1: + raise AttributeError("`get_nearest` only works for single vectors") tiebreak = Vector3d.zvector() if tiebreak is None else tiebreak eps = 1e-9 if inclusive else 0 cosines = x.dot(self) From 9645bdfa2bc7d18fb1db2565ce619dce17697e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 13 May 2024 21:19:13 +0200 Subject: [PATCH 39/49] List @IMBalENce among contributors according to line additions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- .zenodo.json | 5 +++++ orix/__init__.py | 1 + 2 files changed, 6 insertions(+) diff --git a/.zenodo.json b/.zenodo.json index 9b81059b..783e087a 100644 --- a/.zenodo.json +++ b/.zenodo.json @@ -34,6 +34,11 @@ "name": "Anders Christian Mathisen", "affiliation": "Norwegian University of Science and Technology" }, + { + "name": "Zhou Xu", + "orcid": "0000-0002-7599-1166", + "affiliation": "Monash Centre for Electron Microscopy" + }, { "name": "Carter Francis", "orcid": "0000-0003-2564-1851", diff --git a/orix/__init__.py b/orix/__init__.py index c3bb483d..283e5019 100644 --- a/orix/__init__.py +++ b/orix/__init__.py @@ -13,6 +13,7 @@ "Duncan Johnstone", "Niels Cautaerts", "Anders Christian Mathisen", + "Zhou Xu", "Carter Francis", "Simon Høgås", "Viljar Johan Femoen", From 56629c37244b5b7e8665f16ee427366b6434ce25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 13 May 2024 21:28:47 +0200 Subject: [PATCH 40/49] Don't run test workflow for branches named 'pre-commit-ci-update-config' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e98c4988..6cb5c4ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,11 +4,12 @@ on: push: branches: - '*' + branches-ignore: + - 'pre-commit-ci-update-config' pull_request: branches: - '*' workflow_dispatch: - workflow: '*' jobs: code: From 1cdc59a8f4567c12933261c0c58584740198349b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 13 May 2024 22:01:04 +0200 Subject: [PATCH 41/49] Improve type hints in the Miller class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/vector/miller.py | 95 +++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 48 deletions(-) diff --git a/orix/vector/miller.py b/orix/vector/miller.py index f8a573f8..e65e68bd 100644 --- a/orix/vector/miller.py +++ b/orix/vector/miller.py @@ -20,13 +20,23 @@ from copy import deepcopy from itertools import product -from typing import Optional, Tuple, Union +from typing import TYPE_CHECKING, Optional, Tuple, Union + +try: + # New in Python 3.11 + from typing import Self +except ImportError: # pragma: no cover + from typing_extensions import Self from diffpy.structure import Lattice import numpy as np from orix.vector import Vector3d +if TYPE_CHECKING: # pragma: no cover + from orix.crystal_map import Phase + from orix.quaternion import Symmetry + class Miller(Vector3d): r"""Direct crystal lattice vectors (uvw or UVTW) and reciprocal @@ -48,13 +58,12 @@ class Miller(Vector3d): Indices of direct lattice vector(s). Default is ``None``. UVTW Indices of direct lattice vector(s), often preferred over - ``uvw`` in trigonal and hexagonal lattices. Default is ``None``. + *uvw* in trigonal and hexagonal lattices. Default is ``None``. hkl Indices of reciprocal lattice vector(s). Default is ``None``. hkil - Indices of reciprocal lattice vector(s), often preferred - over ``hkl`` in trigonal and hexagonal lattices. Default is - ``None``. + Indices of reciprocal lattice vector(s), often preferred over + *hkl* in trigonal and hexagonal lattices. Default is ``None``. phase A phase with a crystal lattice and symmetry. Must be passed whenever direct or reciprocal lattice vectors are created. @@ -77,13 +86,8 @@ def __init__( UVTW: Union[np.ndarray, list, tuple, None] = None, hkl: Union[np.ndarray, list, tuple, None] = None, hkil: Union[np.ndarray, list, tuple, None] = None, - phase: Optional["orix.crystal_map.Phase"] = None, - ): - """Create a set of direct lattice vectors (uvw or UVTW) or - reciprocal lattice vectors (hkl or hkil) describing directions - with respect to a crystal reference frame defined by a phase's - crystal lattice and symmetry. - """ + phase: Optional["Phase"] = None, + ) -> None: n_passed = np.sum([i is not None for i in [xyz, uvw, UVTW, hkl, hkil]]) if n_passed == 0 or n_passed > 1: raise ValueError( @@ -134,7 +138,7 @@ def coordinate_format(self) -> str: return self._coordinate_format @coordinate_format.setter - def coordinate_format(self, value: str): + def coordinate_format(self, value: str) -> None: """Set the vector coordinate format.""" formats = ["xyz", "uvw", "UVTW", "hkl", "hkil"] if value not in formats: @@ -162,12 +166,12 @@ def hkl(self) -> np.ndarray: return _transform_space(self.data, "c", "r", self.phase.structure.lattice) @hkl.setter - def hkl(self, value: np.ndarray): + def hkl(self, value: np.ndarray) -> None: """Set the reciprocal lattice vectors.""" self.data = _transform_space(value, "r", "c", self.phase.structure.lattice) @property - def hkil(self): + def hkil(self) -> np.ndarray: r"""Return or set the reciprocal lattice vectors expressed as 4-index Miller-Bravais indices. @@ -181,7 +185,7 @@ def hkil(self): return _hkl2hkil(self.hkl) @hkil.setter - def hkil(self, value: np.ndarray): + def hkil(self, value: np.ndarray) -> None: """Set the reciprocal lattice vectors expressed as 4-index Miller-Bravais indices. """ @@ -223,12 +227,12 @@ def uvw(self) -> np.ndarray: return _transform_space(self.data, "c", "d", self.phase.structure.lattice) @uvw.setter - def uvw(self, value: np.ndarray): + def uvw(self, value: np.ndarray) -> None: """Set the direct lattice vectors.""" self.data = _transform_space(value, "d", "c", self.phase.structure.lattice) @property - def UVTW(self): + def UVTW(self) -> np.ndarray: r"""Return or set the direct lattice vectors expressed as 4-index Weber symbols. @@ -249,7 +253,7 @@ def UVTW(self): return _uvw2UVTW(self.uvw) @UVTW.setter - def UVTW(self, value): + def UVTW(self, value: np.ndarray) -> None: """Set the direct lattice vectors expressed as 4-index Weber symbols. """ @@ -341,7 +345,7 @@ def is_hexagonal(self) -> bool: return self.phase.is_hexagonal @property - def unit(self) -> Miller: + def unit(self) -> Self: """Return unit vectors.""" m = self.__class__(xyz=super().unit.data, phase=self.phase) m.coordinate_format = self.coordinate_format @@ -360,7 +364,7 @@ def __repr__(self) -> str: f"{name} {shape}, point group {symmetry}, {coordinate_format}\n" f"{data}" ) - def __getitem__(self, key) -> Miller: + def __getitem__(self, key) -> Self: """NumPy fancy indexing of vectors.""" m = self.__class__(xyz=self.data[key], phase=self.phase).deepcopy() m.coordinate_format = self.coordinate_format @@ -371,10 +375,11 @@ def __getitem__(self, key) -> Miller: @classmethod def from_highest_indices( cls, - phase: "orix.crystal_map.Phase", + phase: "Phase", uvw: Union[np.ndarray, list, tuple, None] = None, hkl: Union[np.ndarray, list, tuple, None] = None, - ) -> Miller: + include_zero_vector: bool = False, + ) -> Self: """Create a set of unique direct or reciprocal lattice vectors from three highest indices and a phase (crystal lattice and symmetry). @@ -401,9 +406,7 @@ def from_highest_indices( return cls(**init_kw).unique() @classmethod - def from_min_dspacing( - cls, phase: "orix.crystal_map.Phase", min_dspacing: float = 0.05 - ) -> Miller: + def from_min_dspacing(cls, phase: "Phase", min_dspacing: float = 0.05) -> Self: """Create a set of unique reciprocal lattice vectors with a a direct space interplanar spacing greater than a lower threshold. @@ -427,10 +430,10 @@ def from_min_dspacing( @classmethod def random( cls, - phase: "orix.crystal_map.Phase", + phase: "Phase", shape: Union[int, tuple] = 1, coordinate_format: str = "xyz", - ) -> Miller: + ) -> Self: """Create random Miller indices. Parameters @@ -464,11 +467,11 @@ def random( # --------------------- Other public methods --------------------- # - def deepcopy(self) -> Miller: + def deepcopy(self) -> Self: """Return a deepcopy of the instance.""" return deepcopy(self) - def round(self, max_index: int = 20) -> Miller: + def round(self, max_index: int = 20) -> Self: """Round a set of index triplet (Miller) or quartet (Miller-Bravais/Weber) to the *closest* smallest integers. @@ -496,9 +499,7 @@ def symmetrise( unique: bool = False, return_multiplicity: bool = False, return_index: bool = False, - ) -> Union[ - Miller, Tuple[Miller, np.ndarray], Tuple[Miller, np.ndarray, np.ndarray] - ]: + ) -> Union[Self, Tuple[Self, np.ndarray], Tuple[Self, np.ndarray, np.ndarray]]: """Return vectors symmetrically equivalent to the vectors. Parameters @@ -585,7 +586,7 @@ def symmetrise( def angle_with( self, - other: Miller, + other: Self, use_symmetry: bool = False, degrees: bool = False, ) -> np.ndarray: @@ -630,7 +631,7 @@ def angle_with( return angles - def cross(self, other: Miller): + def cross(self, other: Self) -> Self: """Return the cross products of the vectors with the other vectors, which is considered the zone axes between the vectors. @@ -652,7 +653,7 @@ def cross(self, other: Miller): m.coordinate_format = new_fmt[self.coordinate_format] return m - def dot(self, other: Miller) -> np.ndarray: + def dot(self, other: Self) -> np.ndarray: """Return the dot products of the vectors and the other vectors. Parameters @@ -669,7 +670,7 @@ def dot(self, other: Miller) -> np.ndarray: self._compatible_with(other, raise_error=True) return super().dot(other) - def dot_outer(self, other: Miller) -> np.ndarray: + def dot_outer(self, other: Self) -> np.ndarray: """Return the outer dot products of the vectors and the other vectors. @@ -687,7 +688,7 @@ def dot_outer(self, other: Miller) -> np.ndarray: self._compatible_with(other, raise_error=True) return super().dot_outer(other) - def flatten(self) -> Miller: + def flatten(self) -> Self: """Return the flattened vectors. Returns @@ -699,7 +700,7 @@ def flatten(self) -> Miller: m.coordinate_format = self.coordinate_format return m - def transpose(self, *axes: Optional[int]) -> Miller: + def transpose(self, *axes: Optional[int]) -> Self: """Return a new instance with the data transposed. The order may be undefined if :attr:`ndim` is originally 2. In @@ -725,7 +726,7 @@ def get_nearest(self, *args) -> NotImplemented: """NotImplemented.""" return NotImplemented - def mean(self, use_symmetry: bool = False) -> Miller: + def mean(self, use_symmetry: bool = False) -> Self: """Return the mean vector of the set of vectors. Parameters @@ -745,7 +746,7 @@ def mean(self, use_symmetry: bool = False) -> Miller: m.coordinate_format = self.coordinate_format return m - def reshape(self, *shape: Union[int, tuple]) -> Miller: + def reshape(self, *shape: Union[int, tuple]) -> Self: """Return a new instance with the vectors reshaped. Parameters @@ -764,7 +765,7 @@ def reshape(self, *shape: Union[int, tuple]) -> Miller: def unique( self, use_symmetry: bool = False, return_index: bool = False - ) -> Union[Miller, Tuple[Miller, np.ndarray]]: + ) -> Union[Self, Tuple[Self, np.ndarray]]: """Unique vectors in ``self``. Parameters @@ -809,9 +810,7 @@ def unique( else: return m - def in_fundamental_sector( - self, symmetry: Optional["orix.quaternion.Symmetry"] = None - ) -> Miller: + def in_fundamental_sector(self, symmetry: Optional["Symmetry"] = None) -> Self: """Project Miller indices to a symmetry's fundamental sector (inverse pole figure). @@ -858,7 +857,7 @@ def in_fundamental_sector( # -------------------- Other private methods --------------------- # - def _compatible_with(self, other: Miller, raise_error: bool = False) -> bool: + def _compatible_with(self, other: Self, raise_error: bool = False) -> bool: """Whether ``self`` and ``other`` are the same (the same crystal lattice and symmetry) with vectors in the same space. @@ -977,7 +976,7 @@ def _hkil2hkl(hkil: np.ndarray) -> np.ndarray: return hkl -def _check_hkil(hkil: np.ndarray): +def _check_hkil(hkil: np.ndarray) -> None: hkil = np.asarray(hkil) if not np.allclose(np.sum(hkil[..., :3], axis=-1), 0, atol=1e-4): raise ValueError( @@ -1016,7 +1015,7 @@ def _UVTW2uvw(UVTW: np.ndarray, convention: Optional[str] = None) -> np.ndarray: return uvw -def _check_UVTW(UVTW: np.ndarray): +def _check_UVTW(UVTW: np.ndarray) -> None: UVTW = np.asarray(UVTW) if not np.allclose(np.sum(UVTW[..., :3], axis=-1), 0, atol=1e-4): raise ValueError( From 0c023ecc6f4637ce9317fd42487eef43c7f8947a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 13 May 2024 22:22:08 +0200 Subject: [PATCH 42/49] Add simple test for Vector3d.get_nearest() 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_vector3d.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/orix/tests/test_vector3d.py b/orix/tests/test_vector3d.py index 3f628822..7ed4c386 100644 --- a/orix/tests/test_vector3d.py +++ b/orix/tests/test_vector3d.py @@ -460,6 +460,16 @@ def test_zero_perpendicular(): _ = Vector3d.zero((1,)).perpendicular +def test_get_nearest(): + v_ref = Vector3d.zvector() + v = Vector3d([[0, 0, 0.9], [0, 0, 0.8], [0, 0, 1.1]]) + v_nearest = v_ref.get_nearest(v) + assert np.allclose(v_nearest.data, [0, 0, 0.9]) + + with pytest.raises(AttributeError, match="`get_nearest` only works for "): + v.get_nearest(v_ref) + + class TestSpareNotImplemented: def test_radd_notimplemented(self, vector): with pytest.raises(TypeError): From b7e873b2b4a86175a4bee7ea023ad25d28d730ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 13 May 2024 22:22:24 +0200 Subject: [PATCH 43/49] Replace np.float64(rotation.a) with rotation.a.astype(np.float64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/vector/neo_euler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orix/vector/neo_euler.py b/orix/vector/neo_euler.py index ea822af1..6bb46f9c 100644 --- a/orix/vector/neo_euler.py +++ b/orix/vector/neo_euler.py @@ -140,7 +140,7 @@ def from_rotation(cls, rotation: "Rotation") -> Rodrigues: -------- Quaternion.to_rodrigues """ - a = np.float64(rotation.a) + a = rotation.a.astype(np.float64) with np.errstate(divide="ignore", invalid="ignore"): data = np.stack((rotation.b / a, rotation.c / a, rotation.d / a), axis=-1) data[np.isnan(data)] = 0 From e616f9a39757ff7242b48d5dc4ebb6b32bb1050d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Tue, 14 May 2024 20:32:58 +0200 Subject: [PATCH 44/49] Remove deprecated from_neo_euler() methods and convention parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- CHANGELOG.rst | 4 ++ orix/quaternion/orientation.py | 33 +-------- orix/quaternion/quaternion.py | 40 +---------- orix/quaternion/rotation.py | 3 - orix/tests/quaternion/test_orientation.py | 73 +------------------ orix/tests/quaternion/test_quaternion.py | 85 ++--------------------- orix/vector/neo_euler.py | 5 +- 7 files changed, 18 insertions(+), 225 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bf9b41a8..75b13d7a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,10 @@ Changed Removed ------- +- Removed deprecated ``from_neo_euler()`` method for ``Quaternion`` and its subclasses. +- Removed deprecated argument ``convention`` in ``from_euler()`` and ``to_euler()`` + methods for ``Quaternion`` and its subclasses. Use ``direction`` instead. Passing + ``convention`` will now raise an error. Deprecated ---------- diff --git a/orix/quaternion/orientation.py b/orix/quaternion/orientation.py index 215b090d..d654f3d5 100644 --- a/orix/quaternion/orientation.py +++ b/orix/quaternion/orientation.py @@ -29,11 +29,10 @@ import numpy as np from scipy.spatial.transform import Rotation as SciPyRotation -from orix._util import deprecated from orix.quaternion.misorientation import Misorientation from orix.quaternion.rotation import Rotation from orix.quaternion.symmetry import C1, Symmetry, _get_unique_symmetry_elements -from orix.vector import Miller, NeoEuler, Vector3d +from orix.vector import Miller, Vector3d class Orientation(Misorientation): @@ -107,7 +106,6 @@ def __sub__(self, other: Orientation) -> Misorientation: # ------------------------ Class methods ------------------------- # - # TODO: Remove use of **kwargs in 1.0 @classmethod def from_euler( cls, @@ -115,7 +113,6 @@ def from_euler( symmetry: Optional[Symmetry] = None, direction: str = "lab2crystal", degrees: bool = False, - **kwargs, ) -> Orientation: """Create orientations from sets of Euler angles :cite:`rowenhorst2015consistent`. @@ -141,7 +138,7 @@ def from_euler( O Orientations. """ - O = super().from_euler(euler, direction=direction, degrees=degrees, **kwargs) + O = super().from_euler(euler, direction=direction, degrees=degrees) if symmetry: O.symmetry = symmetry return O @@ -261,32 +258,6 @@ def from_matrix( O.symmetry = symmetry return O - # TODO: Remove before 0.13.0 - @classmethod - @deprecated(since="0.12", removal="0.13", alternative="from_axes_angles") - def from_neo_euler( - cls, neo_euler: NeoEuler, symmetry: Optional[Symmetry] = None - ) -> Orientation: - """Create orientations from a neo-euler (vector) representation. - - Parameters - ---------- - neo_euler - Vector parametrization of orientation(s). - symmetry - Symmetry of orientation(s). If not given (default), no - symmetry is set. - - Returns - ------- - O - Orientations. - """ - O = super().from_neo_euler(neo_euler) - if symmetry: - O.symmetry = symmetry - return O - @classmethod def from_axes_angles( cls, diff --git a/orix/quaternion/quaternion.py b/orix/quaternion/quaternion.py index e7fdf5e2..5b77a39c 100644 --- a/orix/quaternion/quaternion.py +++ b/orix/quaternion/quaternion.py @@ -28,13 +28,9 @@ from scipy.spatial.transform import Rotation as SciPyRotation from orix._base import Object3d -from orix._util import deprecated, deprecated_argument from orix.quaternion import _conversions from orix.vector import AxAngle, Homochoric, Miller, Rodrigues, Vector3d -# Used to round values below 1e-16 to zero -_FLOAT_EPS = np.finfo(float).eps - class Quaternion(Object3d): r"""Quaternions. @@ -259,31 +255,6 @@ def __eq__(self, other: Union[Any, Quaternion]) -> bool: # ------------------------ Class methods ------------------------- # - # TODO: Remove before 0.13.0 - @classmethod - @deprecated(since="0.12", removal="0.13", alternative="from_axes_angles") - def from_neo_euler(cls, neo_euler: "NeoEuler") -> Quaternion: - """Create unit quaternion(s) from a neo-euler (vector) - representation. - - Parameters - ---------- - neo_euler - Vector parametrization of quaternions. - - Returns - ------- - Q - Unit quaternion(s). - """ - s = np.sin(neo_euler.angle / 2) - a = np.cos(neo_euler.angle / 2) - b = s * neo_euler.axis.x - c = s * neo_euler.axis.y - d = s * neo_euler.axis.z - Q = cls(np.stack([a, b, c, d], axis=-1)).unit - return Q - @classmethod def from_axes_angles( cls, @@ -485,15 +456,12 @@ def from_rodrigues( return Q - # TODO: Remove decorator, **kwargs, and use of "convention" in 0.13 @classmethod - @deprecated_argument("convention", "0.9", "0.13", "direction") def from_euler( cls, euler: Union[np.ndarray, tuple, list], direction: str = "lab2crystal", degrees: bool = False, - **kwargs, ) -> Quaternion: """Create unit quaternions from Euler angle sets :cite:`rowenhorst2015consistent`. @@ -517,9 +485,7 @@ def from_euler( Unit quaternions. """ direction = direction.lower() - if direction == "mtex" or ( - "convention" in kwargs and kwargs["convention"] == "mtex" - ): + if direction == "mtex": # MTEX' rotations are transformations from the crystal to # the lab reference frames. See # https://mtex-toolbox.github.io/MTEXvsBungeConvention.html @@ -804,9 +770,7 @@ def identity(cls, shape: Union[int, tuple] = (1,)) -> Quaternion: # ---------------------- All "to_*" methods- --------------------- # - # TODO: Remove decorator and **kwargs in 0.13 - @deprecated_argument("convention", since="0.9", removal="0.13") - def to_euler(self, degrees: bool = False, **kwargs) -> np.ndarray: + def to_euler(self, degrees: bool = False) -> np.ndarray: r"""Return the unit quaternions as Euler angles in the Bunge convention :cite:`rowenhorst2015consistent`. diff --git a/orix/quaternion/rotation.py b/orix/quaternion/rotation.py index de3f7f2e..3042d3e5 100644 --- a/orix/quaternion/rotation.py +++ b/orix/quaternion/rotation.py @@ -28,9 +28,6 @@ from orix.quaternion import Quaternion from orix.vector import Vector3d -# Used to round values below 1e-16 to zero -_FLOAT_EPS = np.finfo(float).eps - class Rotation(Quaternion): r"""Rotations of coordinate systems, leaving objects in place. diff --git a/orix/tests/quaternion/test_orientation.py b/orix/tests/quaternion/test_orientation.py index cefaf8f0..2a87982b 100644 --- a/orix/tests/quaternion/test_orientation.py +++ b/orix/tests/quaternion/test_orientation.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with orix. If not, see . -import warnings - from diffpy.structure import Lattice, Structure import matplotlib.pyplot as plt import numpy as np @@ -43,7 +41,7 @@ _groups, _proper_groups, ) -from orix.vector import AxAngle, Miller, Vector3d +from orix.vector import Miller, Vector3d # isort: on # fmt: on @@ -522,22 +520,7 @@ def test_from_align_vectors(self): ): _ = Orientation.from_align_vectors(a, b) - def test_from_neo_euler_symmetry(self): - v = AxAngle.from_axes_angles(axes=Vector3d.zvector(), angles=np.pi / 2) - with pytest.warns(np.VisibleDeprecationWarning): - o1 = Orientation.from_neo_euler(v) - assert np.allclose(o1.data, [0.7071, 0, 0, 0.7071]) - assert o1.symmetry.name == "1" - with pytest.warns(np.VisibleDeprecationWarning): - o2 = Orientation.from_neo_euler(v, symmetry=Oh) - o2 = o2.map_into_symmetry_reduced_zone() - assert np.allclose(o2.data, [-1, 0, 0, 0]) - assert o2.symmetry.name == "m-3m" - o3 = Orientation(o1.data, symmetry=Oh) - o3 = o3.map_into_symmetry_reduced_zone() - assert np.allclose(o3.data, o2.data) - - def test_from_axes_angles(self, rotations): + def test_from_axes_angles(self): axis = Vector3d.xvector() - Vector3d.yvector() angle = np.pi / 2 o1 = Orientation.from_axes_angles(axis, angle, Oh) @@ -587,58 +570,6 @@ def test_from_scipy_rotation(self): with pytest.raises(TypeError, match="Value must be an instance of"): _ = Orientation.from_scipy_rotation(r_scipy, (Oh, Oh)) - # TODO: Remove in 0.13 - def test_from_euler_warns(self): - """Orientation.from_euler() warns only once when "convention" - argument is passed. - """ - euler = np.random.rand(10, 3) - - with warnings.catch_warnings(): - warnings.filterwarnings("error") - _ = Orientation.from_euler(euler) - - msg = ( - r"Argument `convention` is deprecated and will be removed in version 0.13. " - r"To avoid this warning, please do not use `convention`. " - r"Use `direction` instead. See the documentation of `from_euler\(\)` for " - "more details." - ) - with pytest.warns(np.VisibleDeprecationWarning, match=msg) as record2: - _ = Orientation.from_euler(euler, convention="whatever") - assert len(record2) == 1 - - # TODO: Remove in 0.13 - def test_from_euler_convention_mtex(self): - """Passing convention="mtex" to Orientation.from_euler() works - but warns once. - """ - euler = np.random.rand(10, 3) - ori1 = Orientation.from_euler(euler, direction="crystal2lab") - with pytest.warns(np.VisibleDeprecationWarning, match=r"Argument `convention`"): - ori2 = Orientation.from_euler(euler, convention="mtex") - assert np.allclose(ori1.data, ori2.data) - - # TODO: Remove in 0.13 - def test_to_euler_convention_warns(self): - """Orientation.to_euler() warns only once when "convention" - argument is passed. - """ - ori1 = Orientation.from_euler(np.random.rand(10, 3)) - - with warnings.catch_warnings(): - warnings.filterwarnings("error") - ori2 = ori1.to_euler() - - msg = ( - r"Argument `convention` is deprecated and will be removed in version 0.13. " - r"To avoid this warning, please do not use `convention`. " - r"See the documentation of `to_euler\(\)` for more details." - ) - with pytest.warns(np.VisibleDeprecationWarning, match=msg): - ori3 = ori1.to_euler(convention="whatever") - assert np.allclose(ori2, ori3) - class TestOrientation: @pytest.mark.parametrize("symmetry", [C1, C2, C3, C4, D2, D3, D6, T, O, Oh]) diff --git a/orix/tests/quaternion/test_quaternion.py b/orix/tests/quaternion/test_quaternion.py index bc69ca94..e3959164 100644 --- a/orix/tests/quaternion/test_quaternion.py +++ b/orix/tests/quaternion/test_quaternion.py @@ -16,8 +16,6 @@ # You should have received a copy of the GNU General Public License # along with orix. If not, see . -import warnings - import dask.array as da from diffpy.structure.spacegroups import sg225 import numpy as np @@ -25,7 +23,7 @@ from orix._base import DimensionError from orix.quaternion import Quaternion -from orix.vector import AxAngle, Homochoric, Rodrigues, Vector3d +from orix.vector import AxAngle, Homochoric, Vector3d @pytest.fixture( @@ -345,9 +343,6 @@ def test_to_from_euler(self, eu): eu4 = Quaternion.from_euler(eu).to_euler(degrees=True) assert np.allclose(np.rad2deg(eu), eu4) - def test_mtex(self, eu): - _ = Quaternion.from_euler(eu, direction="mtex") - def test_direction_values(self, eu): q_mtex = Quaternion.from_euler(eu, direction="mtex") q_c2l = Quaternion.from_euler(eu, direction="crystal2lab") @@ -357,9 +352,6 @@ def test_direction_values(self, eu): assert np.allclose(q_mtex.data, q_c2l.data) assert np.allclose((q_l2c * q_c2l).data, [1, 0, 0, 0]) - def test_direction_kwarg(self, eu): - _ = Quaternion.from_euler(eu) - def test_direction_kwarg_dumb(self, eu): with pytest.raises(ValueError, match="The chosen direction is not one of "): _ = Quaternion.from_euler(eu, direction="dumb_direction") @@ -376,56 +368,6 @@ def test_passing_degrees_warns(self): q = Quaternion.from_euler([90, 0, 0]) assert np.allclose(q.data, [0.5253, 0, 0, -0.8509], atol=1e-4) - # TODO: Remove in 0.13 - def test_from_euler_warns(self, eu): - """Quaternion.from_euler() warns only when "convention" argument - is passed. - """ - # No warning is raised - with warnings.catch_warnings(): - warnings.simplefilter("error") - _ = Quaternion.from_euler(eu) - - msg = ( - r"Argument `convention` is deprecated and will be removed in version 0.13. " - r"To avoid this warning, please do not use `convention`. " - r"Use `direction` instead. See the documentation of `from_euler\(\)` for " - "more details." - ) - with pytest.warns(np.VisibleDeprecationWarning, match=msg): - _ = Quaternion.from_euler(eu, convention="whatever") - - # TODO: Remove in 0.13 - def test_from_euler_convention_mtex(self, eu): - """Passing convention="mtex" to Quaternion.from_euler() works but - warns. - """ - q1 = Quaternion.from_euler(eu, direction="crystal2lab") - with pytest.warns(np.VisibleDeprecationWarning, match=r"Argument `convention`"): - q2 = Quaternion.from_euler(eu, convention="mtex") - assert np.allclose(q1.data, q2.data) - - # TODO: Remove in 0.13 - def test_to_euler_convention_warns(self, eu): - """Quaternion.to_euler() warns only when "convention" argument is - passed. - """ - q1 = Quaternion.from_euler(eu) - - # No warning is raised - with warnings.catch_warnings(): - warnings.simplefilter("error") - q2 = q1.to_euler() - - msg = ( - r"Argument `convention` is deprecated and will be removed in version 0.13. " - r"To avoid this warning, please do not use `convention`. " - r"See the documentation of `to_euler\(\)` for more details." - ) - with pytest.warns(np.VisibleDeprecationWarning, match=msg): - q3 = q1.to_euler(convention="whatever") - assert np.allclose(q2, q3) - class TestFromToMatrix: def test_to_matrix(self): @@ -516,13 +458,9 @@ def test_from_axes_angles(self, rotations, extra_dim): if extra_dim: rotations = rotations.__class__(rotations.data[..., np.newaxis, :]) ax = AxAngle.from_rotation(rotations) - with pytest.warns(np.VisibleDeprecationWarning): - q2 = Quaternion.from_neo_euler(ax) - q3 = Quaternion.from_axes_angles(ax.axis.data, ax.angle) - assert np.allclose(q2.data, q3.data) - - q4 = Quaternion.from_axes_angles(ax.axis, np.rad2deg(ax.angle), degrees=True) - assert np.allclose(q4.data, q3.data) + Q1 = Quaternion.from_axes_angles(ax.axis.data, ax.angle) + Q2 = Quaternion.from_axes_angles(ax.axis, np.rad2deg(ax.angle), degrees=True) + assert np.allclose(Q1.data, Q2.data) def test_to_axes_angles(self, quaternions_conversions, axis_angle_pairs): ax = Quaternion(quaternions_conversions).to_axes_angles() @@ -556,21 +494,6 @@ def test_from_to_rodrigues(self, quaternions_conversions, rodrigues_vectors): with pytest.raises(ValueError, match="Final dimension of vector array must be"): Quaternion.from_rodrigues([1, 2, 3, 4]) - def test_backwards_consistency(self, quaternions_conversions, rodrigues_vectors): - axes = rodrigues_vectors[..., :3] - angles = rodrigues_vectors[..., 3][..., np.newaxis] - with pytest.warns(RuntimeWarning): - ro = Rodrigues(axes * angles) - - with pytest.warns(np.VisibleDeprecationWarning): - q1 = Quaternion.from_neo_euler(ro) - with pytest.warns(UserWarning, match="Highest angle is greater than 179.999 "): - q2 = Quaternion.from_rodrigues(axes, angles) - - assert np.allclose(q1[2].data, 0) - match_idx = [0, 1, 3, 4, 5, 6, 7, 8, 9] - assert np.allclose(q1[match_idx].data, q2[match_idx].data, atol=1e-4) - def test_from_rodrigues_empty(self): q = Quaternion.from_rodrigues([]) assert q.size == 0 diff --git a/orix/vector/neo_euler.py b/orix/vector/neo_euler.py index 6bb46f9c..7aab52b0 100644 --- a/orix/vector/neo_euler.py +++ b/orix/vector/neo_euler.py @@ -30,12 +30,15 @@ from __future__ import annotations import abc -from typing import Union +from typing import TYPE_CHECKING, Union import numpy as np from orix.vector import Vector3d +if TYPE_CHECKING: # pragma: no cover + from orix.quaternion import Rotation + class NeoEuler(Vector3d, abc.ABC): """Base class for neo-Eulerian vectors.""" From 1a4f34429aac14d7db384abcdf585f04e37b349f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Tue, 14 May 2024 20:35:24 +0200 Subject: [PATCH 45/49] Fix GitHub CI: use only branches-ignore, not in addition to branches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- .github/workflows/build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6cb5c4ca..f721f637 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,8 +2,6 @@ name: build on: push: - branches: - - '*' branches-ignore: - 'pre-commit-ci-update-config' pull_request: From 7ee68521f71e6d36249cc3f9ac07fac3754436f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 2 Sep 2024 20:23:40 +0200 Subject: [PATCH 46/49] Temporarily add PyCIFRW as dependency (dependency of diffpy.structure) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index fd889bde..dfcc4c09 100644 --- a/setup.py +++ b/setup.py @@ -93,6 +93,8 @@ "numpy", "numpy-quaternion", "pooch >= 0.13", + # TODO: Remove once https://github.com/diffpy/diffpy.structure/issues/97 is fixed + "pycifrw", "scipy", "tqdm", ], From 7736ab1c8096ab9da8fda6c4062bcf37d2780ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 2 Sep 2024 20:28:30 +0200 Subject: [PATCH 47/49] Minor fixes in crystal map and rotation plots using Matplotlib MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- orix/tests/plot/test_crystal_map_plot.py | 2 +- orix/tests/plot/test_rotation_plot.py | 165 +++++++++++------------ 2 files changed, 82 insertions(+), 85 deletions(-) diff --git a/orix/tests/plot/test_crystal_map_plot.py b/orix/tests/plot/test_crystal_map_plot.py index 86cb2f52..3766a32e 100644 --- a/orix/tests/plot/test_crystal_map_plot.py +++ b/orix/tests/plot/test_crystal_map_plot.py @@ -375,7 +375,7 @@ def test_status_bar_silence_default_format_coord(self, crystal_map): fig = plt.figure() ax = fig.add_subplot(projection=PLOT_MAP) _ = ax.plot_map(crystal_map) - assert ax.format_coord(0, 0) == "x=0 y=0" + assert ax.format_coord(0, 0) == "(x, y) = (0, 0)" fig = plt.figure() ax = fig.add_subplot(projection=PLOT_MAP) diff --git a/orix/tests/plot/test_rotation_plot.py b/orix/tests/plot/test_rotation_plot.py index 4385a9e9..8fe36e1c 100644 --- a/orix/tests/plot/test_rotation_plot.py +++ b/orix/tests/plot/test_rotation_plot.py @@ -25,87 +25,84 @@ from orix.quaternion.symmetry import C1, D6 -def test_init_rodrigues_plot(): - fig = plt.figure() - ax = fig.add_subplot(projection="rodrigues", auto_add_to_figure=False) - assert isinstance(ax, RodriguesPlot) - - -def test_init_axangle_plot(): - fig = plt.figure() - ax = fig.add_subplot(projection="axangle", auto_add_to_figure=False) - assert isinstance(ax, AxAnglePlot) - - -def test_RotationPlot_methods(): - """This code is lifted from demo-3-v0.1.""" - misori = Misorientation([1, 1, 1, 1]) # any will do - ori = Orientation.random() - fig = plt.figure() - ax = fig.add_subplot( - projection="axangle", proj_type="ortho", auto_add_to_figure=False - ) - ax.scatter(misori) - ax.scatter(ori) - ax.plot(misori) - ax.plot(ori) - ax.plot_wireframe(OrientationRegion.from_symmetry(D6, D6)) - plt.close("all") - - # Clear the edge case - ax.transform(np.asarray([1, 1, 1])) - - -def test_full_region_plot(): - empty = OrientationRegion.from_symmetry(C1, C1) - _ = empty.get_plot_data() - - -def test_RotationPlot_transform_fundamental_zone_raises(): - fig = plt.figure() - ax = RotationPlot(fig) - fig.add_axes(ax) - with pytest.raises( - TypeError, match="fundamental_zone is not an OrientationRegion object" - ): - ax.transform(Orientation.random(), fundamental_zone=1) - - -def test_RotationPlot_map_into_symmetry_reduced_zone(): - # orientations are (in, out) of D6 fundamental zone - ori = Orientation(((1, 0, 0, 0), (0.5, 0.5, 0.5, 0.5))) - ori.symmetry = D6 - fz = OrientationRegion.from_symmetry(ori.symmetry) - assert np.allclose(ori < fz, (True, False)) - # test map_into_symmetry_reduced_zone in RotationPlot.transform - fig = ori.scatter(return_figure=True) - xyz_symmetry = fig.axes[0].collections[1]._offsets3d - # compute same plot again but with C1 symmetry where both orientations are in C1 FZ - ori.symmetry = C1 - fig2 = ori.scatter(return_figure=True) - xyz = fig2.axes[0].collections[1]._offsets3d - # test that the plotted points are not the same - assert not np.allclose(xyz_symmetry, xyz) - - -def test_correct_aspect_ratio(): - # Set up figure the "old" way - fig = plt.figure() - ax = fig.add_subplot( - projection="axangle", proj_type="ortho", auto_add_to_figure=False - ) - - # Check aspect ratio - x_old, _, z_old = ax.get_box_aspect() - assert np.allclose(x_old / z_old, 1.334, atol=1e-3) - - fr = OrientationRegion.from_symmetry(D6) - ax._correct_aspect_ratio(fr, set_limits=False) - - x_new, _, z_new = ax.get_box_aspect() - assert np.allclose(x_new / z_new, 3, atol=1e-3) - - # Check data limits - assert np.allclose(ax.get_xlim(), [0, 1]) - ax._correct_aspect_ratio(fr) # set_limits=True is default - assert np.allclose(ax.get_xlim(), [-np.pi / 2, np.pi / 2]) +class TestRodriguesPlot: + def test_creation(self): + fig = plt.figure() + ax = fig.add_subplot(projection="rodrigues") + assert isinstance(ax, RodriguesPlot) + + +class TestAxisAnglePlot: + def test_creation(self): + fig = plt.figure() + ax = fig.add_subplot(projection="axangle") + assert isinstance(ax, AxAnglePlot) + + plt.close("all") + + def test_rotation_plot(self): + M = Misorientation.random() + O = Orientation.random() + fig = plt.figure() + ax = fig.add_subplot(projection="axangle", proj_type="ortho") + ax.scatter(M) + ax.scatter(O) + ax.plot(M) + ax.plot(O) + ax.plot_wireframe(OrientationRegion.from_symmetry(D6, D6)) + + ax.transform(np.asarray([1, 1, 1])) # Edge case + + plt.close("all") + + def test_get_plot_data(self): + empty = OrientationRegion.from_symmetry(C1, C1) + _ = empty.get_plot_data() + + def test_rotation_plot_transform_fundamental_zone_raises(self): + fig = plt.figure() + ax = RotationPlot(fig) + fig.add_axes(ax) + with pytest.raises(TypeError, match="fundamental_zone is not an "): + ax.transform(Orientation.random(), fundamental_zone=1) + + def test_rotation_plot_map_into_symmetry_reduced_zone(self): + # Orientations are (in, out) of D6 fundamental zone + O = Orientation(((1, 0, 0, 0), (0.5, 0.5, 0.5, 0.5))) + O.symmetry = D6 + fz = OrientationRegion.from_symmetry(O.symmetry) + assert np.allclose(O < fz, (True, False)) + + # test map_into_symmetry_reduced_zone in RotationPlot.transform + fig = O.scatter(return_figure=True) + xyz_symmetry = fig.axes[0].collections[1]._offsets3d + + # compute same plot again but with C1 symmetry where both orientations are in C1 FZ + O.symmetry = C1 + fig2 = O.scatter(return_figure=True) + xyz = fig2.axes[0].collections[1]._offsets3d + + # test that the plotted points are not the same + assert not np.allclose(xyz_symmetry, xyz) + + plt.close("all") + + def test_correct_aspect_ratio(self): + fig = plt.figure() + ax = fig.add_subplot(projection="axangle", proj_type="ortho") + + # Check aspect ratio + x_old, _, z_old = ax.get_box_aspect() + assert np.allclose(x_old / z_old, 1.334, atol=1e-3) + + fr = OrientationRegion.from_symmetry(D6) + ax._correct_aspect_ratio(fr, set_limits=False) + + x_new, _, z_new = ax.get_box_aspect() + assert np.allclose(x_new / z_new, 3, atol=1e-3) + + assert np.allclose(ax.get_xlim(), [0, 1], atol=0.1) + ax._correct_aspect_ratio(fr) # set_limits=True is default + assert np.allclose(ax.get_xlim(), [-np.pi / 2, np.pi / 2]) + + plt.close("all") From 54aeadf0a17877b0ee5ee4559c69409cb5671ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 2 Sep 2024 20:45:48 +0200 Subject: [PATCH 48/49] Increment version to 0.13.0 and update CHANGELOG accordingly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- CHANGELOG.rst | 8 ++------ orix/__init__.py | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 75b13d7a..ee0fb9b3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,8 +6,8 @@ All user facing changes to this project are documented in this file. The format on `Keep a Changelog `__, and this project tries its best to adhere to `Semantic Versioning `__. -Unreleased -========== +2024-09-03 - version 0.13.0 +=========================== Added ----- @@ -30,10 +30,6 @@ Deprecated - ``loadang()`` and ``loadctf()`` are deprecated and will be removed in the next minor release. Please use ``io.load()`` instead. -Fixed ------ - - 2024-04-21 - version 0.12.1 =========================== diff --git a/orix/__init__.py b/orix/__init__.py index 283e5019..18ea610e 100644 --- a/orix/__init__.py +++ b/orix/__init__.py @@ -1,5 +1,5 @@ __name__ = "orix" -__version__ = "0.13.dev1" +__version__ = "0.13.0" __author__ = "orix developers" __author_email__ = "pyxem.team@gmail.com" __description__ = "orix is an open-source Python library for handling crystal orientation mapping data." From d94130b994bf42615d8652652642526779dc42a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ha=CC=8Akon=20Wiik=20A=CC=8Anes?= Date: Mon, 2 Sep 2024 20:46:05 +0200 Subject: [PATCH 49/49] Fix links to related projects in docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Håkon Wiik Ånes --- doc/user/related_projects.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/user/related_projects.rst b/doc/user/related_projects.rst index bb911a94..bdb2f8e0 100644 --- a/doc/user/related_projects.rst +++ b/doc/user/related_projects.rst @@ -24,8 +24,8 @@ find useful: orix depends on numpy-quaternion for quaternion multiplication. - `texture `_: Python scripts for analysis of crystallographic texture. -- `pymicro `_`: Python package to work with material +- `pymicro `_: Python package to work with material microstructures and 3D data sets. -- `DREAM.3D `_`: C++ library to reconstruct, instatiate, quantify, +- `DREAM.3D `_: C++ library to reconstruct, instatiate, quantify, mesh, handle and visualize multidimensional (3D), multimodal data (mainly EBSD orientation data). \ No newline at end of file