0.6.0
Loading...
Searching...
No Matches
Non-regression testing with notus.py

The following guidelines describe how to run the non-regression tests that verify Notus against a database of reference values.

Introduction

What is non-regression testing?

Every change to a large code like Notus carries a risk: while fixing or improving one part of the code, you may unintentionally change the result of another. Such an accidental change is called a regression. The danger is that it is silent — the code still compiles and still runs, it simply produces a slightly different (and now wrong) number.

Non-regression testing removes that blind spot. A set of test cases whose correct results are already known is run again after every significant change, and each result is compared to the value recorded the last time everything was known to be correct. As long as the numbers match, you have evidence that your change did not break anything. The moment one stops matching, you know exactly which case regressed, and you know it now rather than months later.

Why it matters for Notus

Non-regression is the automated backbone of the code's verification and validation (V&V) process. Notus is used for research, so its results must be trustworthy and reproducible: a published simulation is only meaningful if the same case, run again, still gives the same answer. Running the test campaign before merging a change is what keeps that guarantee true over time, across many contributors and many years of development.

Concretely, the test cases live under test_cases/, the reference values live in a database (test_cases/database/non-regression.db), and the non-regression mode of notus.py is the tool that runs the cases and confronts them with the database.

This page describes how a case is checked, how to launch and filter the tests, and how to read the results. Building and maintaining the reference database itself is covered in Manipulate the non-regression database.

How it works

The comparison at the heart of it

The whole method rests on one simple idea: compare a fresh result to a trusted one. A trusted result — a reference value — is a number produced by a case at a time when the code was known to be correct, and then stored in the database. Running non-regression means producing the number again and checking it still equals the stored one, within a small tolerance.

The tolerance matters because floating-point computations are never bit-for-bit identical across machines, compilers or run configurations. So the test does not ask "is the new value exactly equal?" but "is it close enough?":

\[ |V_\text{ref} - V_\text{run}| \le \text{tolerance} \]

This is an absolute tolerance, typically very small (of the order of 1e-8 to 1e-14), because the reference cases are chosen so that the physical result is essentially deterministic.

Step by step

When you launch ./notus.py non-regression, the script goes through the following stages:

  1. Collect the cases. It reads every NTS file matching the filter and extracts their @PARAMETER directives to build all parameter combinations. Each combination becomes one job.
  2. Run each job. For every job, Notus is launched with the parameters injected on the command line (-Dtype:name=value ...). Locally the jobs run directly; on a cluster they are submitted through a batch template (see Submitting options). Each run writes the quantities listed in its validation block to a .validation file.
  3. Look up the reference. The script locates the case in the database, keyed by the NTS file path and the exact parameter set, and retrieves the stored reference values and their tolerances.
  4. Compare and decide. Each produced value is checked against its reference with the formula above. A value passes or fails individually, and the case's status is derived from them.

From values to a verdict

The status is reported at two levels. Per case:

  • PASS — every checked value is within tolerance.
  • FAIL — at least one value is outside tolerance (a regression).
  • ERROR — the case could not be evaluated at all: it is absent from the database, or its .validation file is missing or unreadable.

Then, for the whole run, an overall status aggregates them: ERROR if any case errored, otherwise FAIL if any case failed, otherwise PASS. This single line tells you at a glance whether your change is safe to merge.

When you run it in practice

A typical workflow around a code change looks like this: you run the relevant part of the test suite before your change to confirm everything passes, make your change, then run it again. If the same cases still pass, your change is non-regressing. If a case flips to FAIL, you compare the produced value with the reference to understand what moved and whether it is a bug or an intended, legitimate change. In the latter case, once the new result is confirmed correct, the reference in the database is updated (see Manipulate the non-regression database) so that the new value becomes the trusted one from then on. Running the campaign before opening a merge request is part of the standard contribution process.

Preparing the NTS file

A non-regression case is a normal NTS file with a validation block that declares which quantities are checked. Only the values named there are compared against the database:

  post_processing {
     validation {
        reference_solution_error velocity;
        mean_velocity;
     }
  }

When the case is run through notus.py, these quantities are written to the .validation file and matched against the database entry for the case.

Note
The comparison is done on the values the case exports. If the validation block is missing, the run produces no comparable value and the case is reported as ERROR with the hint that the validation block may be missing.

Running the tests

Point the script at one or several NTS files, or at a directory of cases:

  ./notus.py non-regression test_cases/verification/navier/poiseuille_2D.nts

  ./notus.py non-regression test_cases/verification/

