def compute_muscle_torques( time, my_spline, s, angular_frequency, wave_number, phase_shift, ramp_up_time, direction, director_collection, external_torques, ): # Ramp up the muscle torque factor = min(1.0, time / ramp_up_time) # From the node 1 to node nelem-1 # Magnitude of the torque. Am = beta(s) * sin(2pi*t/T + 2pi*s/lambda + phi) # There is an inconsistency with paper and Elastica cpp implementation. In paper sign in # front of wave number is positive, in Elastica cpp it is negative. torque_mag = ( factor * my_spline * np.sin(angular_frequency * time - wave_number * s + phase_shift) ) # Head and tail of the snake is opposite compared to elastica cpp. We need to iterate torque_mag # from last to first element. torque = _batch_product_i_k_to_ik(direction, torque_mag[::-1]) inplace_addition( external_torques[..., 1:], _batch_matvec(director_collection, torque)[..., 1:], ) inplace_substraction( external_torques[..., :-1], _batch_matvec(director_collection[..., :-1], torque[..., 1:]), )
def apply_torques(self, system, time: np.float = 0.0): # Ramp up the muscle torque factor = min(1.0, time / self.ramp_up_time) # From the node 1 to node nelem-1 # s is the position of nodes on the rod, we go from node=1 to node=nelem-1, because there is no # torques applied by first and last node on elements. Reason is that we cannot apply torque in an # infinitesimal segment at the beginning and end of rod, because there is no additional element # (at element=-1 or element=n_elem+1) to provide internal torques to cancel out an external # torque. This coupled with the requirement that the sum of all muscle torques has # to be zero results in this condition. s = np.cumsum(system.rest_lengths) # Magnitude of the torque. Am = beta(s) * sin(2pi*t/T + 2pi*s/lambda + phi) # There is an inconsistency with paper and Elastica cpp implementation. In paper sign in # front of wave number is positive, in Elastica cpp it is negative. torque_mag = (factor * self.my_spline(s) * np.sin(self.angular_frequency * time - self.wave_number * s + self.phase_shift)) # Head and tail of the snake is opposite compared to elastica cpp. We need to iterate torque_mag # from last to first element. torque = np.einsum("j,ij->ij", torque_mag[::-1], self.direction) # TODO: Find a way without doing tow batch_matvec product system.external_torques[..., 1:] += _batch_matvec( system.director_collection, torque)[..., 1:] system.external_torques[..., :-1] -= _batch_matvec( system.director_collection[..., :-1], torque[..., 1:])
def _apply_torques(index_one, torque, rod_one_director_collection, rod_one_external_torques): start_idx = index_one[0] end_idx = index_one[-1] rod_one_external_torques[..., start_idx] += 0.5 * _batch_matvec( rod_one_director_collection[..., start_idx:start_idx + 1], torque).reshape(3) rod_one_external_torques[..., end_idx] -= 0.5 * _batch_matvec( rod_one_director_collection[..., end_idx:end_idx + 1], torque).reshape(3)
def _compute_internal_bending_twist_stresses_from_model( director_collection, rest_voronoi_lengths, internal_couple, bend_matrix, kappa, rest_kappa, ): """ Linear force functional Operates on B : (3,3,n) tensor and curvature kappa (3,n) Returns ------- """ _compute_bending_twist_strains(director_collection, rest_voronoi_lengths, kappa) # concept : needs to compute kappa blocksize = kappa.shape[1] temp = np.empty((3, blocksize)) for i in range(3): for k in range(blocksize): temp[i, k] = kappa[i, k] - rest_kappa[i, k] internal_couple[:] = _batch_matvec(bend_matrix, temp)
def update_accelerations(self, time): """ This method computes translational and angular acceleration and reset external forces and torques to zero. Parameters ---------- time: float Simulation time. Returns ------- """ np.copyto( self.acceleration_collection, (self.internal_forces + self.external_forces) / self.mass, ) np.copyto( self.alpha_collection, _batch_matvec( self.inv_mass_second_moment_of_inertia, (self.internal_torques + self.external_torques), ) * self.dilatation, ) # Reset forces and torques self.external_forces *= 0.0 self.external_torques *= 0.0
def compute_bending_energy(self): kappa_diff = self.kappa - self.rest_kappa bending_internal_torques = _batch_matvec(self.bend_matrix, kappa_diff) return (0.5 * (_batch_dot(kappa_diff, bending_internal_torques) * self.rest_voronoi_lengths).sum())
def compute_shear_energy(self): sigma_diff = self.sigma - self.rest_sigma shear_internal_torques = _batch_matvec(self.shear_matrix, sigma_diff) return (0.5 * (_batch_dot(sigma_diff, shear_internal_torques) * self.rest_lengths).sum())
def _compute_shear_stretch_strains( position_collection, volume, lengths, tangents, radius, rest_lengths, rest_voronoi_lengths, dilatation, voronoi_dilatation, director_collection, sigma, ): # Quick trick : Instead of evaliation Q(et-d^3), use property that Q*d3 = (0,0,1), a constant _compute_all_dilatations( position_collection, volume, lengths, tangents, radius, dilatation, rest_lengths, rest_voronoi_lengths, voronoi_dilatation, ) z_vector = np.array([0.0, 0.0, 1.0]).reshape(3, -1) sigma[:] = dilatation * _batch_matvec(director_collection, tangents) - z_vector
def update_accelerations(self, time): """TODO Do we need to make the collection members abstract? Parameters ---------- time Returns ------- """ np.copyto( self.acceleration_collection, (self.internal_forces + self.external_forces) / self.mass, ) np.copyto( self.alpha_collection, _batch_matvec( self.inv_mass_second_moment_of_inertia, (self.internal_torques + self.external_torques), ), ) # Reset forces and torques self.external_forces *= 0.0 self.external_torques *= 0.0
def apply_torques(self, system, time: np.float = 0.0): n_elems = system.n_elems torque_on_one_element = ( _batch_product_i_k_to_ik(self.torque, np.ones((n_elems))) / n_elems ) system.external_torques += _batch_matvec( system.director_collection, torque_on_one_element )
def test_update_acceleration(self, n_elem): """ In this test case, we initialize a straight rod. We set correct parameters for rod mass, dilatation, mass moment of inertia and call the function update_accelerations and compare the angular and translational acceleration with the correct values. This test case tests, update_accelerations _update_accelerations """ initial, test_rod = constructor(n_elem, nu=0.0) mass = test_rod.mass external_forces = np.zeros(3 * (n_elem + 1)).reshape(3, n_elem + 1) external_torques = np.zeros(3 * n_elem).reshape(3, n_elem) for i in range(0, n_elem): external_torques[..., i] = np.random.rand(3) for i in range(0, n_elem + 1): external_forces[..., i] = np.random.rand(3) test_rod.external_forces[:] = external_forces test_rod.external_torques[:] = external_torques # No dilatation in the rods dilatations = np.ones(n_elem) # Set mass moment of inertia matrix to identity matrix for convenience. # Inverse of identity = identity inv_mass_moment_of_inertia = np.repeat(np.identity(3)[:, :, np.newaxis], n_elem, axis=2) test_rod.inv_mass_second_moment_of_inertia[:] = inv_mass_moment_of_inertia # Compute acceleration test_rod.update_accelerations(time=0) correct_acceleration = external_forces / mass assert_allclose( test_rod.acceleration_collection, correct_acceleration, atol=Tolerance.atol(), ) correct_angular_acceleration = ( _batch_matvec(inv_mass_moment_of_inertia, external_torques) * dilatations) assert_allclose( test_rod.alpha_collection, correct_angular_acceleration, atol=Tolerance.atol(), )
def apply_torques(self, system, time: np.float = 0.0): # Ramp up the muscle torque factor = min(1.0, time / self.ramp_up_time) # From the node 1 to node nelem-1 # Magnitude of the torque. Am = beta(s) * sin(2pi*t/T + 2pi*s/lambda + phi) # There is an inconsistency with paper and Elastica cpp implementation. In paper sign in # front of wave number is positive, in Elastica cpp it is negative. torque_mag = (factor * self.my_spline * np.sin(self.angular_frequency * time - self.wave_number * self.s + self.phase_shift)) # Head and tail of the snake is opposite compared to elastica cpp. We need to iterate torque_mag # from last to first element. torque = np.einsum("j,ij->ij", torque_mag[::-1], self.direction) # TODO: Find a way without doing tow batch_matvec product system.external_torques[..., 1:] += _batch_matvec( system.director_collection, torque)[..., 1:] system.external_torques[..., :-1] -= _batch_matvec( system.director_collection[..., :-1], torque[..., 1:])
def compute_rotational_energy(self): """ This method computes the total rotational energy of the rod. Returns ------- """ J_omega_upon_e = ( _batch_matvec(self.mass_second_moment_of_inertia, self.omega_collection) / self.dilatation ) return 0.5 * np.einsum("ik,ik->k", self.omega_collection, J_omega_upon_e).sum()
def _compute_shear_stretch_strains(self): """ This method computes shear and stretch strains of the elements. Returns ------- """ # Quick trick : Instead of evaliation Q(et-d^3), use property that Q*d3 = (0,0,1), a constant self._compute_all_dilatations() self.sigma = ( self.dilatation * _batch_matvec(self.director_collection, self.tangents) - _get_z_vector() )
def test_batch_matvec(blocksize): input_matrix_collection = np.random.randn(3, 3, blocksize) input_vector_collection = np.random.randn(3, blocksize) test_vector_collection = _batch_matvec(input_matrix_collection, input_vector_collection) correct_vector_collection = [ np.dot(input_matrix_collection[..., i], input_vector_collection[..., i]) for i in range(blocksize) ] correct_vector_collection = np.array(correct_vector_collection).T assert_allclose(test_vector_collection, correct_vector_collection)
def _compute_internal_shear_stretch_stresses_from_model( position_collection, volume, lengths, tangents, radius, rest_lengths, rest_voronoi_lengths, dilatation, voronoi_dilatation, director_collection, sigma, rest_sigma, shear_matrix, internal_stress, ): """ Linear force functional Operates on S : (3,3,n) tensor and sigma (3,n) Returns ------- """ _compute_shear_stretch_strains( position_collection, volume, lengths, tangents, radius, rest_lengths, rest_voronoi_lengths, dilatation, voronoi_dilatation, director_collection, sigma, ) internal_stress[:] = _batch_matvec(shear_matrix, sigma - rest_sigma)
def test_get_functions(self, n_elem): """ In this test case, we initialize a straight rod. First we set velocity and angular velocity to random values, and we recover back initialized values using `get_velocity` and `get_angular_velocity` functions. Then, we set random external forces and torques. We compute translational accelerations and angular accelerations based on external forces and torques. Finally we use `get_acceleration` and `get_angular_acceleration` functions and compare returned translational acceleration and angular acceleration with analytically computed ones. This test case tests, get_velocity get_angular_velocity get_acceleration get_angular_acceleration Note that, viscous dissipation set to 0, since we don't want any contribution from damping torque. """ initial, test_rod = constructor(n_elem, nu=0.0) mass = test_rod.mass velocity = np.zeros(3 * (n_elem + 1)).reshape(3, n_elem + 1) external_forces = np.zeros(3 * (n_elem + 1)).reshape(3, n_elem + 1) omega = np.zeros(3 * n_elem).reshape(3, n_elem) external_torques = np.zeros(3 * n_elem).reshape(3, n_elem) for i in range(0, n_elem): omega[..., i] = np.random.rand(3) external_torques[..., i] = np.random.rand(3) for i in range(0, n_elem + 1): velocity[..., i] = np.random.rand(3) external_forces[..., i] = np.random.rand(3) test_rod.velocity_collection = velocity test_rod.omega_collection = omega assert_allclose(test_rod.get_velocity(), velocity, atol=Tolerance.atol()) assert_allclose(test_rod.get_angular_velocity(), omega, atol=Tolerance.atol()) test_rod.external_forces = external_forces test_rod.external_torques = external_torques correct_acceleration = external_forces / mass assert_allclose(test_rod.get_acceleration(), correct_acceleration, atol=Tolerance.atol()) # No dilatation in the rods dilatations = np.ones(n_elem) # Set angular velocity zero, so that we dont have any # contribution from lagrangian transport and unsteady dilatation. test_rod.omega_collection[:] = 0.0 # Set mass moment of inertia matrix to identity matrix for convenience. # Inverse of identity = identity inv_mass_moment_of_inertia = np.repeat(np.identity(3)[:, :, np.newaxis], n_elem, axis=2) test_rod.inv_mass_second_moment_of_inertia = inv_mass_moment_of_inertia correct_angular_acceleration = ( _batch_matvec(inv_mass_moment_of_inertia, external_torques) * dilatations) assert_allclose( test_rod.get_angular_acceleration(), correct_angular_acceleration, atol=Tolerance.atol(), )
def anisotropic_friction( plane_origin, plane_normal, surface_tol, slip_velocity_tol, k, nu, kinetic_mu_forward, kinetic_mu_backward, kinetic_mu_sideways, static_mu_forward, static_mu_backward, static_mu_sideways, radius, tangents, position_collection, director_collection, velocity_collection, omega_collection, internal_forces, external_forces, internal_torques, external_torques, ): plane_response_force_mag, no_contact_point_idx = apply_normal_force_numba( plane_origin, plane_normal, surface_tol, k, nu, radius, position_collection, velocity_collection, internal_forces, external_forces, ) # First compute component of rod tangent in plane. Because friction forces acts in plane not out of plane. Thus # axial direction has to be in plane, it cannot be out of plane. We are projecting rod element tangent vector in # to the plane. So friction forces can only be in plane forces and not out of plane. tangent_along_normal_direction = _batch_product_i_ik_to_k( plane_normal, tangents) tangent_perpendicular_to_normal_direction = tangents - _batch_product_i_k_to_ik( plane_normal, tangent_along_normal_direction) tangent_perpendicular_to_normal_direction_mag = _batch_norm( tangent_perpendicular_to_normal_direction) # Normalize tangent_perpendicular_to_normal_direction. This is axial direction for plane. Here we are adding # small tolerance (1e-10) for normalization, in order to prevent division by 0. axial_direction = _batch_product_k_ik_to_ik( 1 / (tangent_perpendicular_to_normal_direction_mag + 1e-14), tangent_perpendicular_to_normal_direction, ) element_velocity = node_to_element_pos_or_vel(velocity_collection) # first apply axial kinetic friction velocity_mag_along_axial_direction = _batch_dot(element_velocity, axial_direction) velocity_along_axial_direction = _batch_product_k_ik_to_ik( velocity_mag_along_axial_direction, axial_direction) # Friction forces depends on the direction of velocity, in other words sign # of the velocity vector. velocity_sign_along_axial_direction = np.sign( velocity_mag_along_axial_direction) # Check top for sign convention kinetic_mu = 0.5 * (kinetic_mu_forward * (1 + velocity_sign_along_axial_direction) + kinetic_mu_backward * (1 - velocity_sign_along_axial_direction)) # Call slip function to check if elements slipping or not slip_function_along_axial_direction = find_slipping_elements( velocity_along_axial_direction, slip_velocity_tol) # Now rolling kinetic friction rolling_direction = _batch_vec_oneD_vec_cross(axial_direction, plane_normal) torque_arm = _batch_product_i_k_to_ik(-plane_normal, radius) velocity_along_rolling_direction = _batch_dot(element_velocity, rolling_direction) directors_transpose = _batch_matrix_transpose(director_collection) # w_rot = Q.T @ omega @ Q @ r rotation_velocity = _batch_matvec( directors_transpose, _batch_cross(omega_collection, _batch_matvec(director_collection, torque_arm)), ) rotation_velocity_along_rolling_direction = _batch_dot( rotation_velocity, rolling_direction) slip_velocity_mag_along_rolling_direction = ( velocity_along_rolling_direction + rotation_velocity_along_rolling_direction) slip_velocity_along_rolling_direction = _batch_product_k_ik_to_ik( slip_velocity_mag_along_rolling_direction, rolling_direction) slip_function_along_rolling_direction = find_slipping_elements( slip_velocity_along_rolling_direction, slip_velocity_tol) # Compute unitized total slip velocity vector. We will use this to distribute the weight of the rod in axial # and rolling directions. unitized_total_velocity = (slip_velocity_along_rolling_direction + velocity_along_axial_direction) unitized_total_velocity /= _batch_norm(unitized_total_velocity + 1e-14) # Apply kinetic friction in axial direction. kinetic_friction_force_along_axial_direction = -( (1.0 - slip_function_along_axial_direction) * kinetic_mu * plane_response_force_mag * _batch_dot(unitized_total_velocity, axial_direction) * axial_direction) # If rod element does not have any contact with plane, plane cannot apply friction # force on the element. Thus lets set kinetic friction force to 0.0 for the no contact points. kinetic_friction_force_along_axial_direction[..., no_contact_point_idx] = 0.0 elements_to_nodes_inplace(kinetic_friction_force_along_axial_direction, external_forces) # Apply kinetic friction in rolling direction. kinetic_friction_force_along_rolling_direction = -( (1.0 - slip_function_along_rolling_direction) * kinetic_mu_sideways * plane_response_force_mag * _batch_dot( unitized_total_velocity, rolling_direction) * rolling_direction) # If rod element does not have any contact with plane, plane cannot apply friction # force on the element. Thus lets set kinetic friction force to 0.0 for the no contact points. kinetic_friction_force_along_rolling_direction[..., no_contact_point_idx] = 0.0 elements_to_nodes_inplace(kinetic_friction_force_along_rolling_direction, external_forces) # torque = Q @ r @ Fr external_torques += _batch_matvec( director_collection, _batch_cross(torque_arm, kinetic_friction_force_along_rolling_direction), ) # now axial static friction nodal_total_forces = _batch_vector_sum(internal_forces, external_forces) element_total_forces = nodes_to_elements(nodal_total_forces) force_component_along_axial_direction = _batch_dot(element_total_forces, axial_direction) force_component_sign_along_axial_direction = np.sign( force_component_along_axial_direction) # check top for sign convention static_mu = 0.5 * (static_mu_forward * (1 + force_component_sign_along_axial_direction) + static_mu_backward * (1 - force_component_sign_along_axial_direction)) max_friction_force = (slip_function_along_axial_direction * static_mu * plane_response_force_mag) # friction = min(mu N, pushing force) static_friction_force_along_axial_direction = -( np.minimum(np.fabs(force_component_along_axial_direction), max_friction_force) * force_component_sign_along_axial_direction * axial_direction) # If rod element does not have any contact with plane, plane cannot apply friction # force on the element. Thus lets set static friction force to 0.0 for the no contact points. static_friction_force_along_axial_direction[..., no_contact_point_idx] = 0.0 elements_to_nodes_inplace(static_friction_force_along_axial_direction, external_forces) # now rolling static friction # there is some normal, tangent and rolling directions inconsitency from Elastica total_torques = _batch_matvec(directors_transpose, (internal_torques + external_torques)) # Elastica has opposite defs of tangents in interaction.h and rod.cpp total_torques_along_axial_direction = _batch_dot(total_torques, axial_direction) force_component_along_rolling_direction = _batch_dot( element_total_forces, rolling_direction) noslip_force = -( (radius * force_component_along_rolling_direction - 2.0 * total_torques_along_axial_direction) / 3.0 / radius) max_friction_force = (slip_function_along_rolling_direction * static_mu_sideways * plane_response_force_mag) noslip_force_sign = np.sign(noslip_force) static_friction_force_along_rolling_direction = ( np.minimum(np.fabs(noslip_force), max_friction_force) * noslip_force_sign * rolling_direction) # If rod element does not have any contact with plane, plane cannot apply friction # force on the element. Thus lets set plane static friction force to 0.0 for the no contact points. static_friction_force_along_rolling_direction[..., no_contact_point_idx] = 0.0 elements_to_nodes_inplace(static_friction_force_along_rolling_direction, external_forces) external_torques += _batch_matvec( director_collection, _batch_cross(torque_arm, static_friction_force_along_rolling_direction), )
def compute_rotational_energy(self): J_omega_upon_e = (_batch_matvec(self.mass_second_moment_of_inertia, self.omega_collection) / self.dilatation) return 0.5 * np.einsum("ik,ik->k", self.omega_collection, J_omega_upon_e).sum()
def _compute_internal_torques(self): """ This method computes internal torques acting on elements. Returns ------- """ # Compute \tau_l and cache it using internal_couple # Be careful about usage though self._compute_internal_bending_twist_stresses_from_model() # Compute dilatation rate when needed, dilatation itself is done before # in internal_stresses self._compute_dilatation_rate() voronoi_dilatation_inv_cube_cached = 1.0 / self.voronoi_dilatation ** 3 # Delta(\tau_L / \Epsilon^3) bend_twist_couple_2D = difference_kernel( self.internal_couple * voronoi_dilatation_inv_cube_cached ) # \mathcal{A}[ (\kappa x \tau_L ) * \hat{D} / \Epsilon^3 ] bend_twist_couple_3D = quadrature_kernel( _batch_cross(self.kappa, self.internal_couple) * self.rest_voronoi_lengths * voronoi_dilatation_inv_cube_cached ) # (Qt x n_L) * \hat{l} shear_stretch_couple = ( _batch_cross( _batch_matvec(self.director_collection, self.tangents), self.internal_stress, ) * self.rest_lengths ) # I apply common sub expression elimination here, as J w / e is used in both the lagrangian and dilatation # terms # TODO : the _batch_matvec kernel needs to depend on the representation of J, and should be coded as such J_omega_upon_e = ( _batch_matvec(self.mass_second_moment_of_inertia, self.omega_collection) / self.dilatation ) # (J \omega_L / e) x \omega_L # Warning : Do not do micro-optimization here : you can ignore dividing by dilatation as we later multiply by it # but this causes confusion and violates SRP lagrangian_transport = _batch_cross(J_omega_upon_e, self.omega_collection) # Note : in the computation of dilatation_rate, there is an optimization opportunity as dilatation rate has # a dilatation-like term in the numerator, which we cancel here # (J \omega_L / e^2) . (de/dt) unsteady_dilatation = J_omega_upon_e * self.dilatation_rate / self.dilatation return ( bend_twist_couple_2D + bend_twist_couple_3D + shear_stretch_couple + lagrangian_transport + unsteady_dilatation - self._compute_damping_torques() )
def _compute_internal_torques( position_collection, velocity_collection, tangents, lengths, rest_lengths, director_collection, rest_voronoi_lengths, bend_matrix, rest_kappa, kappa, voronoi_dilatation, mass_second_moment_of_inertia, omega_collection, internal_stress, internal_couple, dilatation, dilatation_rate, dissipation_constant_for_torques, damping_torques, internal_torques, ghost_voronoi_idx, ): # Compute \tau_l and cache it using internal_couple # Be careful about usage though _compute_internal_bending_twist_stresses_from_model( director_collection, rest_voronoi_lengths, internal_couple, bend_matrix, kappa, rest_kappa, ) # Compute dilatation rate when needed, dilatation itself is done before # in internal_stresses _compute_dilatation_rate(position_collection, velocity_collection, lengths, rest_lengths, dilatation_rate) # FIXME: change memory overload instead for the below calls! voronoi_dilatation_inv_cube_cached = 1.0 / voronoi_dilatation**3 # Delta(\tau_L / \Epsilon^3) bend_twist_couple_2D = difference_kernel_for_block_structure( internal_couple * voronoi_dilatation_inv_cube_cached, ghost_voronoi_idx) # \mathcal{A}[ (\kappa x \tau_L ) * \hat{D} / \Epsilon^3 ] bend_twist_couple_3D = quadrature_kernel_for_block_structure( _batch_cross(kappa, internal_couple) * rest_voronoi_lengths * voronoi_dilatation_inv_cube_cached, ghost_voronoi_idx, ) # (Qt x n_L) * \hat{l} shear_stretch_couple = (_batch_cross( _batch_matvec(director_collection, tangents), internal_stress) * rest_lengths) # I apply common sub expression elimination here, as J w / e is used in both the lagrangian and dilatation # terms # TODO : the _batch_matvec kernel needs to depend on the representation of J, and should be coded as such J_omega_upon_e = ( _batch_matvec(mass_second_moment_of_inertia, omega_collection) / dilatation) # (J \omega_L / e) x \omega_L # Warning : Do not do micro-optimization here : you can ignore dividing by dilatation as we later multiply by it # but this causes confusion and violates SRP lagrangian_transport = _batch_cross(J_omega_upon_e, omega_collection) # Note : in the computation of dilatation_rate, there is an optimization opportunity as dilatation rate has # a dilatation-like term in the numerator, which we cancel here # (J \omega_L / e^2) . (de/dt) unsteady_dilatation = J_omega_upon_e * dilatation_rate / dilatation _compute_damping_torques(damping_torques, omega_collection, dissipation_constant_for_torques, lengths) blocksize = internal_torques.shape[1] for i in range(3): for k in range(blocksize): internal_torques[i, k] = (bend_twist_couple_2D[i, k] + bend_twist_couple_3D[i, k] + shear_stretch_couple[i, k] + lagrangian_transport[i, k] + unsteady_dilatation[i, k] - damping_torques[i, k])
def apply_torques(self, system, time: np.float = 0.0): torque_on_one_element = self.torque / system.n_elems system.external_torques += _batch_matvec(system.director_collection, torque_on_one_element)