Open hakonanes opened 6 months ago
Phase information kan be kept by changing https://github.com/pyxem/orix/blob/develop/orix/quaternion/quaternion.py#L238 to if hasattr(other, "phase"):
, which feels a little hacky.
If we override __neg__
in ReciprocalLatticeVector too, this issue is solved.
Should this even be possible?
I might be misunderstanding what rotating a hkl vector entails. What do you mean by preserving the hkl value? When rotating, should they not change? If the shapes are compatible, a Rotation object with multiple rotations will distribute as expected, e.g. a RLV with shape (10,) and a Rotation with shape (4, 1) would create a RLV with shape (4, 10). This is how Miller works currently.
@viljarjf The HKL is dependent on the RLV and the lattice. If we have a set of RLV and we rotate them (and don't rotate the lattice in some way) calculating the hkl no longer works as expected and gives non physical values.
This is exactly the same operation as your rotate_from_matrix
, apart from the default of Rotation being lab2crystal when rotate_from_matrix
implicitly uses crystal2lab. Is that not what you would want?
import diffpy
import numpy as np
from diffsims.crystallography import ReciprocalLatticeVector
from orix.crystal_map import Phase
from orix.quaternion import Rotation
# Shared parameters
latt = diffpy.structure.lattice.Lattice(2.464, 2.464, 6.711, 90, 90, 120)
atoms = [diffpy.structure.atom.Atom(atype='C', xyz=[0.0, 0.0, 0.25], lattice=latt),
diffpy.structure.atom.Atom(atype='C', xyz=[0.0, 0.0, 0.75], lattice=latt),
diffpy.structure.atom.Atom(atype='C', xyz=[1/3, 2/3, 0.25], lattice=latt),
diffpy.structure.atom.Atom(atype='C', xyz=[2/3, 1/3, 0.75], lattice=latt),]
structure_matrix = diffpy.structure.Structure(atoms=atoms, lattice=latt)
calibration = 0.0262
p = Phase("Graphite", point_group="6/mmm", structure=structure_matrix)
euler_angles_new = np.array([[0, 90, 90]])
rot = Rotation.from_euler(euler_angles_new, degrees=True)
rlv = ReciprocalLatticeVector.from_min_dspacing(phase=p, min_dspacing= 1)
rotated_rlv = rlv.rotate_from_matrix(rot.to_matrix()[0])
rlv.hkl ==rotated_rlv.hkl # False
Maybe this will help show the problem related to rotating the RLV.
@hakonanes have you ever used the https://www.diffpy.org/diffpy.structure/mod_lattice.html baserot
parameter. I think that we could have the rotation also rotate the lattice. That would at least allow us to rotate the RLV and be consistent with the hkl values.
@hakonanes This is partially fixed in https://github.com/pyxem/orix/pull/499. Proper HKL values requires some more thought and better handling.
If we override
__neg__
in ReciprocalLatticeVector too, this issue is solved.
@viljarjf, I assume you mean that we can override __mul__()
? That is a valid solution, although at the cost of code duplication.
Should this even be possible?
@CSSFrancis, good point! Now that I think about it, I'd say no. ReciprocalLatticeVector
was initially made to generate the set of strongly scattering reflections for a given unit cell. The intention was never to rotate them. I did this first only in https://github.com/pyxem/diffsims/pull/205#discussion_r1586779387, but was surprised at the result... I thought we subclassed Vector3d
from orix perfectly, which we clearly do not.
I see you've suggested a change. May I suggest that we instead overwrite __mul__()
as @viljarjf suggest, but throw an error explaining that this is not implemented? If we need to rotate the vectors, we can cast them to regular Miller (hkl) first.
have you ever used the https://www.diffpy.org/diffpy.structure/mod_lattice.html baserot parameter.
Nope.
I think that we could have the rotation also rotate the lattice. That would at least allow us to rotate the RLV and be consistent with the hkl values.
Hm... But, then what is the point of rotating the vectors in the first place? When I rotate vectors, I always want new vectors.
@hakonanes So what I've come to is that it should be possible to rotate the RLV as long as we can track the rotation.
This allows us to have consistent hkl
values but change the underlying xyz
values.
Can this be a feature of the private DiffractingVector
class in #205 instead?
@hakonanes sure it can be... What is the argument for not having it part of the RLV. How does kikchuipy use the RLV
for doing simulations? Do you need to do a rotation at some point or how are you handling the geometry considerations?
I'm fine with leaving my reported bug a bit longer.
What is the argument for not having it part of the RLV.
I've got to think a bit on it. Quoting python -c "import this"
:
Now is better than never. Although never is often better than right now.
How does kikchuipy use the RLV for doing simulations?
We use composition instead of inheritance.
Steps for creating geometrical EBSD simulations:
Relevant part of tutorial: https://kikuchipy.org/en/stable/tutorials/geometrical_ebsd_simulations.html#Create-simulations-on-detector.
I've got to think a bit on it. Quoting
python -c "import this"
:Now is better than never. Although never is often better than right now.
That is fine; that is kind of how we coded ourselves into this mess in pyxem :) At some point, we do have to figure out a way to fix it. From a purely pragmatic standpoint, I have to teach people how to do this next week :) and I don't think any of these changes break the API (only fix broken things) so it would be nice to push a fix before then.
I can just move everything to the DiffractingVector class and make it private but https://github.com/pyxem/orix/pull/499 would be a nice addition.
How does kikchuipy use the RLV for doing simulations?
We use composition instead of inheritance.
Steps for creating geometrical EBSD simulations:
- Create a Kikuchi pattern simulator from a set of reciprocal lattice vectors (re-use the phase)
- Supply a detector with projection parameters and one or more crystal rotations to simulate patterns for
- Return simulations storing the detector, the rotations, the (un-rotated) reciprocal lattice vectors (that were visible on the detector for any rotation), and the gnomonic coordinates of Kikuchi lines and zone axes per rotation (the actual simulation)
Relevant part of tutorial: https://kikuchipy.org/en/stable/tutorials/geometrical_ebsd_simulations.html#Create-simulations-on-detector.
Hmmm. Okay, that works as well...
But do you need to rotate reciprocal lattice vectors for the tutorial? This "bug" was reported based on our discussions in #205, not by a user in a real use-case.
But do you need to rotate reciprocal lattice vectors for the tutorial? This "bug" was reported based on our discussions in #205, not by a user in a real use-case.
It would be nice to be able to rotate them. The only reason I didn't rotate them in the simulation at first was to reduce the changes to the RLV.
Essentially what the rotation does is rotates the underlying lattice object (as well as the vectors). I guess the question is: is this only useful for electron diffraction or is this universally useful. In the case of Kikichi diffraction wouldn't this simplify your simulation calculations?
I'm struggling with the concept of kinematic diffraction differences obviously :)
@hakonanes I've looked at the kikuchipy simulation code and do you end up rotating the hkl vectors rather the xyz vectors?
If we override
__neg__
in ReciprocalLatticeVector too, this issue is solved.@viljarjf, I assume you mean that we can override
__mul__()
? That is a valid solution, although at the cost of code duplication.
I did mean __neg__
, as it is Rotation.__mul__
that is called when multiplying with a Rotation on the left. Overriding __mul__
in RLV has no effect, as the error happens before Rotation.__mul__
returns NotImplemented (which would then try to call RLV's __mul__
).
Quaternion.__mul__
handles the phase (if RLV inherits from Miller, or if we expand the subclass check to instead look for a phase
-member), but the override in Rotation.__mul__
uses other.__neg__
, which is inherited from Vector3d
.
This does not throw an error for Miller, as it allows no phase to be supplied to the constructor. RLV requires the phase, and as the first argument at that. This is incompatible with a couple methods where Vector3d
or its subclasses expects the first argument of the constructor to be xyz
. Regardless, this is just implementation details, and this approach seems inelegant in my opinion anyway.
Actually, it seems Miller.__neg__
silently discards the phase, since it is inherited from Vector3d
:
from orix.vector import Miller
from orix.crystal_map import Phase
p = Phase(point_group="m-3m")
v = Miller([1, 0, 0], phase=p)
print(v)
print(-v)
>>> Miller (1,), point group m-3m, xyz
[[1 0 0]]
>>> Miller (1,), point group None, xyz
[[-1 0 0]]
@hakonanes @viljarjf both of these are solved by the PR to orix.
@hakonanes have you ever used the https://www.diffpy.org/diffpy.structure/mod_lattice.html
baserot
parameter. I think that we could have the rotation also rotate the lattice. That would at least allow us to rotate the RLV and be consistent with the hkl values.
This is actually really promising, since what we really want is to rotate the vectors AND the basis. This is different from the normal handling of rotations, which only concerns the former. How about a new method of RLV for this usecase? Something like
class ReciprocalLatticeVector(Vector3d):
...
def rotate_with_basis(self, rot: Rotation):
"""Rotate both vectors and the basis with a given `Rotation`.
This differs from simply multiplying with a `Rotation`,
as that would NOT update the basis.
Parameters
----------
rot : orix.quaternion.Rotation
A rotation to apply to vectors and the basis.
"""
# rotate basis
new_phase = self.phase.deepcopy()
br = new_phase.structure.lattice.baserot
# In case the base rotation is set already
new_br = br @ rot.to_matrix().squeeze()
new_phase.structure.lattice.setLatPar(baserot=new_br)
# rotate vectors
vecs = ~rot * self.to_miller()
self.data = vecs.data
self.phase = new_phase
This avoids having to store the rotation and do a bunch of conversions, while keeping hkl values intact AND rotating the lattice as desired.
from diffsims.crystallography import DiffractingVector, ReciprocalLatticeVector
from orix.crystal_map import Phase
from orix.quaternion import Rotation
import numpy as np
p = Phase(point_group="6/mmm")
r = Rotation.random()
rlv = ReciprocalLatticeVector(p, hkl=[1, 2, 3])
# Somewhat convoluted initialization process, but it is private after all...
miller = ~r * rlv.to_miller()
dv = DiffractingVector(p, miller.data, rotation=r)
old_data = rlv.data.copy()
rlv.rotate_with_basis(r)
assert np.allclose(dv.hkl, rlv.hkl)
assert np.allclose(dv.data, rlv.data)
assert not np.allclose(rlv.data, old_data)
print(rlv)
>>> ReciprocalLatticeVector (1,), (6/mmm)
[[1. 2. 3.]]
@viljarjf this would only work for Rotations with size==(1,) but that isn't really a huge issue as we already are creating seperate RLV for storing the simulation results.
True, that was intentional but not communicated
@hakonanes Do you feel more comfortable with this? This doesn't change anything about the RLV; it only uses already existing attributes in the Lattice object.
I've looked at the kikuchipy simulation code and do you end up rotating the hkl vectors rather the xyz vectors?
Yes, we rotate the (hkl) from the reciprocal lattice directly to the EBSD detector using a transformation matrix combining the three transformations detector -> sample, sample -> cartesian crystal, cartesian crystal -> reciprocal crystal.
I did mean neg, as it is Rotation.mul that is called when multiplying with a Rotation on the left.
Of course, thanks for the thorough explanation. And for spotting the bug with the discarded phase in the negated crystal vector. I've opened https://github.com/pyxem/orix/issues/501 to track that issue.
Do you feel more comfortable with this? This doesn't change anything about the RLV; it only uses already existing attributes in the Lattice object.
The symmetry operations in the Symmetry
object attached to ReciprocalLatticeVector.phase.point_group
assumes the e1 || a, e3 || c*. After rotating the basis, this assumption is invalid, no?
The symmetry operations in the
Symmetry
object attached toReciprocalLatticeVector.phase.point_group
assumes the e1 || a, e3 || c*. After rotating the basis, this assumption is invalid, no?
You're right... I tried messing around by rotating the symmetry operations but I can't make it work properly
We could always go back to the idea of saving the Rotation seperately and then applying the inverse rotation when calculating the hkl. It maybe isn't as elegant of a solution but works.
Describe the bug Rotation of a
ReciprocalLatticeVector
,g
, with aRotation
from orix,R
, throws an error. The multiplication usesQuaternion.__mul__()
. There, theother
being multiplied, hereg
, fails the testisinstance(other, Miller)
and thus phase information is not passed on when creating the rotatedReciprocalLatticeVector
in its__init__()
.Code in orix: https://github.com/pyxem/orix/blob/a1be2697dc0cf02974b00d06d94272de77efeafa/orix/quaternion/quaternion.py#L238-L243
Minimal example
throws
The rotated
ReciprocalLatticeVector
with the phase information intact should be returned.I'm not 100% sure how to fix this.
ReciprocalLatticeVector
should not subclassMiller
, as the coordinates of the former can only be (hkl). Any input would be appreciated.