Skip to content

Conversation

@argerlt
Copy link
Collaborator

@argerlt argerlt commented May 19, 2025

Description of the change

Implements some ancillary capabilities from #442 that were never merged into the develop branch.
I added @hakonanes as a co-author since I essentially copied his homework on this one. Contains two major changes:

  1. Renames map_into_symmetry_reduced_zone() to reduce(), the value of which @hakonanes summed up nicely previously:

The other major change is naming: map_into_symmetry_reduced_zone() -> reduce(), inspired by the attractive naming of scipy.spatial.transforms.Rotation.reduce(). ...
... Using this word allows us also to add reduce as an intuitive parameter to various methods if we don't want to apply symmetry operations, instead of having a use_symmetry parameter (similar to MTEX' noSymmetry). We could then rename Miller.in_fundamental_region() to Miller.reduce() etc.

  1. adds Rotation.from_path_ends, which is roughly equivalent to Vector3d.from_path_ends.

Still requires unit tests and examples.

After this is merged, I think the only things missing from #442 is the proper handling of misorientation symmetry, and the updated tutorials

Progress of the PR

Minimal example of the bug fix or new feature

WIP

For reviewers

  • The PR title is short, concise, and will make sense 1 year later.
  • New functions are imported in corresponding __init__.py.
  • New features, API changes, and deprecations are mentioned in the unreleased
    section in CHANGELOG.rst.
  • Contributor(s) are listed correctly in __credits__ in orix/__init__.py and in
    .zenodo.json.

@argerlt argerlt marked this pull request as draft May 19, 2025 07:23
@argerlt argerlt requested a review from hakonanes May 22, 2025 16:58
@argerlt
Copy link
Collaborator Author

argerlt commented May 22, 2025

@hakonanes , @CSSFrancis , @viljarjf, anyone got advice on how to unit test Orientation.from_path_ends? an easy out would be to just check that the size of the produced paths are the correct length, and the spacing is consistent between points, but that doesn't really test if the paths are in the correct spot. Toy example below to show off the feature in question:

from orix.quaternion import Orientation
from orix.quaternion.symmetry import O
import numpy as np
import matplotlib.pyplot as plt

rot_path = Orientation(
    data=np.array(
        [
            [1, 0, 0, 0],
            [1, 1, 0, 0],
            [1, 0, 1, 0],
            [1, 0, 0, 1],
            [1, 0, -1, 0],
            [1, 0, 0, -1],
            [1, 0, 0, -1],
        ]
    )
)
m3m_path = Orientation(
    data=np.array(
        [
            [1, 0, 0, 0],
            [2, 1, 0, 0],
            [3, 0, 1, 0],
            [4, 0, 0, 1],
            [5, 0, -1, 0],
            [6, 0, 0, -1],
            [7, 0, 0, -1],
            [8, 1, 0, 0],
            [9, 0, 1, 0],
            [10, 0, 0, 1],
            [11, 0, -1, 0],
            [12, 0, 0, -1],
            [13, 0, 0, -1],
        ]
    ),
    symmetry=O,
)

fig, ax = plt.subplots(1, 2)
# plot in homochoric space, no symmetry
Orientation.from_path_ends(rot_path.reduce(), True).reduce().scatter(
    figure=fig, position = [1,2,1], marker=">", c=np.arange(700)
)

# plot in rodrigues space, cubic
Orientation.from_path_ends(m3m_path.reduce(), True).reduce().scatter(
    figure=fig, position = [1,2,2], marker=">", c=np.arange(1300)
)

image

@argerlt argerlt mentioned this pull request May 27, 2025
5 tasks
@argerlt argerlt added enhancement New feature or request status: waiting-for-author This pull request has comments the author must address labels Jul 1, 2025
@argerlt argerlt self-assigned this Jul 1, 2025
@argerlt argerlt added this to the v0.14.0 milestone Jul 1, 2025
@argerlt argerlt marked this pull request as ready for review July 1, 2025 01:16
Copy link
Member

@hakonanes hakonanes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good stuff. However, many things need changing!

argerlt added a commit to argerlt/orix that referenced this pull request Jul 13, 2025
@argerlt argerlt marked this pull request as draft July 17, 2025 20:37
argerlt added a commit to argerlt/orix that referenced this pull request Jul 17, 2025
@argerlt argerlt force-pushed the adopt-`reduce`-and-other-updates-from-#442 branch from 0c5a107 to a1920b5 Compare July 17, 2025 20:51
@argerlt argerlt removed the status: waiting-for-author This pull request has comments the author must address label Jul 18, 2025
@argerlt argerlt marked this pull request as ready for review July 18, 2025 01:54
@hakonanes
Copy link
Member

