def test_outer_shape(self): """ Test assigning `ndim_outer` != 0 is implemented correctly. """ # Outer shape (shared by axes and angles): out_shp = (3, 2) axes_multiple = np.random.random(out_shp + (3, )) ang_multiple = (np.random.random(out_shp) - 0.5) * 180 params_multiple = { 'rot_ax': axes_multiple, 'ang': ang_multiple, 'axis': -1, 'ndim_outer': len(out_shp), 'degrees': True, } params_single = { 'rot_ax': axes_multiple[0, 0], 'ang': ang_multiple[0, 0], 'axis': -1, 'ndim_outer': 0, 'degrees': True, } rot_mat_mult = rotation.axang2rotmat(**params_multiple) rot_mat_sing = rotation.axang2rotmat(**params_single) self.assertTrue(np.allclose(rot_mat_mult[0, 0], rot_mat_sing))
def test_degrees_to_radians(self): """ Test identical rotation matrices are generated whether angles are input in degrees or radians. """ axis = np.random.random((3, )) ang_deg = np.random.random() * 180 ang_rad = np.deg2rad(ang_deg) rot_deg = rotation.axang2rotmat(axis, ang_deg, degrees=True) rot_rad = rotation.axang2rotmat(axis, ang_rad, degrees=False) self.assertTrue(np.allclose(rot_deg, rot_rad))
def get_load_case_random_3D(total_time, num_increments, target_strain, rotation=True, rotation_max_angle=10, rotation_load_case=True, non_random_rotation=None, dump_frequency=1): # Five stretch components, since it's a symmetric matrix and the trace must be zero: stretch_comps = (np.random.random((5, )) - 0.5) * target_strain stretch = np.zeros((3, 3)) * np.nan # Diagonal comps: stretch[[0, 1], [0, 1]] = stretch_comps[:2] stretch[2, 2] = -(stretch[0, 0] + stretch[1, 1]) # Off-diagonal comps: stretch[[1, 0], [0, 1]] = stretch_comps[2] stretch[[2, 0], [0, 2]] = stretch_comps[3] stretch[[1, 2], [2, 1]] = stretch_comps[4] # Add the identity: U = stretch + np.eye(3) defgrad = U rot = None if rotation and non_random_rotation is None: rot = get_random_rotation_matrix(method='axis_angle', max_angle_deg=rotation_max_angle) if not rotation_load_case: defgrad = rot @ U rot = None if non_random_rotation: rot = axang2rotmat(np.array(non_random_rotation['axis']), non_random_rotation['angle_deg'], degrees=True) # Ensure defgrad has a unit determinant: defgrad = defgrad / (np.linalg.det(defgrad)**(1 / 3)) dg_arr = np.ma.masked_array(defgrad, mask=np.zeros((3, 3), dtype=int)) stress_arr = np.ma.masked_array(np.zeros((3, 3), dtype=int), mask=np.ones((3, 3), dtype=int)) load_case = { 'total_time': total_time, 'num_increments': num_increments, 'def_grad_aim': dg_arr, 'stress': stress_arr, 'rotation': rot, 'dump_frequency': dump_frequency, } return load_case
def test_broadcast_axes(self): """ Test a single axis can be broadcast correctly over multiple angles. """ params_multiple = { 'rot_ax': np.random.random((3, )), 'ang': (np.random.random((2, )) - 0.5) * 180, 'axis': 0, 'ndim_outer': 0, 'degrees': True, } params_single = copy.deepcopy(params_multiple) params_single['ang'] = params_single['ang'][0] rot_mat_mult = rotation.axang2rotmat(**params_multiple) rot_mat_sing = rotation.axang2rotmat(**params_single) self.assertTrue(np.allclose(rot_mat_mult[0], rot_mat_sing))
def test_zero_rotation(self): """ Test a rotation about a random axis by zero degrees generates the identity matrix. """ params = { 'rot_ax': np.random.random((3, )), 'ang': 0, 'axis': 0, 'ndim_outer': 0, 'degrees': True, } rot_mat = rotation.axang2rotmat(**params) self.assertTrue(np.allclose(rot_mat, np.eye(3)))
def test_known_rotation(self): """ Test the generated rotation matrix for a known rotation (90 degrees about the z-axis) is correct. """ axis = np.array([0, 0, 1]) ang = np.pi / 2 rot = rotation.axang2rotmat(axis, ang) known_rot = np.array([ [0, -1, 0], [1, 0, 0], [0, 0, 1], ]) self.assertTrue(np.allclose(rot, known_rot))
def test_always_positive_angle(self): """ Test conversion from axis-angle -> matrix -> axis-angle with negative angle flips the axis and angle signs, so the output angle is positive. """ axis_1 = np.random.random((3, 1)) axis_1_norm = axis_1 / np.linalg.norm(axis_1, axis=0) ang_1 = (np.random.random() - 1) * 180 params = { 'rot_ax': axis_1, 'ang': ang_1, 'axis': 0, 'ndim_outer': 0, 'degrees': True, } rot_mat = rotation.axang2rotmat(**params) axis_2, ang_2 = rotation.rotmat2axang(rot_mat, degrees=True) axis_2_norm = axis_2 / np.linalg.norm(axis_2, axis=0) axis_equal = np.allclose(axis_1_norm, -axis_2_norm) angle_equal = np.isclose(ang_1, -ang_2) self.assertTrue(axis_equal and angle_equal)
def test_axis_angle_consistent(self): """ Test conversion from axis-angle -> matrix -> axis-angle with positive angles are consistent. """ axis_1 = np.random.random((3, 1)) axis_1_norm = axis_1 / np.linalg.norm(axis_1, axis=0) ang_1 = np.random.random() * 180 params = { 'rot_ax': axis_1, 'ang': ang_1, 'axis': 0, 'ndim_outer': 0, 'degrees': True, } rot_mat = rotation.axang2rotmat(**params) axis_2, ang_2 = rotation.rotmat2axang(rot_mat, degrees=True) axis_2_norm = axis_2 / np.linalg.norm(axis_2, axis=0) axis_equal = np.allclose(axis_1_norm, axis_2_norm) angle_equal = np.isclose(ang_1, ang_2) self.assertTrue(axis_equal and angle_equal)
def bicrystal_from_csl_vectors(crystal_structure, csl_vecs, box_csl=None, gb_type=None, gb_size=None, edge_conditions=None, overlap_tol=None, reorient=True, wrap=True, maintain_inv_sym=False, boundary_vac=None, relative_shift=None): """ Parameters ---------- crystal_structure : dict or CrystalStructure csl_vecs : list of length 2 of ndarray of shape (3, 3) List of two arrays of three column vectors representing CSL vectors in the lattice basis. The two CSL unit cells defined here rotate onto each other by the CSL rotation angle. The rotation axis is taken as the third vector, which must therefore be the same for both CSL unit cells. box_csl : ndarray of shape (3, 3), optional The linear combination of CSL unit vectors defined in `csl_vecs` used to construct each half of the bicrystal. The first two columns represent vectors defining the boundary plane. The third column represents a vector in the out-of-boundary direction. Only one of `box_csl` and `gb_type` may be specified. gb_type : str, optional Default is None. Must be one of 'tilt_A', 'tilt_B', 'twist' or 'mixed_A'. Only one of `box_csl` and `gb_type` may be specified. gb_size : ndarray of shape (3,) of int, optional If `gb_type` is specified, the unit grain vectors associated with that `gb_type` are scaled by these integers. Default is None, in which case it is set to np.array([1, 1, 1]). edge_conditions : list of list of str Edge conditions for each grain in the bicrystal. See `CrystalBox` for details. maintain_inv_sym : bool, optional If True, the supercell atoms will be checked for inversion symmetry through the centres of both crystals. This check will be repeated following methods which manipulate atom positions. In this way, the two grain boundaries in the bicrystal are ensured to be identical. reorient : bool, optional If True, after construction of the boundary, reorient_to_lammps() is invoked. Default is True. boundary_vac_args : dict, optional If not None, after construction of the boundary, apply_boundary_vac() is invoked with this dict as keyword arguments. Default is None. boundary_vac_flat_args : dict, optional If not None, after construction of the boundary, apply_boundary_vac_flat() is invoked with this dict as keyword arguments. Default is None. boundary_vac_linear_args : dict, optional If not None, after construction of the boundary, apply_boundary_vac_linear() is invoked with this dict as keyword arguments. Default is None. relative_shift_args : dict, optional If not None, after construction of the boundary, apply_relative_shift() is invoked with this dict as keyword arguments. Default is None. wrap : bool, optional If True, after construction of the boundary, wrap_atoms_to_supercell() is invoked. Default is True. Notes ----- Algorithm proceeds as follows: 1. Apply given linear combinations of given CSL unit vectors to form grain vectors of the bicrystal. 2. Multiply the out-of-boundary vector of the second grain by -1, such that rotation of the second grain by the CSL rotation angle will form a bicrystal of two grains. 3. Check grain A is formed from a right-hand basis - since we want the supercell vectors to be formed from a right-hand basis. If not, for both grain A and B, swap the first and second vectors to do this. 4. Fill the two grains with atoms 5. Rotate B onto A?? TODO: - Sort out lattice sites in apply_boundary_vac() & apply_relative_shift() - Rename wrap_atoms_to_supercell to wrap_to_supercell and apply wrapping to lattice sites, and crystal boxes as well. """ # Generate CrystalStructure if necessary: cs = CrystalStructure.init_crystal_structures([crystal_structure])[0] box_csl = np.array(box_csl) csl_vecs = np.array(csl_vecs) # print('csl_vecs: {}'.format(csl_vecs)) if np.all(csl_vecs[0][:, 2] != csl_vecs[1][:, 2]): raise ValueError('Third vectors in `csl_vecs[0]` and csl_vecs[1] ' 'represent the CSL rotation axis and must ' 'therefore be equal.') if box_csl is not None and gb_type is not None: raise ValueError('Only one of `box_csl` and `gb_type` may be ' 'specified.') if box_csl is None and gb_type is None: raise ValueError('Exactly one of `box_csl` and `gb_type` must be ' 'specified.') if gb_type is not None: if gb_size is None: gb_size = np.array([1, 1, 1]) if gb_type not in CSL_FROM_PARAMS_GB_TYPES: raise ValueError('Invalid `gb_type`: {}. Must be one of {}'.format( gb_type, list(CSL_FROM_PARAMS_GB_TYPES.keys()))) box_csl = CSL_FROM_PARAMS_GB_TYPES.get(gb_type) * gb_size lat_vecs = cs.lattice.unit_cell rot_ax_std = np.dot(lat_vecs, csl_vecs[0][:, 2:3]) csl_vecs_std = [np.dot(lat_vecs, c) for c in csl_vecs] # Non-boundary (column) index of `box_csl` and grain arrays: non_boundary_idx = 2 # Enforce a rule that out of boundary grain vector has to be # (a multiple of) a single CSL unit vector. This reduces the # potential "skewness" of the supercell. if np.count_nonzero(box_csl[:, non_boundary_idx]) > 1: raise ValueError('The out of boundary vector, `box_csl[:, {}]`' ' must have exactly one non-zero ' 'element.'.format(non_boundary_idx)) # Scale grains in lattice basis grn_a_lat = np.dot(csl_vecs[0], box_csl) grn_b_lat = np.dot(csl_vecs[1], box_csl) grn_b_lat[:, non_boundary_idx] *= -1 # Get grain vectors in standard Cartesian basis grn_a_std = np.dot(lat_vecs, grn_a_lat) grn_b_std = np.dot(lat_vecs, grn_b_lat) # Get rotation matrix for rotating grain B onto grain A if np.all(csl_vecs[0] == csl_vecs[1]): rot_angles = [0, 0, 0] rot_mat = np.eye(3) else: rot_angles = vecpair_angle(*csl_vecs_std, axis=0) # print('rot_angles: {}'.format(np.rad2deg(rot_angles))) # print('rot_ax_std: {}'.format(rot_ax_std)) if not np.isclose(*rot_angles[0:2]): raise ValueError('Non-equivalent rotation angles found between CSL' ' vectors.') rot_mat = axang2rotmat(rot_ax_std[:, 0], rot_angles[0]) grn_vols = [ np.dot(np.cross(g[:, 0], g[:, 1]), g[:, 2]) for g in (grn_a_std, grn_b_std) ] # print('grn_vols: {}'.format(grn_vols)) # Check grain volumes are the same: if not np.isclose(*np.abs(grn_vols)): raise ValueError('Grain A and B have different volumes.') # Check if grain A forms a right-handed coordinate system: if grn_vols[0] < 0: # Swap boundary vectors to make a right-handed coordinate system: grn_a_lat[:, [0, 1]] = grn_a_lat[:, [1, 0]] grn_b_lat[:, [0, 1]] = grn_b_lat[:, [1, 0]] grn_a_std[:, [0, 1]] = grn_a_std[:, [1, 0]] grn_b_std[:, [0, 1]] = grn_b_std[:, [1, 0]] box_csl[0, 1] = box_csl[1, 0] # Specify bounding box edge conditions for including atoms: if edge_conditions is None: edge_conditions = [['10', '10', '10'], ['10', '10', '10']] edge_conditions[1][non_boundary_idx] = '01' # print('grn_a_std: \n{}'.format(grn_a_std)) # print('grn_b_std: \n{}'.format(grn_b_std)) # Make two crystal boxes: crys_a = CrystalBox(cs, grn_a_std, edge_conditions=edge_conditions[0]) crys_b = CrystalBox(cs, grn_b_std, edge_conditions=edge_conditions[1]) for sites in list(crys_a.sites.values()) + list(crys_b.sites.values()): sites.basis = None # Rotate crystal B onto A: crys_b.rotate(rot_mat) # Shift crystals to form a supercell at the origin zero_shift = -crys_b.box_vecs[:, non_boundary_idx][:, None] crys_a.translate(zero_shift) crys_b.translate(zero_shift) # Define the supercell: sup_std = np.copy(crys_a.box_vecs) sup_std[:, non_boundary_idx] = (crys_a.box_vecs[:, non_boundary_idx] - crys_b.box_vecs[:, non_boundary_idx]) # AtomisticStructure parameters as_params = { 'supercell': sup_std, 'crystals': [crys_a, crys_b], } # Bicrystal parameters bc_params = { 'as_params': as_params, 'maintain_inv_sym': maintain_inv_sym, 'reorient': reorient, 'boundary_vac': boundary_vac, 'relative_shift': relative_shift, 'wrap': wrap, 'non_boundary_idx': 2, 'rot_mat': rot_mat, 'overlap_tol': overlap_tol, } return Bicrystal(**bc_params)
def get_load_case_planar_2D(total_time, num_increments, normal_direction, target_strain_rate=None, target_strain=None, rotation=None, dump_frequency=1): """Get a planar 2D load case. Parameters ---------- total_time : float or int num_increments : int normal_direction : str A single character, "x", "y" or "z", representing the loading plane normal direction. target_strain_rate : (nested) list of float or ndarray of shape (2, 2) Target deformation gradient rate components. Either a 2D array, nested list, or a flat list. If passed as a flat list, the first and fourth elements correspond to the normal components of the deformation gradient rate tensor. The second element corresponds to the first-row, second-column (shear) component and the third element corresponds to the second-row, first-column (shear) component. target_strain : (nested) list of float or ndarray of shape (2, 2) Target deformation gradient components. Either a 2D array, nested list, or a flat list. If passed as a flat list, the first and fourth elements correspond to the normal components of the deformation gradient tensor. The second element corresponds to the first-row, second-column (shear) component and the third element corresponds to the second-row, first-column (shear) component. rotation : dict, optional Dict to specify a rotation of the loading direction. With keys: axis : ndarray of shape (3) or list of length 3 Axis of rotation. angle_deg : float Angle of rotation about `axis` in degrees. dump_frequency : int, optional By default, 1, meaning results are written out every increment. Returns ------- dict Dict representing the load case, with keys: normal_direction : str Passed through from input argument. rotation : dict Passed through from input argument. total_time : float or int Passed through from input argument. num_increments : int Passed through from input argument. rotation_matrix : ndarray of shape (3, 3), optional If `rotation` was specified, this will be the matrix representation of `rotation`. def_grad_rate : numpy.ma.core.MaskedArray of shape (3, 3), optional Deformation gradient rate tensor. Not None if target_strain_rate is specified. Masked values correspond to unmasked values in `stress`. def_grad_aim : numpy.ma.core.MaskedArray of shape (3, 3), optional Deformation gradient aim tensor. Not None if target_strain is specified. Masked values correspond to unmasked values in `stress`. stress : numpy.ma.core.MaskedArray of shape (3, 3) Stress tensor. Masked values correspond to unmasked values in `def_grad_rate` or `def_grad_aim`. dump_frequency : int, optional Passed through from input argument. """ # Validation: msg = 'Specify either `target_strain_rate` or `target_strain`.' if all([t is None for t in [target_strain_rate, target_strain]]): raise ValueError(msg) if all([t is not None for t in [target_strain_rate, target_strain]]): raise ValueError(msg) if rotation: rot_mat = axang2rotmat(np.array(rotation['axis']), rotation['angle_deg'], degrees=True) else: rot_mat = None if target_strain_rate is not None: def_grad_vals = target_strain_rate else: def_grad_vals = target_strain # Flatten list/array: if isinstance(def_grad_vals, list): if isinstance(def_grad_vals[0], list): def_grad_vals = [j for i in def_grad_vals for j in i] elif isinstance(def_grad_vals, np.ndarray): def_grad_vals = def_grad_vals.flatten() dir_idx = ['x', 'y', 'z'] try: normal_dir_idx = dir_idx.index(normal_direction) except ValueError: msg = ( f'Normal direction "{normal_direction}" not allowed. It should be one of ' f'"x", "y" or "z".') raise ValueError(msg) loading_col_idx = list({0, 1, 2} - {normal_dir_idx}) dg_arr = np.ma.masked_array(np.zeros((3, 3)), mask=np.zeros((3, 3))) stress_arr = np.ma.masked_array(np.zeros((3, 3)), mask=np.zeros((3, 3))) dg_row_idx = [ loading_col_idx[0], loading_col_idx[0], loading_col_idx[1], loading_col_idx[1], ] dg_col_idx = [ loading_col_idx[0], loading_col_idx[1], loading_col_idx[0], loading_col_idx[1], ] dg_arr[dg_row_idx, dg_col_idx] = def_grad_vals dg_arr.mask[:, normal_dir_idx] = True stress_arr.mask[:, loading_col_idx] = True def_grad_aim = dg_arr if target_strain is not None else None def_grad_rate = dg_arr if target_strain_rate is not None else None load_case = { 'normal_direction': normal_direction, 'rotation': rotation, 'total_time': total_time, 'num_increments': num_increments, 'rotation_matrix': rot_mat, 'def_grad_rate': def_grad_rate, 'def_grad_aim': def_grad_aim, 'stress': stress_arr, 'dump_frequency': dump_frequency, } return check_load_case(load_case)
def get_load_case_uniaxial(total_time, num_increments, direction, target_strain_rate=None, target_strain=None, rotation=None, dump_frequency=1): """Get a uniaxial load case. Parameters ---------- total_time : float or int num_increments : int direction : str Either a single character, "x", "y" or "z", representing the loading direction. target_strain_rate : float Target strain rate to apply along the loading direction. target_strain : float Target strain to achieve along the loading direction. rotation : dict, optional Dict to specify a rotation of the loading direction. With keys: axis : ndarray of shape (3) or list of length 3 Axis of rotation. angle_deg : float Angle of rotation about `axis` in degrees. dump_frequency : int, optional By default, 1, meaning results are written out every increment. Returns ------- dict Dict representing the load case, with keys: direction : str Passed through from input argument. rotation : dict Passed through from input argument. total_time : float or int Passed through from input argument. num_increments : int Passed through from input argument. rotation_matrix : ndarray of shape (3, 3), optional If `rotation` was specified, this will be the matrix representation of `rotation`. def_grad_rate : numpy.ma.core.MaskedArray of shape (3, 3), optional Deformation gradient rate tensor. Not None if target_strain_rate is specified. Masked values correspond to unmasked values in `stress`. def_grad_aim : numpy.ma.core.MaskedArray of shape (3, 3), optional Deformation gradient aim tensor. Not None if target_strain is specified. Masked values correspond to unmasked values in `stress`. stress : numpy.ma.core.MaskedArray of shape (3, 3) Stress tensor. Masked values correspond to unmasked values in `def_grad_rate` or `def_grad_aim`. dump_frequency : int, optional Passed through from input argument. """ # Validation: msg = 'Specify either `target_strain_rate` or `target_strain`.' if all([t is None for t in [target_strain_rate, target_strain]]): raise ValueError(msg) if all([t is not None for t in [target_strain_rate, target_strain]]): raise ValueError(msg) if rotation: rot_mat = axang2rotmat(np.array(rotation['axis']), rotation['angle_deg'], degrees=True) else: rot_mat = None if target_strain_rate is not None: def_grad_val = target_strain_rate else: def_grad_val = target_strain dir_idx = ['x', 'y', 'z'] try: loading_dir_idx = dir_idx.index(direction) except ValueError: msg = ( f'Loading direction "{direction}" not allowed. It should be one of "x", ' f'"y" or "z".') raise ValueError(msg) dg_arr = np.ma.masked_array(np.zeros((3, 3)), mask=np.eye(3)) stress_arr = np.ma.masked_array(np.zeros((3, 3)), mask=np.logical_not(np.eye(3))) dg_arr[loading_dir_idx, loading_dir_idx] = def_grad_val dg_arr.mask[loading_dir_idx, loading_dir_idx] = False stress_arr.mask[loading_dir_idx, loading_dir_idx] = True def_grad_aim = dg_arr if target_strain is not None else None def_grad_rate = dg_arr if target_strain_rate is not None else None load_case = { 'direction': direction, 'rotation': rotation, 'total_time': total_time, 'num_increments': num_increments, 'rotation_matrix': rot_mat, 'def_grad_rate': def_grad_rate, 'def_grad_aim': def_grad_aim, 'stress': stress_arr, 'dump_frequency': dump_frequency, } return check_load_case(load_case)
def get_load_case_plane_strain(total_time, num_increments, direction, target_strain_rate=None, target_strain=None, rotation=None, dump_frequency=1, strain_rate_mode=None): """Get a plane-strain load case. Parameters ---------- total_time : float or int num_increments : int direction : str String of two characters, ij, where {i,j} ∈ {"x","y","z"}. The first character, i, corresponds to the loading direction and the second, j, corresponds to the zero-strain direction. Zero stress will be specified on the remaining direction. target_strain_rate : float Target strain rate to apply along the loading direction. target_strain : float Target strain to achieve along the loading direction. rotation : dict, optional Dict to specify a rotation of the loading direction. With keys: axis : ndarray of shape (3) or list of length 3 Axis of rotation. angle_deg : float Angle of rotation about `axis` in degrees. dump_frequency : int, optional By default, 1, meaning results are written out every increment. strain_rate_mode : str, optional One of "F_rate", "L", "L_approx". If not specified, default is "F_rate". Use "L_approx" for specifying non-mixed boundary conditions. Returns ------- dict Dict representing the load case, with keys: direction : str Passed through from input argument. rotation : dict Passed through from input argument. total_time : float or int Passed through from input argument. num_increments : int Passed through from input argument. rotation_matrix : ndarray of shape (3, 3), optional If `rotation` was specified, this will be the matrix representation of `rotation`. def_grad_rate : numpy.ma.core.MaskedArray of shape (3, 3), optional Deformation gradient rate tensor. Not None if target_strain_rate is specified and `strain_rate_mode` is None or "F_rate". Masked values correspond to unmasked values in `stress`. def_grad_aim : numpy.ma.core.MaskedArray of shape (3, 3), optional Deformation gradient aim tensor. Not None if target_strain is specified. Masked values correspond to unmasked values in `stress`. vel_grad : (ndarray or numpy.ma.core.MaskedArray) of shape (3, 3), optional Velocity gradient aim tensor. Not None if `strain_rate_mode` is one of "L" (will be a masked array) or "L_approx" (will be an ordinary array). stress : numpy.ma.core.MaskedArray of shape (3, 3) Stress tensor. Masked values correspond to unmasked values in `def_grad_rate` or `def_grad_aim`. dump_frequency : int, optional Passed through from input argument. """ # Validation: msg = 'Specify either `target_strain_rate` or `target_strain`.' if all([t is None for t in [target_strain_rate, target_strain]]): raise ValueError(msg) if all([t is not None for t in [target_strain_rate, target_strain]]): raise ValueError(msg) if strain_rate_mode is None: strain_rate_mode = 'F_rate' if strain_rate_mode not in ['F_rate', 'L', 'L_approx']: msg = 'Strain rate mode must be `F_rate`, `L` or `L_approx`.' raise ValueError(msg) if strain_rate_mode in ['L', 'L_approx'] and target_strain_rate is None: msg = (f'`target_strain_rate` must be specified for `strain_rate_mode`' f'`{strain_rate_mode}`') raise ValueError(msg) if rotation: rot_mat = axang2rotmat(np.array(rotation['axis']), rotation['angle_deg'], degrees=True) else: rot_mat = None if target_strain_rate is not None: def_grad_val = target_strain_rate else: def_grad_val = target_strain dir_idx = ['x', 'y', 'z'] loading_dir, zero_strain_dir = direction try: loading_dir_idx = dir_idx.index(loading_dir) except ValueError: msg = ( f'Loading direction "{loading_dir}" not allowed. It should be one of "x", ' f'"y" or "z".') raise ValueError(msg) if zero_strain_dir not in dir_idx: msg = ( f'Zero-strain direction "{zero_strain_dir}" not allowed. It should be one ' f'of "x", "y" or "z".') raise ValueError(msg) zero_stress_dir = list(set(dir_idx) - {loading_dir, zero_strain_dir})[0] zero_stress_dir_idx = dir_idx.index(zero_stress_dir) dg_arr = np.ma.masked_array(np.zeros((3, 3)), mask=np.zeros((3, 3))) stress_arr = np.ma.masked_array(np.zeros((3, 3)), mask=np.ones((3, 3))) dg_arr[loading_dir_idx, loading_dir_idx] = def_grad_val if strain_rate_mode == 'L': # When using L with mixed BCs, each row must be either L or P: dg_arr.mask[zero_stress_dir_idx] = True stress_arr.mask[zero_stress_dir_idx] = False elif strain_rate_mode == 'L_approx': dg_arr = dg_arr.data # No need for a masked array # Without mixed BCs, we can get volume conservation with Trace(L) = 0: dg_arr[zero_stress_dir_idx, zero_stress_dir_idx] = -def_grad_val stress_arr = None elif strain_rate_mode == 'F_rate': dg_arr.mask[zero_stress_dir_idx, zero_stress_dir_idx] = True stress_arr.mask[zero_stress_dir_idx, zero_stress_dir_idx] = False if strain_rate_mode in ['L', 'L_approx']: def_grad_aim = None def_grad_rate = None vel_grad = dg_arr else: def_grad_aim = dg_arr if target_strain is not None else None def_grad_rate = dg_arr if target_strain_rate is not None else None vel_grad = None load_case = { 'direction': direction, 'rotation': rotation, 'total_time': total_time, 'num_increments': num_increments, 'rotation_matrix': rot_mat, 'def_grad_rate': def_grad_rate, 'def_grad_aim': def_grad_aim, 'vel_grad': vel_grad, 'stress': stress_arr, 'dump_frequency': dump_frequency, } return check_load_case(load_case)