For each case, the script prints a comparison table and a status. When the case is found in the database:

  ────────────────────────────────
         Name         │ Reference value │      Value      │ Tolerance │ Difference │ Passed
  ────────────────────┼─────────────────┼─────────────────┼───────────┼────────────┼───────
  mean velocity       │ 1.001730e-01    │ 1.001730e-01    │ 1e-13     │ 2.1e-16    │ PASS
  Status: PASS

If a case is not yet stored, the values are still printed, but no comparison is possible:

  Status: ERROR (not in database)

At the end, an overall group status is printed: PASS if every case passed, FAIL if any case failed, or ERROR if any case could not be evaluated.

Filtering which cases run

Two filters, both regular expressions, let you target a subset of cases without moving files around.

  • --file-filter REGEX — keep only the cases whose file path matches. The leading test_cases/ prefix is optional.
    ./notus.py non-regression test_cases/ --file-filter poiseuille
    
  • --parameter-filter REGEX — keep only the parameter combinations whose parameters match. The pattern is matched against type:name=value, so all of these are valid:

    ./notus.py non-regression test_cases/ --parameter-filter "string:solver_name=mumps"
    ./notus.py non-regression test_cases/ --parameter-filter ".*:.*=mumps"
    ./notus.py non-regression test_cases/ --parameter-filter "solver_name"
    

    The last form filters on the parameter name only, regardless of its value.

Parametric and convergence cases

Non-regression covers more than single runs.

  • Parametric cases (@PARAMETER) generate one run per parameter combination; each combination is looked up and validated independently in the database.
  • Convergence cases (@CONVERGENCE) are validated too: the observed order of convergence is computed exactly as in the Convergence study with notus.py mode, then compared to the reference order stored in the database. The table then shows the reference order, tolerance, difference and a Passed column.

Scalability cases are not handled here; they have their own validation mode, see Scalability non-regression testing with notus.py.

Special case: cases that must stop

Some verification cases are meant to stop deliberately (for instance to check that an invalid configuration is rejected). These expose a kill status instead of numeric values: the case passes when it stopped as expected, and the table reports the kill status line with a PASS or FAIL accordingly.

Reloading and summaries

On a large test suite you rarely want to re-run everything.

  • --reload STATUS [STATUS ...] — re-run only the cases currently in a given state, e.g. --reload FAIL ERROR to retry what did not pass, without touching the cases that already passed.
  • --summary [STATUS ...] — print a summary of the current results and exit, optionally restricted to some statuses. Valid statuses are STANDBY, SUBMITTED, PASS, FAIL and ERROR.

Useful options

  • --database-path PATH — database to compare against (default: test_cases/database/non-regression.db).
  • -o, --directory DIR — output directory for the run and its .validation files (default: non-regression).
  • -n, --mpi-procs N — force the number of MPI processes.
  • -v, --verbose — verbose output (prints the generated jobs and the processed files).

Cluster submission

Like the other modes, non-regression can be submitted on a cluster in two passes: one to submit the jobs, one to collect and compare the results once they have finished.

With SLURM:

  ./notus.py non-regression --slurm path/to/template --stage submit
  ./notus.py non-regression --slurm path/to/template --stage post-process

With the generic dual-pass job manager:

  ./notus.py non-regression --dual-pass --stage submit       --batch-template path/to/template --batch-command "sbatch {BATCH_FILE}"
  ./notus.py non-regression --dual-pass --stage post-process --batch-template path/to/template --batch-command "sbatch {BATCH_FILE}"

See Submitting options for details on templates and the dual-pass workflow.

FAQ

A case reports "ERROR (not in database)". What do I do?

The case ran correctly but has no reference values yet. Add it to the database with ./notus.py database --add <filter> (see Manipulate the non-regression database). Once stored, it will be compared on the next run.

A case reports "Missing validation file".

Notus did not produce a .validation file. Either the validation block is missing from the NTS file, or, in a dual-pass run, the job has not finished yet (check your job scheduler before launching the post-process stage).

What is the difference between –file-filter and –parameter-filter?

--file-filter selects cases by their file path; --parameter-filter selects, within a case, the parameter combinations to test (by type:name=value). They can be combined to target a single parameter set of a single file.

How is a convergence case validated in non-regression?

The observed order of convergence is recomputed and compared to the reference order stored in the database, with its own tolerance. The case fails if the observed order drifts outside that tolerance.

Are the tolerances absolute or relative?

For standard non-regression values the tolerance is absolute. (Relative, percent-based tolerances are used for CPU times in the scalability validation mode, not here.)