From f6538b0a0d6360d9613050f3580ca990b8219f92 Mon Sep 17 00:00:00 2001 From: Ali-7800 <47090295+Ali-7800@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:23:26 -0600 Subject: [PATCH 1/5] Added CylinderPlaneContact class --- elastica/_contact_functions.py | 66 ++++++++++++++++++++++++++ elastica/contact_forces.py | 87 ++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) diff --git a/elastica/_contact_functions.py b/elastica/_contact_functions.py index 7e40a7f98..245d92319 100644 --- a/elastica/_contact_functions.py +++ b/elastica/_contact_functions.py @@ -780,3 +780,69 @@ def _calculate_contact_forces_rod_plane_with_anisotropic_friction( director_collection, _batch_cross(torque_arm, static_friction_force_along_rolling_direction), ) + + +@numba.njit(cache=True) +def _calculate_contact_forces_cylinder_plane( + plane_origin, + plane_normal, + surface_tol, + k, + nu, + length, + position_collection, + velocity_collection, + external_forces, +): + + # Compute plane response force + # total_forces = system.internal_forces + system.external_forces + total_forces = external_forces + force_component_along_normal_direction = _batch_product_i_ik_to_k( + plane_normal, total_forces + ) + forces_along_normal_direction = _batch_product_i_k_to_ik( + plane_normal, force_component_along_normal_direction + ) + # If the total force component along the plane normal direction is greater than zero that means, + # total force is pushing rod away from the plane not towards the plane. Thus, response force + # applied by the surface has to be zero. + forces_along_normal_direction[ + ..., np.where(force_component_along_normal_direction > 0)[0] + ] = 0.0 + # Compute response force on the element. Plane response force + # has to be away from the surface and towards the element. Thus + # multiply forces along normal direction with negative sign. + plane_response_force = -forces_along_normal_direction + + # Elastic force response due to penetration + element_position = position_collection + distance_from_plane = _batch_product_i_ik_to_k( + plane_normal, (element_position - plane_origin) + ) + plane_penetration = np.minimum(distance_from_plane - length / 2, 0.0) + elastic_force = -k * _batch_product_i_k_to_ik(plane_normal, plane_penetration) + + # Damping force response due to velocity towards the plane + element_velocity = velocity_collection + normal_component_of_element_velocity = _batch_product_i_ik_to_k( + plane_normal, element_velocity + ) + damping_force = -nu * _batch_product_i_k_to_ik( + plane_normal, normal_component_of_element_velocity + ) + + # Compute total plane response force + plane_response_force_total = plane_response_force + elastic_force + damping_force + + # Check if the rigid body is in contact with plane. + no_contact_point_idx = np.where((distance_from_plane - length / 2) > surface_tol)[0] + # If rod element does not have any contact with plane, plane cannot apply response + # force on the element. Thus lets set plane response force to 0.0 for the no contact points. + plane_response_force[..., no_contact_point_idx] = 0.0 + plane_response_force_total[..., no_contact_point_idx] = 0.0 + + # Update the external forces + external_forces += plane_response_force_total + + return (_batch_norm(plane_response_force), no_contact_point_idx) diff --git a/elastica/contact_forces.py b/elastica/contact_forces.py index 8f3fdc6e5..8a1680321 100644 --- a/elastica/contact_forces.py +++ b/elastica/contact_forces.py @@ -16,6 +16,7 @@ _calculate_contact_forces_rod_sphere, _calculate_contact_forces_rod_plane, _calculate_contact_forces_rod_plane_with_anisotropic_friction, + _calculate_contact_forces_cylinder_plane, ) import numpy as np @@ -707,3 +708,89 @@ def apply_contact(self, system_one: RodType, system_two: SystemType) -> None: system_one.internal_torques, system_one.external_torques, ) + + +class CylinderPlaneContact(NoContact): + """ + This class is for applying contact forces between cylinder-plane. + First system is always cylinder and second system is always plane. + For more details regarding the contact module refer to + Eqn 4.8 of Gazzola et al. RSoS (2018). + + How to define contact between cylinder and plane. + >>> simulator.detect_contact_between(cylinder, plane).using( + ... CylinderPlaneContact, + ... k=1e4, + ... nu=10, + ... ) + """ + + def __init__( + self, + k: float, + nu: float, + ): + """ + Parameters + ---------- + k : float + Contact spring constant. + nu : float + Contact damping constant. + """ + super(CylinderPlaneContact, self).__init__() + self.k = k + self.nu = nu + self.surface_tol = 1e-4 + + def _check_systems_validity( + self, + system_one: SystemType, + system_two: AllowedContactType, + ) -> None: + """ + This checks the contact order and type of a SystemType object and an AllowedContactType object. + For the RodPlaneContact class first_system should be a cylinder and second_system should be a plane. + Parameters + ---------- + system_one + SystemType + system_two + AllowedContactType + """ + if not issubclass(system_one.__class__, Cylinder) or not issubclass( + system_two.__class__, Plane + ): + raise TypeError( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a cylinder, second should be a plane".format( + system_one.__class__, system_two.__class__ + ) + ) + + def apply_contact(self, system_one: Cylinder, system_two: SystemType): + """ + This function computes the plane force response on the rigid body, in the + case of contact. Contact model given in Eqn 4.8 Gazzola et. al. RSoS 2018 paper + is used. + + Parameters + ---------- + system_one: object + cylinder object. + system_two: object + Plane object. + + """ + return _calculate_contact_forces_cylinder_plane( + system_two.origin, + system_two.normal, + self.surface_tol, + self.k, + self.nu, + system_one.length, + system_one.position_collection, + system_one.velocity_collection, + system_one.external_forces, + ) From f429ecb45b6d30fa80f49de9a6d0f77b6819c8f4 Mon Sep 17 00:00:00 2001 From: Ali-7800 <47090295+Ali-7800@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:38:45 -0600 Subject: [PATCH 2/5] Refactor: apply_normal_force_numba_rigid_body to _calculate_contact_forces_cylinder_plane --- elastica/interaction.py | 57 +++++---------------------------------- tests/test_interaction.py | 42 ++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 52 deletions(-) diff --git a/elastica/interaction.py b/elastica/interaction.py index 07f5f4390..ae9faf5ca 100644 --- a/elastica/interaction.py +++ b/elastica/interaction.py @@ -16,6 +16,7 @@ from elastica._contact_functions import ( _calculate_contact_forces_rod_plane, _calculate_contact_forces_rod_plane_with_anisotropic_friction, + _calculate_contact_forces_cylinder_plane, ) @@ -548,7 +549,7 @@ def apply_normal_force(self, system): ------- magnitude of the plane response """ - return apply_normal_force_numba_rigid_body( + return _calculate_contact_forces_cylinder_plane( self.plane_origin, self.plane_normal, self.surface_tol, @@ -574,54 +575,8 @@ def apply_normal_force_numba_rigid_body( external_forces, ): - # Compute plane response force - # total_forces = system.internal_forces + system.external_forces - total_forces = external_forces - force_component_along_normal_direction = _batch_product_i_ik_to_k( - plane_normal, total_forces - ) - forces_along_normal_direction = _batch_product_i_k_to_ik( - plane_normal, force_component_along_normal_direction - ) - # If the total force component along the plane normal direction is greater than zero that means, - # total force is pushing rod away from the plane not towards the plane. Thus, response force - # applied by the surface has to be zero. - forces_along_normal_direction[ - ..., np.where(force_component_along_normal_direction > 0)[0] - ] = 0.0 - # Compute response force on the element. Plane response force - # has to be away from the surface and towards the element. Thus - # multiply forces along normal direction with negative sign. - plane_response_force = -forces_along_normal_direction - - # Elastic force response due to penetration - element_position = position_collection - distance_from_plane = _batch_product_i_ik_to_k( - plane_normal, (element_position - plane_origin) - ) - plane_penetration = np.minimum(distance_from_plane - length / 2, 0.0) - elastic_force = -k * _batch_product_i_k_to_ik(plane_normal, plane_penetration) - - # Damping force response due to velocity towards the plane - element_velocity = velocity_collection - normal_component_of_element_velocity = _batch_product_i_ik_to_k( - plane_normal, element_velocity - ) - damping_force = -nu * _batch_product_i_k_to_ik( - plane_normal, normal_component_of_element_velocity + raise NotImplementedError( + "This function is removed in v0.3.2. For cylinder plane contact please use: \n" + "elastica._contact_functions._calculate_contact_forces_cylinder_plane() \n" + "For detail, refer to issue #113." ) - - # Compute total plane response force - plane_response_force_total = plane_response_force + elastic_force + damping_force - - # Check if the rigid body is in contact with plane. - no_contact_point_idx = np.where((distance_from_plane - length / 2) > surface_tol)[0] - # If rod element does not have any contact with plane, plane cannot apply response - # force on the element. Thus lets set plane response force to 0.0 for the no contact points. - plane_response_force[..., no_contact_point_idx] = 0.0 - plane_response_force_total[..., no_contact_point_idx] = 0.0 - - # Update the external forces - external_forces += plane_response_force_total - - return (_batch_norm(plane_response_force), no_contact_point_idx) diff --git a/tests/test_interaction.py b/tests/test_interaction.py index 6a4f48f28..8d2b0e13c 100644 --- a/tests/test_interaction.py +++ b/tests/test_interaction.py @@ -12,6 +12,7 @@ SlenderBodyTheory, nodes_to_elements, elements_to_nodes_inplace, + apply_normal_force_numba_rigid_body, ) from elastica.contact_utils import ( _node_to_element_mass_or_force, @@ -846,7 +847,7 @@ def test_not_impl_error_for_node_to_element_pos_or_vel(self, n_elem): @pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) def test_elements_to_nodes_inplace_error_message(self, n_elem): """ - This function _elements_to_nodes_inplace. We are + This function tests _elements_to_nodes_inplace. We are converting node velocities to element velocities. Here also we are using numba to speed up the process. @@ -1010,3 +1011,42 @@ def test_slender_body_matrix_product_only_xy(self, n_elem): slender_body_theory.apply_forces(rod) assert_allclose(correct_forces, rod.external_forces, atol=Tolerance.atol()) + + +@pytest.mark.parametrize("n_elem", [2, 3, 5, 10, 20]) +def test_apply_normal_force_numba_rigid_body_error_message(n_elem): + """ + This function _elements_to_nodes_inplace. We are + converting node velocities to element velocities. Here also + we are using numba to speed up the process. + + Parameters + ---------- + n_elem + + Returns + ------- + + """ + + position_collection = np.zeros((3, n_elem + 1)) + position_collection[0, :] = np.linspace(0, 1.0, n_elem + 1) + + error_message = ( + "This function is removed in v0.3.2. For cylinder plane contact please use: \n" + "elastica._contact_functions._calculate_contact_forces_cylinder_plane() \n" + "For detail, refer to issue #113." + ) + with pytest.raises(NotImplementedError) as error_info: + apply_normal_force_numba_rigid_body( + plane_origin=np.array([0.0, 0.0, 0.0]), + plane_normal=np.array([0.0, 0.0, 1.0]), + surface_tol=1e-4, + k=1.0, + nu=1.0, + length=1.0, + position_collection=position_collection, + velocity_collection=np.zeros((3, n_elem + 1)), + external_forces=np.zeros((3, n_elem + 1)), + ) + assert error_info.value.args[0] == error_message From 9a1486ae9bcf6a62107fde04a7968f0485347200 Mon Sep 17 00:00:00 2001 From: Ali-7800 <47090295+Ali-7800@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:39:20 -0600 Subject: [PATCH 3/5] CylinderPlaneContact class tests --- tests/test_contact_classes.py | 243 +++++++++++++++++++++++++++++++++- 1 file changed, 237 insertions(+), 6 deletions(-) diff --git a/tests/test_contact_classes.py b/tests/test_contact_classes.py index b26932423..160b2fb71 100644 --- a/tests/test_contact_classes.py +++ b/tests/test_contact_classes.py @@ -10,6 +10,7 @@ RodSphereContact, RodPlaneContact, RodPlaneContactWithAnisotropicFriction, + CylinderPlaneContact, ) from elastica.typing import RodBase from elastica.rigidbody import Cylinder, Sphere @@ -593,7 +594,7 @@ def initializer( # create rod rod = MockRod() start = np.array([0.0, 0.0, 0.0]) - direction = np.array([0.0, 0.0, 0.0]) + direction = np.array([0.0, 0.0, 1.0]) end = start + 1.0 * direction rod.position_collection = np.zeros((3, 3)) for i in range(0, 3): @@ -638,7 +639,7 @@ def test_check_systems_validity_with_invalid_systems( " First system should be a rod, second should be a plane" ).format(mock_rod.__class__, mock_list.__class__) == str(excinfo.value) - "Testing Self Contact wrapper with incorrect type for first argument" + "Testing Rod Plane Contact wrapper with incorrect type for first argument" with pytest.raises(TypeError) as excinfo: rod_plane_contact._check_systems_validity(mock_list, mock_plane) assert ( @@ -747,7 +748,7 @@ def test_rod_plane_contact_when_rod_is_under_plane(self): offset_of_plane_with_respect_to_rod = 2.0 * 0.25 # plane normal changed, it is towards the negative direction, because rod - # is under the rod. + # is under the plane. plane_normal = np.array([0.0, -1.0, 0.0]) [rod, plane, rod_plane_contact, external_forces] = self.initializer( @@ -776,7 +777,7 @@ def test_rod_plane_contact_when_rod_is_under_plane_with_k_without_nu(self, k_w): shift = offset_of_plane_with_respect_to_rod - np.random.random_sample(1).item() # plane normal changed, it is towards the negative direction, because rod - # is under the rod. + # is under the plane. plane_normal = np.array([0.0, -1.0, 0.0]) [rod, plane, rod_plane_contact, external_forces] = self.initializer( @@ -813,7 +814,7 @@ def test_rod_plane_contact_when_rod_is_under_plane_without_k_with_nu(self, nu_w) offset_of_plane_with_respect_to_rod = 2.0 * 0.25 # plane normal changed, it is towards the negative direction, because rod - # is under the rod. + # is under the plane. plane_normal = np.array([0.0, -1.0, 0.0]) [rod, plane, rod_plane_contact, external_forces] = self.initializer( @@ -913,7 +914,7 @@ def test_check_systems_validity_with_invalid_systems( " First system should be a rod, second should be a plane" ).format(mock_rod.__class__, mock_list.__class__) == str(excinfo.value) - "Testing Self Contact wrapper with incorrect type for first argument" + "Testing Rod Plane wrapper with incorrect type for first argument" with pytest.raises(TypeError) as excinfo: rod_plane_contact._check_systems_validity(mock_list, mock_plane) assert ( @@ -1196,3 +1197,233 @@ def test_static_rolling_friction_total_torque_larger_than_static_friction_force( ) assert_allclose(correct_torques, rod.external_torques, atol=Tolerance.atol()) + + +class TestCylinderPlaneContact: + def initializer( + self, + shift=0.0, + k_w=0.0, + nu_w=0.0, + plane_normal=np.array([0.0, 1.0, 0.0]), + ): + # create cylinder + cylinder = MockCylinder() + + # create plane + plane = MockPlane() + plane.origin = np.array([0.0, -cylinder.radius[0] + shift, 0.0]).reshape(3, 1) + plane.normal = plane_normal.reshape( + 3, + ) + cylinder_plane_contact = CylinderPlaneContact(k_w, nu_w) + + fnormal = -10.0 * np.sign(plane_normal[1]) * np.random.random_sample(1).item() + external_forces = np.array([0.0, fnormal, 0.0]).reshape(3, 1) + cylinder.external_forces = external_forces.copy() + + return cylinder, plane, cylinder_plane_contact, external_forces + + def test_check_systems_validity_with_invalid_systems( + self, + ): + mock_cylinder = MockCylinder() + mock_plane = MockPlane() + mock_list = [1, 2, 3] + cylinder_plane_contact = CylinderPlaneContact(k=1.0, nu=0.0) + + "Testing Cylinder Plane Contact wrapper with incorrect type for second argument" + with pytest.raises(TypeError) as excinfo: + cylinder_plane_contact._check_systems_validity(mock_cylinder, mock_list) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a cylinder, second should be a plane" + ).format(mock_cylinder.__class__, mock_list.__class__) == str(excinfo.value) + + "Testing Cylinder Plane wrapper with incorrect type for first argument" + with pytest.raises(TypeError) as excinfo: + cylinder_plane_contact._check_systems_validity(mock_list, mock_plane) + assert ( + "Systems provided to the contact class have incorrect order/type. \n" + " First system is {0} and second system is {1}. \n" + " First system should be a cylinder, second should be a plane" + ).format(mock_list.__class__, mock_plane.__class__) == str(excinfo.value) + + def test_cylinder_plane_contact_without_contact(self): + """ + This test case tests the forces on cylinder, when there is no + contact between cylinder and the plane. + + """ + + shift = -( + (2.0 - 1.0) * np.random.random_sample(1) + 1.0 + ).item() # we move plane away from cylinder + print("q") + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer( + shift + ) + + cylinder_plane_contact.apply_contact(cylinder, plane) + correct_forces = external_forces # since no contact + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) + + def test_cylinder_plane_contact_without_k_and_nu(self): + """ + This function tests wall response on the cylinder. Here + wall stiffness coefficient and damping coefficient set + to zero to check only sum of all forces on the cylinder. + + """ + + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer() + + cylinder_plane_contact.apply_contact(cylinder, plane) + + correct_forces = np.zeros((3, 1)) + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) + + @pytest.mark.parametrize("k_w", [0.1, 0.5, 1.0, 2, 10]) + def test_cylinder_plane_contact_with_k_without_nu(self, k_w): + """ + Here wall stiffness coefficient changed parametrically + and damping coefficient set to zero . + Parameters + ---------- + k_w + + + """ + + shift = np.random.random_sample(1).item() # we move plane towards to cylinder + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer( + shift=shift, k_w=k_w + ) + correct_forces = k_w * np.array([0.0, shift, 0.0]).reshape(3, 1) + + cylinder_plane_contact.apply_contact(cylinder, plane) + + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) + + @pytest.mark.parametrize("nu_w", [0.5, 1.0, 5.0, 7.0, 12.0]) + def test_cylinder_plane_contact_without_k_with_nu(self, nu_w): + """ + Here wall damping coefficient are changed parametrically and + wall response functions tested. + Parameters + ---------- + nu_w + """ + + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer( + nu_w=nu_w + ) + + normal_velocity = np.random.random_sample(1).item() + cylinder.velocity_collection[..., :] += np.array( + [0.0, -normal_velocity, 0.0] + ).reshape(3, 1) + + correct_forces = nu_w * np.array([0.0, normal_velocity, 0.0]).reshape(3, 1) + + cylinder_plane_contact.apply_contact(cylinder, plane) + + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) + + def test_cylinder_plane_contact_when_cylinder_is_under_plane(self): + """ + This test case tests plane response forces on the cylinder + in the case cylinder is under the plane and pushed towards + the plane. + + """ + + # we move plane on top of the cylinder. Note that 1.0 is radius of the cylinder. + offset_of_plane_with_respect_to_cylinder = 2.0 * 1.0 + + # plane normal changed, it is towards the negative direction, because cylinder + # is under the plane. + plane_normal = np.array([0.0, -1.0, 0.0]) + + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer( + shift=offset_of_plane_with_respect_to_cylinder, plane_normal=plane_normal + ) + + cylinder_plane_contact.apply_contact(cylinder, plane) + correct_forces = np.zeros((3, 1)) + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) + + @pytest.mark.parametrize("k_w", [0.1, 0.5, 1.0, 2, 10]) + def test_cylinder_plane_contact_when_cylinder_is_under_plane_with_k_without_nu( + self, k_w + ): + """ + In this test case we move the cylinder under the plane. + Here wall stiffness coefficient changed parametrically + and damping coefficient set to zero . + Parameters + ---------- + k_w + + """ + # we move plane on top of the cylinder. Note that 1.0 is radius of the cylinder. + offset_of_plane_with_respect_to_cylinder = 2.0 * 1.0 + + # we move plane towards to cylinder by random distance + shift = ( + offset_of_plane_with_respect_to_cylinder - np.random.random_sample(1).item() + ) + + # plane normal changed, it is towards the negative direction, because cylinder + # is under the plane. + plane_normal = np.array([0.0, -1.0, 0.0]) + + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer( + shift=shift, k_w=k_w, plane_normal=plane_normal + ) + + # we have to substract cylinder offset because top part + correct_forces = k_w * np.array( + [0.0, shift - offset_of_plane_with_respect_to_cylinder, 0.0] + ).reshape(3, 1) + + cylinder_plane_contact.apply_contact(cylinder, plane) + + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) + + @pytest.mark.parametrize("nu_w", [0.5, 1.0, 5.0, 7.0, 12.0]) + def test_cylinder_plane_contact_when_cylinder_is_under_plane_without_k_with_nu( + self, nu_w + ): + """ + In this test case we move under the plane and test damping force. + Here wall damping coefficient are changed parametrically and + wall response functions tested. + Parameters + ---------- + nu_w + + """ + # we move plane on top of the cylinder. Note that 1.0 is radius of the cylinder. + offset_of_plane_with_respect_to_cylinder = 2.0 * 1.0 + + # plane normal changed, it is towards the negative direction, because cylinder + # is under the plane. + plane_normal = np.array([0.0, -1.0, 0.0]) + + [cylinder, plane, cylinder_plane_contact, external_forces] = self.initializer( + shift=offset_of_plane_with_respect_to_cylinder, + nu_w=nu_w, + plane_normal=plane_normal, + ) + + normal_velocity = np.random.random_sample(1).item() + cylinder.velocity_collection[..., :] += np.array( + [0.0, -normal_velocity, 0.0] + ).reshape(3, 1) + + correct_forces = nu_w * np.array([0.0, normal_velocity, 0.0]).reshape(3, 1) + cylinder_plane_contact.apply_contact(cylinder, plane) + + assert_allclose(correct_forces, cylinder.external_forces, atol=Tolerance.atol()) From f8eadfecac874cd308c09ee41e6720602c6fe4da Mon Sep 17 00:00:00 2001 From: Ali-7800 <47090295+Ali-7800@users.noreply.github.com> Date: Mon, 5 Feb 2024 16:50:33 -0600 Subject: [PATCH 4/5] removed unused import --- elastica/interaction.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/elastica/interaction.py b/elastica/interaction.py index ae9faf5ca..737abf0b4 100644 --- a/elastica/interaction.py +++ b/elastica/interaction.py @@ -4,11 +4,6 @@ import numpy as np from elastica.external_forces import NoForces from numba import njit -from elastica._linalg import ( - _batch_norm, - _batch_product_i_k_to_ik, - _batch_product_i_ik_to_k, -) from elastica.contact_utils import ( _elements_to_nodes_inplace, _node_to_element_velocity, From b1fa5d406198115df09f84c5d5b50547b59a1b22 Mon Sep 17 00:00:00 2001 From: Seung Hyun Kim Date: Thu, 8 Feb 2024 13:17:45 -0600 Subject: [PATCH 5/5] Update .coveragerc bypass deprecation warning/error messages https://coverage.readthedocs.io/en/latest/excluding.html#excluding --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index fffd31429..1e03b34f0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,8 @@ exclude_lines = def __repr__ from import + raise AssertionError + raise NotImplementedError show_missing = true [run]