diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index 4b03e8f56..9fb83af7b 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -144,10 +144,91 @@ class Rotation: p = self.quaternion[...,1:]/np.linalg.norm(self.quaternion[...,1:],axis=-1,keepdims=True) return self.copy(rotation=Rotation(np.block([np.cos(pwr*phi),np.sin(pwr*phi)*p]))._standardize()) + def __ipow__(self,pwr): + """ + Raise quaternion to power (in-place). + + Equivalent to performing the rotation 'pwr' times. + + Parameters + ---------- + pwr : float + Power to raise quaternion to. + + """ + return self**pwr + def __mul__(self,other): - """Standard multiplication is not implemented.""" - raise NotImplementedError('Use "R@b", i.e. matmul, to apply rotation "R" to object "b"') + """ + Compose this rotation with other. + + Parameters + ---------- + other : damask.Rotation of shape(self.shape) + Rotation for comosition. + + """ + if isinstance(other,Rotation): + return self@other + else: + raise TypeError('Use "R@b", i.e. matmul, to apply rotation "R" to object "b"') + + def __imul__(self,other): + """ + Compose this rotation with other (in-place). + + Parameters + ---------- + other : damask.Rotation of shape(self.shape) + Rotation for comosition. + + """ + return self*other + + + def __truediv__(self,other): + """ + Compose this rotation with inverse of other. + + Parameters + ---------- + other : damask.Rotation of shape (self.shape) + Rotation to inverse composition. + + """ + if isinstance(other,Rotation): + return self@~other + else: + raise TypeError('Use "R@b", i.e. matmul, to apply rotation "R" to object "b"') + + def __itruediv__(self,other): + """ + Compose this rotation with inverse of other (in-place). + + Parameters + ---------- + other : damask.Rotation of shape (self.shape) + Rotation to inverse composition. + + """ + return self/other + + + def apply(self,other): + """ + Apply rotation to vector or second/forth order tensor field. + + Parameters + ---------- + other : numpy.ndarray of shape (...,3), (...,3,3), or (...,3,3,3,3) + Vector or tensor on which the rotation is apply + + """ + if isinstance(other,np.ndarray): + return self@other + else: + raise TypeError('Use "R1*R2" or "R1/R2", to compose rotations') def __matmul__(self,other): diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index bc6614fb9..5aed0bea2 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -974,6 +974,45 @@ class TestRotation: R_2 = Rotation.from_Euler_angles([360,0,0],degrees=True) assert np.allclose(R_1.misorientation(R_2).as_matrix(),np.eye(3)) + def test_composition(self): + a,b = (Rotation.from_random(),Rotation.from_random()) + c = a * b + a *= b + assert c == a + + def test_composition_invalid(self): + with pytest.raises(TypeError): + Rotation()*np.ones(3) + + def test_composition_inverse(self): + a,b = (Rotation.from_random(),Rotation.from_random()) + c = a / b + a /= b + assert c == a + + def test_composition_inverse_invalid(self): + with pytest.raises(TypeError): + Rotation()/np.ones(3) + + def test_power(self): + a = Rotation.from_random() + r = (np.random.rand()-.5)*4 + b = a**r + a **= r + assert a == b + + def test_invariant(self): + R = Rotation.from_random() + assert R/R == R*R**(-1) == Rotation() + + @pytest.mark.parametrize('vec',[np.ones(3),np.ones((3,3)), np.ones((3,3,3,3))]) + def test_apply(self,vec): + assert (Rotation().from_random().apply(vec)).all() + + def test_apply_invalid(self): + with pytest.raises(TypeError): + Rotation().apply(Rotation()) + @pytest.mark.parametrize('angle',[10,20,30,40,50,60,70,80,90,100,120]) def test_average(self,angle): R = Rotation.from_axis_angle([[0,0,1,10],[0,0,1,angle]],degrees=True)