Skip to content

Commit

Permalink
Merge pull request #511 from hakonanes/prepare-0.13.0-release
Browse files Browse the repository at this point in the history
Prepare main branch for 0.13.0 release
  • Loading branch information
hakonanes committed Sep 3, 2024
2 parents bc13474 + d94130b commit aca9a84
Show file tree
Hide file tree
Showing 30 changed files with 1,838 additions and 664 deletions.
5 changes: 2 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ name: build

on:
push:
branches:
- '*'
branches-ignore:
- 'pre-commit-ci-update-config'
pull_request:
branches:
- '*'
workflow_dispatch:
workflow: '*'

jobs:
code:
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 5 additions & 0 deletions .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ All user facing changes to this project are documented in this file. The format
on `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`__, and this project tries
its best to adhere to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`__.

2024-09-03 - version 0.13.0
===========================

Added
-----
- We can now read 2D crystal maps from Channel Text Files (CTFs) using ``io.load()``.

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
-------
- 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
----------
- ``loadang()`` and ``loadctf()`` are deprecated and will be removed in the next minor
release. Please use ``io.load()`` instead.

2024-04-21 - version 0.12.1
===========================

Expand Down
4 changes: 2 additions & 2 deletions doc/user/related_projects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ find useful:
orix depends on numpy-quaternion for quaternion multiplication.
- `texture <https://github.com/usnistgov/texture>`_: Python scripts for analysis of
crystallographic texture.
- `pymicro <https://pymicro.readthedocs.io>`_`: Python package to work with material
- `pymicro <https://pymicro.readthedocs.io>`_: Python package to work with material
microstructures and 3D data sets.
- `DREAM.3D <https://dream3d.io/>`_`: C++ library to reconstruct, instatiate, quantify,
- `DREAM.3D <https://dream3d.io/>`_: C++ library to reconstruct, instatiate, quantify,
mesh, handle and visualize multidimensional (3D), multimodal data (mainly EBSD
orientation data).
98 changes: 98 additions & 0 deletions examples/plotting/interactive_xmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""
============================
Interactive crystal map plot
============================
This example shows how to use
:doc:`matplotlib event connections <matplotlib:users/explain/figure/event_handling>` 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()
3 changes: 2 additions & 1 deletion orix/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__name__ = "orix"
__version__ = "0.12.1.post0"
__version__ = "0.13.0"
__author__ = "orix developers"
__author_email__ = "[email protected]"
__description__ = "orix is an open-source Python library for handling crystal orientation mapping data."
Expand All @@ -13,6 +13,7 @@
"Duncan Johnstone",
"Niels Cautaerts",
"Anders Christian Mathisen",
"Zhou Xu",
"Carter Francis",
"Simon Høgås",
"Viljar Johan Femoen",
Expand Down
120 changes: 74 additions & 46 deletions orix/crystal_map/crystal_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# along with orix. If not, see <http://www.gnu.org/licenses/>.

import copy
from typing import Optional, Tuple, Union
from typing import Dict, Optional, Tuple, Union

import matplotlib.pyplot as plt
import numpy as np
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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
----------
Expand All @@ -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.
Expand All @@ -1102,21 +1073,78 @@ 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):
data_shape.append(dim_slice.stop - dim_slice.start)
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
"""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
----------
Expand All @@ -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.
Expand All @@ -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:
Expand Down
Loading

0 comments on commit aca9a84

Please sign in to comment.