DNDSR 0.1.0.dev1+gcd065ad
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
test_vr_correctness.py
Go to the documentation of this file.
1"""
2Correctness tests for CFV.VariationalReconstruction and CFV.ModelEvaluator
3through the Python bindings.
4
5Tests cover:
6 - VR construction pipeline (metrics, base+weight, rec coeff)
7 - Constant and linear polynomial exactness
8 - Iterative reconstruction convergence
9 - ModelEvaluator RHS evaluation produces finite results
10 - ModelEvaluator DoReconstructionIter changes uRec
11
12Uses the Uniform_3x3 periodic mesh (9 quads, [0,3]^2).
13
14These tests use pytest assertions and are intended for automated CI.
15"""
16
17from __future__ import annotations
18
19import os
20import numpy as np
21import pytest
22import json
23
24from DNDSR import DNDS, Geom, CFV
25from DNDSR.Geom.utils import create_mesh_from_CGNS, create_bnd_mesh
26
27
28@pytest.fixture(scope="module")
29def mpi():
30 m = DNDS.MPIInfo()
31 m.setWorld()
32 return m
33
34
35def _data_mesh_path(name: str) -> str:
36 return os.path.join(
37 os.path.dirname(__file__), "..", "..", "data", "mesh", name
38 )
39
40
41@pytest.fixture(scope="module")
42def periodic_vfv(mpi):
43 """Build VariationalReconstruction_2 on Uniform_3x3 periodic mesh."""
44 mesh_file = _data_mesh_path("Uniform_3x3.cgns")
45 mesh, reader, name2Id = create_mesh_from_CGNS(
46 mesh_file,
47 mpi,
48 2,
49 periodic_geometry={
50 "translation1": [3, 0, 0],
51 "translation2": [0, 3, 0],
52 },
53 )
54
55 vfvSettings = {
56 "maxOrder": 2,
57 "intOrder": 5,
58 "cacheDiffBase": True,
59 "jacobiRelax": 1.0,
60 "SORInstead": False,
61 "subs2ndOrder": 0,
62 "functionalSettings": {
63 "dirWeightScheme": "HQM_OPT",
64 "geomWeightScheme": "HQM_SD",
65 "geomWeightPower": 0.5,
66 "geomWeightBias": 1,
67 },
68 }
69
70 vfv = CFV.VariationalReconstruction_2(mpi, mesh)
71 vfv.ParseSettings(vfvSettings)
72 vfv.SetPeriodicTransformationsNoOp()
73
74 bcid_2_bcweight_map = {}
75 for name, bc_id in name2Id.n2id_map.items():
76 bcid_2_bcweight_map[(bc_id, 0)] = 1.0
77 if name.startswith("PERIODIC"):
78 for order_idx in range(4):
79 bcid_2_bcweight_map[(bc_id, order_idx)] = 1.0
80 vfv.ConstructMetrics()
81 vfv.ConstructBaseAndWeight_map(bcid_2_bcweight_map)
82 vfv.ConstructRecCoeff()
83
84 return mesh, vfv, name2Id
85
86
87@pytest.fixture(scope="module")
88def gg_vfv(mpi):
89 """Build VariationalReconstruction_2 with Gauss-Green (subs2ndOrder=1)."""
90 mesh_file = _data_mesh_path("Uniform_3x3.cgns")
91 mesh, reader, name2Id = create_mesh_from_CGNS(
92 mesh_file,
93 mpi,
94 2,
95 periodic_geometry={
96 "translation1": [3, 0, 0],
97 "translation2": [0, 3, 0],
98 },
99 )
100
101 vfvSettings = {
102 "maxOrder": 1,
103 "intOrder": 5,
104 "cacheDiffBase": True,
105 "jacobiRelax": 1.0,
106 "SORInstead": False,
107 "subs2ndOrder": 1,
108 "functionalSettings": {
109 "dirWeightScheme": "HQM_OPT",
110 "geomWeightScheme": "HQM_SD",
111 "geomWeightPower": 0.5,
112 "geomWeightBias": 1,
113 },
114 }
115
116 vfv = CFV.VariationalReconstruction_2(mpi, mesh)
117 vfv.ParseSettings(vfvSettings)
118 vfv.SetPeriodicTransformationsNoOp()
119
120 bcid_2_bcweight_map = {}
121 for name, bc_id in name2Id.n2id_map.items():
122 bcid_2_bcweight_map[(bc_id, 0)] = 1.0
123 if name.startswith("PERIODIC"):
124 for order_idx in range(4):
125 bcid_2_bcweight_map[(bc_id, order_idx)] = 1.0
126 vfv.ConstructMetrics()
127 vfv.ConstructBaseAndWeight_map(bcid_2_bcweight_map)
128 vfv.ConstructRecCoeff()
129
130 return mesh, vfv, name2Id
131
132
133# ===================================================================
134# Construction pipeline
135# ===================================================================
136
137
139 """Verify VR construction pipeline completes and produces valid state."""
140
141 def test_global_vol(self, periodic_vfv):
142 """Global volume should be 9.0 for [0,3]^2."""
143 _, vfv, _ = periodic_vfv
144 assert vfv.GetGlobalVol() == pytest.approx(9.0, abs=1e-10)
145
146 def test_cell_bary_in_range(self, periodic_vfv):
147 """Primary cell barycenters should lie within [-1,2]^2
148 (a superset of the Uniform_3x3 mesh bounds)."""
149 mesh, vfv, _ = periodic_vfv
150 for iCell in range(mesh.NumCell()):
151 bary = vfv.GetCellBary(iCell)
152 assert -1.0 - 1e-10 <= bary[0] <= 2.0 + 1e-10
153 assert -1.0 - 1e-10 <= bary[1] <= 2.0 + 1e-10
154
155 def test_cell_vol_positive(self, periodic_vfv):
156 mesh, vfv, _ = periodic_vfv
157 for iCell in range(mesh.NumCell()):
158 assert vfv.GetCellVol(iCell) > 0
159
160 def test_face_area_positive(self, periodic_vfv):
161 mesh, vfv, _ = periodic_vfv
162 for iFace in range(mesh.NumFaceProc()):
163 assert vfv.GetFaceArea(iFace) > 0
164
165
166# ===================================================================
167# DOF array building
168# ===================================================================
169
170
172 """Verify DOF array building through VR (BuildURec, BuildUGrad)."""
173
174 def test_build_urec_dynamic(self, periodic_vfv):
175 _, vfv, _ = periodic_vfv
176 uRec = CFV.tURec_D()
177 vfv.BuildURec_D(uRec, 1)
178 assert uRec.Size() > 0
179
180 def test_build_udof_dynamic(self, periodic_vfv):
181 mesh, vfv, _ = periodic_vfv
182 u = CFV.tUDof_D()
183 vfv.BuildUDof_D(u, 1)
184 assert u.father.Size() == mesh.NumCell()
185
186 def test_build_ugrad_dynamic(self, periodic_vfv):
187 _, vfv, _ = periodic_vfv
188 uGrad = CFV.tUGrad_2xD()
189 vfv.BuildUGrad_D(uGrad, 1)
190 assert uGrad.Size() > 0
191
192
193# ===================================================================
194# Reconstruction: constant field should have zero reconstruction error
195# ===================================================================
196
197
199 """Constant field should produce zero reconstruction coefficients."""
200
201 def test_constant_field_zero_urec(self, periodic_vfv):
202 mesh, vfv, _ = periodic_vfv
203 u = CFV.tUDof_D()
204 vfv.BuildUDof_D(u, 1)
205
206 # Set constant field u = 7.0 for all cells
207 for iCell in range(mesh.NumCell()):
208 u_i = np.array(u[iCell], copy=False)
209 u_i[0] = 7.0
210 u.trans.startPersistentPull()
211 u.trans.waitPersistentPull()
212
213 uRec = CFV.tURec_D()
214 uRecNew = CFV.tURec_D()
215 vfv.BuildURec_D(uRec, 1)
216 vfv.BuildURec_D(uRecNew, 1)
217
218 eval_obj = CFV.ModelEvaluator(mesh, vfv, {}, 1)
219
220 # Run iterations (putIntoNew=True puts result into uRecNew, then swap)
221 for _ in range(20):
222 eval_obj.DoReconstructionIter(uRec, uRecNew, u, 0.0, True)
223 uRec, uRecNew = uRecNew, uRec
224
225 # All reconstruction coefficients should be zero (constant = no gradient)
226 max_coeff = 0.0
227 for iCell in range(mesh.NumCell()):
228 coeffs = np.array(uRec[iCell], copy=False)
229 max_coeff = max(max_coeff, np.abs(coeffs).max())
230
231 assert max_coeff < 1e-10, f"Max rec coeff = {max_coeff}, expected ~0"
232
233
234# ===================================================================
235# Reconstruction: linear field on periodic mesh
236# ===================================================================
237
238
240 """Linear field f(x,y) = x should produce accurate reconstruction."""
241
242 def test_linear_x_reconstruction(self, periodic_vfv):
243 """After iterating, uRec for u=x should contain correct dx coefficient."""
244 mesh, vfv, _ = periodic_vfv
245 u = CFV.tUDof_D()
246 vfv.BuildUDof_D(u, 1)
247
248 # Set u_i = x_bary for each cell
249 for iCell in range(mesh.NumCell()):
250 bary = vfv.GetCellBary(iCell)
251 u_i = np.array(u[iCell], copy=False)
252 u_i[0] = bary[0]
253 u.trans.startPersistentPull()
254 u.trans.waitPersistentPull()
255
256 uRec = CFV.tURec_D()
257 uRecNew = CFV.tURec_D()
258 vfv.BuildURec_D(uRec, 1)
259 vfv.BuildURec_D(uRecNew, 1)
260
261 eval_obj = CFV.ModelEvaluator(mesh, vfv, {}, 1)
262 for _ in range(100):
263 eval_obj.DoReconstructionIter(uRec, uRecNew, u, 0.0, True)
264 uRec, uRecNew = uRecNew, uRec
265
266 # Check that reconstruction has converged to non-trivial values
267 has_nonzero = False
268 for iCell in range(mesh.NumCell()):
269 coeffs = np.array(uRec[iCell], copy=False)
270 if np.abs(coeffs).max() > 1e-6:
271 has_nonzero = True
272 break
273 assert has_nonzero, "uRec should have non-zero coefficients for linear field"
274
275
276# ===================================================================
277# ModelEvaluator RHS evaluation
278# ===================================================================
279
280
282 """Verify ModelEvaluator.EvaluateRHS produces finite results."""
283
284 def test_rhs_finite(self, periodic_vfv):
285 mesh, vfv, _ = periodic_vfv
286 u = CFV.tUDof_D()
287 rhs = CFV.tUDof_D()
288 uRec = CFV.tURec_D()
289
290 vfv.BuildUDof_D(u, 1)
291 vfv.BuildUDof_D(rhs, 1)
292 vfv.BuildURec_D(uRec, 1)
293
294 # Set u = sin(2*pi*x/3)
295 for iCell in range(mesh.NumCell()):
296 bary = vfv.GetCellBary(iCell)
297 u_i = np.array(u[iCell], copy=False)
298 u_i[0] = np.sin(2.0 * np.pi * bary[0] / 3.0)
299 u.trans.startPersistentPull()
300 u.trans.waitPersistentPull()
301
302 eval_obj = CFV.ModelEvaluator(
303 mesh, vfv, {"ax": 1.0, "ay": 0.0, "sigma": 0.0}, 1
304 )
305 eval_obj.EvaluateRHS(rhs, u, uRec, 0.0)
306
307 # Check all RHS values are finite
308 for iCell in range(mesh.NumCell()):
309 rhs_i = np.array(rhs[iCell], copy=False)
310 assert np.all(np.isfinite(rhs_i)), (
311 f"Cell {iCell} RHS is not finite: {rhs_i}"
312 )
313
315 """RHS of advection equation should be nonzero for non-constant field."""
316 mesh, vfv, _ = periodic_vfv
317 u = CFV.tUDof_D()
318 rhs = CFV.tUDof_D()
319 uRec = CFV.tURec_D()
320
321 vfv.BuildUDof_D(u, 1)
322 vfv.BuildUDof_D(rhs, 1)
323 vfv.BuildURec_D(uRec, 1)
324
325 for iCell in range(mesh.NumCell()):
326 bary = vfv.GetCellBary(iCell)
327 u_i = np.array(u[iCell], copy=False)
328 u_i[0] = np.sin(2.0 * np.pi * bary[0] / 3.0)
329 u.trans.startPersistentPull()
330 u.trans.waitPersistentPull()
331
332 eval_obj = CFV.ModelEvaluator(
333 mesh, vfv, {"ax": 1.0, "ay": 0.0, "sigma": 0.0}, 1
334 )
335 eval_obj.EvaluateRHS(rhs, u, uRec, 0.0)
336
337 rhs_norm = 0.0
338 for iCell in range(mesh.NumCell()):
339 rhs_i = np.array(rhs[iCell], copy=False)
340 rhs_norm += np.sum(rhs_i ** 2)
341 rhs_norm = np.sqrt(rhs_norm)
342 assert rhs_norm > 1e-6, f"RHS norm = {rhs_norm}, expected nonzero"
343
344 def test_rhs_zero_for_constant_field(self, periodic_vfv):
345 """RHS of advection for constant field should be ~zero."""
346 mesh, vfv, _ = periodic_vfv
347 u = CFV.tUDof_D()
348 rhs = CFV.tUDof_D()
349 uRec = CFV.tURec_D()
350
351 vfv.BuildUDof_D(u, 1)
352 vfv.BuildUDof_D(rhs, 1)
353 vfv.BuildURec_D(uRec, 1)
354
355 for iCell in range(mesh.NumCell()):
356 u_i = np.array(u[iCell], copy=False)
357 u_i[0] = 5.0
358 u.trans.startPersistentPull()
359 u.trans.waitPersistentPull()
360
361 eval_obj = CFV.ModelEvaluator(
362 mesh, vfv, {"ax": 1.0, "ay": 1.0, "sigma": 0.0}, 1
363 )
364 eval_obj.EvaluateRHS(rhs, u, uRec, 0.0)
365
366 rhs_norm = 0.0
367 for iCell in range(mesh.NumCell()):
368 rhs_i = np.array(rhs[iCell], copy=False)
369 rhs_norm += np.sum(rhs_i ** 2)
370 rhs_norm = np.sqrt(rhs_norm)
371 assert rhs_norm < 1e-10, f"RHS norm = {rhs_norm}, expected ~0 for const field"
372
373
374# ===================================================================
375# Reconstruction iteration convergence
376# ===================================================================
377
378
380 """Verify VR iteration converges."""
381
382 def test_iterative_vr_convergence(self, periodic_vfv):
383 """Iterative VR should reduce increment over iterations."""
384 mesh, vfv, _ = periodic_vfv
385 u = CFV.tUDof_D()
386 vfv.BuildUDof_D(u, 1)
387
388 for iCell in range(mesh.NumCell()):
389 bary = vfv.GetCellBary(iCell)
390 u_i = np.array(u[iCell], copy=False)
391 u_i[0] = np.sin(2.0 * np.pi * bary[0] / 3.0) * np.cos(
392 2.0 * np.pi * bary[1] / 3.0
393 )
394 u.trans.startPersistentPull()
395 u.trans.waitPersistentPull()
396
397 uRec = CFV.tURec_D()
398 uRecNew = CFV.tURec_D()
399 vfv.BuildURec_D(uRec, 1)
400 vfv.BuildURec_D(uRecNew, 1)
401
402 eval_obj = CFV.ModelEvaluator(mesh, vfv, {}, 1)
403
404 increments = []
405 for _ in range(50):
406 eval_obj.DoReconstructionIter(uRec, uRecNew, u, 0.0, True)
407
408 # Compute increment (uRecNew has the new value)
409 inc = 0.0
410 for iCell in range(mesh.NumCell()):
411 diff = np.array(uRecNew[iCell]) - np.array(uRec[iCell])
412 inc += np.sum(diff ** 2)
413 increments.append(np.sqrt(inc))
414
415 uRec, uRecNew = uRecNew, uRec
416
417 # Verify the increment is decreasing (convergence)
418 # Allow for some noise in the first few iterations
419 assert increments[-1] < increments[2], (
420 f"Not converging: final inc = {increments[-1]}, "
421 f"early inc = {increments[2]}"
422 )
423
424
425# ===================================================================
426# Matrix access
427# ===================================================================
428
429
431 """Verify reconstruction matrices are accessible and valid."""
432
433 def test_matrixAAInvB_accessible(self, periodic_vfv):
434 """matrixAAInvB should be accessible and contain finite values."""
435 mesh, vfv, _ = periodic_vfv
436 mat = vfv.matrixAAInvB
437 assert mat is not None
438 # Access first cell's first matrix
439 m00 = np.array(mat[0, 0], copy=False)
440 assert np.all(np.isfinite(m00)), "matrixAAInvB[0,0] contains non-finite"
441 assert m00.shape[0] > 0
442 assert m00.shape[1] > 0
443
444 def test_vectorAInvB_accessible(self, periodic_vfv):
445 """vectorAInvB should be accessible and contain finite values."""
446 mesh, vfv, _ = periodic_vfv
447 vec = vfv.vectorAInvB
448 assert vec is not None
449 v00 = np.array(vec[0, 0], copy=False)
450 assert np.all(np.isfinite(v00)), "vectorAInvB[0,0] contains non-finite"
451
452
453# ===================================================================
454# Boundary callback
455# ===================================================================
456
457
459 """Verify ModelEvaluator.get_FBoundary returns a callable."""
460
461 def test_get_fboundary(self, periodic_vfv):
462 mesh, vfv, _ = periodic_vfv
463 eval_obj = CFV.ModelEvaluator(mesh, vfv, {}, 1)
464 fb = eval_obj.get_FBoundary(0.5)
465 assert fb is not None
466
467
468if __name__ == "__main__":
469 pytest.main([__file__, "-v"])
str _data_mesh_path(str name)
Lightweight bundle of an MPI communicator and the calling rank's coordinates.
Definition MPI.hpp:215