def __init__( self, position_start, position_end, director_start, director_end, twisting_time, slack, number_of_rotations, ): """ Parameters ---------- position_start : numpy.ndarray 2D (dim, 1) array containing data with 'float' type. Initial position of first node. position_end : numpy.ndarray 2D (dim, 1) array containing data with 'float' type. Initial position of last node. director_start : numpy.ndarray 3D (dim, dim, blocksize) array containing data with 'float' type. Initial director of first element. director_end : numpy.ndarray 3D (dim, dim, blocksize) array containing data with 'float' type. Initial director of last element. twisting_time : float Time to complete twist. slack : float Slack applied to rod. number_of_rotations : float Number of rotations applied to rod. """ FreeRod.__init__(self) self.twisting_time = twisting_time angel_vel_scalar = (2.0 * number_of_rotations * np.pi / self.twisting_time) / 2.0 shrink_vel_scalar = slack / (self.twisting_time * 2.0) direction = (position_end - position_start ) / np.linalg.norm(position_end - position_start) self.final_start_position = position_start + slack / 2.0 * direction self.final_end_position = position_end - slack / 2.0 * direction self.ang_vel = angel_vel_scalar * direction self.shrink_vel = shrink_vel_scalar * direction theta = number_of_rotations * np.pi self.final_start_directors = (_get_rotation_matrix( theta, direction.reshape(3, 1)).reshape(3, 3) @ director_start ) # rotation_matrix wants vectors 3,1 self.final_end_directors = (_get_rotation_matrix( -theta, direction.reshape(3, 1)).reshape(3, 3) @ director_end ) # rotation_matrix wants vectors 3,1
def test_get_rotation_matrix_correctness_across_blocksizes(blocksize): dim = 3 dt = np.random.random_sample() vector_collection = np.random.randn(dim).reshape(-1, 1) # No need for copying the vector collection here, as we now create # new arrays inside correct_rot_mat_collection = _get_rotation_matrix(dt, vector_collection) correct_rot_mat_collection = np.tile(correct_rot_mat_collection, blocksize) # Construct test_vector_collection = np.tile(vector_collection, blocksize) test_rot_mat_collection = _get_rotation_matrix(dt, test_vector_collection) assert test_rot_mat_collection.shape == (3, 3, blocksize) assert_allclose(test_rot_mat_collection, correct_rot_mat_collection)
def test_get_rotation_matrix_correctness_against_canned_example(): """ Computer test code at page 5-7 from "Rotation, Reflection, and Frame Changes Orthogonal tensors in computational engineering mechanics" by Rebecca Brennen, 2018, IOP Returns ------- """ vector_collection = np.array([1, 3.2, 7]) vector_collection /= np.linalg.norm(vector_collection) vector_collection = vector_collection.reshape(-1, 1) theta = np.deg2rad(76.0) test_rot_mat = _get_rotation_matrix(theta, vector_collection) # Previous correct matrix, which did not have a transpose """ correct_rot_mat = np.array( [ [0.254506, -0.834834, 0.488138], [0.915374, 0.370785, 0.156873], [-0.311957, 0.406903, 0.858552], ] ).reshape(3, 3, 1) """ # Transpose for similar reasons mentioned before correct_rot_mat = np.array([ [0.254506, -0.834834, 0.488138], [0.915374, 0.370785, 0.156873], [-0.311957, 0.406903, 0.858552], ]).T.reshape(3, 3, 1) assert_allclose(test_rot_mat, correct_rot_mat, atol=1e-6)
def __iadd__(self, scaled_deriv_array): """ overloaded += operator The add for directors is customized to reflect Rodrigues' rotation formula. Parameters ---------- scaled_deriv_array : numpy.ndarray containing dt * (v, ω), as retured from _DynamicState's `kinematic_rates` method Returns ------- self : _KinematicState instance with inplace modified data Note ------- Takes a numpy.ndarray and not a _KinematicState object (as one expects). This is done for efficiency reasons, see _DynamicState's `kinematic_rates` method """ # x += v*dt self.position_collection += scaled_deriv_array[..., :self.n_nodes] # TODO Avoid code repeat # Devs : see `_State.__iadd__` for reasons why we do matmul here # print(_get_rotation_matrix(1.0, scaled_deriv_array[..., self.n_nodes:])) np.einsum( "ijk,jlk->ilk", _get_rotation_matrix(1.0, scaled_deriv_array[..., self.n_nodes:]), self.director_collection.copy(), out=self.director_collection, ) return self
def overload_operator_kinematic_numba( n_nodes, prefac, position_collection, director_collection, velocity_collection, omega_collection, ): """overloaded += operator The add for directors is customized to reflect Rodrigues' rotation formula. Parameters ---------- scaled_deriv_array : np.ndarray containing dt * (v, ω), as retured from _DynamicState's `kinematic_rates` method Returns ------- self : _KinematicState instance with inplace modified data Caveats ------- Takes a np.ndarray and not a _KinematicState object (as one expects). This is done for efficiency reasons, see _DynamicState's `kinematic_rates` method """ # x += v*dt for i in range(3): for k in range(n_nodes): position_collection[i, k] += prefac * velocity_collection[i, k] rotation_matrix = _get_rotation_matrix(1.0, prefac * omega_collection) director_collection[:] = _batch_matmul(rotation_matrix, director_collection) return
def __iadd__(self, scaled_deriv_array): """overloaded += operator The add for directors is customized to reflect Rodrigues' rotation formula. Parameters ---------- scaled_deriv_array : np.ndarray containing dt * (v, ω), as retured from _DynamicState's `kinematic_rates` method Returns ------- self : _KinematicState instance with inplace modified data Caveats ------- Takes a np.ndarray and not a _KinematicState object (as one expects). This is done for efficiency reasons, see _DynamicState's `kinematic_rates` method """ velocity_collection = scaled_deriv_array[0] omega_collection = scaled_deriv_array[1] # x += v*dt self.position_collection += velocity_collection # Devs : see `_State.__iadd__` for reasons why we do matmul here np.einsum( "ijk,jlk->ilk", _get_rotation_matrix(1.0, omega_collection), self.director_collection.copy(), out=self.director_collection, ) return self
def __init__( self, position_start, position_end, director_start, director_end, twisting_time, time_twis_start, number_of_rotations, ): FreeRod.__init__(self) self.twisting_time = twisting_time self.time_twis_start = time_twis_start theta = 2.0 * number_of_rotations * np.pi angel_vel_scalar = theta / self.twisting_time direction = -(position_end - position_start) / np.linalg.norm( position_end - position_start) axis_of_rotation_in_material_frame = director_end @ direction axis_of_rotation_in_material_frame /= np.linalg.norm( axis_of_rotation_in_material_frame) self.final_end_directors = (_get_rotation_matrix( -theta, axis_of_rotation_in_material_frame.reshape(3, 1)).reshape( 3, 3) @ director_end) # rotation_matrix wants vectors 3,1 self.ang_vel = angel_vel_scalar * axis_of_rotation_in_material_frame self.position_start = position_start self.director_start = director_start
def test_get_rotation_matrix_gives_unit_determinant(): dim = 3 blocksize = 16 dt = np.random.random_sample() test_rot_mat_collection = _get_rotation_matrix( dt, np.random.randn(dim, blocksize)) test_det_collection = np.linalg.det(test_rot_mat_collection.T) correct_det_collection = 1.0 + 0.0 * test_det_collection assert_allclose(correct_det_collection, test_det_collection)
def test_block_structure_kinematic_update(n_rods): """ This function is testing validity __iadd__ operation of kinematic_states. Parameters ---------- n_rods Returns ------- """ world_rods = [ MockRod(np.random.randint(10, 30 + 1)) for _ in range(n_rods) ] block_structure = BlockStructureWithSymplecticStepper(world_rods) position = block_structure.position_collection.copy() velocity = block_structure.velocity_collection.copy() directors = block_structure.director_collection.copy() omega = block_structure.omega_collection.copy() prefac = np.random.randn() correct_position = position + prefac * velocity correct_director = np.zeros(directors.shape) np.einsum( "ijk,jlk->ilk", _get_rotation_matrix(1.0, prefac * omega), directors.copy(), out=correct_director, ) # block_structure.kinematic_states += block_structure.kinematic_rates(0, prefac) overload_operator_kinematic_numba( block_structure.n_nodes, prefac, block_structure.position_collection, block_structure.director_collection, block_structure.velocity_collection, block_structure.omega_collection, ) assert_allclose(correct_position, block_structure.position_collection, atol=Tolerance.atol()) assert_allclose(correct_director, block_structure.director_collection, atol=Tolerance.atol())
def test_get_rotation_matrix_correct_rotation_about_y(ycomp, dt): vector_collection = np.array([0.0, ycomp, 0.0]).reshape(-1, 1) test_rot_mat = _get_rotation_matrix(dt, vector_collection) test_theta = ycomp * dt # Transpose for similar reasons mentioned before correct_rot_mat = np.array([ [np.cos(test_theta), 0.0, -np.sin(test_theta)], [0.0, 1.0, 0.0], [np.sin(test_theta), 0.0, np.cos(test_theta)], ]).reshape(3, 3, 1) assert test_rot_mat.shape == (3, 3, 1) assert_allclose(test_rot_mat, correct_rot_mat, atol=Tolerance.atol())
def test_get_rotation_matrix_gives_orthonormal_matrices(): dim = 3 blocksize = 16 dt = np.random.random_sample() rot_mat = _get_rotation_matrix(dt, np.random.randn(dim, blocksize)) r_rt = np.einsum("ijk,ljk->ilk", rot_mat, rot_mat) rt_r = np.einsum("jik,jlk->ilk", rot_mat, rot_mat) test_mat = np.array([np.eye(dim) for _ in range(blocksize)]).T # We can't get there fully, but 1e-15 suffices in precision assert_allclose(r_rt, test_mat, atol=Tolerance.atol()) assert_allclose(rt_r, test_mat, atol=Tolerance.atol())
def test_get_rotation_matrix_correctness_in_three_dimensions(): # A rotation of 120 degrees about x=y=z gives # the permutation matrix P # {\begin{bmatrix}0&0&1\\1&0&0\\0&1&0\end{bmatrix}} # Basically x becomes y, y becomes z, z becomes x # For our case then (with directors aligned on the rows, # rather than columns) # we have # {\begin{bmatrix}0&1&0\\0&0&1\\1&0&0\end{bmatrix}} vector_collection = np.array([1.0, 1.0, 1.0]) / np.sqrt(3.0) vector_collection = vector_collection.reshape(-1, 1) theta = np.deg2rad(120.0) test_rot_mat = _get_rotation_matrix(theta, vector_collection) # previous correct matrix # correct_rot_mat = np.roll(np.eye(3), -1, axis=1).reshape(3, 3, 1) correct_rot_mat = np.roll(np.eye(3), 1, axis=1).reshape(3, 3, 1) assert_allclose(test_rot_mat, correct_rot_mat, atol=Tolerance.atol())
def test_get_rotation_matrix_correct_rotation_about_z(zcomp, dt): vector_collection = np.array([0.0, 0.0, zcomp]).reshape(-1, 1) test_rot_mat = _get_rotation_matrix(dt, vector_collection) test_theta = zcomp * dt # Notice that the correct_rot_mat seems to be a transpose # ie if you take a vector v, and do Rv, it seems to do a # rotation be -test_theta. # The catch in this case is that our directors (d_1, d_2, d_3) # are all aligned row-wise rather than columnwise. Thus we need # to multiply them by a R.transpose, which is equivalent to # multiplying by a R(-theta). correct_rot_mat = np.array([ [np.cos(test_theta), np.sin(test_theta), 0.0], [-np.sin(test_theta), np.cos(test_theta), 0.0], [0.0, 0.0, 1.0], ]).reshape(3, 3, 1) assert test_rot_mat.shape == (3, 3, 1) assert_allclose(test_rot_mat, correct_rot_mat, atol=Tolerance.atol())
def get_linear_state_transition_operator(self, time, dt): return _get_rotation_matrix(dt, self.omega)
def analytical_solution(self, time): # http://scipp.ucsc.edu/~haber/ph5B/sho09.pdf # return _batch_matmul(self._state, _get_rotation_matrix(time, self.omega)) return np.einsum("ijk,ljk->ilk", self.initial_value, _get_rotation_matrix(time, self.omega))
def __iadd__(self, scaled_deriv_array): """ overloaded += operator The add for directors is customized to reflect Rodrigues' rotation formula. Parameters ---------- scaled_deriv_array : numpy.ndarray containing dt * (v, ω, dv/dt, dω/dt) ,as returned from _DerivativeState's __mul__ method Returns ------- self : _State with inplace modified data """ # x += v*dt self.position_collection += scaled_deriv_array[..., :self.n_nodes] # TODO : Verify the math in this note r""" Developer Note -------------- Here the overloaded `+=` operator is exploited to perform matrix multiplication for the directors, which is counter- intutive at first. While this provides a stable interface to interact the rod states with the timesteppers and the rest of the world, the reasons behind including it here also has a depper mathematical significance. Firstly, position lies in the vector space corresponding to R^{3} and update is done this space (with the + and * operators defined as usual), hence the `+=` operator (or `__iadd__`) is reflected as `+=` operator in the position update (line 163 above). For directors rather, which lie in a restricteed R^{3} \otimes R^{3} tensorial space, the space with Q^T.Q = Q.Q^T = I, the + operator can be thought of as an equivalent `*=` update for a 'exponential' multiplication with a rotation matrix (e^{At}). . This does not correspond to the position update. However, if we view this in a logarithmic space the `*=` becomse the '+=' operator once again! After performing this `+=` operation, we bring it back into its original space using the exponential operator. So we are still indirectly doing the '+=' update. To avoid all this hassle with the operators and spaces, we simply define '+=' or '__iadd__' in the case of directors as an equivalent '*=' (matrix multiply) with the RHS below. """ # TODO Q *= exp(w*dt) , whats' the formua again? # TODO the scale factor 1.0 does not seem to be necessary, although # we perform more work in the present framework (muliply dt to entire vector, then take # norm) rather than vector norm then multiple by dt (1/3 operation costs) # TODO optimize (somehow) extra copy away : if we don't make a copy # its even more slower, maybe due to aliasing effects np.einsum( "ijk,jlk->ilk", _get_rotation_matrix( 1.0, scaled_deriv_array[..., self.n_nodes:self.n_kinematic_rates]), self.director_collection.copy(), out=self.director_collection, ) # (v,ω) += (dv/dt, dω/dt)*dt self.kinematic_rate_collection += scaled_deriv_array[ ..., self.n_kinematic_rates:] return self