diff --git a/CHANGELOG.md b/CHANGELOG.md index 1511ab43b..696afb2d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `ComponentModeler.batch_data` convenience property to access the `BatchData` corresponding to the component modeler run. - Added optimization methods to the Design plugin. The plugin has been expanded to include Bayesian optimization, genetic algorithms and particle swarm optimization. Explanations of these methods are available in new and updated notebooks. - Added new support functions for the Design plugin: automated batching of `Simulation` objects, and summary functions with `DesignSpace.estimate_cost` and `DesignSpace.summarize`. +- Added validation and repair methods for `TriangleMesh` with inward-facing normals. ### Changed - Priority is given to `snapping_points` in `GridSpec` when close to structure boundaries, which reduces the chance of them being skipped. diff --git a/tests/test_components/test_geometry.py b/tests/test_components/test_geometry.py index 2ddbafa19..666b1033c 100644 --- a/tests/test_components/test_geometry.py +++ b/tests/test_components/test_geometry.py @@ -836,9 +836,9 @@ def test_custom_surface_geometry(tmp_path, log_capture): # test inconsistent winding vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]) - faces = np.array([[2, 1, 3], [0, 3, 2], [0, 1, 3], [0, 2, 1]]) + faces = np.array([[2, 3, 1], [0, 2, 3], [0, 3, 1], [0, 1, 2]]) tetrahedron = trimesh.Trimesh(vertices, faces) - with AssertLogLevel(log_capture, "WARNING"): + with AssertLogLevel(log_capture, "WARNING", contains_str="face orientations"): geom = td.TriangleMesh.from_trimesh(tetrahedron) with AssertLogLevel(log_capture, None): geom = geom.fix_winding() @@ -847,11 +847,20 @@ def test_custom_surface_geometry(tmp_path, log_capture): vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]) faces = np.array([[0, 3, 2], [0, 1, 3], [0, 2, 1]]) tetrahedron = trimesh.Trimesh(vertices, faces) - with AssertLogLevel(log_capture, "WARNING"): + with AssertLogLevel(log_capture, "WARNING", contains_str="watertight"): geom = td.TriangleMesh.from_trimesh(tetrahedron) with AssertLogLevel(log_capture, None): geom = geom.fill_holes() + # test inward normals + vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]) + faces = np.array([[2, 1, 3], [0, 3, 2], [0, 1, 3], [0, 2, 1]]) + tetrahedron = trimesh.Trimesh(vertices, faces) + with AssertLogLevel(log_capture, "WARNING", contains_str="outward"): + geom = td.TriangleMesh.from_trimesh(tetrahedron) + with AssertLogLevel(log_capture, None): + geom = geom.fix_normals() + # test zero area triangles vertices = np.array([[1, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]) faces = np.array([[1, 2, 3], [0, 3, 2], [0, 1, 3], [0, 2, 1]]) diff --git a/tidy3d/components/geometry/mesh.py b/tidy3d/components/geometry/mesh.py index 0f300e810..d2e258ee6 100644 --- a/tidy3d/components/geometry/mesh.py +++ b/tidy3d/components/geometry/mesh.py @@ -97,11 +97,23 @@ def _check_mesh(cls, val: TriangleMeshDataset) -> TriangleMeshDataset: if not mesh.is_winding_consistent: log.warning( "The provided mesh does not have consistent winding (face orientations). " - "This can lead to incorrect permittivity distributions. " + "This can lead to incorrect permittivity distributions, " + "and can also cause problems with plotting and mesh validation. " "You can try 'TriangleMesh.fix_winding', which attempts to repair the mesh. " "Otherwise, the mesh may require manual repair. You can use a " "'PermittivityMonitor' to check if the permittivity distribution is correct. " ) + if not mesh.is_volume: + log.warning( + "The provided mesh does not represent a valid volume, possibly due to " + "incorrect normal vector orientation. " + "This can lead to incorrect permittivity distributions, " + "and can also cause problems with plotting and mesh validation. " + "You can try 'TriangleMesh.fix_normals', " + "which attempts to fix the normals to be consistent and outward-facing. " + "Otherwise, the mesh may require manual repair. You can use a " + "'PermittivityMonitor' to check if the permittivity distribution is correct." + ) return val @@ -123,6 +135,15 @@ def fill_holes(self) -> TriangleMesh: trimesh.repair.fill_holes(mesh) return TriangleMesh.from_trimesh(mesh) + @verify_packages_import(["trimesh"]) + def fix_normals(self) -> TriangleMesh: + """Try to fix normals to be consistent and outward-facing.""" + import trimesh + + mesh = TriangleMesh._triangles_to_trimesh(self.mesh_dataset.surface_mesh) + trimesh.repair.fix_normals(mesh) + return TriangleMesh.from_trimesh(mesh) + @classmethod @verify_packages_import(["trimesh"]) def from_stl(