def __new__(cls, strain_matrix, dfm=None): """ Create a Strain object. Note that the constructor uses __new__ rather than __init__ according to the standard method of subclassing numpy ndarrays. Note also that the default constructor does not include the deformation gradient Args: strain_matrix (3x3 array-like): the 3x3 array-like representing the Green-Lagrange strain """ obj = SquareTensor(strain_matrix).view(cls) obj._dfm = dfm if not obj.is_symmetric(): raise ValueError("Strain objects must be initialized " "with a symmetric array-like.") if dfm is None: warnings.warn( "Constructing a strain object without a deformation " "matrix makes many methods unusable. Use " "Strain.from_deformation to construct a Strain object" " from a deformation gradient.") elif (np.array(dfm) - obj < 1e-5).all(): warnings.warn("Warning: deformation matrix does not correspond " "to input strain_matrix value") return obj
def __new__(cls, strain_matrix, dfm=None): """ Create a Strain object. Note that the constructor uses __new__ rather than __init__ according to the standard method of subclassing numpy ndarrays. Note also that the default constructor does not include the deformation gradient Args: strain_matrix (3x3 array-like): the 3x3 array-like representing the Green-Lagrange strain """ obj = SquareTensor(strain_matrix).view(cls) obj._dfm = dfm if not obj.is_symmetric(): raise ValueError("Strain objects must be initialized " "with a symmetric array-like.") if dfm is None: warnings.warn("Constructing a strain object without a deformation " "matrix makes many methods unusable. Use " "Strain.from_deformation to construct a Strain object" " from a deformation gradient.") elif (np.array(dfm) - obj < 1e-5).all(): warnings.warn("Warning: deformation matrix does not correspond " "to input strain_matrix value") return obj
def thermal_expansion_coeff(self, structure, temperature, mode="debye"): """ Gets thermal expansion coefficient from third-order constants. Args: temperature (float): Temperature in kelvin, if not specified will return non-cv-normalized value structure (Structure): Structure to be used in directional heat capacity determination, only necessary if temperature is specified mode (string): mode for finding average heat-capacity, current supported modes are 'debye' and 'dulong-petit' """ soec = ElasticTensor(self[0]) v0 = (structure.volume * 1e-30 / structure.num_sites) if mode == "debye": td = soec.debye_temperature(structure) t_ratio = temperature / td integrand = lambda x: (x**4 * np.exp(x)) / (np.exp(x) - 1)**2 cv = 9 * 8.314 * t_ratio**3 * quad(integrand, 0, t_ratio**-1)[0] elif mode == "dulong-petit": cv = 3 * 8.314 else: raise ValueError("Mode must be debye or dulong-petit") tgt = self.get_tgt(temperature, structure) alpha = np.einsum('ijkl,ij', soec.compliance_tensor, tgt) alpha *= cv / (1e9 * v0 * 6.022e23) return SquareTensor(alpha)
def convert_strain_to_deformation(strain): strain = SquareTensor(strain) ftdotf = 2 * strain + np.eye(3) eigs, eigvecs = np.linalg.eigh(ftdotf) rotated = ftdotf.rotate(np.transpose(eigvecs)) rotated = rotated.round(10) defo = Deformation(np.sqrt(rotated)) result = defo.rotate(eigvecs) return result
def test_rotate(self): self.assertArrayEqual( self.vec.rotate([[0, -1, 0], [1, 0, 0], [0, 0, 1]]), [0, 1, 0]) self.assertArrayAlmostEqual(self.non_symm.rotate(self.rotation), SquareTensor([[0.531, 0.485, 0.271], [0.700, 0.5, 0.172], [0.171, 0.233, 0.068]]), decimal=3) self.assertRaises(ValueError, self.non_symm.rotate, self.symm_rank2)
def test_properties(self): # transpose self.assertArrayEqual(self.non_symm.trans, SquareTensor([[0.1, 0.4, 0.2], [0.2, 0.5, 0.5], [0.3, 0.6, 0.5]])) self.assertArrayEqual(self.rand_sqtensor.trans, np.transpose(self.rand_sqtensor)) self.assertArrayEqual(self.symm_sqtensor, self.symm_sqtensor.trans) # inverse self.assertArrayEqual(self.non_symm.inv, np.linalg.inv(self.non_symm)) with self.assertRaises(ValueError): self.non_invertible.inv # determinant self.assertEqual(self.rand_sqtensor.det, np.linalg.det(self.rand_sqtensor)) self.assertEqual(self.non_invertible.det, 0.0) self.assertEqual(self.non_symm.det, 0.009) # symmetrized self.assertArrayEqual(self.rand_sqtensor.symmetrized, 0.5 * (self.rand_sqtensor + self.rand_sqtensor.trans)) self.assertArrayEqual(self.symm_sqtensor, self.symm_sqtensor.symmetrized) self.assertArrayAlmostEqual(self.non_symm.symmetrized, SquareTensor([[0.1, 0.3, 0.25], [0.3, 0.5, 0.55], [0.25, 0.55, 0.5]])) # invariants i1 = np.trace(self.rand_sqtensor) i2 = self.rand_sqtensor[0, 0] * self.rand_sqtensor[1, 1] + \ self.rand_sqtensor[1, 1] * self.rand_sqtensor[2, 2] + \ self.rand_sqtensor[2, 2] * self.rand_sqtensor[0, 0] - \ self.rand_sqtensor[0, 1] * self.rand_sqtensor[1, 0] - \ self.rand_sqtensor[0, 2] * self.rand_sqtensor[2, 0] - \ self.rand_sqtensor[2, 1] * self.rand_sqtensor[1, 2] i3 = np.linalg.det(self.rand_sqtensor) self.assertArrayAlmostEqual([i1, i2, i3], self.rand_sqtensor.principal_invariants)
def generate_elastic_workflow(structure, tags=None): """ Generates a standard production workflow. Notes: Uses a primitive structure transformed into the conventional basis (for equivalent deformations). Adds the "minimal" category to the minimal portion of the workflow necessary to generate the elastic tensor, and the "minimal_full_stencil" category to the portion that includes all of the strain stencil, but is symmetrically complete """ if tags == None: tags = [] # transform the structure ieee_rot = Tensor.get_ieee_rotation(structure) if not SquareTensor(ieee_rot).is_rotation(tol=0.005): raise ValueError("Rotation matrix does not satisfy rotation conditions") symm_op = SymmOp.from_rotation_and_translation(ieee_rot) ieee_structure = structure.copy() ieee_structure.apply_operation(symm_op) # construct workflow wf = wf_elastic_constant(ieee_structure) # Set categories, starting with optimization opt_fws = get_fws_and_tasks(wf, fw_name_constraint="optimization") wf.fws[opt_fws[0][0]].spec['elastic_category'] = "minimal" # find minimal set of fireworks using symmetry reduction fws_by_strain = {Strain(fw.tasks[-1]['pass_dict']['strain']): n for n, fw in enumerate(wf.fws) if 'deformation' in fw.name} unique_tensors = symmetry_reduce(list(fws_by_strain.keys()), ieee_structure) for unique_tensor in unique_tensors: fw_index = get_tkd_value(fws_by_strain, unique_tensor) if np.isclose(unique_tensor, 0.005).any(): wf.fws[fw_index].spec['elastic_category'] = "minimal" else: wf.fws[fw_index].spec['elastic_category'] = "minimal_full_stencil" # Add tags if tags: wf = add_tags(wf, tags) wf = add_modify_incar(wf) priority = 500 - structure.num_sites wf = add_priority(wf, priority) for fw in wf.fws: if fw.spec.get('elastic_category') == 'minimal': fw.spec['_priority'] += 2000 elif fw.spec.get('elastic_category') == 'minimal_full_stencil': fw.spec['_priority'] += 1000 return wf
def piola_kirchoff_1(self, def_grad): """ calculates the first Piola-Kirchoff stress Args: def_grad (3x3 array-like): deformation gradient tensor """ if not self.is_symmetric: raise ValueError("The stress tensor is not symmetric, \ PK stress is based on a symmetric stress tensor.") def_grad = SquareTensor(def_grad) return def_grad.det * np.dot(self, def_grad.inv.trans)
def __new__(cls, stress_matrix): """ Create a Stress object. Note that the constructor uses __new__ rather than __init__ according to the standard method of subclassing numpy ndarrays. Args: stress_matrix (3x3 array-like): the 3x3 array-like representing the stress """ obj = SquareTensor(stress_matrix).view(cls) return obj
def setUp(self): self.rand_sqtensor = SquareTensor(np.random.randn(3, 3)) self.symm_sqtensor = SquareTensor([[0.1, 0.3, 0.4], [0.3, 0.5, 0.2], [0.4, 0.2, 0.6]]) self.non_invertible = SquareTensor([[0.1, 0, 0], [0.2, 0, 0], [0, 0, 0]]) self.non_symm = SquareTensor([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.2, 0.5, 0.5]]) self.low_val = SquareTensor([[1e-6, 1 + 1e-5, 1e-6], [1 + 1e-6, 1e-6, 1e-6], [1e-7, 1e-7, 1 + 1e-5]]) self.low_val_2 = SquareTensor([[1e-6, -1 - 1e-6, 1e-6], [1 + 1e-7, 1e-6, 1e-6], [1e-7, 1e-7, 1 + 1e-6]]) a = 3.14 * 42.5 / 180 self.rotation = SquareTensor([[math.cos(a), 0, math.sin(a)], [0, 1, 0], [-math.sin(a), 0, math.cos(a)]])
def convert_strain_to_deformation(strain, tol=1e-5): strain = SquareTensor(strain) ftdotf = 2 * strain + np.eye(3) eigs, eigvecs = np.linalg.eig(ftdotf) rotated = ftdotf.rotate(np.transpose(eigvecs)) rotated = rotated.round(10) defo = Deformation(np.sqrt(rotated)) result = defo.rotate(eigvecs) rd = np.abs(strain - 0.5 * (np.dot(np.transpose(result), result) - np.eye(3))) assert (rd < tol).all(), "Strain-generated deformation is not valid!" return result
def __new__(cls, deformation_gradient): """ Create a Deformation object. Note that the constructor uses __new__ rather than __init__ according to the standard method of subclassing numpy ndarrays. Args: deformation_gradient (3x3 array-like): the 3x3 array-like representing the deformation gradient """ obj = SquareTensor(deformation_gradient).view(cls) return obj
def piola_kirchoff_2(self, def_grad): """ calculates the second Piola-Kirchoff stress Args: def_grad (3x3 array-like): rate of deformation tensor """ def_grad = SquareTensor(def_grad) if not self.is_symmetric: raise ValueError("The stress tensor is not symmetric, \ PK stress is based on a symmetric stress tensor.") return def_grad.det * np.dot(np.dot(def_grad.inv, self), def_grad.inv.trans)
def convert_strain_to_deformation(strain): """ This function converts a strain to a deformation gradient that will produce that strain Args: strain (3x3 array-like): strain matrix """ strain = SquareTensor(strain) ftdotf = 2*strain + np.eye(3) eigs, eigvecs = np.linalg.eigh(ftdotf) rotated = ftdotf.rotate(np.transpose(eigvecs)) rotated = rotated.round(10) defo = Deformation(np.sqrt(rotated)) result = defo.rotate(eigvecs) return result
def get_tgt(self, temperature=None, structure=None, quad=None): """ Gets the thermodynamic Gruneisen tensor (TGT) by via an integration of the GGT weighted by the directional heat capacity. See refs: R. N. Thurston and K. Brugger, Phys. Rev. 113, A1604 (1964). K. Brugger Phys. Rev. 137, A1826 (1965). Args: temperature (float): Temperature in kelvin, if not specified will return non-cv-normalized value structure (float): Structure to be used in directional heat capacity determination, only necessary if temperature is specified quad (dict): quadrature for integration, should be dictionary with "points" and "weights" keys defaults to quadpy.sphere.Lebedev(19) as read from file """ if temperature and not structure: raise ValueError("If using temperature input, you must also " "include structure") if not quad: quad = loadfn( os.path.join(os.path.dirname(__file__), "quad_data.json")) points = quad['points'] weights = quad['weights'] num, denom, c = np.zeros((3, 3)), 0, 1 for p, w in zip(points, weights): gk = ElasticTensor(self[0]).green_kristoffel(p) rho_wsquareds, us = np.linalg.eigh(gk) us = [u / np.linalg.norm(u) for u in np.transpose(us)] for u in us: # TODO: this should be benchmarked if temperature: c = self.get_heat_capacity(temperature, structure, p, u) num += c * self.get_ggt(p, u) * w denom += c * w return SquareTensor(num / denom)
def convert_strain_to_deformation(strain, shape="upper"): """ This function converts a strain to a deformation gradient that will produce that strain. Supports three methods: Args: strain (3x3 array-like): strain matrix shape: (string): method for determining deformation, supports "upper" produces an upper triangular defo "lower" produces a lower triangular defo "symmetric" produces a symmetric defo """ strain = SquareTensor(strain) ftdotf = 2 * strain + np.eye(3) if shape == "upper": result = scipy.linalg.cholesky(ftdotf) elif shape == "symmetric": result = scipy.linalg.sqrtm(ftdotf) else: raise ValueError("shape must be \"upper\" or \"symmetric\"") return Deformation(result)
def setUp(self): self.vec = TensorBase([1., 0., 0.]) self.rand_rank2 = TensorBase(np.random.randn(3,3)) self.rand_rank3 = TensorBase(np.random.randn(3,3,3)) self.rand_rank4 = TensorBase(np.random.randn(3,3,3,3)) a = 3.14 * 42.5 / 180 self.non_symm = SquareTensor([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.2, 0.5, 0.5]]) self.rotation = SquareTensor([[math.cos(a), 0, math.sin(a)], [0, 1, 0], [-math.sin(a), 0, math.cos(a)]]) self.low_val = TensorBase([[1e-6, 1 + 1e-5, 1e-6], [1 + 1e-6, 1e-6, 1e-6], [1e-7, 1e-7, 1 + 1e-5]]) self.symm_rank2 = TensorBase([[1, 2, 3], [2, 4, 5], [3, 5, 6]]) self.symm_rank3 = TensorBase([[[1, 2, 3], [2, 4, 5], [3, 5, 6]], [[2, 4, 5], [4, 7, 8], [5, 8, 9]], [[3, 5, 6], [5, 8, 9], [6, 9, 10]]]) self.symm_rank4 = TensorBase([[[[1.2, 0.4, -0.92], [0.4, 0.05, 0.11], [-0.92, 0.11, -0.02]], [[0.4, 0.05, 0.11], [0.05, -0.47, 0.09], [0.11, 0.09, -0.]], [[-0.92, 0.11, -0.02], [0.11, 0.09, 0.], [-0.02, 0., -0.3]]], [[[0.4, 0.05, 0.11], [0.05, -0.47, 0.09], [0.11, 0.09, 0.]], [[0.05, -0.47, 0.09], [-0.47, 0.17, 0.62], [0.09, 0.62, 0.3]], [[0.11, 0.09, 0.], [0.09, 0.62, 0.3], [0., 0.3, -0.18]]], [[[-0.92, 0.11, -0.02], [0.11, 0.09, 0.], [-0.02, 0, -0.3]], [[0.11, 0.09, 0.], [0.09, 0.62, 0.3], [0., 0.3, -0.18]], [[-0.02, 0., -0.3], [0., 0.3, -0.18], [-0.3, -0.18, -0.51]]]]) # Structural symmetries tested using BaNiO3 piezo/elastic tensors self.fit_r3 = TensorBase([[[0., 0., 0.03839], [0., 0., 0.], [0.03839, 0., 0.]], [[0., 0., 0.], [0., 0., 0.03839], [0., 0.03839, 0.]], [[6.89822, 0., 0.], [0., 6.89822, 0.], [0., 0., 27.4628]]]) self.fit_r4 = TensorBase([[[[157.9, 0., 0.], [0., 63.1, 0.], [0., 0., 29.4]], [[0., 47.4, 0.], [47.4, 0., 0.], [0., 0., 0.]], [[0., 0., 4.3], [0., 0., 0.], [4.3, 0., 0.]]], [[[0., 47.4, 0.], [47.4, 0., 0.], [0., 0., 0.]], [[63.1, 0., 0.], [0., 157.9, 0.], [0., 0., 29.4]], [[0., 0., 0.], [0., 0., 4.3], [0., 4.3, 0.]]], [[[0., 0., 4.3], [0., 0., 0.], [4.3, 0., 0.]], [[0., 0., 0.], [0., 0., 4.3], [0., 4.3, 0.]], [[29.4, 0., 0.], [0., 29.4, 0.], [0., 0., 207.6]]]]) self.unfit4 = TensorBase([[[[161.26, 0., 0.], [0., 62.76, 0.], [0., 0., 30.18]], [[0., 47.08, 0.], [47.08, 0., 0.], [0., 0., 0.]], [[0., 0., 4.23], [0., 0., 0.], [4.23, 0., 0.]]], [[[0., 47.08, 0.], [47.08, 0., 0.], [0., 0., 0.]], [[62.76, 0., 0.], [0., 155.28, -0.06], [0., -0.06, 28.53]], [[0., 0., 0.], [0., -0.06, 4.44], [0., 4.44, 0.]]], [[[0., 0., 4.23], [0., 0., 0.], [4.23, 0., 0.]], [[0., 0., 0.], [0., -0.06, 4.44], [0., 4.44, 0.]], [[30.18, 0., 0.], [0., 28.53, 0.], [0., 0., 207.57]]]]) self.structure = self.get_structure('BaNiO3') ieee_file_path = os.path.join(test_dir, "ieee_conversion_data.json") with open(ieee_file_path) as f: self.ieee_data = json.load(f)
def test_get_scaled(self): self.assertArrayEqual(self.non_symm.get_scaled(10.), SquareTensor([[1, 2, 3], [4, 5, 6], [2, 5, 5]]))
class SquareTensorTest(PymatgenTest): def setUp(self): self.rand_sqtensor = SquareTensor(np.random.randn(3, 3)) self.symm_sqtensor = SquareTensor([[0.1, 0.3, 0.4], [0.3, 0.5, 0.2], [0.4, 0.2, 0.6]]) self.non_invertible = SquareTensor([[0.1, 0, 0], [0.2, 0, 0], [0, 0, 0]]) self.non_symm = SquareTensor([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.2, 0.5, 0.5]]) self.low_val = SquareTensor([[1e-6, 1 + 1e-5, 1e-6], [1 + 1e-6, 1e-6, 1e-6], [1e-7, 1e-7, 1 + 1e-5]]) self.low_val_2 = SquareTensor([[1e-6, -1 - 1e-6, 1e-6], [1 + 1e-7, 1e-6, 1e-6], [1e-7, 1e-7, 1 + 1e-6]]) a = 3.14 * 42.5 / 180 self.rotation = SquareTensor([[math.cos(a), 0, math.sin(a)], [0, 1, 0], [-math.sin(a), 0, math.cos(a)]]) def test_new(self): non_sq_matrix = [[0.1, 0.2, 0.1], [0.1, 0.2, 0.3], [0.1, 0.2, 0.3], [0.1, 0.1, 0.1]] bad_matrix = [[0.1, 0.2], [0.2, 0.3, 0.4], [0.2, 0.3, 0.5]] too_high_rank = np.zeros((3, 3, 3)) self.assertRaises(ValueError, SquareTensor, non_sq_matrix) self.assertRaises(ValueError, SquareTensor, bad_matrix) self.assertRaises(ValueError, SquareTensor, too_high_rank) def test_properties(self): # transpose self.assertArrayEqual( self.non_symm.trans, SquareTensor([[0.1, 0.4, 0.2], [0.2, 0.5, 0.5], [0.3, 0.6, 0.5]])) self.assertArrayEqual(self.rand_sqtensor.trans, np.transpose(self.rand_sqtensor)) self.assertArrayEqual(self.symm_sqtensor, self.symm_sqtensor.trans) # inverse self.assertArrayEqual(self.non_symm.inv, np.linalg.inv(self.non_symm)) with self.assertRaises(ValueError): self.non_invertible.inv # determinant self.assertEqual(self.rand_sqtensor.det, np.linalg.det(self.rand_sqtensor)) self.assertEqual(self.non_invertible.det, 0.0) self.assertEqual(self.non_symm.det, 0.009) # symmetrized self.assertArrayEqual( self.rand_sqtensor.symmetrized, 0.5 * (self.rand_sqtensor + self.rand_sqtensor.trans)) self.assertArrayEqual(self.symm_sqtensor, self.symm_sqtensor.symmetrized) self.assertArrayAlmostEqual( self.non_symm.symmetrized, SquareTensor([[0.1, 0.3, 0.25], [0.3, 0.5, 0.55], [0.25, 0.55, 0.5]])) # invariants i1 = np.trace(self.rand_sqtensor) i2 = self.rand_sqtensor[0, 0] * self.rand_sqtensor[1, 1] + \ self.rand_sqtensor[1, 1] * self.rand_sqtensor[2, 2] + \ self.rand_sqtensor[2, 2] * self.rand_sqtensor[0, 0] - \ self.rand_sqtensor[0, 1] * self.rand_sqtensor[1, 0] - \ self.rand_sqtensor[0, 2] * self.rand_sqtensor[2, 0] - \ self.rand_sqtensor[2, 1] * self.rand_sqtensor[1, 2] i3 = np.linalg.det(self.rand_sqtensor) self.assertArrayAlmostEqual([i1, i2, i3], self.rand_sqtensor.principal_invariants) def test_is_rotation(self): self.assertTrue(self.rotation.is_rotation()) self.assertFalse(self.symm_sqtensor.is_rotation()) self.assertTrue(self.low_val_2.is_rotation()) self.assertFalse(self.low_val_2.is_rotation(tol=1e-8)) def test_get_scaled(self): self.assertArrayEqual(self.non_symm.get_scaled(10.), SquareTensor([[1, 2, 3], [4, 5, 6], [2, 5, 5]])) def test_polar_decomposition(self): u, p = self.rand_sqtensor.polar_decomposition() self.assertArrayAlmostEqual(np.dot(u, p), self.rand_sqtensor) self.assertArrayAlmostEqual(np.eye(3), np.dot(u, np.conjugate(np.transpose(u))))
def setUp(self): self.vec = TensorBase([1., 0., 0.]) self.rand_rank2 = TensorBase(np.random.randn(3, 3)) self.rand_rank3 = TensorBase(np.random.randn(3, 3, 3)) self.rand_rank4 = TensorBase(np.random.randn(3, 3, 3, 3)) a = 3.14 * 42.5 / 180 self.non_symm = SquareTensor([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.2, 0.5, 0.5]]) self.rotation = SquareTensor([[math.cos(a), 0, math.sin(a)], [0, 1, 0], [-math.sin(a), 0, math.cos(a)]]) self.low_val = TensorBase([[1e-6, 1 + 1e-5, 1e-6], [1 + 1e-6, 1e-6, 1e-6], [1e-7, 1e-7, 1 + 1e-5]]) self.symm_rank2 = TensorBase([[1, 2, 3], [2, 4, 5], [3, 5, 6]]) self.symm_rank3 = TensorBase([[[1, 2, 3], [2, 4, 5], [3, 5, 6]], [[2, 4, 5], [4, 7, 8], [5, 8, 9]], [[3, 5, 6], [5, 8, 9], [6, 9, 10]]]) self.symm_rank4 = TensorBase([[[[1.2, 0.4, -0.92], [0.4, 0.05, 0.11], [-0.92, 0.11, -0.02]], [[0.4, 0.05, 0.11], [0.05, -0.47, 0.09], [0.11, 0.09, -0.]], [[-0.92, 0.11, -0.02], [0.11, 0.09, 0.], [-0.02, 0., -0.3]]], [[[0.4, 0.05, 0.11], [0.05, -0.47, 0.09], [0.11, 0.09, 0.]], [[0.05, -0.47, 0.09], [-0.47, 0.17, 0.62], [0.09, 0.62, 0.3]], [[0.11, 0.09, 0.], [0.09, 0.62, 0.3], [0., 0.3, -0.18]]], [[[-0.92, 0.11, -0.02], [0.11, 0.09, 0.], [-0.02, 0, -0.3]], [[0.11, 0.09, 0.], [0.09, 0.62, 0.3], [0., 0.3, -0.18]], [[-0.02, 0., -0.3], [0., 0.3, -0.18], [-0.3, -0.18, -0.51]]]]) # Structural symmetries tested using BaNiO3 piezo/elastic tensors self.fit_r3 = TensorBase([[[0., 0., 0.03839], [0., 0., 0.], [0.03839, 0., 0.]], [[0., 0., 0.], [0., 0., 0.03839], [0., 0.03839, 0.]], [[6.89822, 0., 0.], [0., 6.89822, 0.], [0., 0., 27.4628]]]) self.fit_r4 = TensorBase([[[[157.9, 0., 0.], [0., 63.1, 0.], [0., 0., 29.4]], [[0., 47.4, 0.], [47.4, 0., 0.], [0., 0., 0.]], [[0., 0., 4.3], [0., 0., 0.], [4.3, 0., 0.]]], [[[0., 47.4, 0.], [47.4, 0., 0.], [0., 0., 0.]], [[63.1, 0., 0.], [0., 157.9, 0.], [0., 0., 29.4]], [[0., 0., 0.], [0., 0., 4.3], [0., 4.3, 0.]]], [[[0., 0., 4.3], [0., 0., 0.], [4.3, 0., 0.]], [[0., 0., 0.], [0., 0., 4.3], [0., 4.3, 0.]], [[29.4, 0., 0.], [0., 29.4, 0.], [0., 0., 207.6]]]]) self.unfit4 = TensorBase([[[[161.26, 0., 0.], [0., 62.76, 0.], [0., 0., 30.18]], [[0., 47.08, 0.], [47.08, 0., 0.], [0., 0., 0.]], [[0., 0., 4.23], [0., 0., 0.], [4.23, 0., 0.]]], [[[0., 47.08, 0.], [47.08, 0., 0.], [0., 0., 0.]], [[62.76, 0., 0.], [0., 155.28, -0.06], [0., -0.06, 28.53]], [[0., 0., 0.], [0., -0.06, 4.44], [0., 4.44, 0.]]], [[[0., 0., 4.23], [0., 0., 0.], [4.23, 0., 0.]], [[0., 0., 0.], [0., -0.06, 4.44], [0., 4.44, 0.]], [[30.18, 0., 0.], [0., 28.53, 0.], [0., 0., 207.57]]]]) self.structure = self.get_structure('BaNiO3') ieee_file_path = os.path.join(test_dir, "ieee_conversion_data.json") with open(ieee_file_path) as f: self.ieee_data = json.load(f)
class TensorBaseTest(PymatgenTest): def setUp(self): self.vec = TensorBase([1., 0., 0.]) self.rand_rank2 = TensorBase(np.random.randn(3, 3)) self.rand_rank3 = TensorBase(np.random.randn(3, 3, 3)) self.rand_rank4 = TensorBase(np.random.randn(3, 3, 3, 3)) a = 3.14 * 42.5 / 180 self.non_symm = SquareTensor([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.2, 0.5, 0.5]]) self.rotation = SquareTensor([[math.cos(a), 0, math.sin(a)], [0, 1, 0], [-math.sin(a), 0, math.cos(a)]]) self.low_val = TensorBase([[1e-6, 1 + 1e-5, 1e-6], [1 + 1e-6, 1e-6, 1e-6], [1e-7, 1e-7, 1 + 1e-5]]) self.symm_rank2 = TensorBase([[1, 2, 3], [2, 4, 5], [3, 5, 6]]) self.symm_rank3 = TensorBase([[[1, 2, 3], [2, 4, 5], [3, 5, 6]], [[2, 4, 5], [4, 7, 8], [5, 8, 9]], [[3, 5, 6], [5, 8, 9], [6, 9, 10]]]) self.symm_rank4 = TensorBase([[[[1.2, 0.4, -0.92], [0.4, 0.05, 0.11], [-0.92, 0.11, -0.02]], [[0.4, 0.05, 0.11], [0.05, -0.47, 0.09], [0.11, 0.09, -0.]], [[-0.92, 0.11, -0.02], [0.11, 0.09, 0.], [-0.02, 0., -0.3]]], [[[0.4, 0.05, 0.11], [0.05, -0.47, 0.09], [0.11, 0.09, 0.]], [[0.05, -0.47, 0.09], [-0.47, 0.17, 0.62], [0.09, 0.62, 0.3]], [[0.11, 0.09, 0.], [0.09, 0.62, 0.3], [0., 0.3, -0.18]]], [[[-0.92, 0.11, -0.02], [0.11, 0.09, 0.], [-0.02, 0, -0.3]], [[0.11, 0.09, 0.], [0.09, 0.62, 0.3], [0., 0.3, -0.18]], [[-0.02, 0., -0.3], [0., 0.3, -0.18], [-0.3, -0.18, -0.51]]]]) # Structural symmetries tested using BaNiO3 piezo/elastic tensors self.fit_r3 = TensorBase([[[0., 0., 0.03839], [0., 0., 0.], [0.03839, 0., 0.]], [[0., 0., 0.], [0., 0., 0.03839], [0., 0.03839, 0.]], [[6.89822, 0., 0.], [0., 6.89822, 0.], [0., 0., 27.4628]]]) self.fit_r4 = TensorBase([[[[157.9, 0., 0.], [0., 63.1, 0.], [0., 0., 29.4]], [[0., 47.4, 0.], [47.4, 0., 0.], [0., 0., 0.]], [[0., 0., 4.3], [0., 0., 0.], [4.3, 0., 0.]]], [[[0., 47.4, 0.], [47.4, 0., 0.], [0., 0., 0.]], [[63.1, 0., 0.], [0., 157.9, 0.], [0., 0., 29.4]], [[0., 0., 0.], [0., 0., 4.3], [0., 4.3, 0.]]], [[[0., 0., 4.3], [0., 0., 0.], [4.3, 0., 0.]], [[0., 0., 0.], [0., 0., 4.3], [0., 4.3, 0.]], [[29.4, 0., 0.], [0., 29.4, 0.], [0., 0., 207.6]]]]) self.unfit4 = TensorBase([[[[161.26, 0., 0.], [0., 62.76, 0.], [0., 0., 30.18]], [[0., 47.08, 0.], [47.08, 0., 0.], [0., 0., 0.]], [[0., 0., 4.23], [0., 0., 0.], [4.23, 0., 0.]]], [[[0., 47.08, 0.], [47.08, 0., 0.], [0., 0., 0.]], [[62.76, 0., 0.], [0., 155.28, -0.06], [0., -0.06, 28.53]], [[0., 0., 0.], [0., -0.06, 4.44], [0., 4.44, 0.]]], [[[0., 0., 4.23], [0., 0., 0.], [4.23, 0., 0.]], [[0., 0., 0.], [0., -0.06, 4.44], [0., 4.44, 0.]], [[30.18, 0., 0.], [0., 28.53, 0.], [0., 0., 207.57]]]]) self.structure = self.get_structure('BaNiO3') ieee_file_path = os.path.join(test_dir, "ieee_conversion_data.json") with open(ieee_file_path) as f: self.ieee_data = json.load(f) def test_new(self): bad_2 = np.zeros((4, 4)) bad_3 = np.zeros((4, 4, 4)) self.assertRaises(ValueError, TensorBase, bad_2) self.assertRaises(ValueError, TensorBase, bad_3) self.assertEqual(self.rand_rank2.rank, 2) self.assertEqual(self.rand_rank3.rank, 3) self.assertEqual(self.rand_rank4.rank, 4) def test_zeroed(self): self.assertArrayEqual( self.low_val.zeroed(), TensorBase([[0, 1 + 1e-5, 0], [1 + 1e-6, 0, 0], [0, 0, 1 + 1e-5]])) self.assertArrayEqual( self.low_val.zeroed(tol=1e-6), TensorBase([[1e-6, 1 + 1e-5, 1e-6], [1 + 1e-6, 1e-6, 1e-6], [0, 0, 1 + 1e-5]])) self.assertArrayEqual( TensorBase([[1e-6, -30, 1], [1e-7, 1, 0], [1e-8, 0, 1]]).zeroed(), TensorBase([[0, -30, 1], [0, 1, 0], [0, 0, 1]])) def test_transform(self): # Rank 3 tensor = TensorBase(np.arange(0, 27).reshape(3, 3, 3)) symm_op = SymmOp.from_axis_angle_and_translation([0, 0, 1], 30, False, [0, 0, 1]) new_tensor = tensor.transform(symm_op) self.assertArrayAlmostEqual( new_tensor, [[[-0.871, -2.884, -1.928], [-2.152, -6.665, -4.196], [-1.026, -2.830, -1.572]], [[0.044, 1.531, 1.804], [4.263, 21.008, 17.928], [5.170, 23.026, 18.722]], [[1.679, 7.268, 5.821], [9.268, 38.321, 29.919], [8.285, 33.651, 26.000]]], 3) def test_rotate(self): self.assertArrayEqual( self.vec.rotate([[0, -1, 0], [1, 0, 0], [0, 0, 1]]), [0, 1, 0]) self.assertArrayAlmostEqual(self.non_symm.rotate(self.rotation), SquareTensor([[0.531, 0.485, 0.271], [0.700, 0.5, 0.172], [0.171, 0.233, 0.068]]), decimal=3) self.assertRaises(ValueError, self.non_symm.rotate, self.symm_rank2) def test_symmetrized(self): self.assertTrue(self.rand_rank2.symmetrized.is_symmetric()) self.assertTrue(self.rand_rank3.symmetrized.is_symmetric()) self.assertTrue(self.rand_rank4.symmetrized.is_symmetric()) def test_is_symmetric(self): self.assertTrue(self.symm_rank2.is_symmetric()) self.assertTrue(self.symm_rank3.is_symmetric()) self.assertTrue(self.symm_rank4.is_symmetric()) tol_test = self.symm_rank4 tol_test[0, 1, 2, 2] += 1e-6 self.assertFalse(self.low_val.is_symmetric(tol=1e-8)) def test_fit_to_structure(self): new_fit = self.unfit4.fit_to_structure(self.structure) self.assertArrayAlmostEqual(new_fit, self.fit_r4, 1) def test_is_fit_to_structure(self): self.assertFalse(self.unfit4.is_fit_to_structure(self.structure)) self.assertTrue(self.fit_r3.is_fit_to_structure(self.structure)) self.assertTrue(self.fit_r4.is_fit_to_structure(self.structure)) def test_convert_to_ieee(self): for xtal in self.ieee_data.keys(): orig = TensorBase(self.ieee_data[xtal]['original_tensor']) ieee = TensorBase(self.ieee_data[xtal]['ieee_tensor']) struct = Structure.from_dict(self.ieee_data[xtal]['structure']) diff = np.max(abs(ieee - orig.convert_to_ieee(struct))) err_msg = "{} IEEE conversion failed with max diff {}".format( xtal, diff) self.assertArrayAlmostEqual(ieee, orig.convert_to_ieee(struct), err_msg=err_msg, decimal=3)
class TensorBaseTest(PymatgenTest): def setUp(self): self.vec = TensorBase([1., 0., 0.]) self.rand_rank2 = TensorBase(np.random.randn(3,3)) self.rand_rank3 = TensorBase(np.random.randn(3,3,3)) self.rand_rank4 = TensorBase(np.random.randn(3,3,3,3)) a = 3.14 * 42.5 / 180 self.non_symm = SquareTensor([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.2, 0.5, 0.5]]) self.rotation = SquareTensor([[math.cos(a), 0, math.sin(a)], [0, 1, 0], [-math.sin(a), 0, math.cos(a)]]) self.low_val = TensorBase([[1e-6, 1 + 1e-5, 1e-6], [1 + 1e-6, 1e-6, 1e-6], [1e-7, 1e-7, 1 + 1e-5]]) self.symm_rank2 = TensorBase([[1, 2, 3], [2, 4, 5], [3, 5, 6]]) self.symm_rank3 = TensorBase([[[1, 2, 3], [2, 4, 5], [3, 5, 6]], [[2, 4, 5], [4, 7, 8], [5, 8, 9]], [[3, 5, 6], [5, 8, 9], [6, 9, 10]]]) self.symm_rank4 = TensorBase([[[[1.2, 0.4, -0.92], [0.4, 0.05, 0.11], [-0.92, 0.11, -0.02]], [[0.4, 0.05, 0.11], [0.05, -0.47, 0.09], [0.11, 0.09, -0.]], [[-0.92, 0.11, -0.02], [0.11, 0.09, 0.], [-0.02, 0., -0.3]]], [[[0.4, 0.05, 0.11], [0.05, -0.47, 0.09], [0.11, 0.09, 0.]], [[0.05, -0.47, 0.09], [-0.47, 0.17, 0.62], [0.09, 0.62, 0.3]], [[0.11, 0.09, 0.], [0.09, 0.62, 0.3], [0., 0.3, -0.18]]], [[[-0.92, 0.11, -0.02], [0.11, 0.09, 0.], [-0.02, 0, -0.3]], [[0.11, 0.09, 0.], [0.09, 0.62, 0.3], [0., 0.3, -0.18]], [[-0.02, 0., -0.3], [0., 0.3, -0.18], [-0.3, -0.18, -0.51]]]]) # Structural symmetries tested using BaNiO3 piezo/elastic tensors self.fit_r3 = TensorBase([[[0., 0., 0.03839], [0., 0., 0.], [0.03839, 0., 0.]], [[0., 0., 0.], [0., 0., 0.03839], [0., 0.03839, 0.]], [[6.89822, 0., 0.], [0., 6.89822, 0.], [0., 0., 27.4628]]]) self.fit_r4 = TensorBase([[[[157.9, 0., 0.], [0., 63.1, 0.], [0., 0., 29.4]], [[0., 47.4, 0.], [47.4, 0., 0.], [0., 0., 0.]], [[0., 0., 4.3], [0., 0., 0.], [4.3, 0., 0.]]], [[[0., 47.4, 0.], [47.4, 0., 0.], [0., 0., 0.]], [[63.1, 0., 0.], [0., 157.9, 0.], [0., 0., 29.4]], [[0., 0., 0.], [0., 0., 4.3], [0., 4.3, 0.]]], [[[0., 0., 4.3], [0., 0., 0.], [4.3, 0., 0.]], [[0., 0., 0.], [0., 0., 4.3], [0., 4.3, 0.]], [[29.4, 0., 0.], [0., 29.4, 0.], [0., 0., 207.6]]]]) self.unfit4 = TensorBase([[[[161.26, 0., 0.], [0., 62.76, 0.], [0., 0., 30.18]], [[0., 47.08, 0.], [47.08, 0., 0.], [0., 0., 0.]], [[0., 0., 4.23], [0., 0., 0.], [4.23, 0., 0.]]], [[[0., 47.08, 0.], [47.08, 0., 0.], [0., 0., 0.]], [[62.76, 0., 0.], [0., 155.28, -0.06], [0., -0.06, 28.53]], [[0., 0., 0.], [0., -0.06, 4.44], [0., 4.44, 0.]]], [[[0., 0., 4.23], [0., 0., 0.], [4.23, 0., 0.]], [[0., 0., 0.], [0., -0.06, 4.44], [0., 4.44, 0.]], [[30.18, 0., 0.], [0., 28.53, 0.], [0., 0., 207.57]]]]) self.structure = self.get_structure('BaNiO3') ieee_file_path = os.path.join(test_dir, "ieee_conversion_data.json") with open(ieee_file_path) as f: self.ieee_data = json.load(f) def test_new(self): bad_2 = np.zeros((4, 4)) bad_3 = np.zeros((4, 4, 4)) self.assertRaises(ValueError, TensorBase, bad_2) self.assertRaises(ValueError, TensorBase, bad_3) self.assertEqual(self.rand_rank2.rank, 2) self.assertEqual(self.rand_rank3.rank, 3) self.assertEqual(self.rand_rank4.rank, 4) def test_zeroed(self): self.assertArrayEqual(self.low_val.zeroed(), TensorBase([[0, 1 + 1e-5, 0], [1 + 1e-6, 0, 0], [0, 0, 1 + 1e-5]])) self.assertArrayEqual(self.low_val.zeroed(tol=1e-6), TensorBase([[1e-6, 1 + 1e-5, 1e-6], [1 + 1e-6, 1e-6, 1e-6], [0, 0, 1 + 1e-5]])) self.assertArrayEqual(TensorBase([[1e-6, -30, 1], [1e-7, 1, 0], [1e-8, 0, 1]]).zeroed(), TensorBase([[0, -30, 1], [0, 1, 0], [0, 0, 1]])) def test_transform(self): # Rank 3 tensor = TensorBase(np.arange(0, 27).reshape(3, 3, 3)) symm_op = SymmOp.from_axis_angle_and_translation([0, 0, 1], 30, False, [0, 0, 1]) new_tensor = tensor.transform(symm_op) self.assertArrayAlmostEqual(new_tensor, [[[-0.871, -2.884, -1.928], [-2.152, -6.665, -4.196], [-1.026, -2.830, -1.572]], [[0.044, 1.531, 1.804], [4.263, 21.008, 17.928], [5.170, 23.026, 18.722]], [[1.679, 7.268, 5.821], [9.268, 38.321, 29.919], [8.285, 33.651, 26.000]]], 3) def test_rotate(self): self.assertArrayEqual(self.vec.rotate([[0, -1, 0], [1, 0, 0], [0, 0, 1]]), [0, 1, 0]) self.assertArrayAlmostEqual(self.non_symm.rotate(self.rotation), SquareTensor([[0.531, 0.485, 0.271], [0.700, 0.5, 0.172], [0.171, 0.233, 0.068]]), decimal=3) self.assertRaises(ValueError, self.non_symm.rotate, self.symm_rank2) def test_symmetrized(self): self.assertTrue(self.rand_rank2.symmetrized.is_symmetric()) self.assertTrue(self.rand_rank3.symmetrized.is_symmetric()) self.assertTrue(self.rand_rank4.symmetrized.is_symmetric()) def test_is_symmetric(self): self.assertTrue(self.symm_rank2.is_symmetric()) self.assertTrue(self.symm_rank3.is_symmetric()) self.assertTrue(self.symm_rank4.is_symmetric()) tol_test = self.symm_rank4 tol_test[0, 1, 2, 2] += 1e-6 self.assertFalse(self.low_val.is_symmetric(tol=1e-8)) def test_fit_to_structure(self): new_fit = self.unfit4.fit_to_structure(self.structure) self.assertArrayAlmostEqual(new_fit, self.fit_r4, 1) def test_is_fit_to_structure(self): self.assertFalse(self.unfit4.is_fit_to_structure(self.structure)) self.assertTrue(self.fit_r3.is_fit_to_structure(self.structure)) self.assertTrue(self.fit_r4.is_fit_to_structure(self.structure)) def test_convert_to_ieee(self): for xtal in self.ieee_data.keys(): orig = TensorBase(self.ieee_data[xtal]['original_tensor']) ieee = TensorBase(self.ieee_data[xtal]['ieee_tensor']) struct = Structure.from_dict(self.ieee_data[xtal]['structure']) diff = np.max(abs(ieee - orig.convert_to_ieee(struct))) err_msg = "{} IEEE conversion failed with max diff {}".format( xtal, diff) self.assertArrayAlmostEqual(ieee, orig.convert_to_ieee(struct), err_msg = err_msg, decimal=3)
class SquareTensorTest(PymatgenTest): def setUp(self): self.rand_sqtensor = SquareTensor(np.random.randn(3, 3)) self.symm_sqtensor = SquareTensor([[0.1, 0.3, 0.4], [0.3, 0.5, 0.2], [0.4, 0.2, 0.6]]) self.non_invertible = SquareTensor([[0.1, 0, 0], [0.2, 0, 0], [0, 0, 0]]) self.non_symm = SquareTensor([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.2, 0.5, 0.5]]) self.low_val = SquareTensor([[1e-6, 1 + 1e-5, 1e-6], [1 + 1e-6, 1e-6, 1e-6], [1e-7, 1e-7, 1 + 1e-5]]) self.low_val_2 = SquareTensor([[1e-6, -1 - 1e-6, 1e-6], [1 + 1e-7, 1e-6, 1e-6], [1e-7, 1e-7, 1 + 1e-6]]) a = 3.14 * 42.5 / 180 self.rotation = SquareTensor([[math.cos(a), 0, math.sin(a)], [0, 1, 0], [-math.sin(a), 0, math.cos(a)]]) def test_new(self): non_sq_matrix = [[0.1, 0.2, 0.1], [0.1, 0.2, 0.3], [0.1, 0.2, 0.3], [0.1, 0.1, 0.1]] bad_matrix = [[0.1, 0.2], [0.2, 0.3, 0.4], [0.2, 0.3, 0.5]] too_high_rank = np.zeros((3,3,3)) self.assertRaises(ValueError, SquareTensor, non_sq_matrix) self.assertRaises(ValueError, SquareTensor, bad_matrix) self.assertRaises(ValueError, SquareTensor, too_high_rank) def test_properties(self): # transpose self.assertArrayEqual(self.non_symm.trans, SquareTensor([[0.1, 0.4, 0.2], [0.2, 0.5, 0.5], [0.3, 0.6, 0.5]])) self.assertArrayEqual(self.rand_sqtensor.trans, np.transpose(self.rand_sqtensor)) self.assertArrayEqual(self.symm_sqtensor, self.symm_sqtensor.trans) # inverse self.assertArrayEqual(self.non_symm.inv, np.linalg.inv(self.non_symm)) with self.assertRaises(ValueError): self.non_invertible.inv # determinant self.assertEqual(self.rand_sqtensor.det, np.linalg.det(self.rand_sqtensor)) self.assertEqual(self.non_invertible.det, 0.0) self.assertEqual(self.non_symm.det, 0.009) # symmetrized self.assertArrayEqual(self.rand_sqtensor.symmetrized, 0.5 * (self.rand_sqtensor + self.rand_sqtensor.trans)) self.assertArrayEqual(self.symm_sqtensor, self.symm_sqtensor.symmetrized) self.assertArrayAlmostEqual(self.non_symm.symmetrized, SquareTensor([[0.1, 0.3, 0.25], [0.3, 0.5, 0.55], [0.25, 0.55, 0.5]])) # invariants i1 = np.trace(self.rand_sqtensor) i2 = self.rand_sqtensor[0, 0] * self.rand_sqtensor[1, 1] + \ self.rand_sqtensor[1, 1] * self.rand_sqtensor[2, 2] + \ self.rand_sqtensor[2, 2] * self.rand_sqtensor[0, 0] - \ self.rand_sqtensor[0, 1] * self.rand_sqtensor[1, 0] - \ self.rand_sqtensor[0, 2] * self.rand_sqtensor[2, 0] - \ self.rand_sqtensor[2, 1] * self.rand_sqtensor[1, 2] i3 = np.linalg.det(self.rand_sqtensor) self.assertArrayAlmostEqual([i1, i2, i3], self.rand_sqtensor.principal_invariants) def test_is_rotation(self): self.assertTrue(self.rotation.is_rotation()) self.assertFalse(self.symm_sqtensor.is_rotation()) self.assertTrue(self.low_val_2.is_rotation()) self.assertFalse(self.low_val_2.is_rotation(tol=1e-8)) def test_get_scaled(self): self.assertArrayEqual(self.non_symm.get_scaled(10.), SquareTensor([[1, 2, 3], [4, 5, 6], [2, 5, 5]])) def test_polar_decomposition(self): u, p = self.rand_sqtensor.polar_decomposition() self.assertArrayAlmostEqual(np.dot(u, p), self.rand_sqtensor) self.assertArrayAlmostEqual(np.eye(3), np.dot(u, np.conjugate(np.transpose(u))))