pyxem / orix

Analysing crystal orientations and symmetry in Python
https://orix.readthedocs.io
GNU General Public License v3.0
78 stars 45 forks source link

Minor maintenance updates before 0.12.0 release #478

Closed hakonanes closed 2 months ago

hakonanes commented 3 months ago

Description of the change

This PR contains a few maintenance updates before the 0.12.0 release.

In particular, one test, test_restrict_to_fundamental_sector(), failed on my Mac with Python 3.11 and Matplotlib 3.8.3. The relevant part of the test ensures that the edges of the fundamental sectors of C1 and C6 are different, but failed because a different number of edges were returned. I'm not sure why. Anyways, to ensure the edges are different, we only need to check the first couple edge vectors. I've made it so we check the first ten.

Other changes:

Progress of the PR

For reviewers

review-notebook-app[bot] commented 3 months ago

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

hakonanes commented 3 months ago

The only thing left I'd like to look at is the new gallery example on sampling reduced rotations by @CSSFrancis:

https://github.com/pyxem/orix/blob/d7c1b9a04c124f1ce829db50fc5d2d7410208914/examples/rotations/sampling_rotations.py#L2-L45

A couple questions:

  1. The only necessary phase information to run the example are the symmetry operations of the proper point group 432. Can we reduce the part with the phase setup to something like pg432 = symmetry.O? How to create a phase is covered in other parts of the documentation, like in the crystal map tutorial (https://orix.readthedocs.io/en/stable/tutorials/crystal_map.html). We can of course expand upon this by adding a Phase section to the gallery.
  2. In the example, we sample SO(3). But, the plotting only shows S2. We should show the sampled orientations, not just the rotated vectors. Otherwise, if rotated vectors was the goal, I'm wondering as a user why I can't just use the S2 sampling instead. What do you think, @CSSFrancis?
hakonanes commented 3 months ago

If it's OK with you @CSSFrancis, I'd like to re-work the example a bit along those lines.

CSSFrancis commented 3 months ago
  1. The only necessary phase information to run the example are the symmetry operations of the proper point group 432. Can we reduce the part with the phase setup to something like pg432 = symmetry.O? How to create a phase is covered in other parts of the documentation, like in the crystal map tutorial (https://orix.readthedocs.io/en/stable/tutorials/crystal_map.html). We can of course expand upon this by adding a Phase section to the gallery.

I think that is fine for the most part but it is sometimes nice to point out how someone can get that information from a Phase object. At least for people doing orientation mapping I think they are more likely to be working with the Phase rather than the symmetry.

  1. In the example, we sample SO(3). But, the plotting only shows S2. We should show the sampled orientations, not just the rotated vectors. Otherwise, if rotated vectors was the goal, I'm wondering as a user why I can't just use the S2 sampling instead. What do you think, @CSSFrancis?

This is also related to the orientation mapping. I guess the reason is that we want the rotations because that is the result of the orientation mapping code. I think this is a good example of converting rotations to vectors for visualization by essentially defining a beam direction. You lose in-plane rotation information but in this case there isn't any. Maybe this is an anti-pattern and I don't understand the orix code as well as I should....

CSSFrancis commented 3 months ago

@hakonanes I don't have a great understanding of orix but it seems like the S2 sampling could return a constrained rotation based on some vector direction rather than return a vector and that might be more useful in relation to the other parts of orix.

hakonanes commented 3 months ago

At least for people doing orientation mapping I think they are more likely to be working with the Phase rather than the symmetry.

This is a good point. I've opened #480 to discuss this further.

it seems like the S2 sampling could return a constrained rotation based on some vector direction

This is what you added in get_sample_reduced_fundamental(), which is great. I just didn't get the motivation behind your added example. Basically, the new function returns rotations which map the vector (0, 0, 1) onto the fundamental sector on the sphere of the corresponding Laue group.

May this then be a more descriptive example?

import matplotlib.pyplot as plt
import numpy as np

from orix import plot, sampling
from orix.quaternion import symmetry
from orix.vector import Vector3d

# Input: sampling resolution and symmetry
res = 2  # Degrees
laue = symmetry.D4h  # 4/mmm

R = sampling.get_sample_reduced_fundamental(res, point_group=laue)

# Show equivalence of vectors v1 and v2
v1 = R * Vector3d.zvector()

v2 = sampling.sample_S2(res)
v2 = v2[v2 <= laue.fundamental_sector]

assert np.allclose(v1.data, v2.data)
# True

fig, (ax0, ax1) = plt.subplots(
    ncols=2, subplot_kw={"projection": "ipf", "symmetry": pg}, layout="tight"
)
ax0.scatter(v1, s=5)
ax1.scatter(v2, c="C1", s=5)
ax0.set_title("Rotated Z vectors", loc="left")
ax1.set_title("Directly sampled", loc="left")
fig.suptitle("Vectors in the fundamental sector of 4/mmm", y=0.8)

test

CSSFrancis commented 3 months ago

At least for people doing orientation mapping I think they are more likely to be working with the Phase rather than the symmetry.

This is a good point. I've opened #480 to discuss this further.

Perfect!

it seems like the S2 sampling could return a constrained rotation based on some vector direction

This is what you added in get_sample_reduced_fundamental(), which is great. I just didn't get the motivation behind your added example. Basically, the new function returns rotations which map the vector (0, 0, 1) onto the fundamental sector on the sphere of the corresponding Laue group.

May this then be a more descriptive example?

That is much more descriptive :) I don't think I thought as much as I should about what the example should show.

hakonanes commented 3 months ago

I've added the above example to the examples gallery: https://orix--478.org.readthedocs.build/en/478/examples/rotations/rotations_mapping_fundamental_sector.html

I've got the following questions regarding the new function get_sample_zone_axis():

  1. Where are the hard-coded fundamental sector vertices (corners) from? Many of them differ from the vertices we use in orix. We have eleven Laue classes, with a different fundamental sector for each one (not just 6/7 crystal systems, excluding triclinic). For these, only mmm, 4/mmm, and m-3m match. We don't get one for -1 (triclinic), while the remaining are different. I suggest to get the corners from Phase.point_group.fundamental_sector.vertices.
  2. Adding unit vectors are different than adding non-unit vectors, say (1, 1, 1) + (0, 0, 1) vs. (0.5574, 0.5574, 0.5574) + (0, 0, 1). Thus, the midpoint vectors returned when density="7" and return_directions=True are not equidistant from the two (non-unit) vectors that are added. Is this intentional?
  3. And what is the purpose of these midpoint vectors in down-stream packages?

We need to change this function for it to align with the rest of orix.

hakonanes commented 3 months ago

I've updated the TODO list in the top comment with what's left before we're ready for the 0.12.0 release.

CSSFrancis commented 3 months ago

I've added the above example to the examples gallery: https://orix--478.org.readthedocs.build/en/478/examples/rotations/rotations_mapping_fundamental_sector.html

I've got the following questions regarding the new function get_sample_zone_axis():

  1. Where are the hard-coded fundamental sector vertices (corners) from? Many of them differ from the vertices we use in orix. We have eleven Laue classes, with a different fundamental sector for each one (not just 6/7 crystal systems, excluding triclinic). For these, only mmm, 4/mmm, and m-3m match. We don't get one for -1 (triclinic), while the remaining are different. I suggest to get the corners from Phase.point_group.fundamental_sector.vertices.

I'd imagine that this is a holdover from diffsims. I just copied what was previously in diffsims but in relation to orix I think the approach is a bit naive. We should drop the hard coded and just use the fundamental sector. I still think the method to return the Rotations for the fundamental sectors/ zone axes is useful. Usually I like to use this to give examples of high symmetry zone axes to help with aligning on the TEM.

  1. Adding unit vectors are different than adding non-unit vectors, say (1, 1, 1) + (0, 0, 1) vs. (0.5574, 0.5574, 0.5574) + (0, 0, 1). Thus, the midpoint vectors returned when density="7" and return_directions=True are not equidistant from the two (non-unit) vectors that are added. Is this intentional?

This is most likely a bug. And using the fundamental sectors.vectors would be much better.

  1. And what is the purpose of these midpoint vectors in down-stream packages?

We need to change this function for it to align with the rest of orix.

Honestly coming from a non crystallography background I looked in orix but am not particularly familiar with terms like fundamental sector and fundamental_sector.vecticies is fairly hidden. So something like Zone Axis, high symmetry axis etc is much more descriptive to me. I'm not sure that there is a difference between the two? But it would be good to have something in the documentation related to Zone Axis or high symmetry which helps those of us with a TEM background. Adding an example like "Getting High Symmetry Axes" would be nice, but maybe the Great Circle Example should have been enough for me :).

Edit: This doesn't make sense... I'm mixing this up a bit. Let me try again

Edit x2: Okay now this makes more sense.

hakonanes commented 3 months ago

Thank you for clarifying!

Since the get_sample_zone_axis() function won't be used directly by down-stream packages, I suggest to remove it but add descriptive examples to get the desired result.

The below is one route to some nice high-symmetry zone axes is. If it's OK with you @CSSFrancis, I'll replace the function and corresponding tests with this example.

from orix.crystal_map import Phase
from orix.vector import Miller

phase = Phase(point_group="mmm")  # m-3m, 6/mmm, 4/mmm, -3m, mmm, 2/m
t = Miller.from_highest_indices(phase, uvw=[1, 1, 1])
t = t.in_fundamental_sector()  # Project all to the fundamental sector
t = t.unit.unique(use_symmetry=True).round()  # Unit vectors ensure e.g. [222] == [111]
print(t)
# Miller (7,), point group mmm, uvw
# [[0. 0. 1.]
#  [0. 1. 1.]
#  [0. 1. 0.]
#  [1. 1. 1.]
#  [1. 0. 1.]
#  [1. 1. 0.]
#  [1. 0. 0.]]

Plotting the annotated vectors, only showing the fundamental sector

fig = t.scatter(
    vector_labels=[str(vi).replace(".", "") for vi in t.data],
    text_kwargs={"size": 15, "offset": (0, 0.02), "bbox": {"fc": "w", "pad": 1, "alpha": 0.75}},
    return_figure=True,
)
fig.axes[0].restrict_to_sector(t.phase.point_group.fundamental_sector)

test

CSSFrancis commented 3 months ago

Thank you for clarifying!

Since the get_sample_zone_axis() function won't be used directly by down-stream packages, I suggest to remove it but add descriptive examples to get the desired result.

The below is one route to some nice high-symmetry zone axes is. If it's OK with you @CSSFrancis, I'll replace the function and corresponding tests with this example.

from orix.crystal_map import Phase
from orix.vector import Miller

phase = Phase(point_group="mmm")  # m-3m, 6/mmm, 4/mmm, -3m, mmm, 2/m
t = Miller.from_highest_indices(phase, uvw=[1, 1, 1])
t = t.in_fundamental_sector()  # Project all to the fundamental sector
t = t.unit.unique(use_symmetry=True).round()  # Unit vectors ensure e.g. [222] == [111]
print(t)
# Miller (7,), point group mmm, uvw
# [[0. 0. 1.]
#  [0. 1. 1.]
#  [0. 1. 0.]
#  [1. 1. 1.]
#  [1. 0. 1.]
#  [1. 1. 0.]
#  [1. 0. 0.]]

Plotting the annotated vectors, only showing the fundamental sector

fig = t.scatter(
    vector_labels=[str(vi).replace(".", "") for vi in t.data],
    text_kwargs={"size": 15, "offset": (0, 0.02), "bbox": {"fc": "w", "pad": 1, "alpha": 0.75}},
    return_figure=True,
)
fig.axes[0].restrict_to_sector(t.phase.point_group.fundamental_sector)

I think that is useful but it would be nice to include an example the returns the Rotations as well as the vectors for simulation purposes.

I think a workflow like:


import diffsims as ds
from orix.crystal_map import Phase

phase = Phase.from_cif("strucuture.cif")
sim = ds.simulations.Simulation()
zone_axes_patterns = sim.get_zone_axes_patterns(phase)

is something we should be able to do fairly easily. If this is only used in diffsims then no reason to have a separate function but I suspect that this is a general enough function that others might be interested in it.

I think something like the ASTAR/ JEMS GUI is something that we could fairly easily replicate with orix/diffsims and it would be very useful to a lot of people.

hakonanes commented 2 months ago

it would be nice to include an example the returns the Rotations as well as the vectors for simulation purposes.

I've added the above example with an additional section showing how to get the rotations rotating the Z-vector (0, 0, 1) onto the high-symmetry crystal directions.

To have the syntax you suggest in diffsims, I suggest to make a function there instead of in orix. My feeling is that orix should be free of diffraction-specific functionality and stick to crystal orientations and directions. It may be that a more specific class method could be added to the ReciprocalLatticeVector class. This class is orix' Miller(hkl) class specifically tailored toward diffraction.

hakonanes commented 2 months ago

@CSSFrancis, I'd appreciate it if you could have a look at the new examples, the rephrased docstring of get_sample_reduced_fundamental(), and the reworked test of that function.

hakonanes commented 2 months ago

It's probably not super simple as the align vectors can handle the case where you are trying to align multiple vectors but we could also just add the function Rotation.from_align_vector which assumes a direct rotation from one vector to another.

Good point. Well, in the API, we only support the case where the two end point vectors have the same size. So, we could just say that when the sizes differ, we do broadcasting, instead of raising an error. In the simplest case, we can support 1-to-n and n-to-1. It may be slow, since the SciPy function we call does not support broadcasting, but it is possible.

CSSFrancis commented 2 months ago

LGTM! @hakonanes You can merge if you would like.