dlynch90 commited on
Commit
ae9e761
·
verified ·
1 Parent(s): a13364e

Create test_fem.py

Browse files
Files changed (1) hide show
  1. tests/test_fem.py +316 -0
tests/test_fem.py ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Comprehensive FEM Tests - 100% Vendor-Based Testing
2
+
3
+ Tests for mesh generation, boundary conditions, and FEM solver.
4
+ Uses pytest and numpy for validation.
5
+ """
6
+
7
+ import pytest
8
+ import numpy as np
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ # Add parent directory to path
13
+ sys.path.insert(0, str(Path(__file__).parent.parent))
14
+
15
+ try:
16
+ import skfem
17
+ SKFEM_AVAILABLE = True
18
+ except ImportError:
19
+ SKFEM_AVAILABLE = False
20
+
21
+ try:
22
+ import pygmsh
23
+ import meshio
24
+ MESH_LIBS_AVAILABLE = True
25
+ except ImportError:
26
+ MESH_LIBS_AVAILABLE = False
27
+
28
+ pytestmark = pytest.mark.skipif(
29
+ not (SKFEM_AVAILABLE and MESH_LIBS_AVAILABLE),
30
+ reason="FEM libraries not available"
31
+ )
32
+
33
+
34
+ class TestMeshGenerator:
35
+ """Test mesh generation using vendor libraries."""
36
+
37
+ def test_import_mesh_generator(self):
38
+ """Test mesh_generator module imports."""
39
+ from fem_core.mesh_generator import MeshGenerator
40
+ assert MeshGenerator is not None
41
+
42
+ def test_rectangle_mesh_generation(self):
43
+ """Test 2D rectangular mesh generation."""
44
+ from fem_core.mesh_generator import MeshGenerator
45
+
46
+ gen = MeshGenerator()
47
+ mesh = gen.generate_rectangle_mesh(width=2.0, height=1.0, nx=5, ny=5)
48
+
49
+ assert mesh is not None
50
+ assert len(mesh.points) > 0
51
+ assert len(mesh.cells) > 0
52
+
53
+ def test_circle_mesh_generation(self):
54
+ """Test circular mesh generation."""
55
+ from fem_core.mesh_generator import MeshGenerator
56
+
57
+ gen = MeshGenerator()
58
+ mesh = gen.generate_circle_mesh(radius=0.5, mesh_size=0.1)
59
+
60
+ assert mesh is not None
61
+ assert len(mesh.points) > 0
62
+
63
+ def test_mesh_info(self):
64
+ """Test mesh information extraction."""
65
+ from fem_core.mesh_generator import MeshGenerator
66
+
67
+ gen = MeshGenerator()
68
+ mesh = gen.generate_rectangle_mesh(1.0, 1.0, 3, 3)
69
+ info = gen.get_mesh_info(mesh)
70
+
71
+ assert 'num_points' in info
72
+ assert 'num_cells' in info
73
+ assert info['num_points'] > 0
74
+ assert info['num_cells'] > 0
75
+
76
+
77
+ class TestBoundaryConditions:
78
+ """Test boundary condition handling."""
79
+
80
+ def test_import_boundary_conditions(self):
81
+ """Test boundary_conditions module imports."""
82
+ from fem_core.boundary_conditions import (
83
+ BoundaryCondition,
84
+ BoundaryType,
85
+ BoundaryConditionHandler
86
+ )
87
+ assert BoundaryCondition is not None
88
+ assert BoundaryType is not None
89
+
90
+ def test_dirichlet_bc_creation(self):
91
+ """Test Dirichlet BC creation."""
92
+ from fem_core.boundary_conditions import (
93
+ create_dirichlet_bc,
94
+ BoundaryType
95
+ )
96
+
97
+ bc = create_dirichlet_bc(value=1.0)
98
+ assert bc.bc_type == BoundaryType.DIRICHLET
99
+ assert bc.value == 1.0
100
+
101
+ def test_neumann_bc_creation(self):
102
+ """Test Neumann BC creation."""
103
+ from fem_core.boundary_conditions import (
104
+ create_neumann_bc,
105
+ BoundaryType
106
+ )
107
+
108
+ bc = create_neumann_bc(value=0.5)
109
+ assert bc.bc_type == BoundaryType.NEUMANN
110
+ assert bc.value == 0.5
111
+
112
+ def test_bc_evaluation(self):
113
+ """Test BC evaluation at points."""
114
+ from fem_core.boundary_conditions import create_dirichlet_bc
115
+
116
+ # Constant value
117
+ bc = create_dirichlet_bc(value=2.0)
118
+ x = np.array([[0.0, 0.0], [1.0, 1.0]])
119
+ values = bc.evaluate(x)
120
+ assert np.allclose(values, 2.0)
121
+
122
+ # Function value
123
+ bc_func = create_dirichlet_bc(value=lambda x: np.sin(x[0]))
124
+ values = bc_func.evaluate(x)
125
+ assert len(values) == 2
126
+
127
+
128
+ class TestFEMSolver:
129
+ """Test FEM solver functionality."""
130
+
131
+ def test_import_solver(self):
132
+ """Test solver module imports."""
133
+ from fem_core.solver import FEMSolver, solve_poisson_2d
134
+ assert FEMSolver is not None
135
+ assert solve_poisson_2d is not None
136
+
137
+ def test_solver_initialization(self):
138
+ """Test FEM solver initialization."""
139
+ from fem_core.solver import FEMSolver
140
+
141
+ # Create simple mesh using scikit-fem
142
+ mesh = skfem.MeshTri()
143
+ solver = FEMSolver(mesh)
144
+
145
+ assert solver.mesh is not None
146
+ assert solver.basis is not None
147
+
148
+ def test_poisson_solve_simple(self):
149
+ """Test Poisson equation with simple source."""
150
+ from fem_core.solver import FEMSolver
151
+
152
+ # Unit square mesh
153
+ mesh = skfem.MeshTri()
154
+ mesh = mesh.refined(2) # Refine for better accuracy
155
+
156
+ solver = FEMSolver(mesh)
157
+
158
+ # Constant source term
159
+ def source(x):
160
+ return np.ones_like(x[0])
161
+
162
+ solution = solver.solve_poisson(source, dirichlet_val=0.0)
163
+
164
+ assert solution is not None
165
+ assert len(solution) == solver.basis.N
166
+ assert np.all(np.isfinite(solution))
167
+
168
+ def test_poisson_manufactured_solution(self):
169
+ """Test Poisson with manufactured solution."""
170
+ from fem_core.solver import FEMSolver
171
+
172
+ # Manufactured solution: u = x*(1-x)*y*(1-y)
173
+ # Then -Laplacian(u) = 2*y*(1-y) + 2*x*(1-x)
174
+
175
+ mesh = skfem.MeshTri()
176
+ mesh = mesh.refined(3)
177
+
178
+ solver = FEMSolver(mesh)
179
+
180
+ def source(x):
181
+ return 2*x[1]*(1-x[1]) + 2*x[0]*(1-x[0])
182
+
183
+ def exact(x):
184
+ return x[0]*(1-x[0])*x[1]*(1-x[1])
185
+
186
+ solution = solver.solve_poisson(source, dirichlet_val=0.0)
187
+
188
+ # Check boundary conditions
189
+ boundary_dofs = solver.basis.get_dofs()
190
+ assert np.allclose(solution[boundary_dofs], 0.0, atol=1e-10)
191
+
192
+ def test_helmholtz_solve(self):
193
+ """Test Helmholtz equation solver."""
194
+ from fem_core.solver import FEMSolver
195
+
196
+ mesh = skfem.MeshTri()
197
+ mesh = mesh.refined(2)
198
+
199
+ solver = FEMSolver(mesh)
200
+
201
+ k_squared = 1.0
202
+
203
+ def source(x):
204
+ return np.ones_like(x[0])
205
+
206
+ solution = solver.solve_helmholtz(k_squared, source, dirichlet_val=0.0)
207
+
208
+ assert solution is not None
209
+ assert np.all(np.isfinite(solution))
210
+
211
+
212
+ class TestIntegration:
213
+ """Integration tests for complete workflows."""
214
+
215
+ def test_full_poisson_workflow(self):
216
+ """Test complete Poisson solve workflow."""
217
+ from fem_core.mesh_generator import create_unit_square_mesh
218
+ from fem_core.solver import solve_poisson_2d
219
+
220
+ # Generate mesh
221
+ mesh = create_unit_square_mesh(n=5)
222
+
223
+ # Define problem
224
+ def source(x):
225
+ return -2.0 * (x[0]**2 + x[1]**2)
226
+
227
+ # Solve
228
+ solution, solver = solve_poisson_2d(mesh, source, bc_value=0.0)
229
+
230
+ assert solution is not None
231
+ assert solver is not None
232
+ assert len(solution) > 0
233
+
234
+ def test_convergence_rate(self):
235
+ """Test mesh convergence for Poisson equation."""
236
+ from fem_core.solver import FEMSolver
237
+
238
+ # Manufactured solution
239
+ def exact(x):
240
+ return np.sin(np.pi*x[0]) * np.sin(np.pi*x[1])
241
+
242
+ def source(x):
243
+ return 2*np.pi**2 * np.sin(np.pi*x[0]) * np.sin(np.pi*x[1])
244
+
245
+ errors = []
246
+ mesh_sizes = []
247
+
248
+ for refinement in [1, 2, 3]:
249
+ mesh = skfem.MeshTri()
250
+ mesh = mesh.refined(refinement)
251
+
252
+ solver = FEMSolver(mesh)
253
+ solution = solver.solve_poisson(source, dirichlet_val=0.0)
254
+
255
+ # Compute L2 error (simplified)
256
+ points = solver.basis.doflocs
257
+ exact_vals = exact(points)
258
+ error = np.linalg.norm(solution - exact_vals) / np.sqrt(len(solution))
259
+
260
+ errors.append(error)
261
+ mesh_sizes.append(1.0 / (2**refinement))
262
+
263
+ # Check that error decreases with refinement
264
+ assert errors[0] > errors[1] > errors[2]
265
+
266
+
267
+ # Empirical validation tests
268
+ class TestEmpiricalValidation:
269
+ """Empirical validation of FEM implementation."""
270
+
271
+ def test_symmetry_of_stiffness_matrix(self):
272
+ """Verify stiffness matrix is symmetric."""
273
+ from fem_core.solver import FEMSolver
274
+ from skfem import BilinearForm, asm
275
+ from skfem.helpers import dot, grad
276
+
277
+ mesh = skfem.MeshTri()
278
+ solver = FEMSolver(mesh)
279
+
280
+ @BilinearForm
281
+ def laplacian(u, v, _):
282
+ return dot(grad(u), grad(v))
283
+
284
+ K = asm(laplacian, solver.basis)
285
+
286
+ # Check symmetry
287
+ assert np.allclose(K.toarray(), K.T.toarray())
288
+
289
+ def test_mass_conservation(self):
290
+ """Test mass conservation in heat equation."""
291
+ # Placeholder for heat equation mass conservation test
292
+ assert True
293
+
294
+ def test_boundary_value_enforcement(self):
295
+ """Verify boundary values are enforced correctly."""
296
+ from fem_core.solver import FEMSolver
297
+
298
+ mesh = skfem.MeshTri()
299
+ mesh = mesh.refined(2)
300
+
301
+ solver = FEMSolver(mesh)
302
+
303
+ def source(x):
304
+ return np.ones_like(x[0])
305
+
306
+ bc_value = 5.0
307
+ solution = solver.solve_poisson(source, dirichlet_val=bc_value)
308
+
309
+ boundary_dofs = solver.basis.get_dofs()
310
+
311
+ # All boundary DOFs should equal bc_value
312
+ assert np.allclose(solution[boundary_dofs], bc_value, atol=1e-10)
313
+
314
+
315
+ if __name__ == "__main__":
316
+ pytest.main([__file__, "-v"])