diff --git a/python/damask/_orientation.py b/python/damask/_orientation.py index bbb682e82..5cc412c98 100644 --- a/python/damask/_orientation.py +++ b/python/damask/_orientation.py @@ -86,11 +86,11 @@ class Orientation(Rotation): """ crystal_families = ['triclinic', - 'monoclinic', - 'orthorhombic', - 'tetragonal', - 'hexagonal', - 'cubic'] + 'monoclinic', + 'orthorhombic', + 'tetragonal', + 'hexagonal', + 'cubic'] lattice_symmetries = { 'aP': 'triclinic', @@ -136,8 +136,7 @@ class Orientation(Rotation): Rotation.__init__(self) if rotation is None else Rotation.__init__(self,rotation=rotation) - if ( lattice is not None - and lattice not in self.lattice_symmetries + if ( lattice not in self.lattice_symmetries and lattice not in self.crystal_families): raise KeyError(f'Lattice "{lattice}" is unknown') @@ -206,7 +205,7 @@ class Orientation(Rotation): def __repr__(self): """Represent.""" return '\n'.join(([] if self.lattice is None else [f'Bravais lattice {self.lattice}']) - + ([] if self.family is None else [f'Crystal family {self.family}']) + + ([f'Crystal family {self.family}']) + [super().__repr__()]) @@ -239,7 +238,7 @@ class Orientation(Rotation): """ 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) + return np.logical_and(matching_type,super(self.__class__,self.reduced).__eq__(other.reduced)) def __ne__(self,other): """ @@ -254,6 +253,57 @@ class Orientation(Rotation): return np.logical_not(self==other) + def isclose(self,other,rtol=1e-5,atol=1e-8,equal_nan=True): + """ + Report where values are approximately equal to corresponding ones of other Orientation. + + Parameters + ---------- + other : Orientation + Orientation to compare against. + rtol : float, optional + Relative tolerance of equality. + atol : float, optional + Absolute tolerance of equality. + equal_nan : bool, optional + Consider matching NaN values as equal. Defaults to True. + + Returns + ------- + mask : numpy.ndarray bool + Mask indicating where corresponding orientations are close. + + """ + matching_type = all([hasattr(other,attr) and getattr(self,attr) == getattr(other,attr) + for attr in ['family','lattice','parameters']]) + return np.logical_and(matching_type,super(self.__class__,self.reduced).isclose(other.reduced)) + + + + def allclose(self,other,rtol=1e-5,atol=1e-8,equal_nan=True): + """ + Test whether all values are approximately equal to corresponding ones of other Orientation. + + Parameters + ---------- + other : Orientation + Orientation to compare against. + rtol : float, optional + Relative tolerance of equality. + atol : float, optional + Absolute tolerance of equality. + equal_nan : bool, optional + Consider matching NaN values as equal. Defaults to True. + + Returns + ------- + answer : bool + Whether all values are close between both orientations. + + """ + return np.all(self.isclose(other,rtol,atol,equal_nan)) + + def __mul__(self,other): """ Compose this orientation with other. @@ -491,9 +541,6 @@ class Orientation(Rotation): is added to the left of the Rotation array. """ - if self.family is None: - 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')) @@ -501,9 +548,6 @@ class Orientation(Rotation): @property def reduced(self): """Select symmetrically equivalent orientation that falls into fundamental zone according to symmetry.""" - if self.family is None: - raise ValueError('Missing crystal symmetry') - eq = self.equivalent ok = eq.in_FZ ok &= np.cumsum(ok,axis=0) == 1 @@ -532,9 +576,6 @@ class Orientation(Rotation): https://doi.org/10.1107/S0108767391006864 """ - if self.family is None: - raise ValueError('Missing crystal symmetry') - rho_abs = np.abs(self.as_Rodrigues_vector(compact=True))*(1.-1.e-9) with np.errstate(invalid='ignore'): @@ -575,9 +616,6 @@ class Orientation(Rotation): https://doi.org/10.1107/S0108767391006864 """ - if self.family is None: - raise ValueError('Missing crystal symmetry') - rho = self.as_Rodrigues_vector(compact=True)*(1.0-1.0e-9) with np.errstate(invalid='ignore'): @@ -728,8 +766,6 @@ class Orientation(Rotation): 'beta': np.pi/2., 'gamma': np.pi/2., } - else: - raise KeyError(f'Crystal family "{self.family}" is unknown') @property @@ -1026,8 +1062,6 @@ class Orientation(Rotation): Bunge Eulers / deg: (11.40, 21.86, 0.60) """ - if self.family is None or other.family is None: - raise ValueError('missing crystal symmetry') if self.family != other.family: raise NotImplementedError('disorientation between different crystal families') @@ -1084,9 +1118,6 @@ class Orientation(Rotation): https://doi.org/10.1107/S0021889801003077 """ - if self.family is None: - raise ValueError('Missing crystal symmetry') - eq = self.equivalent m = eq.misorientation(self[...,0].reshape((1,)+self.shape[:-1]+(1,)) .broadcast_to(eq.shape))\ @@ -1128,9 +1159,6 @@ class Orientation(Rotation): Index of symmetrically equivalent orientation that rotated vector to SST. """ - if self.family is None: - raise ValueError('Missing crystal symmetry') - eq = self.equivalent blend = util.shapeblender(eq.shape,np.array(vector).shape[:-1]) poles = eq.broadcast_to(blend,mode='right') @ np.broadcast_to(np.array(vector),blend+(3,)) diff --git a/python/damask/_rotation.py b/python/damask/_rotation.py index a067209b2..fff53fc73 100644 --- a/python/damask/_rotation.py +++ b/python/damask/_rotation.py @@ -103,29 +103,20 @@ class Rotation: """ 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. """ - 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) + return np.logical_or(np.all(self.quaternion == other.quaternion,axis=-1), + np.all(self.quaternion == -1.0*other.quaternion,axis=-1)) + 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 @@ -135,6 +126,57 @@ class Rotation: return np.logical_not(self==other) + def isclose(self,other,rtol=1e-5,atol=1e-8,equal_nan=True): + """ + Report where values are approximately equal to corresponding ones of other Rotation. + + Parameters + ---------- + other : Rotation + Rotation to compare against. + rtol : float, optional + Relative tolerance of equality. + atol : float, optional + Absolute tolerance of equality. + equal_nan : bool, optional + Consider matching NaN values as equal. Defaults to True. + + Returns + ------- + mask : numpy.ndarray bool + Mask indicating where corresponding rotations are close. + + """ + s = self.quaternion + o = other.quaternion + return np.logical_or(np.all(np.isclose(s, o,rtol,atol,equal_nan),axis=-1), + np.all(np.isclose(s,-1.0*o,rtol,atol,equal_nan),axis=-1)) + + + def allclose(self,other,rtol=1e-5,atol=1e-8,equal_nan=True): + """ + Test whether all values are approximately equal to corresponding ones of other Rotation. + + Parameters + ---------- + other : Rotation + Rotation to compare against. + rtol : float, optional + Relative tolerance of equality. + atol : float, optional + Absolute tolerance of equality. + equal_nan : bool, optional + Consider matching NaN values as equal. Defaults to True. + + Returns + ------- + answer : bool + Whether all values are close between both rotations. + + """ + return np.all(self.isclose(other,rtol,atol,equal_nan)) + + def __array__(self): """Initializer for numpy.""" return self.quaternion diff --git a/python/tests/test_Config.py b/python/tests/test_Config.py index c64573d93..d2f0b5e72 100644 --- a/python/tests/test_Config.py +++ b/python/tests/test_Config.py @@ -53,7 +53,7 @@ class TestConfig: def test_abstract_is_complete(self): assert Config().is_complete is None - - @pytest.mark.parametrize('data',[Rotation.from_random(),Orientation.from_random()]) + + @pytest.mark.parametrize('data',[Rotation.from_random(),Orientation.from_random(lattice='cI')]) def test_rotation_orientation(self,data): assert str(Config(a=data)) == str(Config(a=data.as_quaternion())) diff --git a/python/tests/test_ConfigMaterial.py b/python/tests/test_ConfigMaterial.py index 5820c0f0a..c968f429a 100644 --- a/python/tests/test_ConfigMaterial.py +++ b/python/tests/test_ConfigMaterial.py @@ -140,6 +140,5 @@ class TestConfigMaterial: if update: cur.save(ref_path/'measured.material.yaml') for i,m in enumerate(ref['material']): - assert Rotation(m['constituents'][0]['O']) == \ - Rotation(cur['material'][i]['constituents'][0]['O']) + assert Rotation(m['constituents'][0]['O']).isclose(Rotation(cur['material'][i]['constituents'][0]['O'])) assert cur.is_valid and cur['phase'] == ref['phase'] and cur['homogenization'] == ref['homogenization'] diff --git a/python/tests/test_Orientation.py b/python/tests/test_Orientation.py index 40d4d0116..dce0cf40f 100644 --- a/python/tests/test_Orientation.py +++ b/python/tests/test_Orientation.py @@ -37,9 +37,15 @@ class TestOrientation: assert not ( Orientation(R,lattice) != Orientation(R,lattice) if shape is None else \ (Orientation(R,lattice) != Orientation(R,lattice)).any()) + @pytest.mark.parametrize('lattice',Orientation.crystal_families) + @pytest.mark.parametrize('shape',[None,5,(4,6)]) + def test_close(self,lattice,shape): + R = Orientation.from_random(lattice=lattice,shape=shape) + assert R.isclose(R.reduced).all() and R.allclose(R.reduced) + @pytest.mark.parametrize('a,b',[ - (dict(rotation=[1,0,0,0]), - dict(rotation=[0.5,0.5,0.5,0.5])), + (dict(rotation=[1,0,0,0],lattice='triclinic'), + dict(rotation=[0.5,0.5,0.5,0.5],lattice='triclinic')), (dict(rotation=[1,0,0,0],lattice='cubic'), dict(rotation=[1,0,0,0],lattice='hexagonal')), @@ -222,7 +228,7 @@ class TestOrientation: blend = util.shapeblender(o.shape,p.shape) for loc in np.random.randint(0,blend,(10,len(blend))): assert o[tuple(loc[:len(o.shape)])].disorientation(p[tuple(loc[-len(p.shape):])]) \ - == o.disorientation(p)[tuple(loc)] + .isclose(o.disorientation(p)[tuple(loc)]) @pytest.mark.parametrize('lattice',Orientation.crystal_families) def test_disorientation360(self,lattice): @@ -335,33 +341,9 @@ class TestOrientation: o.family = invalid_family o.symmetry_operations # noqa - def test_missing_symmetry_equivalent(self): - with pytest.raises(ValueError): - Orientation(lattice=None).equivalent # noqa - - def test_missing_symmetry_reduced(self): - with pytest.raises(ValueError): - Orientation(lattice=None).reduced # noqa - - def test_missing_symmetry_in_FZ(self): - with pytest.raises(ValueError): - Orientation(lattice=None).in_FZ # noqa - - def test_missing_symmetry_in_disorientation_FZ(self): - with pytest.raises(ValueError): - Orientation(lattice=None).in_disorientation_FZ # noqa - - def test_missing_symmetry_disorientation(self): - with pytest.raises(ValueError): - Orientation(lattice=None).disorientation(Orientation(lattice=None)) # noqa - - def test_missing_symmetry_average(self): - with pytest.raises(ValueError): - Orientation(lattice=None).average() # noqa - - def test_missing_symmetry_to_SST(self): - with pytest.raises(ValueError): - Orientation(lattice=None).to_SST(np.zeros(3)) # noqa + def test_invalid_rot(self): + with pytest.raises(TypeError): + Orientation.from_random(lattice='cubic') * np.ones(3) def test_missing_symmetry_immutable(self): with pytest.raises(KeyError): diff --git a/python/tests/test_Rotation.py b/python/tests/test_Rotation.py index b28a849c5..9d0f26bfc 100644 --- a/python/tests/test_Rotation.py +++ b/python/tests/test_Rotation.py @@ -960,7 +960,7 @@ class TestRotation: if axis_angle[3] > np.pi: axis_angle[3] -= 2.*np.pi axis_angle *= -1 - assert R**pwr == Rotation.from_axis_angle(axis_angle) + assert (R**pwr).isclose(Rotation.from_axis_angle(axis_angle)) def test_rotate_inverse(self): R = Rotation.from_random() @@ -1027,7 +1027,7 @@ class TestRotation: def test_invariant(self): R = Rotation.from_random() - assert R/R == R*R**(-1) == Rotation() + assert (R/R).isclose(R*R**(-1)) and (R/R).isclose(Rotation()) @pytest.mark.parametrize('item',[np.ones(3),np.ones((3,3)), np.ones((3,3,3,3))]) def test_apply(self,item):