I don't have a good suggestion for testing orientation paths.

However, here's another test which may be useful, just checking that a path from identity to some other one produces the same as a fiber defined by one axes and increasing angles:

    def test_from_path_ends_fiber(self):
        Q1 = Quaternion.identity()
        Q2 = Quaternion.from_axes_angles([1, 1, 1], 60, degrees=True)
        Q12 = Quaternion.stack((Q1, Q2))
        Q_path1 = Quaternion.from_axes_angles([1, 1, 1], np.arange(59), degrees=True)
        Q_path2 = Quaternion.from_path_ends(Q12, steps=Q_path1.size)
        assert np.allclose(Q_path1.dot(Q_path2), 1, atol=1e-3)

Copy link
Member

@hakonanes hakonanes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there. I suggest to add examples to the examples gallery showing how to use this, one for each of the three classes.

"""
out = super().from_path_ends(points=points, closed=closed, steps=steps)
path = cls(out.data)
# copy the symmetry if it exists.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a good pattern. If the points aren't misorientations, the quaternion method should be used instead. We should do either of

  1. allow a symmetry parameter
  2. assume points are misorientations

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer assuming points are misorientations.

Copy link
Collaborator Author

@argerlt argerlt Jul 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that what this already does? regardless of what you pass in, it treats it like a quaternion, but then it casts it to a misorientation (I did it this way because it works with Orientation as well). Then, if there is symmetry, it copies it over, and if not, it doesn't.

This is what I'm checking with test_orientation.test_from_path_ends, it matches up with what i would expect, where the class used to call from_path_ends determines the class of the produced object, and if symmetry exists, it's copied over as well.

    q = Quaternion.random(10)
    r = Rotation.random(10)
    o = Orientation.random(10, Oh)
    m = Misorientation.random(10, [D3, Oh])

    # make sure the result is the correct class.
    a = Quaternion.from_path_ends(q)
    b = Rotation.from_path_ends(q)
    c = Orientation.from_path_ends(r)
    d = Quaternion.from_path_ends(o)
    e = Orientation.from_path_ends(q)
    f = Misorientation.from_path_ends(m)
    g = Orientation.from_path_ends(o)
    assert isinstance(a, Quaternion)
    assert isinstance(b, Rotation)
    assert isinstance(c, Orientation)
    assert isinstance(d, Quaternion)
    assert isinstance(e, Orientation)
    assert isinstance(f, Misorientation)
    assert isinstance(g, Orientation)

    # make sure symmetry information is preserved.
    assert f.symmetry == m.symmetry
    assert g.symmetry == o.symmetry

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point is that we should accept a symmetry parameter because this doesn't make much sense to me:

import orix.quaternion as oqu

Q = oqu.Quaternion.random(2)
M = oqu.Misorientation.from_path_ends(Q)

If the user just want a path trough rotation space, they should use Quaternion.from_path_ends(). If they wanted misorientations through misorientation space, they should pass symmetries along with the input quaternions.

Copy link
Collaborator Author

@argerlt argerlt Oct 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDIT: Nevermind, this point is valid but seperate from the issue at hand.

@hakonanes, Coming back to this months later:

It occurs to me that it's incorrect/misrepresentative to claim this function traces a path through "Misorientation Space", as from_path_ends does not choose a path with respect to symmetry. This is a bit like claiming Miller space and Vector space are the same.

Additionally, I don't you should (or maybe even can, for that matter) make this function symmetry-aware, because you end up in a traveling salesman conundrum. Finding the shortest path from A-to-B or B-to-C is fine, but A-to-B-to-C-to-D... etc and checking all the possible symmetry combinations gets hard fast, and is way outside the scope of this function.

With that in mind, I think maybe this should only be a function in Quaternion, and if it's called from an inherited class like Misorientation or Orientatation, it returns a Quaternion anyway, to hammer home this point. I would then add a note to the effect of

This function traces a path between points in SO(3)|C_1, in which there is one and only one direct path between every point. The equivalent is not well-defined for (mis)orientations, which have multiple symmetrically-equivalent points with nonequivalent paths between them, thus creating ambiguity in the definition of "shortest path" or "direct path" for symmetry-aware spaces. Additionally, the concept of "paths" does not hold with regard to inversions either.  
Thus, to make this point clear, the function always returns a path of Quaternions, which can then be converted as desired by the user to a symmetry-aware object of their choice.

Thoughts?

@argerlt
Copy link
Collaborator Author

argerlt commented Jul 18, 2025

I don't have a good suggestion for testing orientation paths.

However, here's another test which may be useful, just checking that a path from identity to some other one produces the same as a fiber defined by one axes and increasing angles:

    def test_from_path_ends_fiber(self):
        Q1 = Quaternion.identity()
        Q2 = Quaternion.from_axes_angles([1, 1, 1], 60, degrees=True)
        Q12 = Quaternion.stack((Q1, Q2))
        Q_path1 = Quaternion.from_axes_angles([1, 1, 1], np.arange(59), degrees=True)
        Q_path2 = Quaternion.from_path_ends(Q12, steps=Q_path1.size)
        assert np.allclose(Q_path1.dot(Q_path2), 1, atol=1e-3)

This seems good enough. Also, as stated in the docstring, paths are not symmetry-aware, so this plus the expected size checks, plus the proper symmetry copying seems sufficient.

@argerlt argerlt marked this pull request as draft July 19, 2025 03:28
@hakonanes
Copy link
Member

I suggest to not mark a PR as a draft after there's been a review.

@argerlt
Copy link
Collaborator Author

argerlt commented Aug 12, 2025

I suggest to not mark a PR as a draft after there's been a review.

I was trying to signify I had started fixing this PR, but it wasn't yet to the point where it was worth maintainers taking time to review it. If there is a better way to signify as much, I can switch.

I have to swap between computers throughout the week based on other work happening, so pushing partial patches is convenient, and also a way to get unofficial input from others.. @CSSFrancis and I were using labels for a bit, but I was trying out the Draft designation instead since only maintainers can add labels, and I thought draft's might be more inclusive.

@hakonanes
Copy link
Member

OK, I see. I'd say after the first review, it's not a draft anymore. But that doesn't stop you from updating the branch further!

Comment on lines 294 to 295
if hasattr(points, "_symmetry"):
path._symmetry = points._symmetry
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean above (https://github.com/pyxem/orix/pull/558/files#r2215490352) is that we should drop the check and just do this:

Suggested change
if hasattr(points, "_symmetry"):
path._symmetry = points._symmetry
path._symmetry = points._symmetry

If not (mis)orientations are given, this will raise an attribute error. Users should see this error to learn that they should instead use the equivalent rotation or quaternion method, instead of the call passing silently.

If we want to be more kind to the user, and perhaps save unnecessary computation, we can make a check at the top for type:

if not isinstance(points, Misorientation):
    raise TypeError(f"Points must be a (mis)orientation, not of type {type(points)}")

After this is 100% safe to access the symmetry.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can make this change, if you'd like.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think this is fine.

I added your suggestion, but then when going through unit tests, I realized the solution given as-is could recast misorientations as orientations, (sort of bad) but also rewrite the right-only symmetry tuple with a left-right symmetry tuple (VERY bad). I believe adding Orientation.from_path_ends fixes this.

If you don't like this version, feel free to change it. As long as I can draw lines through orientation space, I'm happy.

@argerlt argerlt force-pushed the adopt-`reduce`-and-other-updates-from-#442 branch from 1f51542 to e5c307f Compare September 4, 2025 01:57
@argerlt
Copy link
Collaborator Author

argerlt commented Sep 4, 2025

@hakonanes , give me 24 hours on this one please. I believe all the code is good to go, but the example I wrote is half-baked and needs some work.

@hakonanes
Copy link
Member

No problem!

@argerlt argerlt marked this pull request as ready for review September 5, 2025 01:31
@review-notebook-app
Copy link

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@argerlt
Copy link
Collaborator Author

argerlt commented Sep 5, 2025

I finished the example (see picture below), cleaned up some code, removed some obsolete refrenes to map_into_symmetry_reduced_zone, and also ended up adding a HomochoricPlot class since it made sense.

This PR pre-assumes #583 is approved, as it fails without that update.

Figure_1

@argerlt
Copy link
Collaborator Author

argerlt commented Nov 10, 2025

This PR was broken up into #558 and #600, and is now redundant.

@argerlt argerlt closed this Nov 10, 2025
@argerlt argerlt deleted the adopt-`reduce`-and-other-updates-from-#442 branch December 23, 2025 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants