Actual source code: ex306.c

  1: static char help[] = "Regression test for the MatSOR_SeqAIJ_Inode() block-diagonal cache.\n\n\
  2:    This test catches the typo in MatInvertDiagonalForSOR_SeqAIJ_Inode() introduced\n\
  3:    in MR !8797 where the cache validity check at inode.c:2432 reads a->idiagState\n\
  4:    (the point-SOR cache token) instead of a->inode.ibdiagState (the inode block-SOR\n\
  5:    cache token).  With the bug, the inverted block diagonal is recomputed on every\n\
  6:    MatSOR() call, producing a runtime regression but not a correctness failure --\n\
  7:    so we observe the cache invariant directly by poisoning the cached buffer and\n\
  8:    checking whether it survives a second MatSOR() call on an unchanged matrix.\n\n";

 10: #include <petscmat.h>

 12: /* The bug lives in private inode-cache fields.  Including the private header is
 13:    the cleanest way to test the cache invariant; this is the established practice
 14:    for tests that target a specific implementation detail rather than public API. */
 15: #include <../src/mat/impls/aij/seq/aij.h>

 17: int main(int argc, char **args)
 18: {
 19:   Mat         A;
 20:   Vec         b, x_pristine, x_poisoned;
 21:   Mat_SeqAIJ *a;
 22:   PetscInt    nblock = 20, blocksize = 3, n;
 23:   PetscBool   same;

 25:   PetscFunctionBeginUser;
 26:   PetscCall(PetscInitialize(&argc, &args, NULL, help));
 27:   n = nblock * blocksize;

 29:   PetscCall(MatCreate(PETSC_COMM_SELF, &A));
 30:   PetscCall(MatSetSizes(A, n, n, n, n));
 31:   PetscCall(MatSetType(A, MATSEQAIJ));
 32:   PetscCall(MatSeqAIJSetPreallocation(A, 3 * blocksize, NULL));

 34:   /* Block-tridiagonal matrix: each row has one diagonal entry of value 4.0
 35:      and -1.0 entries at every other column in its own block and the two
 36:      neighbouring blocks.  All `blocksize` rows of a block share the same column
 37:      pattern, so MatSeqAIJCheckInode() forms `nblock` inodes of size `blocksize`. */
 38:   for (PetscInt Ib = 0; Ib < nblock; Ib++) {
 39:     PetscInt    Jlo = PetscMax(Ib - 1, 0), Jhi = PetscMin(Ib + 1, nblock - 1);
 40:     PetscInt    ncols = (Jhi - Jlo + 1) * blocksize, cols[9];
 41:     PetscScalar vals[9];

 43:     for (PetscInt Jb = Jlo, k = 0; Jb <= Jhi; Jb++)
 44:       for (PetscInt jj = 0; jj < blocksize; jj++, k++) cols[k] = Jb * blocksize + jj;

 46:     for (PetscInt ii = 0; ii < blocksize; ii++) {
 47:       PetscInt row = Ib * blocksize + ii;

 49:       for (PetscInt k = 0; k < ncols; k++) vals[k] = (cols[k] == row) ? 4.0 : -1.0;
 50:       PetscCall(MatSetValues(A, 1, &row, ncols, cols, vals, INSERT_VALUES));
 51:     }
 52:   }
 53:   PetscCall(MatAssemblyBegin(A, MAT_FINAL_ASSEMBLY));
 54:   PetscCall(MatAssemblyEnd(A, MAT_FINAL_ASSEMBLY));

 56:   PetscCall(MatCreateVecs(A, &b, &x_pristine));
 57:   PetscCall(VecDuplicate(x_pristine, &x_poisoned));
 58:   PetscCall(VecSet(b, 1.0));
 59:   PetscCall(VecSet(x_pristine, 0.0));
 60:   PetscCall(VecSet(x_poisoned, 0.0));

 62:   /* First MatSOR() call: triggers MatInvertDiagonalForSOR_SeqAIJ_Inode() and
 63:      populates a->inode.ibdiag with correctly inverted blocks. */
 64:   PetscCall(MatSOR(A, b, 1.0, SOR_FORWARD_SWEEP, 0.0, 1, 1, x_pristine));

 66:   /* Confirm we actually reached the inode SOR path -- otherwise the test is
 67:      vacuous and would silently pass against a buggy build. */
 68:   a = (Mat_SeqAIJ *)A->data;
 69:   PetscCheck(a->inode.use && a->inode.size_csr && a->inode.node_count > 0 && a->inode.ibdiag != NULL, PETSC_COMM_SELF, PETSC_ERR_PLIB, "Inode SOR path was not exercised (use=%d node_count=%" PetscInt_FMT " ibdiag=%p); test cannot detect the bug.",
 70:              (int)a->inode.use, a->inode.node_count, (void *)a->inode.ibdiag);

 72:   /* Poison the cached inverted block diagonal.  A is unchanged, so its
 73:      PetscObject state does not advance and the cache *should* be honoured by
 74:      the next MatSOR() call.  A buggy implementation that rebuilds ibdiag on
 75:      every call will overwrite our zeros with the correct inverse and recover
 76:      x_pristine.  A correct implementation will use the zeros and produce a
 77:      materially different x_poisoned. */
 78:   for (PetscInt i = 0; i < a->inode.bdiagsize; i++) a->inode.ibdiag[i] = 0.0;

 80:   PetscCall(MatSOR(A, b, 1.0, SOR_FORWARD_SWEEP, 0.0, 1, 1, x_poisoned));

 82:   PetscCall(VecEqual(x_pristine, x_poisoned, &same));
 83:   PetscCheck(!same, PETSC_COMM_SELF, PETSC_ERR_PLIB, "MatSOR_SeqAIJ_Inode() rebuilt its cached block diagonal despite an unchanged matrix state. The cache validity check in MatInvertDiagonalForSOR_SeqAIJ_Inode() is comparing the wrong PetscObjectState field (regression of MR !8797).");

 85:   PetscCall(MatDestroy(&A));
 86:   PetscCall(VecDestroy(&b));
 87:   PetscCall(VecDestroy(&x_pristine));
 88:   PetscCall(VecDestroy(&x_poisoned));
 89:   PetscCall(PetscFinalize());
 90:   return 0;
 91: }

 93: /*TEST

 95:    test:
 96:      suffix: 0
 97:      args: -mat_no_inode false
 98:      output_file: output/empty.out

100: TEST*/