From a4000c0a82d64e9cb254cbcbe40879cb00d49b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Mon, 28 Sep 2026 09:34:28 +0200 Subject: [PATCH] fix(integrate): sort boundary edges as pairs Assisted-by: GLM 5.3 Flash --- CHANGELOG.md | 6 ++++++ integrate/fem2d.go | 18 +++++++++++++++--- integrate/fem2d_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f00fc46..fa309f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 standard errors, t and F statistics read factored sums of squares instead of reporting infinity beside zero evidence. +**Integration and optimisation.** + +- `TriangleMesh2D.BoundaryEdges` returns the mesh's own edges: the + boundary pairs sort as pairs, never as one flat index list that + interleaves the endpoints of unrelated edges. + ## [1.0.0] - 2026-09-03 The initial release of Tensor, a scientific computing library in pure diff --git a/integrate/fem2d.go b/integrate/fem2d.go index e79b840..1138a44 100644 --- a/integrate/fem2d.go +++ b/integrate/fem2d.go @@ -114,13 +114,25 @@ func (m *TriangleMesh2D) BoundaryEdges() []int { count[key(b, c)]++ count[key(c, a)]++ } - edges := make([]int, 0, 8) + sets := make([][2]int, 0, len(count)) for e, n := range count { if n == 1 { - edges = append(edges, e[0], e[1]) + sets = append(sets, e) } } - slices.Sort(edges) + // The pairs sort as pairs, never as one flat index list: a flat sort + // interleaves the endpoints of unrelated edges and hands back pairs + // the mesh does not carry. + slices.SortFunc(sets, func(x, y [2]int) int { + if x[0] != y[0] { + return x[0] - y[0] + } + return x[1] - y[1] + }) + edges := make([]int, 0, 2*len(sets)) + for _, e := range sets { + edges = append(edges, e[0], e[1]) + } return edges } diff --git a/integrate/fem2d_test.go b/integrate/fem2d_test.go index 7b1001b..4db1ea9 100644 --- a/integrate/fem2d_test.go +++ b/integrate/fem2d_test.go @@ -434,6 +434,34 @@ func TestTriangleMesh2DBoundaryEdges(t *testing.T) { } } +// TestTriangleMesh2DBoundaryEdgesAreMeshEdges pins the pair contract of +// BoundaryEdges: every returned pair must be an edge the mesh actually +// carries, and the pairs must come out sorted, which a sort of the flat +// index list cannot deliver (it interleaves unrelated endpoints). +func TestTriangleMesh2DBoundaryEdgesAreMeshEdges(t *testing.T) { + mesh, err := GridTriangleMesh2D(0, 0, 1, 1, 1, 1) + if err != nil { + t.Fatalf("GridTriangleMesh2D: %v", err) + } + edges := mesh.BoundaryEdges() + // The square's four sides: (0,1), (0,2), (1,3), (2,3) in sorted + // pair order. + want := []int{0, 1, 0, 2, 1, 3, 2, 3} + if len(edges) != len(want) { + t.Fatalf("boundary edge count %d, want %d", len(edges)/2, len(want)/2) + } + for p := 0; p < len(edges); p += 2 { + if edges[p] > edges[p+1] { + t.Fatalf("edge [%d,%d] is not sorted as a pair", edges[p], edges[p+1]) + } + } + for p := range len(want) { + if edges[p] != want[p] { + t.Fatalf("boundary edges %v, want %v", edges, want) + } + } +} + // TestSolvePoissonFEM2DDuplicateDirichletNode pins the documented rule // for a node listed more than once: the last value is the prescribed // one and the node enters the assembled system exactly once, so the