From 6fe1ff8e393ad0af27bd3634a7774a303f304e7c Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 3 Jan 2021 11:50:45 +0100 Subject: [PATCH 01/21] fixed test for rodrigues parametrization for angle close to 180deg, the sign of the axis does not matter --- python/damask/_rotation.py | 1 - python/tests/test_Rotation.py | 19 +++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index 780e81891..cec3854cc 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -1052,7 +1052,6 @@ class Rotation: @staticmethod def _om2ax(om): """Rotation matrix to axis angle pair.""" - #return Rotation._qu2ax(Rotation._om2qu(om)) # HOTFIX diag_delta = -_P*np.block([om[...,1,2:3]-om[...,2,1:2], om[...,2,0:1]-om[...,0,2:3], om[...,0,1:2]-om[...,1,0:1] diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index c60029046..f8f1a3da7 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -526,7 +526,7 @@ class TestRotation: o = backward(forward(m)) u = np.array([np.pi*2,np.pi,np.pi*2]) ok = np.allclose(m,o,atol=atol) - ok = ok or np.allclose(np.where(np.isclose(m,u),m-u,m),np.where(np.isclose(o,u),o-u,o),atol=atol) + ok |= np.allclose(np.where(np.isclose(m,u),m-u,m),np.where(np.isclose(o,u),o-u,o),atol=atol) if np.isclose(m[1],0.0,atol=atol) or np.isclose(m[1],np.pi,atol=atol): sum_phi = np.unwrap([m[0]+m[2],o[0]+o[2]]) ok |= np.isclose(sum_phi[0],sum_phi[1],atol=atol) @@ -550,19 +550,22 @@ class TestRotation: assert ok and np.isclose(np.linalg.norm(o[:3]),1.0) and o[3]<=np.pi+1.e-9, f'{m},{o},{rot.as_quaternion()}' @pytest.mark.parametrize('forward,backward',[(Rotation._ro2qu,Rotation._qu2ro), - #(Rotation._ro2om,Rotation._om2ro), - #(Rotation._ro2eu,Rotation._eu2ro), + (Rotation._ro2om,Rotation._om2ro), + (Rotation._ro2eu,Rotation._eu2ro), (Rotation._ro2ax,Rotation._ax2ro), (Rotation._ro2ho,Rotation._ho2ro), (Rotation._ro2cu,Rotation._cu2ro)]) def test_Rodrigues_internal(self,set_of_rotations,forward,backward): """Ensure invariance of conversion from Rodrigues-Frank vector and back.""" - cutoff = np.tan(np.pi*.5*(1.-1e-4)) + cutoff = np.tan(np.pi*.5*(1.-1e-5)) for rot in set_of_rotations: m = rot.as_Rodrigues_vector() o = backward(forward(m)) ok = np.allclose(np.clip(m,None,cutoff),np.clip(o,None,cutoff),atol=atol) - ok = ok or np.isclose(m[3],0.0,atol=atol) + ok |= np.isclose(m[3],0.0,atol=atol) + if m[3] > cutoff: + ok |= np.allclose(m[:3],-1*o[:3]) + assert ok and np.isclose(np.linalg.norm(o[:3]),1.0), f'{m},{o},{rot.as_quaternion()}' @pytest.mark.parametrize('forward,backward',[(Rotation._ho2qu,Rotation._qu2ho), @@ -592,7 +595,7 @@ class TestRotation: o = backward(forward(m)) ok = np.allclose(m,o,atol=atol) if np.count_nonzero(np.isclose(np.abs(o),np.pi**(2./3.)*.5)): - ok = ok or np.allclose(m*-1.,o,atol=atol) + ok |= np.allclose(m*-1.,o,atol=atol) assert ok and np.max(np.abs(o)) < np.pi**(2./3.) * 0.5 + 1.e-9, f'{m},{o},{rot.as_quaternion()}' @pytest.mark.parametrize('vectorized, single',[(Rotation._qu2om,qu2om), @@ -719,7 +722,7 @@ class TestRotation: o = Rotation.from_axis_angle(rot.as_axis_angle()).as_axis_angle() ok = np.allclose(m,o,atol=atol) if np.isclose(m[3],np.pi,atol=atol): - ok = ok or np.allclose(m*np.array([-1.,-1.,-1.,1.]),o,atol=atol) + ok |= np.allclose(m*np.array([-1.,-1.,-1.,1.]),o,atol=atol) assert ok and np.isclose(np.linalg.norm(o[:3]),1.0) \ and o[3]<=np.pi+1.e-9, f'{m},{o},{rot.as_quaternion()}' @@ -740,7 +743,7 @@ class TestRotation: m = rot.as_Rodrigues_vector() o = Rotation.from_homochoric(rot.as_homochoric()*P*-1,P).as_Rodrigues_vector() ok = np.allclose(np.clip(m,None,cutoff),np.clip(o,None,cutoff),atol=atol) - ok = ok or np.isclose(m[3],0.0,atol=atol) + ok |= np.isclose(m[3],0.0,atol=atol) assert ok and np.isclose(np.linalg.norm(o[:3]),1.0), f'{m},{o},{rot.as_quaternion()}' @pytest.mark.parametrize('P',[1,-1]) From 35ca1ffb0a9827fbcee58e46eeb4a936b5bac792 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 3 Jan 2021 12:03:40 +0100 Subject: [PATCH 02/21] consistent copy functionality --- python/damask/_config.py | 11 +++++++++++ python/damask/_configmaterial.py | 4 ++-- python/damask/_grid.py | 7 ++----- python/damask/_orientation.py | 2 +- python/damask/_rotation.py | 3 +-- python/damask/_table.py | 6 ++---- 6 files changed, 19 insertions(+), 14 deletions(-) diff --git a/python/damask/_config.py b/python/damask/_config.py index 76955588f..91be5ebf1 100644 --- a/python/damask/_config.py +++ b/python/damask/_config.py @@ -1,3 +1,4 @@ +import copy from io import StringIO import abc @@ -35,6 +36,14 @@ class Config(dict): output.seek(0) return ''.join(output.readlines()) + + def __copy__(self): + """Create deep copy.""" + return copy.deepcopy(self) + + copy = __copy__ + + @classmethod def load(cls,fname): """ @@ -52,6 +61,7 @@ class Config(dict): fhandle = fname return cls(yaml.safe_load(fhandle)) + def save(self,fname,**kwargs): """ Save to yaml file. @@ -95,6 +105,7 @@ class Config(dict): """Check for completeness.""" pass + @property @abc.abstractmethod def is_valid(self): diff --git a/python/damask/_configmaterial.py b/python/damask/_configmaterial.py index b94e9897a..43a59eb1e 100644 --- a/python/damask/_configmaterial.py +++ b/python/damask/_configmaterial.py @@ -204,7 +204,7 @@ class ConfigMaterial(Config): Limit renaming to selected constituents. """ - dup = copy.deepcopy(self) + dup = self.copy() for i,m in enumerate(dup['material']): if ID and i not in ID: continue for c in m['constituents']: @@ -228,7 +228,7 @@ class ConfigMaterial(Config): Limit renaming to selected homogenization IDs. """ - dup = copy.deepcopy(self) + dup = self.copy() for i,m in enumerate(dup['material']): if ID and i not in ID: continue try: diff --git a/python/damask/_grid.py b/python/damask/_grid.py index 8380bbc5b..76ce7ba64 100644 --- a/python/damask/_grid.py +++ b/python/damask/_grid.py @@ -57,13 +57,10 @@ class Grid: def __copy__(self): - """Copy grid.""" + """Create deep copy.""" return copy.deepcopy(self) - - def copy(self): - """Copy grid.""" - return self.__copy__() + copy = __copy__ def diff(self,other): diff --git a/python/damask/_orientation.py b/python/damask/_orientation.py index 05301561f..4bd8a1e96 100644 --- a/python/damask/_orientation.py +++ b/python/damask/_orientation.py @@ -199,7 +199,7 @@ class Orientation(Rotation): def __copy__(self,**kwargs): - """Copy.""" + """Create deep copy.""" return self.__class__(rotation=kwargs['rotation'] if 'rotation' in kwargs else self.quaternion, lattice =kwargs['lattice'] if 'lattice' in kwargs else self.lattice if self.lattice is not None else self.family, diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index cec3854cc..492ca8d2d 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -78,9 +78,8 @@ class Rotation: ]) - # ToDo: Check difference __copy__ vs __deepcopy__ def __copy__(self,**kwargs): - """Copy.""" + """Create deep copy.""" return self.__class__(rotation=kwargs['rotation'] if 'rotation' in kwargs else self.quaternion) copy = __copy__ diff --git a/python/damask/_table.py b/python/damask/_table.py index e6e6c4eeb..78a8a276e 100644 --- a/python/damask/_table.py +++ b/python/damask/_table.py @@ -42,12 +42,10 @@ class Table: return len(self.data) def __copy__(self): - """Copy Table.""" + """Create deep copy.""" return copy.deepcopy(self) - def copy(self): - """Copy Table.""" - return self.__copy__() + copy = __copy__ def _label_discrete(self): From 9a278daa3f2b19a4389dd7cb173974f4ccf5eee0 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 3 Jan 2021 12:07:02 +0100 Subject: [PATCH 03/21] copy not needed YAML writer does not write out references anymore --- python/damask/_configmaterial.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/python/damask/_configmaterial.py b/python/damask/_configmaterial.py index 43a59eb1e..83ebdd5a5 100644 --- a/python/damask/_configmaterial.py +++ b/python/damask/_configmaterial.py @@ -1,5 +1,3 @@ -import copy - import numpy as np from . import Config @@ -289,7 +287,7 @@ class ConfigMaterial(Config): c = [{} for _ in range(length)] if constituents is None else \ [{'constituents':u} for u in ConfigMaterial._constituents(**constituents)] - if len(c) == 1: c = [copy.deepcopy(c[0]) for _ in range(length)] + if len(c) == 1: c = [c[0] for _ in range(length)] if length != 1 and length != len(c): raise ValueError('Cannot add entries of different length') @@ -301,7 +299,7 @@ class ConfigMaterial(Config): else: for i in range(len(c)): c[i][k] = v - dup = copy.deepcopy(self) + dup = self.copy() dup['material'] = dup['material'] + c if 'material' in dup else c return dup From 5f1399acc37529e5b2bfa3d12be666823259f23b Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 3 Jan 2021 12:09:21 +0100 Subject: [PATCH 04/21] consistent behavior with other classes python dictionary operates in-place, so wrappers for out-of-place behavior let it use like the other DAMASK classes --- python/damask/_config.py | 24 ++++++++++++++++++++++++ python/tests/test_Config.py | 4 ++++ 2 files changed, 28 insertions(+) diff --git a/python/damask/_config.py b/python/damask/_config.py index 91be5ebf1..c7e937656 100644 --- a/python/damask/_config.py +++ b/python/damask/_config.py @@ -99,6 +99,30 @@ class Config(dict): fhandle.write(yaml.dump(self,Dumper=NiceDumper,**kwargs)) + def add(self,d): + """ + Add dictionary. + + d : dict + Dictionary to append. + """ + duplicate = self.copy() + duplicate.update(d) + return duplicate + + + def delete(self,key): + """ + Delete item. + + key : dict + Label of the key to remove. + """ + duplicate = self.copy() + del duplicate[key] + return duplicate + + @property @abc.abstractmethod def is_complete(self): diff --git a/python/tests/test_Config.py b/python/tests/test_Config.py index 67c419b3e..0319fb6de 100644 --- a/python/tests/test_Config.py +++ b/python/tests/test_Config.py @@ -22,6 +22,10 @@ class TestConfig: with open(tmp_path/'config.yaml') as f: assert Config.load(f) == config + def test_add_remove(self): + config = Config() + assert config.add({'hello':'world'}).delete('hello') == config + def test_repr(self,tmp_path): config = Config() config['A'] = 1 From 80b8693a66dcc99e038a5a2a429f3e5a6e984912 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 3 Jan 2021 12:10:39 +0100 Subject: [PATCH 05/21] avoid adding to existing data, i.e. when reading a file --- python/damask/_configmaterial.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/python/damask/_configmaterial.py b/python/damask/_configmaterial.py index 83ebdd5a5..6415ee4dc 100644 --- a/python/damask/_configmaterial.py +++ b/python/damask/_configmaterial.py @@ -11,11 +11,10 @@ class ConfigMaterial(Config): 'homogenization': {}, 'phase': {}} - def __init__(self,d={}): + def __init__(self,d=_defaults): """Initialize object with default dictionary keys.""" super().__init__(d) - for k,v in self._defaults.items(): - if k not in self: self[k] = v + def save(self,fname='material.yaml',**kwargs): """ From 98723cb0ed5155d83bf6e2605e35466d575e40bd Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 3 Jan 2021 15:50:15 +0100 Subject: [PATCH 06/21] need to handle special case of Re() = 0 ensuring that the real part is positive seems to be a good idea on first sight, but it would be easier to simply acknowledge that qu = -qu --- python/damask/_rotation.py | 4 +++- python/tests/test_Rotation.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index 492ca8d2d..4b03e8f56 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -105,8 +105,10 @@ class Rotation: Rotation to check for equality. """ + ambiguous = np.isclose(self.quaternion[...,0],0) return np.prod(self.shape,dtype=int) == np.prod(other.shape,dtype=int) \ - and np.allclose(self.quaternion,other.quaternion) + and ( np.allclose(self.quaternion,other.quaternion) \ + or np.allclose(self.quaternion[ambiguous],-1*other.quaternion[ambiguous])) @property diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index f8f1a3da7..bc6614fb9 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -786,6 +786,12 @@ class TestRotation: def test_equal(self): assert Rotation.from_random(rng_seed=1) == Rotation.from_random(rng_seed=1) + def test_equal_ambiguous(self): + qu = np.random.rand(10,4) + qu[:,0] = 0. + qu/=np.linalg.norm(qu,axis=1,keepdims=True) + assert Rotation(qu) == Rotation(-qu) + def test_inversion(self): r = Rotation.from_random() assert r == ~~r From f48a4463535761071bafeab9639122163b564750 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 3 Jan 2021 15:54:41 +0100 Subject: [PATCH 07/21] compatible with scipy.spatial.transform.Rotation also introduced inplace variants and '/' as multiplicative inverse of '*' --- python/damask/_rotation.py | 85 ++++++++++++++++++++++++++++++++++- python/tests/test_Rotation.py | 39 ++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) 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) From b705be96833e4154f644a90922bcf4b8cd7561f6 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 3 Jan 2021 19:27:56 +0100 Subject: [PATCH 08/21] don't mix space and tabstops --- .gitmodules | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 0587fff4c..c415745bc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,5 +1,5 @@ [submodule "PRIVATE"] - path = PRIVATE + path = PRIVATE url = ../PRIVATE.git branch = master - shallow = true + shallow = true From d8b4b7e0f596986f1d3aaeb72d757037a41af009 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 3 Jan 2021 21:49:01 +0100 Subject: [PATCH 09/21] != and == work componentwise --- python/damask/_orientation.py | 19 +++++++++++++++---- python/damask/_rotation.py | 27 ++++++++++++++++++++++----- python/tests/test_Orientation.py | 9 ++++++--- python/tests/test_Rotation.py | 18 +++++++++++++----- 4 files changed, 56 insertions(+), 17 deletions(-) diff --git a/python/damask/_orientation.py b/python/damask/_orientation.py index 4bd8a1e96..d5be5a751 100644 --- a/python/damask/_orientation.py +++ b/python/damask/_orientation.py @@ -225,10 +225,21 @@ class Orientation(Rotation): Orientation to check for equality. """ - return super().__eq__(other) \ - and self.family == other.family \ - and self.lattice == other.lattice \ - and self.parameters == other.parameters + matching_type = all([hasattr(other,attr) and getattr(self,attr) == getattr(other,attr) + for attr in ['family','lattice','parameters']]) + return np.logical_and(super().__eq__(other),matching_type) + + def __ne__(self,other): + """ + Not equal to other. + + Parameters + ---------- + other : Orientation + Orientation to check for equality. + + """ + return np.logical_not(self==other) def __matmul__(self,other): diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index 9fb83af7b..50b7a3678 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -66,7 +66,7 @@ class Rotation: def __repr__(self): """Represent rotation as unit quaternion, rotation matrix, and Bunge-Euler angles.""" - if self == Rotation(): + if self.shape == () and self == Rotation(): return 'Rotation()' else: return f'Quaternions {self.shape}:\n'+str(self.quaternion) \ @@ -105,10 +105,27 @@ class Rotation: Rotation to check for equality. """ - ambiguous = np.isclose(self.quaternion[...,0],0) - return np.prod(self.shape,dtype=int) == np.prod(other.shape,dtype=int) \ - and ( np.allclose(self.quaternion,other.quaternion) \ - or np.allclose(self.quaternion[ambiguous],-1*other.quaternion[ambiguous])) + s = self.quaternion + o = other.quaternion + if self.shape == () == other.shape: + return np.allclose(s,o) or (np.isclose(s[0],0.0) and np.allclose(s,-1.0*o)) + else: + return np.all(np.isclose(s,o),-1) + np.all(np.isclose(s,-1.0*o),-1) * np.isclose(s[...,0],0.0) + + def __ne__(self,other): + """ + Not equal to other. + + Equality is determined taking limited floating point precision into + account. See numpy.allclose for details. + + Parameters + ---------- + other : Rotation + Rotation to check for equality. + + """ + return np.logical_not(self==other) @property diff --git a/python/tests/test_Orientation.py b/python/tests/test_Orientation.py index 5ab0361a8..436b73c04 100644 --- a/python/tests/test_Orientation.py +++ b/python/tests/test_Orientation.py @@ -25,13 +25,16 @@ class TestOrientation: @pytest.mark.parametrize('shape',[None,5,(4,6)]) def test_equal(self,lattice,shape): R = Rotation.from_random(shape) - assert Orientation(R,lattice) == Orientation(R,lattice) + assert Orientation(R,lattice) == Orientation(R,lattice) if shape is None else \ + (Orientation(R,lattice) == Orientation(R,lattice)).all() + @pytest.mark.parametrize('lattice',Orientation.crystal_families) @pytest.mark.parametrize('shape',[None,5,(4,6)]) def test_unequal(self,lattice,shape): R = Rotation.from_random(shape) - assert not(Orientation(R,lattice) != Orientation(R,lattice)) + assert not ( Orientation(R,lattice) != Orientation(R,lattice) if shape is None else \ + (Orientation(R,lattice) != Orientation(R,lattice)).any()) @pytest.mark.parametrize('a,b',[ (dict(rotation=[1,0,0,0]), @@ -403,7 +406,7 @@ class TestOrientation: def test_relationship_vectorize(self,set_of_quaternions,lattice,model): r = Orientation(rotation=set_of_quaternions[:200].reshape((50,4,4)),lattice=lattice).related(model) for i in range(200): - assert r.reshape((-1,200))[:,i] == Orientation(set_of_quaternions[i],lattice).related(model) + assert (r.reshape((-1,200))[:,i] == Orientation(set_of_quaternions[i],lattice).related(model)).all() @pytest.mark.parametrize('model',['Bain','KS','GT','GT_prime','NW','Pitsch']) @pytest.mark.parametrize('lattice',['cF','cI']) diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index 5aed0bea2..014efda99 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -783,14 +783,22 @@ class TestRotation: else: assert r.shape == shape - def test_equal(self): - assert Rotation.from_random(rng_seed=1) == Rotation.from_random(rng_seed=1) + @pytest.mark.parametrize('shape',[None,5,(4,6)]) + def test_equal(self,shape): + R = Rotation.from_random(shape,rng_seed=1) + assert R == R if shape is None else (R == R).all() + + @pytest.mark.parametrize('shape',[None,5,(4,6)]) + def test_unequal(self,shape): + R = Rotation.from_random(shape,rng_seed=1) + assert not (R != R if shape is None else (R != R).any()) + def test_equal_ambiguous(self): qu = np.random.rand(10,4) qu[:,0] = 0. qu/=np.linalg.norm(qu,axis=1,keepdims=True) - assert Rotation(qu) == Rotation(-qu) + assert (Rotation(qu) == Rotation(-qu)).all() def test_inversion(self): r = Rotation.from_random() @@ -807,7 +815,7 @@ class TestRotation: p = Rotation.from_random(shape=shape) s = r.append(p) print(f'append 2x {shape} --> {s.shape}') - assert s[0,...] == r[0,...] and s[-1,...] == p[-1,...] + assert np.logical_and(s[0,...] == r[0,...], s[-1,...] == p[-1,...]).all() @pytest.mark.parametrize('quat,standardized',[ ([-1,0,0,0],[1,0,0,0]), @@ -829,7 +837,7 @@ class TestRotation: @pytest.mark.parametrize('order',['C','F']) def test_flatten_reshape(self,shape,order): r = Rotation.from_random(shape=shape) - assert r == r.flatten(order).reshape(shape,order) + assert (r == r.flatten(order).reshape(shape,order)).all() @pytest.mark.parametrize('function',[Rotation.from_quaternion, Rotation.from_Euler_angles, From acbb564afc58eaf30524cedd535de028837ea3dd Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Mon, 4 Jan 2021 07:23:14 +0100 Subject: [PATCH 10/21] restored functionalitity for adding list. got accidently lost --- python/damask/_rotation.py | 12 ++++++++++-- python/tests/test_Rotation.py | 8 ++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index 50b7a3678..b7be4f16d 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -303,8 +303,16 @@ class Rotation: def append(self,other): - """Extend rotation array along first dimension with other array.""" - return self.copy(rotation=np.vstack((self.quaternion,other.quaternion))) + """ + Extend rotation array along first dimension with other array(s). + + Parameters + ---------- + other : Rotation or list of Rotations. + + """ + return self.copy(rotation=np.vstack(tuple(map(lambda x:x.quaternion, + [self]+other if isinstance(other,list) else [self,other])))) def flatten(self,order = 'C'): diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index 014efda99..3def59213 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -817,6 +817,14 @@ class TestRotation: print(f'append 2x {shape} --> {s.shape}') assert np.logical_and(s[0,...] == r[0,...], s[-1,...] == p[-1,...]).all() + @pytest.mark.parametrize('shape',[None,1,(1,),(4,2),(3,3,2)]) + def test_append_list(self,shape): + r = Rotation.from_random(shape=shape) + p = Rotation.from_random(shape=shape) + s = r.append([r,p]) + print(f'append 3x {shape} --> {s.shape}') + assert np.logical_and(s[0,...] == r[0,...], s[-1,...] == p[-1,...]).all() + @pytest.mark.parametrize('quat,standardized',[ ([-1,0,0,0],[1,0,0,0]), ([-0.5,-0.5,-0.5,-0.5],[0.5,0.5,0.5,0.5]), From 94cfe28128037cd06990a52ab0fd266ce3ed7b9b Mon Sep 17 00:00:00 2001 From: Philip Eisenlohr Date: Wed, 6 Jan 2021 18:03:10 -0500 Subject: [PATCH 11/21] polishing help; Rotation.apply(Rotation) now acceptable. --- python/damask/_config.py | 2 +- python/damask/_rotation.py | 11 ++++------- python/tests/test_Rotation.py | 11 ++++------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/python/damask/_config.py b/python/damask/_config.py index c7e937656..9aa031ff0 100644 --- a/python/damask/_config.py +++ b/python/damask/_config.py @@ -115,7 +115,7 @@ class Config(dict): """ Delete item. - key : dict + key : str or scalar Label of the key to remove. """ duplicate = self.copy() diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index b7be4f16d..d68cea6d3 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -234,18 +234,15 @@ class Rotation: def apply(self,other): """ - Apply rotation to vector or second/forth order tensor field. + Apply rotation to vector, second or fourth order tensor, or rotation object. Parameters ---------- - other : numpy.ndarray of shape (...,3), (...,3,3), or (...,3,3,3,3) - Vector or tensor on which the rotation is apply + other : numpy.ndarray of shape (...,3), (...,3,3), or (...,3,3,3,3) or Rotation + Vector, tensor, or rotation object on which to apply the rotation. """ - if isinstance(other,np.ndarray): - return self@other - else: - raise TypeError('Use "R1*R2" or "R1/R2", to compose rotations') + return self@other def __matmul__(self,other): diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index 3def59213..707bc0210 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -1021,13 +1021,10 @@ class TestRotation: 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('item',[Rotation(),np.ones(3),np.ones((3,3)), np.ones((3,3,3,3))]) + def test_apply(self,item): + r = Rotation.from_random() + assert r.apply(item) == r@item if isinstance(item,Rotation) else (r.apply(item) == r@item).all() @pytest.mark.parametrize('angle',[10,20,30,40,50,60,70,80,90,100,120]) def test_average(self,angle): From f0351e403a9b307ed7cba614db3a22df5dac8213 Mon Sep 17 00:00:00 2001 From: Philip Eisenlohr Date: Thu, 7 Jan 2021 11:01:15 -0500 Subject: [PATCH 12/21] removed duplicate test --- python/tests/test_Rotation.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index 1067e8a87..707bc0210 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -825,14 +825,6 @@ class TestRotation: print(f'append 3x {shape} --> {s.shape}') assert np.logical_and(s[0,...] == r[0,...], s[-1,...] == p[-1,...]).all() - @pytest.mark.parametrize('shape',[None,1,(1,),(4,2),(3,3,2)]) - def test_append_list(self,shape): - r = Rotation.from_random(shape=shape) - p = Rotation.from_random(shape=shape) - s = r.append([r,p]) - print(f'append 3x {shape} --> {s.shape}') - assert s[0,...] == r[0,...] and s[-1,...] == p[-1,...] - @pytest.mark.parametrize('quat,standardized',[ ([-1,0,0,0],[1,0,0,0]), ([-0.5,-0.5,-0.5,-0.5],[0.5,0.5,0.5,0.5]), From 2b91bad53ee63d9b8701244fa7c516d3084d0c80 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Sun, 10 Jan 2021 10:17:16 +0100 Subject: [PATCH 13/21] https://stackoverflow.com/questions/14950378 --- src/C_routines.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/C_routines.c b/src/C_routines.c index 4b07c0ee0..3d62a87c2 100644 --- a/src/C_routines.c +++ b/src/C_routines.c @@ -43,7 +43,7 @@ void gethostname_c(char hostname[], int *stat){ void getusername_c(char username[], int *stat){ - struct passwd *pw = getpwuid(geteuid()); + struct passwd *pw = getpwuid(getuid()); if(pw && strlen(pw->pw_name) <= STRLEN){ strncpy(username,pw->pw_name,STRLEN+1); *stat = 0; From 72c940a46d0f338bd54c554284388493be46f376 Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 12 Jan 2021 17:02:13 +0100 Subject: [PATCH 14/21] [skip ci] updated version information after successful test of v3.0.0-alpha2-255-g1e50fcc77 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index f123674b3..7e44dd77b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v3.0.0-alpha2-230-g0fc670d01 +v3.0.0-alpha2-255-g1e50fcc77 From 66af1f1818425d7c25375ccb1b28b981afb51d66 Mon Sep 17 00:00:00 2001 From: Test User Date: Tue, 12 Jan 2021 23:33:27 +0100 Subject: [PATCH 15/21] [skip ci] updated version information after successful test of v3.0.0-alpha2-258-g715504ee5 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 7e44dd77b..364cb0e13 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v3.0.0-alpha2-255-g1e50fcc77 +v3.0.0-alpha2-258-g715504ee5 From 2d6e6a2370230834521a2ecac4d892ce73dffeca Mon Sep 17 00:00:00 2001 From: Philip Eisenlohr Date: Tue, 12 Jan 2021 18:56:40 -0500 Subject: [PATCH 16/21] Rotation composition uses "*"; application of Rotation to object uses "@"; "apply()" works on both --- python/damask/_orientation.py | 23 +++++----- python/damask/_rotation.py | 83 +++++++++++++++++++++-------------- python/tests/test_Rotation.py | 4 +- 3 files changed, 65 insertions(+), 45 deletions(-) diff --git a/python/damask/_orientation.py b/python/damask/_orientation.py index d5be5a751..cf31f4089 100644 --- a/python/damask/_orientation.py +++ b/python/damask/_orientation.py @@ -242,24 +242,25 @@ class Orientation(Rotation): return np.logical_not(self==other) - def __matmul__(self,other): + def __mul__(self,other): """ - Rotation of vector, second or fourth order tensor, or rotation object. + Compose this orientation with other. Parameters ---------- - other : numpy.ndarray, Rotation, or Orientation - Vector, second or fourth order tensor, or rotation object that is rotated. + other : Rotation or Orientation + Object for composition. Returns ------- - other_rot : numpy.ndarray or Rotation - Rotated vector, second or fourth order tensor, or rotation object. + composition : Orientation + Compound rotation self*other, i.e. first other then self rotation. """ - return self.copy(rotation=Rotation.__matmul__(self,Rotation(other.quaternion))) \ - if isinstance(other,self.__class__) else \ - Rotation.__matmul__(self,other) + if isinstance(other,Orientation) or isinstance(other,Rotation): + return self.copy(rotation=Rotation.__mul__(self,Rotation(other.quaternion))) + else: + raise TypeError('Use "O@b", i.e. matmul, to apply Orientation "O" to object "b"') @classmethod @@ -440,7 +441,7 @@ class Orientation(Rotation): raise ValueError('Missing crystal symmetry') o = self.symmetry_operations.broadcast_to(self.symmetry_operations.shape+self.shape,mode='right') - return self.copy(rotation=o@Rotation(self.quaternion).broadcast_to(o.shape,mode='left')) + return self.copy(rotation=o*Rotation(self.quaternion).broadcast_to(o.shape,mode='left')) @property @@ -619,7 +620,7 @@ class Orientation(Rotation): o,lattice = self.relation_operations(model,return_lattice=True) target = Orientation(lattice=lattice) o = o.broadcast_to(o.shape+self.shape,mode='right') - return self.copy(rotation=o@Rotation(self.quaternion).broadcast_to(o.shape,mode='left'), + return self.copy(rotation=o*Rotation(self.quaternion).broadcast_to(o.shape,mode='left'), lattice=lattice, b = self.b if target.ratio['b'] is None else self.a*target.ratio['b'], c = self.c if target.ratio['c'] is None else self.a*target.ratio['c'], diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index ace2a3dba..b5b393971 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -35,6 +35,11 @@ class Rotation: - b = Q @ a - b = np.dot(Q.as_matrix(),a) + Compound rotations R1 (first) and R2 (second): + + - R = R2 * R1 + - R = Rotation.from_matrix(np.dot(R2.as_matrix(),R1.as_matrix()) + References ---------- D. Rowenhorst et al., Modelling and Simulation in Materials Science and Engineering 23:083501, 2015 @@ -96,8 +101,8 @@ class Rotation: """ Equal to other. - Equality is determined taking limited floating point precision into - account. See numpy.allclose for details. + Equality is determined taking limited floating point precision into account. + See numpy.allclose for details. Parameters ---------- @@ -182,14 +187,25 @@ class Rotation: Parameters ---------- - other : damask.Rotation of shape(self.shape) - Rotation for comosition. + other : Rotation of shape(self.shape) + Rotation for composition. + + Returns + ------- + composition : Rotation + Compound rotation self*other, i.e. first other then self rotation. """ if isinstance(other,Rotation): - return self@other + q_m = self.quaternion[...,0:1] + p_m = self.quaternion[...,1:] + q_o = other.quaternion[...,0:1] + p_o = other.quaternion[...,1:] + q = (q_m*q_o - np.einsum('...i,...i',p_m,p_o).reshape(self.shape+(1,))) + p = q_m*p_o + q_o*p_m + _P * np.cross(p_m,p_o) + return Rotation(np.block([q,p]))._standardize() else: - raise TypeError('Use "R@b", i.e. matmul, to apply rotation "R" to object "b"') + raise TypeError('Use "R@b", i.e. matmul, to apply rotation "R" to object "b"') def __imul__(self,other): """ @@ -197,8 +213,8 @@ class Rotation: Parameters ---------- - other : damask.Rotation of shape(self.shape) - Rotation for comosition. + other : Rotation of shape(self.shape) + Rotation for composition. """ return self*other @@ -213,11 +229,16 @@ class Rotation: other : damask.Rotation of shape (self.shape) Rotation to inverse composition. + Returns + ------- + composition : Rotation + Compound rotation self*(~other), i.e. first inverse of other then self rotation. + """ if isinstance(other,Rotation): - return self@~other + return self*~other else: - raise TypeError('Use "R@b", i.e. matmul, to apply rotation "R" to object "b"') + raise TypeError('Use "R@b", i.e. matmul, to apply rotation "R" to object "b"') def __itruediv__(self,other): """ @@ -225,7 +246,7 @@ class Rotation: Parameters ---------- - other : damask.Rotation of shape (self.shape) + other : Rotation of shape (self.shape) Rotation to inverse composition. """ @@ -234,41 +255,39 @@ class Rotation: def apply(self,other): """ - Apply rotation to vector, second or fourth order tensor, or rotation object. + Apply rotation to Rotation, vector, second order tensor, or fourth order tensor. Parameters ---------- - other : numpy.ndarray of shape (...,3), (...,3,3), or (...,3,3,3,3) or Rotation - Vector, tensor, or rotation object on which to apply the rotation. + other : Rotation or numpy.ndarray of shape (...,3), (...,3,3), or (...,3,3,3,3) + Rotation, vector, or tensor on which to apply the rotation. + + Returns + ------- + rotated : Rotation or numpy.ndarray of shape (...,3), (...,3,3), or (...,3,3,3,3) + Composed rotation or rotated vector/tensor, i.e. transformed to frame defined by rotation. """ - return self@other + return self*other if isinstance(other,Rotation) else self@other def __matmul__(self,other): """ - Rotation of vector, second or fourth order tensor, or rotation object. + Rotation of vector, second order tensor, or fourth order tensor. Parameters ---------- - other : numpy.ndarray or Rotation - Vector, second or fourth order tensor, or rotation object that is rotated. + other : numpy.ndarray of shape (...,3), (...,3,3), or (...,3,3,3,3) + Vector or tensor on which to apply the rotation. Returns ------- - other_rot : numpy.ndarray or Rotation - Rotated vector, second or fourth order tensor, or rotation object. + rotated : numpy.ndarray of shape (...,3), (...,3,3), or (...,3,3,3,3) + Rotated vector or tensor, i.e. transformed to frame defined by rotation. """ if isinstance(other,Rotation): - q_m = self.quaternion[...,0:1] - p_m = self.quaternion[...,1:] - q_o = other.quaternion[...,0:1] - p_o = other.quaternion[...,1:] - q = (q_m*q_o - np.einsum('...i,...i',p_m,p_o).reshape(self.shape+(1,))) - p = q_m*p_o + q_o*p_m + _P * np.cross(p_m,p_o) - return Rotation(np.block([q,p]))._standardize() - + raise TypeError('Use "R1*R2", i.e. multiplication, to compose rotations "R1" and "R2"') elif isinstance(other,np.ndarray): if self.shape + (3,) == other.shape: q_m = self.quaternion[...,0] @@ -392,7 +411,7 @@ class Rotation: Rotation to which the misorientation is computed. """ - return other@~self + return other*~self ################################################################################################ @@ -915,7 +934,7 @@ class Rotation: np.sqrt(1-u**2)*np.sin(Theta), u, omega]) - return Rotation.from_axis_angle(p) @ center + return Rotation.from_axis_angle(p) * center @staticmethod @@ -966,8 +985,8 @@ class Rotation: f[::2,:3] *= -1 # flip half the rotation axes to negative sense return R_align.broadcast_to(N) \ - @ Rotation.from_axis_angle(p,normalize=True) \ - @ Rotation.from_axis_angle(f) + * Rotation.from_axis_angle(p,normalize=True) \ + * Rotation.from_axis_angle(f) #################################################################################################### diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index 707bc0210..ff4894632 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -956,7 +956,7 @@ class TestRotation: def test_rotate_inverse(self): R = Rotation.from_random() - assert np.allclose(np.eye(3),(~R@R).as_matrix()) + assert np.allclose(np.eye(3),(~R*R).as_matrix()) @pytest.mark.parametrize('data',[np.random.rand(3), np.random.rand(3,3), @@ -1024,7 +1024,7 @@ class TestRotation: @pytest.mark.parametrize('item',[Rotation(),np.ones(3),np.ones((3,3)), np.ones((3,3,3,3))]) def test_apply(self,item): r = Rotation.from_random() - assert r.apply(item) == r@item if isinstance(item,Rotation) else (r.apply(item) == r@item).all() + assert r.apply(item) == r*item if isinstance(item,Rotation) else (r.apply(item) == r@item).all() @pytest.mark.parametrize('angle',[10,20,30,40,50,60,70,80,90,100,120]) def test_average(self,angle): From f4247e0f3559370b56d894394b3c3cc84200ba62 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Wed, 13 Jan 2021 09:35:42 +0100 Subject: [PATCH 17/21] simpler (and scipy compatible) --- python/damask/_rotation.py | 26 +++++--------------------- python/tests/test_Rotation.py | 4 ++-- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index b5b393971..0c6824c35 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -253,24 +253,6 @@ class Rotation: return self/other - def apply(self,other): - """ - Apply rotation to Rotation, vector, second order tensor, or fourth order tensor. - - Parameters - ---------- - other : Rotation or numpy.ndarray of shape (...,3), (...,3,3), or (...,3,3,3,3) - Rotation, vector, or tensor on which to apply the rotation. - - Returns - ------- - rotated : Rotation or numpy.ndarray of shape (...,3), (...,3,3), or (...,3,3,3,3) - Composed rotation or rotated vector/tensor, i.e. transformed to frame defined by rotation. - - """ - return self*other if isinstance(other,Rotation) else self@other - - def __matmul__(self,other): """ Rotation of vector, second order tensor, or fourth order tensor. @@ -286,9 +268,7 @@ class Rotation: Rotated vector or tensor, i.e. transformed to frame defined by rotation. """ - if isinstance(other,Rotation): - raise TypeError('Use "R1*R2", i.e. multiplication, to compose rotations "R1" and "R2"') - elif isinstance(other,np.ndarray): + if isinstance(other,np.ndarray): if self.shape + (3,) == other.shape: q_m = self.quaternion[...,0] p_m = self.quaternion[...,1:] @@ -308,9 +288,13 @@ class Rotation: return np.einsum('...im,...jn,...ko,...lp,...mnop',R,R,R,R,other) else: raise ValueError('Can only rotate vectors, 2nd order tensors, and 4th order tensors') + elif isinstance(other,Rotation): + raise TypeError('Use "R1*R2", i.e. multiplication, to compose rotations "R1" and "R2"') else: raise TypeError(f'Cannot rotate {type(other)}') + apply = __matmul__ + def _standardize(self): """Standardize quaternion (ensure positive real hemisphere).""" diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index ff4894632..6bee44e7f 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -1021,10 +1021,10 @@ class TestRotation: R = Rotation.from_random() assert R/R == R*R**(-1) == Rotation() - @pytest.mark.parametrize('item',[Rotation(),np.ones(3),np.ones((3,3)), np.ones((3,3,3,3))]) + @pytest.mark.parametrize('item',[np.ones(3),np.ones((3,3)), np.ones((3,3,3,3))]) def test_apply(self,item): r = Rotation.from_random() - assert r.apply(item) == r*item if isinstance(item,Rotation) else (r.apply(item) == r@item).all() + assert (r.apply(item) == r@item).all() @pytest.mark.parametrize('angle',[10,20,30,40,50,60,70,80,90,100,120]) def test_average(self,angle): From 196902948f6612e48d14c6ecb567a550ca8ab9d4 Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Wed, 13 Jan 2021 09:50:58 +0100 Subject: [PATCH 18/21] simplified --- python/damask/_rotation.py | 38 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index 0c6824c35..441fb5b01 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -70,17 +70,9 @@ class Rotation: def __repr__(self): - """Represent rotation as unit quaternion, rotation matrix, and Bunge-Euler angles.""" - if self.shape == () and self == Rotation(): - return 'Rotation()' - else: - return f'Quaternions {self.shape}:\n'+str(self.quaternion) \ - if self.quaternion.shape != (4,) else \ - '\n'.join([ - 'Quaternion: (real={:.3f}, imag=<{:+.3f}, {:+.3f}, {:+.3f}>)'.format(*(self.quaternion)), - 'Matrix:\n{}'.format(np.round(self.as_matrix(),8)), - 'Bunge Eulers / deg: ({:3.2f}, {:3.2f}, {:3.2f})'.format(*self.as_Euler_angles(degrees=True)), - ]) + """Represent rotation as unit quaternion(s).""" + return f'Quaternion{" " if self.quaternion.shape == (4,) else "s of shape "+str(self.quaternion.shape)+chr(10)}'\ + + str(self.quaternion) def __copy__(self,**kwargs): @@ -150,35 +142,31 @@ class Rotation: return dup - def __pow__(self,pwr): + def __pow__(self,exp): """ - Raise quaternion to power. - - Equivalent to performing the rotation 'pwr' times. + Perform the rotation 'exp' times. Parameters ---------- - pwr : float - Power to raise quaternion to. + exp : float + Exponent. """ phi = np.arccos(self.quaternion[...,0:1]) 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()) + return self.copy(rotation=Rotation(np.block([np.cos(exp*phi),np.sin(exp*phi)*p]))._standardize()) - def __ipow__(self,pwr): + def __ipow__(self,exp): """ - Raise quaternion to power (in-place). - - Equivalent to performing the rotation 'pwr' times. + Perform the rotation 'exp' times (in-place). Parameters ---------- - pwr : float - Power to raise quaternion to. + exp : float + Exponent. """ - return self**pwr + return self**exp def __mul__(self,other): From 4c35da8627cb14255be645d26e9161bf2816bbcd Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Wed, 13 Jan 2021 11:33:28 +0100 Subject: [PATCH 19/21] simplified --- python/damask/_grid.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/python/damask/_grid.py b/python/damask/_grid.py index 76ce7ba64..0edec05f9 100644 --- a/python/damask/_grid.py +++ b/python/damask/_grid.py @@ -763,24 +763,19 @@ class Grid: if fill is None: fill = np.nanmax(self.material) + 1 dtype = float if np.isnan(fill) or int(fill) != fill or self.material.dtype==np.float else int - Eulers = R.as_Euler_angles(degrees=True) - material_in = self.material.copy() - + material = self.material # These rotations are always applied in the reference coordinate system, i.e. (z,x,z) not (z,x',z'') # see https://www.cs.utexas.edu/~theshark/courses/cs354/lectures/cs354-14.pdf - for angle,axes in zip(Eulers[::-1], [(0,1),(1,2),(0,1)]): - material_out = ndimage.rotate(material_in,angle,axes,order=0, - prefilter=False,output=dtype,cval=fill) - if np.prod(material_in.shape) == np.prod(material_out.shape): - # avoid scipy interpolation errors for rotations close to multiples of 90° - material_in = np.rot90(material_in,k=np.rint(angle/90.).astype(int),axes=axes) - else: - material_in = material_out + for angle,axes in zip(R.as_Euler_angles(degrees=True)[::-1], [(0,1),(1,2),(0,1)]): + material_temp = ndimage.rotate(material,angle,axes,order=0,prefilter=False,output=dtype,cval=fill) + # avoid scipy interpolation errors for rotations close to multiples of 90° + material = material_temp if np.prod(material_temp.shape) != np.prod(material.shape) else \ + np.rot90(material,k=np.rint(angle/90.).astype(int),axes=axes) - origin = self.origin-(np.asarray(material_in.shape)-self.cells)*.5 * self.size/self.cells + origin = self.origin-(np.asarray(material.shape)-self.cells)*.5 * self.size/self.cells - return Grid(material = material_in, - size = self.size/self.cells*np.asarray(material_in.shape), + return Grid(material = material, + size = self.size/self.cells*np.asarray(material.shape), origin = origin, comments = self.comments+[util.execution_stamp('Grid','rotate')], ) From 4b0b28805435861ba6b4917fb90104cae22c667d Mon Sep 17 00:00:00 2001 From: Martin Diehl Date: Wed, 13 Jan 2021 12:25:06 +0100 Subject: [PATCH 20/21] 2021! 10 years of DAMASK --- LICENSE | 2 +- PRIVATE | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 3ffc3b9e3..4290d15bd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2011-20 Max-Planck-Institut für Eisenforschung GmbH +Copyright 2011-21 Max-Planck-Institut für Eisenforschung GmbH DAMASK is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/PRIVATE b/PRIVATE index 7846c7112..1d0c95c5c 160000 --- a/PRIVATE +++ b/PRIVATE @@ -1 +1 @@ -Subproject commit 7846c71126705cc5d41dd79f2d595f4864434068 +Subproject commit 1d0c95c5c1c0e7e6f57bdfc94b695e47a6ad6c60 From c01fbe79bec51716ed92dfb3749a88fa0398c01f Mon Sep 17 00:00:00 2001 From: Test User Date: Wed, 13 Jan 2021 19:31:03 +0100 Subject: [PATCH 21/21] [skip ci] updated version information after successful test of v3.0.0-alpha2-279-g8182c9c54 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 364cb0e13..6e2dff56f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v3.0.0-alpha2-258-g715504ee5 +v3.0.0-alpha2-279-g8182c9c54