diff --git a/.github/workflows/docs-website.yml b/.github/workflows/docs-website.yml new file mode 100644 index 000000000000..e5a78bca2492 --- /dev/null +++ b/.github/workflows/docs-website.yml @@ -0,0 +1,84 @@ +name: Docs Website + +on: + push: + branches: ['docs_site', 'docs/developer-guides'] + pull_request: + branches: ['docs_site', 'develop'] + workflow_dispatch: + inputs: + site_repo: + description: Website repository + default: rois1995/su2code.github.io + site_ref: + description: Website commit SHA or branch (use a SHA to reproduce a build) + default: docs/user-guides + +permissions: + contents: read + +concurrency: + group: docs-website-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - name: Checkout SU2 + uses: actions/checkout@v4 + with: + path: su2 + fetch-depth: 0 + filter: blob:none + persist-credentials: false + - name: Get release tags for migration reference + run: git -C su2 fetch --quiet --filter=blob:none https://github.com/su2code/SU2.git 'refs/tags/v7.*:refs/tags/v7.*' 'refs/tags/v8.*:refs/tags/v8.*' + - name: Checkout website + uses: actions/checkout@v4 + with: + repository: ${{ inputs.site_repo || 'rois1995/su2code.github.io' }} + ref: ${{ inputs.site_ref || 'docs/user-guides' }} + path: site + fetch-depth: 0 + filter: blob:none + persist-credentials: false + - name: Setup Ruby and locked gems + uses: ruby/setup-ruby@v1 + with: + working-directory: site + ruby-version: .ruby-version + bundler: '2.4.22' + bundler-cache: true + - name: Install code reference tools + run: sudo apt-get update && sudo apt-get install -y doxygen graphviz + - name: Build and check website + env: + BUNDLE_PATH: ${{ github.workspace }}/site/vendor/bundle + run: python3 su2/Docs/website/tools/build_docs.py --su2 su2 --site site --build-dir "$RUNNER_TEMP/docs-site" --baseurl '/${{ github.event.repository.name }}' --mark-changes + - name: Upload checked preview + uses: actions/upload-artifact@v4 + with: + name: docs-preview + path: ${{ runner.temp }}/docs-site/site + retention-days: 7 + - name: Upload Pages artifact + if: github.repository == 'rois1995/SU2' && github.ref == 'refs/heads/docs_site' && github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v3 + with: + path: ${{ runner.temp }}/docs-site/site + + deploy: + if: github.repository == 'rois1995/SU2' && github.ref == 'refs/heads/docs_site' && github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-24.04 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy checked website + id: deployment + uses: actions/deploy-pages@v4 diff --git a/Docs/website/README.md b/Docs/website/README.md new file mode 100644 index 000000000000..6d8610add6ff --- /dev/null +++ b/Docs/website/README.md @@ -0,0 +1,86 @@ +# SU2 documentation website + +The combined site uses two repositories. Developer pages and the generation/build/check tools live here; user pages, model explanations, setup guides, tutorials, V&V, navigation and search live in the website repository. Builds stage copies of both sources; the source checkouts are not overwritten. + +## Where an explanation belongs + +- **Setup guides:** how to choose settings for a class of problems, why they matter, convergence checks and troubleshooting. Link to worked cases instead of repeating their full instructions. +- **Tutorials:** complete worked cases, with meshes, configurations, commands and post-processing. +- **V&V:** numerical evidence, comparisons and their scope; distinguish implementation verification from physical validation. +- **Model pages:** assumptions, exact model variant, definitions and units, governing equations or precise equation references, and differences between the literature and SU2. +- **Developer pages:** source locations, control flow, extension steps and appropriate regression or unit checks. +- **Generated references:** option/output/error facts extracted from the source. Explain extraction limits rather than guessing defaults or applicability. + +The website's **Using the Documentation** page explains this structure to readers. Keep general explanations in one place and cross-link them. In a model or developer explanation, identify the source file and checked commit; for a theoretical claim, cite the relevant paper or equation. A regression config establishes usage, not a universal numerical recommendation. Retain draft labels until the stated review is complete. + +## Editing + +Keep completed improvements, remaining gaps and their evidence in `pages/Documentation-Status.md`, listed under **Developer Docs**. Update its status and dated history when a batch is completed; use the generated reports for current build counts. + +Developer pages live in `pages/`, with `title` and `permalink: /docs_v7//` front matter. `Adding-...` pages appear under **How to Add...**; other new pages appear under **Developer Docs**. Images in `pages/img/` are copied to `docs_files/website/`. + +After checking a developer page against the implementation, update its `Last checked against SU2 develop ` line. The Docs Check page flags named files changed since that commit; this is a review prompt, not a proof that the page is wrong or complete. + +Option descriptions belong above their registration in `Common/src/CConfig.cpp`, using the existing Doxygen `DESCRIPTION:`, `OPTIONS:` and `DEFAULT:` conventions. Keep examples in `config_template.cfg` consistent with the parser. Defaults set elsewhere may not be extractable; a template example is not necessarily a default. Simple string literals and string constructors are shown as configuration values; unresolved string expressions are labelled as C++ expressions. The reference also links to relevant pages and regression configurations. + +The generator copies these pages and Markdown notes from code folders into a staged website. It produces the option, output, error and migration references and the Docs Check page, then updates navigation. Do not edit these generated copies in the website repository. + +## Local build + +Requirements: Python 3.8 or newer, Git, Ruby **3.3.12** (the website's `.ruby-version`), Bundler **2.4.22**, Doxygen and Graphviz. Use an existing Ruby version manager to select the required Ruby; the system Ruby need not change. The website's tracked `Gemfile.lock` fixes gem versions. + +With the two `docs_site` checkouts, run from the SU2 source root: + +```sh +# Set these paths to your two existing checkouts/output folder. +WEBSITE_SOURCE=/path/to/su2code.github.io +DOCS_BUILD=/path/to/docs-build + +gem install bundler -v 2.4.22 +BUNDLE_GEMFILE="$WEBSITE_SOURCE/Gemfile" bundle _2.4.22_ install +python3 Docs/website/tools/build_docs.py --site "$WEBSITE_SOURCE" --build-dir "$DOCS_BUILD" --baseurl /SU2 --mark-changes +mkdir -p "$DOCS_BUILD/serve" +ln -sfn ../site "$DOCS_BUILD/serve/SU2" +python3 -m http.server 8765 --directory "$DOCS_BUILD/serve" +``` + +Open `http://localhost:8765/SU2/`. For a direct server without the symlink/prefix, build with `--baseurl ''`, serve `$DOCS_BUILD/site` and open `http://localhost:8765/`. + +`--mark-changes` preserves the red highlighting of added/modified menu items, menu sections and page titles compared with the official site. Both CI workflows pass it. This debug comparison fetches upstream website history; if unavailable, generated/developer pages remain highlighted and the build reports the limitation. + +The build directory must be outside both source trees. It must be empty on first use; subsequent runs replace only its managed `source`, `site` and `doxygen` subdirectories. Keep source files elsewhere. Omitted download folders link to the official site. + +The same command builds Doxygen, stages the site, generates references, builds Jekyll, adjusts subpath links, and checks the assembled result. It runs the Python tests first and fails on new documentation problems. There is no separate local assembly recipe. + +## Checks and existing debt + +Run the small checks directly when changing tooling: + +```sh +python3 -m unittest discover -s Docs/website/tools +``` + +The full build checks local links and anchors in current documentation, Tutorials and V&V; configuration option names and enum keywords in configuration blocks; descriptions for newly added options; search entries and searchable output-field names. It does not request external URLs or establish that numerical examples are converged. Solver-specific option combinations still require appropriate SU2 tests. + +`tools/check-baseline.json` retains 24 options missing source descriptions. The 22 October follow-up resolved the 180 historical links, eight unknown template names and one obsolete enum keyword; those baseline arrays are empty. Existing link findings remain visible in the machine-readable `docs-check.json`; new findings fail the build. Remove an entry when its problem is fixed. Do not regenerate the baseline to approve new regressions. The generated Docs Check page also lists source-description and stale-page work. + +Each assembled site includes: + +- `build-info.json`: both source commit SHAs, whether either working tree was dirty, the gem lock hash, base path, build flags and tool versions. +- `docs-check.json`: new problems, checked-reference/search counts and existing link findings. + +For a reproducible rebuild, check out the two recorded SHAs, select the recorded Ruby and locked gems, and use the recorded base path and flags. Dirty working trees are useful for previews but cannot be reconstructed from commit SHAs alone. Doxygen/Graphviz versions are recorded, not pinned; changes in those tools can change generated output. The date on the option reference comes from the source commits through `SOURCE_DATE_EPOCH`. + +## CI and publishing + +`.github/workflows/docs-website.yml` checks pushes to `docs_site` and `docs/developer-guides`, and pull requests targeting `develop` or `docs_site`. The PR preparation uses website branch `docs/user-guides` by default. Pull requests and pushes to the preparation branch upload checked previews without deploying. Publishing is restricted to `docs_site` runs in `rois1995/SU2`; the workflow does not publish from the official SU2 repository. Checkout references are resolved once; the manifest records the exact revisions used. + +The website repository's `docs-check.yml` checks pushes to `docs_site`, `docs/user-guides` and `docs/naca0021`, and pull requests targeting `develop`, `docs_site` or `docs/user-guides`. It uses SU2 branch `docs/developer-guides` by default and uploads a checked preview. Website-only changes therefore get checks without cross-repository write credentials. After those checks pass, publish from the SU2 **Docs Website** workflow, choosing the website commit SHA as `site_ref`: + +```sh +gh workflow run docs-website.yml --repo rois1995/SU2 --ref docs_site -f site_repo=rois1995/su2code.github.io -f site_ref= +``` + +To reproduce a historical deployment, select its SU2 revision as well. Normal branch builds follow the current website branch; use a commit SHA when reviewing or reproducing a particular pair of changes. Changes to workflow files require GitHub credentials allowed to update workflows; ordinary repository-only OAuth scopes may reject them. + +The paired sources currently use the `rois1995` forks. Publish both preparation branches to the forks before relying on their combined checks; a run started before the other branch is available may need to be repeated. Official website publishing remains a separate integration decision. Do not push these development branches directly to the official repositories. diff --git a/Docs/website/pages/Adding-Output-Fields.md b/Docs/website/pages/Adding-Output-Fields.md new file mode 100644 index 000000000000..9a350dee681d --- /dev/null +++ b/Docs/website/pages/Adding-Output-Fields.md @@ -0,0 +1,280 @@ +--- +title: Adding Output Fields +permalink: /docs_v7/Adding-Output-Fields/ +--- + +This page shows how to add a new field to the output of SU2. A field can go to the screen and history output, or to the volume output (restart and visualization files). The page is for developers. For how users select fields, see [History and Solution Output](../Custom-Output/). + +--- + +- [Terms](#terms) +- [Where the output classes are](#where-the-output-classes-are) +- [How the output works](#how-the-output-works) +- [Add a history field](#add-a-history-field) +- [Add a volume field](#add-a-volume-field) +- [Turbulence, transition and species fields](#turbulence-transition-and-species-fields) +- [How users select the new field](#how-users-select-the-new-field) +- [Checklist](#checklist) + +--- + +## Terms + +- **History field**: one value per iteration. It can be printed on screen (`SCREEN_OUTPUT`) and written to the history file (`HISTORY_OUTPUT`). +- **Per-surface history field**: one history value for each marker of a list, for example for each marker in `MARKER_MONITORING`. +- **Volume field**: one value per mesh point. It is written to the restart and visualization files (`VOLUME_OUTPUT`). +- **Group**: a name shared by several fields. Users can request a whole group instead of single fields. + +Each field has two labels: + +- a **name**, used in the configuration file and in the code, for example `DRAG`; +- a **header**, printed on screen or in the file, for example `CD`. + +## Where the output classes are + +All output code is in `SU2_CFD/include/output/` and `SU2_CFD/src/output/`. The base class is [`COutput`](../../doxygen/classCOutput.html). `COutputFactory::CreateOutput` picks the child class from the `SOLVER` option: + +| `SOLVER` | Output class | +|---|---| +| `EULER`, `NAVIER_STOKES`, `RANS` | `CFlowCompOutput` | +| `INC_EULER`, `INC_NAVIER_STOKES`, `INC_RANS` | `CFlowIncOutput` | +| `NEMO_EULER`, `NEMO_NAVIER_STOKES` | `CNEMOCompOutput` | +| `FEM_EULER`, `FEM_NAVIER_STOKES`, `FEM_RANS`, `FEM_LES` | `CFlowCompFEMOutput` | +| `HEAT_EQUATION` | `CHeatOutput` | +| `ELASTICITY` | `CElasticityOutput` | +| compressible flow with `MATH_PROBLEM= CONTINUOUS_ADJOINT` or `DISCRETE_ADJOINT` | `CAdjFlowCompOutput` | +| incompressible flow with `MATH_PROBLEM= DISCRETE_ADJOINT` | `CAdjFlowIncOutput` | +| `HEAT_EQUATION` with `MATH_PROBLEM= DISCRETE_ADJOINT` | `CAdjHeatOutput` | +| `ELASTICITY` with `MATH_PROBLEM= DISCRETE_ADJOINT` | `CAdjElasticityOutput` | + +Each class `CXxxOutput` is in `SU2_CFD/src/output/CXxxOutput.cpp`, with the header `SU2_CFD/include/output/CXxxOutput.hpp`. + +Shared parents and other output classes: + +- [`CFVMOutput`](../../doxygen/classCFVMOutput.html): parent of the finite volume outputs. It adds the coordinates (`AddCoordinates`, `LoadCoordinates`) and common fields such as the mesh quality (`AddCommonFVMOutputs`, `LoadCommonFVMOutputs`). +- [`CFlowOutput`](../../doxygen/classCFlowOutput.html): parent of the flow outputs (`CFlowCompOutput`, `CFlowIncOutput`, `CNEMOCompOutput`, `CFlowCompFEMOutput`). It holds: + - the turbulence, transition and species fields; + - the aerodynamic coefficients; + - the `MARKER_ANALYZE` fields. + + It also sets the wall fields (skin friction, heat flux, y+) in `LoadSurfaceData`. +- [`CAdjFlowOutput`](../../doxygen/classCAdjFlowOutput.html): parent of the adjoint flow outputs, with the adjoint turbulence and species fields. +- [`CMultizoneOutput`](../../doxygen/classCMultizoneOutput.html): the history of a multizone run. It copies the history fields of every zone and adds the suffix `[]` to their names. So a new history field of a single-zone output also appears there, with no change to `CMultizoneOutput`. +- `CBaselineOutput` (used by `SU2_SOL` and `SU2_DOT`) and `CMeshOutput` (used by `SU2_DEF`). + +## How the output works + +`COutput` calls virtual functions ("hooks") that the child classes implement. To add a field, you change these hooks: + +| Hook | When it is called | What it does | +|---|---|---| +| `SetHistoryOutputFields(CConfig*)` | once, at start-up (from `PreprocessHistoryOutput`) | declares the history fields with `AddHistoryOutput` and `AddHistoryOutputPerSurface` | +| `LoadHistoryData(CConfig*, CGeometry*, CSolver**)` | every iteration (from `SetHistoryOutput`) | sets the values with `SetHistoryOutputValue` and `SetHistoryOutputPerSurfaceValue` | +| `SetVolumeOutputFields(CConfig*)` | once, at start-up (from `PreprocessVolumeOutput`) | declares the volume fields with `AddVolumeOutput` | +| `LoadVolumeData(CConfig*, CGeometry*, CSolver**, unsigned long iPoint)` | when files are written, for each point owned by the rank. For time-domain problems, at every call of `SetResultFiles`, to update the time averages. | sets the values with `SetVolumeOutputValue` | +| `LoadSurfaceData(CConfig*, CGeometry*, CSolver**, unsigned long iPoint, unsigned short iMarker, unsigned long iVertex)` | after `LoadVolumeData`, for each vertex of the solid wall markers | sets volume fields that only exist at walls | + +Other hooks: + +- The DG output uses `LoadVolumeDataFEM` instead of `LoadVolumeData`. +- Multizone output uses `SetMultizoneHistoryOutputFields` and `LoadMultizoneHistoryData`. +- `SetAdditionalScreenOutput` prints extra screen output, and `WriteAdditionalFiles` writes extra files, for the current solver. + +The functions to declare and set fields are members of `COutput` (`SU2_CFD/include/output/COutput.hpp`): + +```cpp +void AddHistoryOutput(string name, string field_name, ScreenOutputFormat format, + string groupname, string description, + HistoryFieldType field_type = HistoryFieldType::DEFAULT); +void SetHistoryOutputValue(string name, su2double value); + +void AddHistoryOutputPerSurface(string name, string field_name, ScreenOutputFormat format, + string groupname, const vector& marker_names, + HistoryFieldType field_type = HistoryFieldType::DEFAULT); +void SetHistoryOutputPerSurfaceValue(string name, su2double value, unsigned short iMarker); + +void AddVolumeOutput(const string& name, const string& field_name, + const string& group_name, const string& description); +void SetVolumeOutputValue(const string& name, unsigned long iPoint, su2double value); +``` + +`ScreenOutputFormat` sets how the value is printed on screen: `INTEGER`, `FIXED`, `SCIENTIFIC` or `PERCENT`. + +`HistoryFieldType` says what kind of value the field holds. SU2 uses it to make extra fields and to check convergence: + +| `HistoryFieldType` | Use it for | What SU2 adds | +|---|---|---| +| `RESIDUAL` | residuals, stored as `log10` of the value | The field `REL_`: the value minus a reference value (the initial residual, or a larger value seen later). Also `AVG_` for the groups `RMS_RES`, `MAX_RES` and `BGS_RES`. In `CONV_FIELD`, the field converges when it is below `CONV_RESIDUAL_MINVAL`. | +| `COEFFICIENT` | integrated quantities, such as forces or mass flow | `TAVG_` (time average) when `TIME_DOMAIN= YES`. `D_` when `DIRECT_DIFF` is set. `CAUCHY_` when the field is in `CONV_FIELD`. In `CONV_FIELD`, the field converges by the Cauchy criterion (`CONV_CAUCHY_ELEMS`, `CONV_CAUCHY_EPS`). | +| `DEFAULT` | everything else, for example iteration counts | Nothing. A `DEFAULT` field in `CONV_FIELD` never meets the convergence criterion. | +| `AUTO_RESIDUAL`, `AUTO_COEFFICIENT` | fields made by SU2 itself (the `REL_`, `AVG_`, `TAVG_`, `D_`, `CAUCHY_` fields) | Do not use them for new fields. | + +These steps are in `COutput::PostprocessHistoryFields`, `COutput::PostprocessHistoryData` and `COutput::ConvergenceMonitoring` (`SU2_CFD/src/output/COutput.cpp`). + +## Add a history field + +1. Compute the value in the solver. Store it in a member and add a getter. If the output class accesses the solver through a `CSolver*`, add a virtual getter with a default value to `CSolver` (`SU2_CFD/include/solvers/CSolver.hpp`). Override it in your solver. +2. In `SetHistoryOutputFields` of the output class, call `AddHistoryOutput`. Pick a name, a short header, a format, a group, a description and a `HistoryFieldType`. +3. In `LoadHistoryData`, call `SetHistoryOutputValue` with the same name. +4. Put the same `if` condition around both calls. If the field was not added, `SetHistoryOutputValue` stops SU2 with the error "Cannot find output field with name ...". + +### Example: the buffet sensor + +`CNSSolver::Buffet_Monitoring` computes the buffet sensor and stores it in `Total_Buffet_Metric`. `CSolver` has the virtual getter `GetTotal_Buffet_Metric()` (it returns 0), and `CNSSolver` overrides it. + +In `CFlowCompOutput::SetHistoryOutputFields` (`SU2_CFD/src/output/CFlowCompOutput.cpp`): + +```cpp + if (config->GetViscous()) { + AddHistoryOutput("BUFFET", "Buffet", ScreenOutputFormat::SCIENTIFIC, "AERO_COEFF", "Buffet sensor", HistoryFieldType::COEFFICIENT); + } +``` + +In `CFlowCompOutput::LoadHistoryData` (same file): + +```cpp + CSolver* flow_solver = solver[FLOW_SOL]; + // ... + if (config->GetViscous()) { + SetHistoryOutputValue("BUFFET", flow_solver->GetTotal_Buffet_Metric()); + } +``` + +The field joins the group `AERO_COEFF`. Because it is a `COEFFICIENT`: + +- it can be used in `CONV_FIELD` with the Cauchy criterion; +- it gets a `TAVG_BUFFET` field in unsteady runs. + +### Per-surface fields + +`AddHistoryOutputPerSurface` makes one field for each marker in the list. The header of each one is `field_name(marker)`. Set the values with the index of the marker in the list. + +This example is from `CFlowOutput::AddAerodynamicCoefficients` and `CFlowOutput::SetAerodynamicCoefficients` (`SU2_CFD/src/output/CFlowOutput.cpp`): + +```cpp + vector Marker_Monitoring; + for (unsigned short iMarker_Monitoring = 0; iMarker_Monitoring < config->GetnMarker_Monitoring(); iMarker_Monitoring++){ + Marker_Monitoring.push_back(config->GetMarker_Monitoring_TagBound(iMarker_Monitoring)); + } + /// DESCRIPTION: Drag coefficient + AddHistoryOutputPerSurface("DRAG_ON_SURFACE", "CD", ScreenOutputFormat::FIXED, "AERO_COEFF_SURF", Marker_Monitoring, HistoryFieldType::COEFFICIENT); +``` + +```cpp + for (unsigned short iMarker_Monitoring = 0; iMarker_Monitoring < config->GetnMarker_Monitoring(); iMarker_Monitoring++) { + SetHistoryOutputPerSurfaceValue("DRAG_ON_SURFACE", flow_solver->GetSurface_CD(iMarker_Monitoring), iMarker_Monitoring); +``` + +Users can select per-surface fields in `SCREEN_OUTPUT` and `HISTORY_OUTPUT`, but not in `CONV_FIELD`. + +### Notes + +- `LoadHistoryData` runs every iteration, also when the field is not requested. Keep it cheap. If the value is expensive, compute it in the solver only when needed. +- For residual fields, store `log10` of the residual, as the existing fields do (for example `log10(flow_solver->GetRes_RMS(0))`). +- Always write a description. `SU2_CFD -d` (dry run, `CDummyDriver`) prints the list of fields with their descriptions. History fields with an empty description are left out of that list. +- Python code can read history fields through the wrapper (`GetOutputNames`, `GetOutputValue` of `CDriverBase`). +- Some Python scripts in `SU2_PY` look up the group and type of a history field in `SU2_PY/SU2/io/historyMap.py`. Examples are the objectives of unsteady optimizations in `SU2_PY/SU2/io/config.py`, and `direct_differentiation.py`. If the new field should be usable there: + 1. Write the `AddHistoryOutput(...)` call on one line, at the start of the line. `SU2_PY/updateHistoryMap.py` makes `historyMap.py` by reading the lines that start with `AddHistoryOutput(` in the output source files listed in the script. It does not find calls written in another way. + 2. Run `SU2_PY/updateHistoryMap.py` (it needs `SU2_HOME`). + 3. Commit the new `historyMap.py`. + +## Add a volume field + +1. Make the value available per point, usually through a getter of the `CVariable` child (`solver->GetNodes()`). If the output class uses a `CVariable*`, add a virtual getter with a default value to `CVariable`. Override it in your variable class. +2. In `SetVolumeOutputFields`, call `AddVolumeOutput` with a name, a header, a group and a description. +3. In `LoadVolumeData`, call `SetVolumeOutputValue(name, iPoint, value)`. +4. Put the same `if` condition around both calls. + +**Important:** `LoadVolumeData` must make the same calls, in the same order, for every point. The reason is that `SetVolumeOutputValue` caches the position of each field. It builds the cache at the first point, and reuses it for all other points in the order of the calls. So conditions may depend on the configuration, but not on `iPoint`. The same holds for `LoadSurfaceData`: for each vertex, make either no calls or the full sequence. + +### Example: the Roe low-dissipation function + +`CVariable` has the virtual getter `GetRoe_Dissipation(iPoint)` (it returns 0), and `CNSVariable` overrides it. + +In `CFlowCompOutput::SetVolumeOutputFields` (`SU2_CFD/src/output/CFlowCompOutput.cpp`): + +```cpp + // Roe Low Dissipation + if (config->GetKind_RoeLowDiss() != NO_ROELOWDISS) { + AddVolumeOutput("ROE_DISSIPATION", "Roe_Dissipation", "ROE_DISSIPATION", "Value of the Roe dissipation"); + } +``` + +In `CFlowCompOutput::LoadVolumeData` (same file): + +```cpp + const auto* Node_Flow = solver[FLOW_SOL]->GetNodes(); + // ... + if (config->GetKind_RoeLowDiss() != NO_ROELOWDISS){ + SetVolumeOutputValue("ROE_DISSIPATION", iPoint, Node_Flow->GetRoe_Dissipation(iPoint)); + } +``` + +### Wall fields + +Set values that only exist on walls in `LoadSurfaceData`. These values are stored per marker and vertex. `LoadSurfaceData` is called only for the markers where `config->GetSolid_Wall(iMarker)` is true. You still declare the field with `AddVolumeOutput` in `SetVolumeOutputFields`. + +From `CFlowOutput::LoadSurfaceData` (`SU2_CFD/src/output/CFlowOutput.cpp`): + +```cpp + if (!config->GetViscous_Wall(iMarker)) return; + // ... + SetVolumeOutputValue("Y_PLUS", iPoint, solver[FLOW_SOL]->GetYPlus(iMarker, iVertex)); +``` + +### Groups and restart files + +- Common volume groups are `COORDINATES`, `SOLUTION`, `PRIMITIVE`, `RESIDUAL`, `LIMITER`, `GRID_VELOCITY` and `MESH_QUALITY`. You can also make a new group, as `ROE_DISSIPATION` above. +- The restart file holds the groups `COORDINATES`, `SOLUTION`, `SENSITIVITY` and `GRID_VELOCITY` (with the default `WRT_RESTART_COMPACT= YES`). +- The solvers read the restart file **by position**: coordinates first, then the `SOLUTION` fields in the order of the `AddVolumeOutput` calls. So add a field to `SOLUTION` only if it is a new solution variable. In that case, change the restart reading (`LoadRestart` of the solver) at the same time. Put any other new field in another group. +- Surface files (`SURFACE_PARAVIEW`, `SURFACE_CSV`, ...) take their data from the volume fields, on the markers in `MARKER_PLOTTING`. There is no separate list of surface fields. +- For time-averaged fields, use `SetAvgVolumeOutputValue` instead of `SetVolumeOutputValue` (see `CFlowOutput::LoadTimeAveragedData`). + +## Turbulence, transition and species fields + +Add turbulence, transition and species fields in `CFlowOutput` (`SU2_CFD/src/output/CFlowOutput.cpp`). The compressible, incompressible and NEMO flow outputs share them. The functions below switch on the model (`TurbModelFamily(config->GetKind_Turb_Model())`, `config->GetKind_Trans_Model()`, `config->GetKind_Species_Model()`): + +| Function | Content | +|---|---| +| `AddHistoryOutputFields_ScalarRMS_RES`, `AddHistoryOutputFields_ScalarMAX_RES`, `AddHistoryOutputFields_ScalarBGS_RES`, `AddHistoryOutputFieldsScalarLinsol` | declare the history fields | +| `LoadHistoryDataScalar` | set the history values | +| `SetVolumeOutputFieldsScalarSolution`, `SetVolumeOutputFieldsScalarResidual`, `SetVolumeOutputFieldsScalarPrimitive`, `SetVolumeOutputFieldsScalarLimiter`, `SetVolumeOutputFieldsScalarSource`, `SetVolumeOutputFieldsScalarLookup`, `SetVolumeOutputFieldsScalarMisc` | declare the volume fields | +| `LoadVolumeDataScalar` | set the volume values | + +`CFlowCompOutput`, `CFlowIncOutput` and `CNEMOCompOutput` call these functions from their own hooks. So a new turbulence or species field added there appears in all three outputs. + +For the adjoint outputs, the same pattern is in `CAdjFlowOutput` (`AddHistoryOutputFields_AdjScalarRMS_RES`, `LoadHistoryDataAdjScalar`, `SetVolumeOutputFieldsAdjScalarSolution`, `LoadVolumeDataAdjScalar`, ...). + +## How users select the new field + +Users request single fields or whole groups in the configuration file. SU2 ignores names that do not exist for the current setup, and prints a message at start-up. + +``` +SCREEN_OUTPUT= INNER_ITER, RMS_DENSITY, LIFT, DRAG, BUFFET +HISTORY_OUTPUT= ITER, RMS_RES, AERO_COEFF +VOLUME_OUTPUT= COORDINATES, SOLUTION, PRIMITIVE, ROE_DISSIPATION +CONV_FIELD= RMS_DENSITY +``` + +- `SCREEN_OUTPUT`, `HISTORY_OUTPUT` and `VOLUME_OUTPUT` take field names or groups. +- `CONV_FIELD` takes only history field names, and no per-surface fields. +- `CUSTOM_OUTPUTS` lets users define new history values from expressions of existing fields, without code changes (see [User Defined Functions](../Custom-Output/#user-defined-functions)). + +See [History and Solution Output](../Custom-Output/) for the user documentation, and the [Option Reference](../Option-Reference/) for the options. + +## Checklist + +1. Run `SU2_CFD -d .cfg`. Check that the field appears in the list, in the right group, with its description. +2. Run a short case with the field in `SCREEN_OUTPUT`, `HISTORY_OUTPUT` or `VOLUME_OUTPUT`, and check the values. For volume fields, open the file in ParaView or Tecplot. +3. For a history field, check the extra fields that its type creates, for example `REL_` or `TAVG_`. If it is meant to work in `CONV_FIELD`, check that too. +4. Check a parallel run. The master rank prints the history values, so they must be global values. Reduce them over all ranks in the solver, for example with `SU2_MPI::Allreduce` (as `CNSSolver::Buffet_Monitoring` does). +5. Add or update a regression test. The test runner compares the last columns of the screen output at iteration `test_iter` with `test_vals` (see `TestCases/TestCase.py`). So: + - put the new field in the `SCREEN_OUTPUT` of a test case; + - add its value to `test_vals` in `TestCases/serial_regression.py`, `TestCases/parallel_regression.py` or the `_AD` versions. + + See [Running Regression Tests](../Running-Regression-Tests/). +6. Did you add the field to a group that existing tests print on screen (for example `RMS_RES` or `AERO_COEFF`)? Then the screen columns of those tests change. Run them and update their `test_vals`. +7. If needed, add or update the unit tests of the output classes in `UnitTests/SU2_CFD/output/`. +8. If the field is of general interest, document it for users in [History and Solution Output](../Custom-Output/). + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Adding-a-Boundary-Condition.md b/Docs/website/pages/Adding-a-Boundary-Condition.md new file mode 100644 index 000000000000..2753d006e551 --- /dev/null +++ b/Docs/website/pages/Adding-a-Boundary-Condition.md @@ -0,0 +1,559 @@ +--- +title: Adding a Boundary Condition +permalink: /docs_v7/Adding-a-Boundary-Condition/ +--- + +This page shows how to add a boundary condition (BC) to the finite volume solvers of `SU2_CFD`: the marker option, how the BC is called in each iteration, and how to write it for the flow solver and for the turbulence, species and heat solvers. + +The examples are copied from the code. See [Code Structure](../Code-Structure/) for the classes, and [Adding a Config Option](../Adding-a-Config-Option/) for options in general. + +## Files you will touch + +| File | What goes there | +|---|---| +| `Common/include/option_structure.hpp` | a value in `enum BC_TYPE` | +| `Common/include/CConfig.hpp` | the marker count and names, the BC data, their getters | +| `Common/src/CConfig.cpp` | the `MARKER_...` option in `CConfig::SetConfig_Options`, the marker list in `CConfig::SetMarkers`, the getters, the table printed by `CConfig::SetOutput`, `GetViscous_Wall` / `GetSolid_Wall` for walls | +| `config_template.cfg` | the option with a short explanation | +| `SU2_CFD/src/integration/CIntegration.cpp` | the call in `CIntegration::Space_Integration` | +| `SU2_CFD/include/solvers/CSolver.hpp` | a virtual `BC_...` function with an empty body | +| the solver files, e.g. `SU2_CFD/src/solvers/CEulerSolver.cpp`, `SU2_CFD/src/solvers/CIncEulerSolver.cpp`, `SU2_CFD/src/solvers/CTurbSASolver.cpp`, `SU2_CFD/src/solvers/CTurbSSTSolver.cpp`, `SU2_CFD/src/solvers/CSpeciesSolver.cpp`, `SU2_CFD/src/solvers/CHeatSolver.cpp` | the implementations | +| `TestCases/` | a regression test | + +--- + +- [1. Add the BC kind](#1-add-the-bc-kind) +- [2. Add the marker option](#2-add-the-marker-option) +- [3. How the marker reaches the solvers](#3-how-the-marker-reaches-the-solvers) +- [4. Call the BC in Space_Integration](#4-call-the-bc-in-space_integration) +- [5. Declare the BC in CSolver](#5-declare-the-bc-in-csolver) +- [6. Implement it in the flow solvers](#6-implement-it-in-the-flow-solvers) +- [7. Strong (Dirichlet) and weak BCs](#7-strong-dirichlet-and-weak-bcs) +- [8. Turbulence, species and heat solvers](#8-turbulence-species-and-heat-solvers) +- [9. Other places that test the BC kind](#9-other-places-that-test-the-bc-kind) +- [10. Multigrid](#10-multigrid) +- [11. Adjoint](#11-adjoint) +- [12. Output and Python](#12-output-and-python) +- [13. Test](#13-test) +- [Checklist](#checklist) + +--- + +## 1. Add the BC kind + +Add a value for the new kind to `enum BC_TYPE`. The kinds are an unscoped enum with fixed integer values in `Common/include/option_structure.hpp`: + +```cpp +enum BC_TYPE { + EULER_WALL = 1, /*!< \brief Boundary Euler wall definition. */ + FAR_FIELD = 2, /*!< \brief Boundary far-field definition. */ + SYMMETRY_PLANE = 3, /*!< \brief Boundary symmetry plane definition. */ + INLET_FLOW = 4, /*!< \brief Boundary inlet flow definition. */ + OUTLET_FLOW = 5, /*!< \brief Boundary outlet flow definition. */ + // ... + SUPERSONIC_INLET = 19, /*!< \brief Boundary supersonic inlet definition. */ + SUPERSONIC_OUTLET = 20, /*!< \brief Boundary supersonic inlet definition. */ + // ... + CHT_WALL_INTERFACE = 50, /*!< \brief Domain interface definition. */ + SMOLUCHOWSKI_MAXWELL = 55, /*!< \brief Smoluchoski/Maxwell wall boundary condition. */ + SEND_RECEIVE = 99, /*!< \brief Boundary send-receive definition. */ +}; +``` + +Pick a value that is not used yet. `SEND_RECEIVE` is reserved for the markers that hold the MPI halo points (local copies of points that another rank owns). `CConfig` stores the kinds as `unsigned short`. + +## 2. Add the marker option + +Add the `MARKER_...` option that users write in the configuration file. The rest of this page follows two examples: `MARKER_SUPERSONIC_OUTLET` (no data) and `MARKER_SUPERSONIC_INLET` (with data). + +1. **Members.** In `Common/include/CConfig.hpp`, add the count and the names to the private members, next to the others: + + ```cpp + nMarker_Supersonic_Outlet, /*!< \brief Number of supersonic outlet flow markers. */ + // ... + *Marker_Supersonic_Outlet, /*!< \brief Supersonic outlet flow markers. */ + ``` + +2. **Option.** Add the option in `CConfig::SetConfig_Options` (`Common/src/CConfig.cpp`): + - a list of names uses `addStringListOption`; + - a BC with values uses one of the special parsers, for example `addStringDoubleListOption` (one value per marker, as in `MARKER_OUTLET` and `MARKER_ISOTHERMAL`) or `addInletOption`. + + ```cpp + /*!\brief MARKER_SUPERSONIC_INLET \n DESCRIPTION: Supersonic inlet boundary marker(s) + * \n Format: (inlet marker, temperature, static pressure, velocity_x, velocity_y, velocity_z, ... ), i.e. primitive variables specified. \ingroup Config*/ + addInletOption("MARKER_SUPERSONIC_INLET", nMarker_Supersonic_Inlet, Marker_Supersonic_Inlet, Inlet_Temperature, Inlet_Pressure, Inlet_Velocity); + /*!\brief MARKER_SUPERSONIC_OUTLET \n DESCRIPTION: Supersonic outlet boundary marker(s) \ingroup Config*/ + addStringListOption("MARKER_SUPERSONIC_OUTLET", nMarker_Supersonic_Outlet, Marker_Supersonic_Outlet); + ``` + + The parsers are the `COption...` classes in `Common/include/option_structure.inl`. See [Adding a Config Option](../Adding-a-Config-Option/) for the comment format and `config_template.cfg`. + +3. **Marker list.** In `CConfig::SetMarkers`, add the count to the sum that sizes the arrays: + + ```cpp + nMarker_CfgFile = nMarker_Euler + nMarker_FarField + nMarker_SymWall + + // ... + nMarker_Supersonic_Inlet + nMarker_Supersonic_Outlet + nMarker_Displacement + nMarker_Load + + // ... + ``` + + Then add a loop that writes the names and the kind: + + ```cpp + for (iMarker_Supersonic_Outlet = 0; iMarker_Supersonic_Outlet < nMarker_Supersonic_Outlet; iMarker_Supersonic_Outlet++) { + Marker_CfgFile_TagBound[iMarker_CfgFile] = Marker_Supersonic_Outlet[iMarker_Supersonic_Outlet]; + Marker_CfgFile_KindBC[iMarker_CfgFile] = SUPERSONIC_OUTLET; + iMarker_CfgFile++; + } + ``` + + Declare the loop counter (`iMarker_Supersonic_Outlet`) at the top of the function. + +4. **Getters for the data.** The data is stored per marker of the option, in the order of the configuration file. The getters search the marker by name, for example: + + ```cpp + su2double CConfig::GetOutlet_Pressure(const string& val_marker) const { + unsigned short iMarker_Outlet; + for (iMarker_Outlet = 0; iMarker_Outlet < nMarker_Outlet; iMarker_Outlet++) + if (Marker_Outlet[iMarker_Outlet] == val_marker) break; + return Outlet_Pressure[iMarker_Outlet]; + } + ``` + + The solvers pass the marker name, `config->GetMarker_All_TagBound(val_marker)`. The search is a loop over strings. So call the getter once per marker, outside the vertex loop. + +5. **Screen table.** At start-up, `CConfig::SetOutput` prints one line per BC kind (`BoundaryTable << "Supersonic outlet boundary";`). Add one for the new kind. + +## 3. How the marker reaches the solvers + +This step needs no new code. It explains how the marker data reaches the solvers, and which functions the solvers use to read it. + +`CConfig` keeps two marker lists: + +| List | Indexed by | Filled in | +|---|---|---| +| `Marker_CfgFile_TagBound`, `Marker_CfgFile_KindBC`, ... | the markers of the configuration file | `CConfig::SetMarkers` | +| `Marker_All_TagBound`, `Marker_All_KindBC`, `Marker_All_SendRecv`, ... | the markers of the local mesh partition (`iMarker` in the solvers) | the geometry, after partitioning | + +`CPhysicalGeometry` copies the kind of each physical marker of the mesh. It sets `SEND_RECEIVE` for the halo markers: + +```cpp + if (Marker_Tag != "SEND_RECEIVE") { + /*--- Update config information storing the boundary information in the right place ---*/ + + Tag_to_Marker[config->GetMarker_CfgFile_TagBound(Marker_Tag)] = Marker_Tag; + config->SetMarker_All_KindBC(iMarker, config->GetMarker_CfgFile_KindBC(Marker_Tag)); + // ... + } +``` + +If a mesh marker is in no `MARKER_...` option, the run stops in `CConfig::GetMarker_CfgFile_TagBound` ("The configuration file doesn't have any definition for marker ..."). + +In the solvers, use: + +- `config->GetnMarker_All()`, `config->GetMarker_All_KindBC(iMarker)`, `config->GetMarker_All_TagBound(iMarker)`; +- `geometry->nVertex[iMarker]` and `geometry->vertex[iMarker][iVertex]` (`GetNode()`, `GetNormal(...)`, `GetNormal_Neighbor()`); +- `geometry->nodes->GetDomain(iPoint)`, which is `false` for halo points. A marker of a partition can contain halo points. Their values come from the owning rank through MPI, so the BC skips them. + +## 4. Call the BC in Space_Integration + +Add a `case` for the new kind in `CIntegration::Space_Integration` (`SU2_CFD/src/integration/CIntegration.cpp`). All finite volume solvers call their BCs from this function. It serves: + +- the flow solver, from `CMultiGridIntegration` on every grid level, and from `CNewtonIntegration`; +- the turbulence, transition, species, heat, radiation and Poisson solvers, from `CSingleGridIntegration`. + +The equation system selects the solver: + +```cpp + unsigned short MainSolver = config->GetContainerPosition(RunTime_EqSystem); +``` + +So one `case` serves every solver. Each solver decides what the BC means for its equations, through its own `BC_...` function. + +The order in `Space_Integration` is: + +1. `Centered_Residual` or `Upwind_Residual`, `Viscous_Residual`, `Source_Residual`, and `SetResidual_DualTime` for dual time stepping. +2. `BC_Fluid_Interface` (sliding interfaces) and the Giles preprocessing. +3. The **weak** BCs, which add a boundary flux (see [step 7](#7-strong-dirichlet-and-weak-bcs)). They are one `switch` over all markers: + + ```cpp + for (iMarker = 0; iMarker < config->GetnMarker_All(); iMarker++) { + KindBC = config->GetMarker_All_KindBC(iMarker); + switch (KindBC) { + // ... + case SUPERSONIC_INLET: + solver_container[MainSolver]->BC_Supersonic_Inlet(geometry, solver_container, conv_bound_numerics, visc_bound_numerics, config, iMarker); + break; + case OUTLET_FLOW: + solver_container[MainSolver]->BC_Outlet(geometry, solver_container, conv_bound_numerics, visc_bound_numerics, config, iMarker); + break; + case SUPERSONIC_OUTLET: + solver_container[MainSolver]->BC_Supersonic_Outlet(geometry, solver_container, conv_bound_numerics, visc_bound_numerics, config, iMarker); + break; + // ... + } + } + ``` + +4. `Impose_Fixed_Values`. +5. The **strong** BCs, which impose values at the points. They are a second `switch` (`ISOTHERMAL`, `HEAT_FLUX`, `HEAT_TRANSFER`, `CUSTOM_BOUNDARY`, `CHT_WALL_INTERFACE`, `SMOLUCHOWSKI_MAXWELL`). +6. `BC_Periodic`, if there are periodic markers. +7. `BC_Sym_Plane` and `BC_Euler_Wall`, last. + +Add your `case` to the weak or the strong `switch`. A BC that overwrites the solution or the residual of its points belongs in the strong one. It then runs after the weak BCs of neighbouring markers that share corner points. + +The numerics objects passed in are those of the current thread: + +```cpp + CNumerics* conv_bound_numerics = numerics[CONV_BOUND_TERM + omp_get_thread_num()*MAX_TERMS]; + CNumerics* visc_bound_numerics = numerics[VISC_BOUND_TERM + omp_get_thread_num()*MAX_TERMS]; +``` + +A kind with no `case` is not applied at all. Some cases depend on the grid level: `GILES_BOUNDARY` and the turbomachinery form of `RIEMANN_BOUNDARY` only run on `MESH_0`. + +Other solvers have their own dispatch: the structural solver in `CStructuralIntegration` (`CLAMPED_BOUNDARY`, `LOAD_BOUNDARY`, ...) and the DG solver in `CFEM_DG_EulerSolver`. + +## 5. Declare the BC in CSolver + +Declare the new BC function in the base class, so that every solver has it. Add a virtual function with an empty body to `SU2_CFD/include/solvers/CSolver.hpp`, with the usual signature: + +```cpp + inline virtual void BC_Supersonic_Outlet(CGeometry *geometry, + CSolver **solver_container, + CNumerics *conv_numerics, + CNumerics *visc_numerics, + CConfig *config, + unsigned short val_marker) { } +``` + +The empty default means: **a solver that does not override the function adds nothing at this boundary**. In an edge-based finite volume scheme, a boundary face that gets no flux acts as a wall with zero convective and zero diffusive flux for that equation. Decide for every solver whether that is right (see [step 8](#8-turbulence-species-and-heat-solvers)). + +Declare the override in each solver header that implements it (for example `SU2_CFD/include/solvers/CEulerSolver.hpp`), with `override` or `final`. + +## 6. Implement it in the flow solvers + +Write the BC for the flow solvers. The flow BCs are in these classes: + +- compressible: `CEulerSolver` and `CNSSolver`; +- incompressible: `CIncEulerSolver` and `CIncNSSolver`; +- shared by both: `CFVMFlowSolverBase` (`BC_Sym_Plane`, `BC_Euler_Wall`, `BC_Custom`, `BC_Periodic`, `BC_Fluid_Interface`, in `SU2_CFD/include/solvers/CFVMFlowSolverBase.inl`); +- NEMO: the NEMO solvers have their own. + +A weak BC builds a ghost state at each vertex: an outer state, as if there were a point outside the boundary. It then computes the boundary flux with the convective numerics, as for an edge between the boundary point and its ghost. `CEulerSolver::BC_Supersonic_Inlet` is a short example (some lines removed): + +```cpp +void CEulerSolver::BC_Supersonic_Inlet(CGeometry *geometry, CSolver **solver_container, + CNumerics *conv_numerics, CNumerics *visc_numerics, + CConfig *config, unsigned short val_marker) { + SU2_ZONE_SCOPED + const su2double Gas_Constant = config->GetGas_ConstantND(); + const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + // ... + + SU2_OMP_FOR_DYN(OMP_MIN_SIZE) + for (auto iVertex = 0ul; iVertex < geometry->nVertex[val_marker]; iVertex++) { + const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); + + if (!geometry->nodes->GetDomain(iPoint)) continue; + + /*--- Retrieve the inlet profile, note that total conditions are reused as static. ---*/ + + const su2double Temperature = Inlet_Ttotal[val_marker][iVertex] / config->GetTemperature_Ref(); + const su2double Pressure = Inlet_Ptotal[val_marker][iVertex] / config->GetPressure_Ref(); + // ... velocity, density, energy + + /*--- Primitive variables, using the derived quantities. ---*/ + + auto* V_inlet = GetCharacPrimVar(val_marker, iVertex); + V_inlet[prim_idx.Temperature()] = Temperature; + V_inlet[prim_idx.Pressure()] = Pressure; + V_inlet[prim_idx.Density()] = Density; + V_inlet[prim_idx.Enthalpy()] = Energy + Pressure / Density; + for (unsigned short iDim = 0; iDim < nDim; iDim++) + V_inlet[iDim+prim_idx.Velocity()] = Velocity[iDim]; + + /*--- Current solution at this boundary node. ---*/ + + const auto* V_domain = nodes->GetPrimitive(iPoint); + + /*--- Normal vector for this vertex (negate for outward convention). ---*/ + + su2double Normal[MAXNDIM] = {0.0}; + geometry->vertex[val_marker][iVertex]->GetNormal(Normal); + for (unsigned short iDim = 0; iDim < nDim; iDim++) Normal[iDim] = -Normal[iDim]; + + /*--- Set various quantities in the solver class. ---*/ + + conv_numerics->SetNormal(Normal); + conv_numerics->SetPrimitive(V_domain, V_inlet); + + if (dynamic_grid) + conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), + geometry->nodes->GetGridVel(iPoint)); + + /*--- Compute the residual using an upwind scheme. ---*/ + + auto residual = conv_numerics->ComputeResidual(config); + + LinSysRes.AddBlock(iPoint, residual); + + /*--- Jacobian contribution for implicit integration. ---*/ + + if (implicit) + Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i); + + /*--- Viscous contribution, omited to improve convergence. ---*/ + + } + END_SU2_OMP_FOR + +} +``` + +The pattern: + +- **`implicit`** is read from `config` at each call. `CNewtonIntegration` switches the time scheme to explicit for some residual evaluations. The BC must then not touch the Jacobian. +- **Loop**: `SU2_OMP_FOR_DYN(OMP_MIN_SIZE)` ... `END_SU2_OMP_FOR`. The integration opens a parallel region, and every thread of it calls the BC. The macro splits the vertices among the threads. Local variables are private to each thread. +- **Halo check**: `if (!geometry->nodes->GetDomain(iPoint)) continue;`. +- **Ghost state**: write it to `GetCharacPrimVar(val_marker, iVertex)`, with the primitive indices `prim_idx` (`Temperature()`, `Pressure()`, `Density()`, `Velocity()`, `Enthalpy()`, ...). `CFVMFlowSolverBase::Allocate` allocates this array for every vertex of every marker. The scalar solvers read it later in the same iteration to build their own ghost state (see [step 8](#8-turbulence-species-and-heat-solvers)). So fill all the primitives they need, including the viscosities if they use the diffusive boundary flux. +- **Normal**: the vertex normal points into the domain. Negate it for the flux. +- **Numerics**: call `SetNormal`, then `SetPrimitive` (domain state, ghost state). Add `SetSecondary` for non-ideal gases (see `BC_Sym_Plane`), and `SetGridVel` for moving grids. Then call `ComputeResidual(config)`. +- **Residual**: add the flux to the row of the boundary point only, with `LinSysRes.AddBlock(iPoint, residual)`. The ghost has no row. +- **Jacobian**: add only the diagonal block, with `Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i)`. + +A **viscous** contribution uses `visc_numerics` in the same way (see `CEulerSolver::BC_Supersonic_Outlet`): + +- the ghost coordinate is the coordinate of the normal neighbour, reflected; +- the gradients are those of the boundary point. + +```cpp + visc_numerics->SetNormal(Normal); + su2double Coord_Reflected[MAXNDIM]; + unsigned long Point_Normal = geometry->vertex[val_marker][iVertex]->GetNormal_Neighbor(); + GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), + geometry->nodes->GetCoord(iPoint), Coord_Reflected); + visc_numerics->SetCoord(geometry->nodes->GetCoord(iPoint), Coord_Reflected); + + /*--- Primitive variables, and gradient ---*/ + + visc_numerics->SetPrimitive(V_domain, V_domain); + visc_numerics->SetPrimVarGradient(nodes->GetGradient_Primitive(iPoint), nodes->GetGradient_Primitive(iPoint)); +``` + +For the **incompressible** solver, `CIncEulerSolver::BC_Far_Field` follows the same pattern. It also sets the entries that only the incompressible primitive vector has (`prim_idx.Beta()`, `prim_idx.CpTotal()`). It handles the pressure-based formulation separately. + +**Per-vertex data.** Some BCs have data that varies along the marker, such as an inlet profile or values set from Python. Store such data per vertex, like `Inlet_Ttotal[val_marker][iVertex]`, allocated with `AllocVectorOfVectors(nVertex, ...)` in the solver. Each grid level has its own solver object, so the data exists on each level. + +`CDriver::PreprocessInlet` fills the inlet data: + +- from a profile file with `LoadInletProfile` (only `INLET_FLOW` and `SUPERSONIC_INLET`); +- otherwise with `SetUniformInlet` of each solver. + +`CFVMFlowSolverBase::SetUniformInlet` copies the values of `MARKER_SUPERSONIC_INLET` into the inlet arrays. This is why `BC_Supersonic_Inlet` reads `Inlet_Ttotal`. + +## 7. Strong (Dirichlet) and weak BCs + +Choose how the BC acts on the equations: + +- A **weak** BC adds a boundary flux to the residual (step 6). +- A **strong** (Dirichlet) BC imposes the solution values at the boundary points. + +To impose a value strongly: + +1. Set the value in `Solution_Old`, because the update adds the correction to it. Also set it in `Solution` if it is read before the update. +2. Zero the residual of the imposed equations. +3. Zero the multigrid truncation error of those equations (`SetRes_TruncErrorZero`, `SetVel_ResTruncError_Zero`). +4. If implicit, replace the rows of the Jacobian by identity rows with `Jacobian.DeleteValsRowi(iPoint, iVar)`. This function sets row `iVar` of the diagonal block to zero, with 1 on the diagonal. It also zeroes the same row in all off-diagonal blocks. The linear solver then returns a zero correction for that equation. + +`CHeatSolver::BC_ConjugateHeat_Interface` (fluid side) is a complete strong BC for one equation: + +```cpp + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0ul; iVertex < geometry->nVertex[val_marker]; iVertex++) { + + const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); + + if (geometry->nodes->GetDomain(iPoint)) { + const su2double T_Conjugate = GetConjugateHeatVariable(val_marker, iVertex, 0) / Temperature_Ref; + + nodes->SetSolution_Old(iPoint, &T_Conjugate); + LinSysRes(iPoint, 0) = 0.0; + nodes->SetRes_TruncErrorZero(iPoint); + + if (implicit) Jacobian.DeleteValsRowi(iPoint, 0); + } + } + END_SU2_OMP_FOR +``` + +For a system, impose only some rows. The no-slip wall of the incompressible solver (`CIncNSSolver`) fixes the velocity rows and keeps the others: + +```cpp + if (dynamic_grid) { + nodes->SetVelocity_Old(iPoint, geometry->nodes->GetGridVel(iPoint)); + } else { + su2double zero[MAXNDIM] = {0.0}; + nodes->SetVelocity_Old(iPoint, zero); + } + + for (unsigned short iDim = 0; iDim < nDim; iDim++) + LinSysRes(iPoint, iDim+1) = 0.0; + nodes->SetVel_ResTruncError_Zero(iPoint); + + if (pressure_based) nodes->SetStrongBC(iPoint); + + /*--- Enforce the no-slip boundary condition in a strong way by + modifying the velocity-rows of the Jacobian (1 on the diagonal). ---*/ + + if (implicit) { + for (unsigned short iVar = 1; iVar <= nDim; iVar++) + Jacobian.DeleteValsRowi(iPoint, iVar); + } +``` + +A third form modifies the residual and the Jacobian rows instead of replacing them. `CFVMFlowSolverBase::BC_Sym_Plane` removes the normal component of the momentum residual. It multiplies the Jacobian blocks in the row of the point by `(I - n n^T)`. + +The pressure-based incompressible solver also needs `nodes->SetStrongBC(iPoint)` at strongly imposed points (see `CIncEulerSolver::BC_Far_Field` and `CIncNSSolver`). + +## 8. Turbulence, species and heat solvers + +Decide what each scalar solver does at the new boundary, and implement it. + +These solvers derive from `CScalarSolver`. They get the same `switch` in `Space_Integration`, with `MainSolver` = `TURB_SOL`, `TRANS_SOL`, `SPECIES_SOL`, `HEAT_SOL`, ... For them, `conv_numerics` and `visc_numerics` are `nullptr`: `CDriver::InitializeNumerics` does not create `CONV_BOUND_TERM` or `VISC_BOUND_TERM` for scalar solvers. Their boundary fluxes are computed by their edge kernel, the class that computes the flux of one edge (see [Adding a Solver](../Adding-a-Solver/#6-numerics)). + +What each one does today if it does not override a BC: + +| Function | Default for scalar solvers | +|---|---| +| any `BC_...` not overridden | `CSolver`'s empty body: no boundary flux | +| `BC_Sym_Plane` | `CScalarSolver`: empty, "Convective and viscous fluxes across symmetry plane are equal to zero." | +| `BC_Euler_Wall` | `CScalarSolver`: empty, `final` | +| `BC_Supersonic_Inlet`, `BC_Supersonic_Outlet` | `CScalarSolver`: call `BC_Inlet` and `BC_Outlet` (`final`) | +| `BC_Riemann`, `BC_Periodic` | `CScalarSolver`, `final` | +| `BC_Far_Field`, `BC_Fluid_Interface` | `CScalarSolver`, overridden by most models | + +Overrides in the scalar solvers: + +| Solver | `BC_...` it implements | +|---|---| +| `CTurbSASolver` | `Far_Field`, `Inlet`, `Outlet`, `HeatFlux_Wall`, `Isothermal_Wall`, `Engine_Inflow`, `Engine_Exhaust`, `ActDisk_Inlet`, `ActDisk_Outlet`, `Inlet_Turbo`, `Inlet_MixingPlane`, `Fluid_Interface` | +| `CTurbSSTSolver` | `Far_Field`, `Inlet`, `Outlet`, `HeatFlux_Wall`, `Isothermal_Wall`, `Inlet_Turbo`, `Inlet_MixingPlane`, `Fluid_Interface` | +| `CTransLMSolver` | `Far_Field`, `Inlet`, `Outlet`, `HeatFlux_Wall`, `Isothermal_Wall`, `Fluid_Interface` | +| `CSpeciesSolver` | `Far_Field`, `Inlet`, `Outlet`, `HeatFlux_Wall`, `Isothermal_Wall`, `Fluid_Interface` | +| `CHeatSolver` | `Far_Field`, `Inlet`, `Outlet`, `HeatFlux_Wall`, `Isothermal_Wall`, `ConjugateHeat_Interface`, `Fluid_Interface` | + +So for a new flow BC, decide for each scalar solver: + +- Is a zero flux right (walls, symmetry)? +- Or does the scalar need an inflow value or an outflow extrapolation? + +Then override the function, or forward to an existing one as `CScalarSolver` does for the supersonic BCs. + +A weak scalar BC has two passes: + +1. fill the ghost state of every vertex; +2. call the boundary flux of the model. + +`CTurbSSTSolver::BC_Outlet`: + +```cpp +void CTurbSSTSolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, + CConfig *config, unsigned short val_marker) { + SU2_ZONE_SCOPED + + auto* flowSolver = solver_container[FLOW_SOL]; + + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { + const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); + + /*--- Neumann: the turbulent variable is copied from the interior before computing the flux. ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, nodes->GetSolution(iPoint, iVar)); + + SetGhostPrimitives(iVertex, flowSolver->GetCharacPrimVar(val_marker, iVertex)); + + SetGhostGeometry(geometry, val_marker, iVertex); + } + END_SU2_OMP_FOR + + /*--- The diffusive term at this boundary causes serious convergence problems. ---*/ + BoundaryFlux(geometry, solver_container, config, + ScalarFluxOptions::BoundaryConvective(*config, config->GetBounded_Turb()), val_marker); +} +``` + +- `ghostNodes` holds the scalar ghost state. +- `SetGhostPrimitives` copies the flow ghost state that the flow BC wrote to `GetCharacPrimVar` (density, velocity, viscosities). +- `SetGhostGeometry` stores the outward normal and clears the flag `ghostSkip` of the vertex, so the vertex gets a boundary flux. +- `SetGhostDiffusionState` adds the reflected coordinate and gradient when the diffusive term is included. +- `BoundaryFlux` calls `CScalarSolver::BoundaryFluxResidual` with the model's kernel. It skips halo points and the vertices whose `ghostSkip` is set. It adds the flux to the row of the boundary point, and adds the diagonal Jacobian block. +- `ScalarFluxOptions::BoundaryConvective`, `BoundaryFull` and `BoundaryDiffusive` choose the terms. + +Walls of the turbulence models are strong BCs. For example, `CTurbSSTSolver::BC_HeatFlux_Wall` sets `k` and `omega`, zeroes the residual and calls `Jacobian.DeleteValsRowi` for both rows. The heat solver writes its own wall fluxes (`CHeatSolver::BC_Isothermal_Wall`, `BC_HeatFlux_Wall`). + +## 9. Other places that test the BC kind + +The BC kind is tested in many places besides the `switch` of `Space_Integration`. Search the code for an existing kind that behaves like yours, for example `grep -rn "SUPERSONIC_INLET" Common SU2_CFD`, and update each place you find. The main places: + +| Where | What | +|---|---| +| `CConfig::GetViscous_Wall`, `CConfig::GetSolid_Wall` (`Common/src/CConfig.cpp`) | the list of no-slip walls (`HEAT_FLUX`, `ISOTHERMAL`, `HEAT_TRANSFER`, `SMOLUCHOWSKI_MAXWELL`, `CHT_WALL_INTERFACE`) and of solid walls (those plus `EULER_WALL`) | +| `CGeometry::ComputeWallDistance` | wall distance for the turbulence models, from `GetViscous_Wall` markers | +| `CPhysicalGeometry` | `SetSolidBoundary` / `SetViscousBoundary` of the points, from the two functions above | +| `CFVMFlowSolverBase::Pressure_Forces`, `Friction_Forces`; `CFlowOutput` | forces, skin friction, y+ and heat flux only on solid / viscous walls | +| `CFVMFlowSolverBase::Allocate` | `VertexTraction` (fluid-structure interaction) only on solid walls | +| `CMultiGridIntegration` | corrections on viscous walls (see below) | +| `CDriver::PreprocessInlet`, `CFVMFlowSolverBase::SetUniformInlet`, `CSpeciesSolver::SetUniformInlet`, `CSolver::LoadInletProfile` | inlet data for `INLET_FLOW` and `SUPERSONIC_INLET` | +| `CDriverBase::GetMarkerTypes` | the kind names given to the Python wrapper | + +Add a new **wall** kind to `GetViscous_Wall` (no-slip) or `GetSolid_Wall` (slip). Otherwise the turbulence models do not see it in the wall distance, and the forces and wall outputs skip it. + +## 10. Multigrid + +Check that the flow BC works on the coarse multigrid levels. + +The coarse grids (`CMultiGridGeometry`) are built by agglomeration: groups of fine-grid control volumes are merged into one coarse control volume. The coarse grids keep the markers of the fine grid. Boundary control volumes are agglomerated first. Points where two different BC kinds meet are kept apart (`FindMixedBoundaryNodes`). The flow BC is then called on every level with `iMesh`, through `CMultiGridIntegration::MultiGrid_Cycle`. Check that: + +- the per-vertex data of the BC exists on each level (each level has its own solver); +- level-dependent parts are guarded with `iMesh == MESH_0`, as for `GILES_BOUNDARY`; +- a strong BC zeroes the truncation error (step 7). `CMultiGridIntegration` also treats some walls: + - on `GetViscous_Wall` markers, it zeroes the velocity corrections (`GetProlongated_Correction`) and the velocity truncation error (`SetForcing_Term`). It also sets the wall velocity in the restricted solution (`SetRestricted_Solution`); + - on `EULER_WALL` markers, it projects the corrections on the wall (`MultigridProjectEulerWall`); +- `CMultiGridIntegration::SmoothProlongated_Correction` restores the residuals at all boundary markers except `INTERNAL_BOUNDARY`, `NEARFIELD_BOUNDARY`, `PERIODIC_BOUNDARY` and `SEND_RECEIVE`. + +The scalar solvers run on one grid only, `config->GetFinestMesh()` (`CSingleGridIntegration`). + +## 11. Adjoint + +- **Discrete adjoint.** The primal iteration, BCs included, is recorded on the AD tape and differentiated automatically. The tape is the record of operations that automatic differentiation runs backwards. Use `su2double` for all values that depend on the solution, and do not apply `SU2_TYPE::GetValue` to them. Values read from `config` are constants for the adjoint, unless they are registered as inputs. `CDiscAdjSolver::RegisterVariables` registers the free-stream Mach number, angle of attack, temperature and pressure for compressible flow. The data of a new BC is not registered. +- **Continuous adjoint.** `CAdjEulerSolver` and `CAdjNSSolver` have their own `BC_...` functions. They get the same dispatch, with `MainSolver = ADJFLOW_SOL`. Without an override, the empty default applies. + +## 12. Output and Python + +Any marker can be listed in `MARKER_MONITORING`, `MARKER_PLOTTING` and `MARKER_ANALYZE`. `CConfig::SetMarkers` and the geometry handle that for every kind. Values that only your BC computes (for example a mass flow) need output fields: see [Adding Output Fields](../Adding-Output-Fields/). + +For Python: + +- Add a name in `CDriverBase::GetMarkerTypes`. Otherwise the wrapper reports `"UNKNOWN_TYPE"`. +- Add the option to `read_config` in `SU2_PY/SU2/io/config.py`, but only if a script must read the marker list. + +## 13. Test + +1. Run a small case with the new marker: serial, with MPI, and with OpenMP if the solver supports it (`SU2_CFD -t `). Check a partition where the marker is split between ranks. +2. Check implicit and explicit time integration, and multigrid (`MGLEVEL`) for the flow solver. +3. Run it with a turbulence model, to see what the turbulence solver does at the new marker. +4. Add a regression test. For example, `MARKER_SUPERSONIC_INLET` is tested by `TestCases/euler/ramp/inv_ramp.cfg` (the test `ramp` in `TestCases/parallel_regression.py`). See [Running Regression Tests](../Running-Regression-Tests/#add-a-new-test). + +## Checklist + +1. `BC_TYPE` value in `Common/include/option_structure.hpp`. +2. Members, `MARKER_...` option, `nMarker_CfgFile` sum and loop in `CConfig::SetMarkers`, getters, `CConfig::SetOutput`, `config_template.cfg`. +3. `case` in the weak or strong `switch` of `CIntegration::Space_Integration`. +4. Virtual `BC_...` in `SU2_CFD/include/solvers/CSolver.hpp`. +5. Implementation in the flow solvers (compressible, incompressible, NEMO as needed), with halo check, OpenMP loop, `implicit` check and ghost state in `CharacPrimVar`. +6. Decision and implementation for each scalar solver (turbulence, transition, species, heat). +7. `GetViscous_Wall` / `GetSolid_Wall` for walls; inlet data if it is an inlet. +8. Multigrid, adjoint and Python checks. +9. Regression test. + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Adding-a-Config-Option.md b/Docs/website/pages/Adding-a-Config-Option.md new file mode 100644 index 000000000000..e215d47d82af --- /dev/null +++ b/Docs/website/pages/Adding-a-Config-Option.md @@ -0,0 +1,349 @@ +--- +title: Adding a Config Option +permalink: /docs_v7/Adding-a-Config-Option/ +--- + +This page shows how to add an option to the SU2 configuration file, how to document it, and how to rename or remove +one. The examples are taken from the code. + +The class `CConfig` reads the configuration file. A new option touches these files: + +| File | What goes there | +|---|---| +| `Common/include/CConfig.hpp` | The member that stores the value, and its getter | +| `Common/src/CConfig.cpp` | The registration in `CConfig::SetConfig_Options`, its comment, and the checks in `CConfig::SetPostprocessing` | +| `Common/include/option_structure.hpp` | The enum and its map, for options that take a keyword | +| `config_template.cfg` | An example line with a short explanation | + +`Docs/website/tools/build_site.py` generates the [Option Reference](../Option-Reference/) of this website from +`Common/src/CConfig.cpp`, `Common/include/option_structure.hpp` and `config_template.cfg`. So what you write in the +code is what users read on the website. + +`CConfig.hpp`, `option_structure.hpp` and `CConfig.cpp` are excluded from clang-format (see +`.pre-commit-config.yaml`), so nothing fixes their layout for you. Follow the layout of the code around your +change. + +## 1. Add the member and the getter + +Declare the member in the private part of `CConfig` in `Common/include/CConfig.hpp`, with a short Doxygen comment: + +```cpp + su2double hRoughness; /*!< \brief RMS roughness for Transition model. */ +``` + +Add a `const` getter in the public part: + +```cpp + /*! + * \brief Get RMS roughness for Transtion model from config + * \return Value of roughness. + */ + su2double GethRoughness(void) const { return hRoughness; } +``` + +The rest of the code reads the value through the getter, e.g. `config->GethRoughness()`. +See the [Code Reference](../Code-Reference/) for the existing members of `CConfig`. + +## 2. Register the option + +Add one `add...Option` call in `CConfig::SetConfig_Options` in `Common/src/CConfig.cpp`. Put it in the group where +the option belongs. The arguments are: + +- first, the name used in the configuration file, in capital letters; +- second, the member; +- last, for most calls, the default value. + +```cpp + /*!\brief HROUGHNESS \n DESCRIPTION: Value of RMS roughness for transition model \n DEFAULT: 1E-6 \ingroup Config*/ + addDoubleOption("HROUGHNESS", hRoughness, 1e-6); +``` + +The name must be unique. Each `add...Option` function checks this with an `assert`. The `assert` is active only in a +debug build (`--buildtype=debug`). + +### Kinds of options + +The functions are declared in `Common/include/CConfig.hpp`. The common ones are: + +| Function | Arguments after the name | Example in `CConfig.cpp` | +|---|---|---| +| `addDoubleOption` | member (`su2double`), default | `addDoubleOption("HROUGHNESS", hRoughness, 1e-6);` | +| `addBoolOption` | member (`bool`), default | `addBoolOption("WRT_RESTART_COMPACT", Wrt_Restart_Compact, true);` | +| `addIntegerOption`, `addLongOption`, `addUnsignedLongOption`, `addUnsignedShortOption` | member, default | `addUnsignedLongOption("SCREEN_WRT_FREQ_INNER", ScreenWrtFreq[2], 1);` | +| `addStringOption` | member (`string`), default | `addStringOption("FLUID_NAME", FluidName, string("nitrogen"));` | +| `addEnumOption` | member, map, default | `addEnumOption("KIND_TURB_MODEL", Kind_Turb_Model, Turb_Model_Map, TURB_MODEL::NONE);` | +| `addEnumListOption` | size, pointer, map | `addEnumListOption("SST_OPTIONS", nSST_Options, SST_Options, SST_Options_Map);` | +| `addDoubleListOption`, `addShortListOption`, `addUShortListOption`, `addULongListOption`, `addStringListOption` | size, pointer | `addDoubleListOption("PRANDTL_LAM", nPrandtl_Lam , Prandtl_Lam);` | +| `addDoubleArrayOption`, `addUShortArrayOption`, `addULongArrayOption` | fixed size, `allow_fewer`, array | `addDoubleArrayOption("BODY_FORCE_VECTOR", 3, false, body_force);` | +| `addPythonOption` | none | `addPythonOption("OPT_OBJECTIVE");` | + +In the configuration file, the values of lists and arrays are separated by spaces or commas. They may be put in +parentheses, e.g. `SST_OPTIONS= (V2003m, VORTICITY)`. + +Notes on the kinds: + +- **Bool** options accept `YES` and `NO`. +- **Enum** options accept one keyword of the map. The member can be the enum type. See [Add an enum](#4-add-an-enum). +- **Lists** have any length. The function sets the size member to the number of values and allocates the array. + The option object frees it. `NONE` gives an empty list. + + **Lists have no default**: if the option is not in the file, the size is 0. If you need a default, set it in + `SetPostprocessing` (see below). +- **Arrays** have a fixed size. With `allow_fewer = true`, fewer values are accepted. + + **Arrays have no default argument**: fill the array before the call, as the code does for `BODY_FORCE_VECTOR`: + + ```cpp + body_force[0] = 0.0; body_force[1] = 0.0; body_force[2] = 0.0; + /* DESCRIPTION: Vector of body force values (BodyForce_X, BodyForce_Y, BodyForce_Z) */ + addDoubleArrayOption("BODY_FORCE_VECTOR", 3, false, body_force); + ``` + +- **Python** options are read only by the Python scripts in `SU2_PY`. The C++ code accepts them without checking + them, and has no member for them. + +Some options have a special format: boundary conditions, design variables, FFD (free-form deformation) boxes, ... +They have their own functions, such as `addInletOption`, `addRiemannOption`, `addPeriodicOption` and +`addDVParamOption`. Their parsers are the `COption...` classes in `Common/include/option_structure.inl`. Before you +write a new parser, look at an existing option of the same kind. + +### Multizone problems + +In a multizone problem, each zone has its own configuration file. If an option is not set in the file of a zone, the +zone takes the value from the main configuration file (`CConfig::SetDefaultFromConfig`). `SCREEN_OUTPUT` and +`HISTORY_OUTPUT` are never taken from the main file. Options that are not set anywhere take their default value. + +## 3. Write the comment + +Put a Doxygen comment on the line above the `add...Option` call, in this form: + +```cpp +/*!\brief NAME \n DESCRIPTION: what the option does \n Options: allowed values \n DEFAULT: value \ingroup Config*/ +``` + +Real examples: + +```cpp + /*!\brief REYNOLDS_NUMBER \n DESCRIPTION: Reynolds number (non-dimensional, based on the free-stream values). Needed for viscous solvers. For incompressible solvers the Reynolds length will always be 1.0 \n DEFAULT: 0.0 \ingroup Config */ + addDoubleOption("REYNOLDS_NUMBER", Reynolds, 0.0); + + /*!\brief KIND_TURB_MODEL \n DESCRIPTION: Specify turbulence model \n Options: see \link Turb_Model_Map \endlink \n DEFAULT: NONE \ingroup Config*/ + addEnumOption("KIND_TURB_MODEL", Kind_Turb_Model, Turb_Model_Map, TURB_MODEL::NONE); +``` + +`build_site.py` reads the comment with the rules below. If you do not follow them, the Option Reference will be +incomplete. + +- The comment can start with `/*!` or `/*`. It must contain `DESCRIPTION:` or `\brief`. Other comments, such as + `/*--- ... ---*/`, are ignored. +- If there are several such comments between the previous `add...Option` call and this one, only the last one is + used. +- `\n` separates the parts. The keys are `DESCRIPTION:`, `Options:` (or `OPTIONS:`) and `DEFAULT:`, each with + a colon. `\link Name \endlink` is shown as `Name`. +- The description should say what the option does, the unit if there is one, and which solvers use it. + +The website adds these by itself. You do not write them in the comment: + +- the type, from the `add...Option` function; +- for enum options, all the keywords of the map; +- the default, when it is an argument of the call (for an enum, the keyword of the map that has this value). + Array defaults are initialized separately: write a `DEFAULT:` comment if you want the reference to show them; + otherwise the reference says that the default was not extracted; +- the line of the option in `config_template.cfg`, or "Not in config_template.cfg"; +- the comment above that line in `config_template.cfg`, when it says more than the comment in the code (or in its + place, when the code has no description); +- the pages of the website and the tutorials that name the option; +- up to three test case files in `TestCases/` that use the option, with their values; +- a link to the line of the call in `CConfig.cpp`. + +The [Docs Check](../Docs-Check/) page lists the options with no description, a very short one, or no example. + +### Categories + +The category comments in `SetConfig_Options` group the options into pages. For example: + +```cpp + /*!\par CONFIG_CATEGORY: Freestream Conditions \ingroup Config*/ +``` + +Every option after such a comment belongs to that category, until the next one. Put a new option after the category +where it belongs. Add a new category only for a new group of options. + +## 4. Add an enum + +An option that takes a keyword needs an enum and a map in `Common/include/option_structure.hpp`. Use an +`enum class`. This is the enum of `VOLUME_OUTPUT_PRECISION`: + +```cpp +/*! + * \brief Floating-point type of the fields of the volume and surface files (CGNS and Paraview XML). + */ +enum class VOLUME_OUTPUT_PRECISION { + SINGLE, /*!< \brief Single precision (float). */ + DOUBLE /*!< \brief Double precision (double). */ +}; +static const MapType VolumeOutputPrecision_Map = { + MakePair("SINGLE", VOLUME_OUTPUT_PRECISION::SINGLE) + MakePair("DOUBLE", VOLUME_OUTPUT_PRECISION::DOUBLE) +}; +``` + +The member in `CConfig.hpp`, the registration and the getter: + +```cpp + VOLUME_OUTPUT_PRECISION Volume_Output_Precision; /*!< \brief Floating-point type of the volume and surface fields. */ +``` + +```cpp + addEnumOption("VOLUME_OUTPUT_PRECISION", Volume_Output_Precision, VolumeOutputPrecision_Map, + VOLUME_OUTPUT_PRECISION::SINGLE); +``` + +```cpp + bool GetVolume_Output_Double_Precision(void) const { + return Volume_Output_Precision == VOLUME_OUTPUT_PRECISION::DOUBLE; + } +``` + +Many getters of enum options return the enum itself, e.g. `GetKind_Turb_Model()`. + +Things to know about the maps: + +- `MakePair` is a macro that adds the comma itself. Do not put commas between the `MakePair` lines. +- The maps are real `std::map` objects only where `ENABLE_MAPS` is defined before `CConfig.hpp` is included. These + places are `Common/src/CConfig.cpp` and `SU2_DEF/src/drivers/CDiscAdjDeformationDriver.cpp`. Everywhere else, + `MapType` is the empty class `CEmptyMap`, to make compiling faster. In the rest of the code, use the enum, not the + map. +- `build_site.py` finds the keywords in `Common/include` with the pattern + `static const MapType Name = { MakePair(...) ... };`. Keep this form. +- Write the default of `addEnumOption` exactly as in the map (`VOLUME_OUTPUT_PRECISION::SINGLE`). Then the website + can show it as a keyword. + +## 5. Check the value and set derived values + +Put the checks of the new option in `CConfig::SetPostprocessing`. + +The constructors of `CConfig` call these steps, among others, in this order: + +1. `SetConfig_Parsing` reads the values. +2. `SetDefault` sets the default of the options that were not in the file. +3. `SetPostprocessing` makes the checks and computes the derived values. +4. `SetMarkers` processes the boundary markers. + +Most constructors then call `SetOutput`, which prints the summary of the settings at start-up. + +When a check fails, stop with a clear message that names the options and the allowed values: + +```cpp + if (Kind_Turb_Model == TURB_MODEL::NONE && Kind_Trans_Model != TURB_TRANS_MODEL::NONE) { + SU2_MPI::Error("KIND_TURB_MODEL cannot be NONE to use a transition model", CURRENT_FUNCTION); + } +``` + +`OptionIsSet("NAME")` tells if the option was in the configuration file. Use it in two cases: + +- when the default depends on other options; +- to change a value without an error, but only when the user did not set the option. + +```cpp + if (MUSCL_Flow && (Kind_ConvNumScheme_Flow == SPACE_CENTERED)) { + if (OptionIsSet("MUSCL_FLOW")) { + SU2_MPI::Error("Centered schemes do not use MUSCL reconstruction (use MUSCL_FLOW= NO).", CURRENT_FUNCTION); + } else { + MUSCL_Flow = false; + } + } +``` + +Lists get their defaults here. For example, the lists of fluid properties use a small helper: + +```cpp + auto SetDefaultIfEmpty = [](su2double*& array, unsigned short& size, const su2double& default_val) { + if (array == nullptr) { + array = new su2double[1]; + array[0] = default_val; + size = 1; + } + }; +``` + +```cpp + SetDefaultIfEmpty(Prandtl_Lam, nPrandtl_Lam, Prandtl_Lam_Default); +``` + +Options that name boundary markers are also processed in `CConfig::SetMarkers`. Follow an existing marker option, +such as `MARKER_PLOTTING`. + +## 6. Add the option to config_template.cfg + +Add the option to `config_template.cfg`, in the section where it belongs. Write one or more comment lines that say +what it does and the allowed values. Then write the option with its default value: + +``` +% Value of RMS roughness for transition model +HROUGHNESS= 1.0e-6 +``` + +The Option Reference shows this line, and counts the options that are missing from the template. The checklist of +the pull request template also asks for it. + +## Rename or remove an option + +SU2 stops when the configuration file has an option that does not exist. The message names the line and the option, +and suggests the closest existing name: + +``` +Line 12 NAME: invalid option name. Check current SU2 options in config_template.cfg. +Did you mean ...? +``` + +For a renamed or removed option, `CConfig::SetConfig_Parsing` can print a better message. It has a chain of `if` +blocks with the old names. Add the old name there, with the new name if there is one: + +```cpp + else if (!option_name.compare("DYN_TIMESTEP")) + newString.append("DYN_TIMESTEP is deprecated. Use TIME_STEP instead.\n\n"); + else if (!option_name.compare("DYN_TIME")) + newString.append("DYN_TIME is deprecated. Use MAX_TIME instead.\n\n"); +``` + +For a removed keyword of an enum option, there is a second chain after the call to `SetValue`. It runs only when the +value is not valid. For example, for `KIND_TURB_MODEL`: + +```cpp + if (!option_name.compare("KIND_TURB_MODEL")) { + if (option_value[0] == "SST_SUST") + errorString.append("Option KIND_TURB_MODEL=SST_SUST is deprecated. Use KIND_TURB_MODEL=SST, SST_OPTIONS=SUSTAINING instead.\n"); +``` + +SU2 has no other mechanism. It does not translate old names to new ones: the run stops with the message. + +When you rename or remove an option, also: + +1. Update `config_template.cfg`. +2. Update every configuration file in `TestCases/` that uses it (search with `git grep`). The TestCases and + Tutorials repositories can have files that use it too. +3. Update the website pages and tutorials that name it. The [Docs Check](../Docs-Check/) page lists the options named + in the docs that do not exist in the code. + +## Python scripts + +A new C++ option needs no change in Python. `read_config` in `SU2_PY/SU2/io/config.py` stores every option it does +not know as a string. Add a case there only if a Python script must read the value as a number, a list or a +dictionary. The Python wrapper (`SU2_PY/pySU2/pySU2.i`) wraps the drivers, not `CConfig`. + +## Test the option + +Add a regression test that uses the new option, or add the option to the configuration file of an existing test. +See [Running Regression Tests](../Running-Regression-Tests/). + +## Checklist + +1. Member and getter in `Common/include/CConfig.hpp`. +2. `add...Option` call with a `/*!\brief ... \ingroup Config*/` comment in `CConfig::SetConfig_Options`. +3. Enum and map in `Common/include/option_structure.hpp`, if the option takes a keyword. +4. Checks and defaults in `CConfig::SetPostprocessing`. +5. Example in `config_template.cfg`. +6. A regression test that uses the option. + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Adding-a-Fluid-Model.md b/Docs/website/pages/Adding-a-Fluid-Model.md new file mode 100644 index 000000000000..75951a795ed6 --- /dev/null +++ b/Docs/website/pages/Adding-a-Fluid-Model.md @@ -0,0 +1,433 @@ +--- +title: Adding a Fluid Model +permalink: /docs_v7/Adding-a-Fluid-Model/ +--- + +This page shows how to add a new thermodynamic model to the finite volume flow solvers of SU2. Such a model is an equation of state: the relations between pressure, density, temperature and energy of the fluid. The page also covers, more briefly, new models for viscosity, thermal conductivity and mass diffusivity. + +You will learn: + +- what the compressible and incompressible solvers ask from a fluid model; +- where the model is created; +- how the model works with the non-dimensionalization, that is, the scaling of all quantities by reference values. + +For the overall layout of the code, see [Code Structure](../Code-Structure/). For options in general, see [Adding a Config Option](../Adding-a-Config-Option/). + +--- + +- [Files you will touch](#files-you-will-touch) +- [The FLUID_MODEL option](#the-fluid_model-option) +- [The CFluidModel class](#the-cfluidmodel-class) +- [What the solvers call](#what-the-solvers-call) +- [Existing models](#existing-models) +- [Add a compressible model](#add-a-compressible-model) +- [Add an incompressible model](#add-an-incompressible-model) +- [Non-dimensionalization and free-stream values](#non-dimensionalization-and-free-stream-values) +- [Transport property models](#transport-property-models) +- [NEMO](#nemo) +- [Discrete adjoint](#discrete-adjoint) +- [Tests](#tests) +- [Checklist](#checklist) + +--- + +## Files you will touch + +| File | What goes there | +|---|---| +| `Common/include/option_structure.hpp` | A value in `enum ENUM_FLUIDMODEL` and its keyword in `FluidModel_Map` | +| `Common/include/CConfig.hpp`, `Common/src/CConfig.cpp` | Options for the parameters of the model, checks in `CConfig::SetPostprocessing` | +| `config_template.cfg` | The keyword in the comment above `FLUID_MODEL`, and the new options | +| `SU2_CFD/include/fluid/CMyFluid.hpp`, `SU2_CFD/src/fluid/CMyFluid.cpp` | The model class (a header alone is fine for simple models) | +| `SU2_CFD/src/meson.build` | The new `.cpp` in the `fluid/...` list | +| `SU2_CFD/src/solvers/CEulerSolver.cpp` (compressible) or `SU2_CFD/src/solvers/CIncEulerSolver.cpp` (incompressible) | The `new` of the model in `SetNondimensionalization` (two places) | +| `SU2_CFD/src/solvers/CFEM_DG_EulerSolver.cpp` | Optional: the same for the DG solver | +| `SU2_CFD/src/output/CFlowOutput.cpp` | Optional: the description in `CFlowOutput::WriteForcesBreakdown` | +| `UnitTests/SU2_CFD/fluid/CFluidModel_tests.cpp`, `CFluidModel_tests_AD.cpp` | Consistency tests of the model | +| `TestCases/serial_regression.py`, `TestCases/parallel_regression.py` | A regression test | + +## The FLUID_MODEL option + +The option is registered in `CConfig::SetConfig_Options`: + +```cpp + addEnumOption("FLUID_MODEL", Kind_FluidModel, FluidModel_Map, STANDARD_AIR); +``` + +The enum and the map are in `Common/include/option_structure.hpp`. `ENUM_FLUIDMODEL` is a plain `enum`, so the values have no prefix: write `IDEAL_GAS`, not `ENUM_FLUIDMODEL::IDEAL_GAS`. + +```cpp +enum ENUM_FLUIDMODEL { + STANDARD_AIR = 0, /*!< \brief Standard air gas model. */ + IDEAL_GAS = 1, /*!< \brief Ideal gas model. */ + VW_GAS = 2, /*!< \brief Van Der Waals gas model. */ + PR_GAS = 3, /*!< \brief Perfect Real gas model. */ + CONSTANT_DENSITY = 4, /*!< \brief Constant density gas model. */ + INC_IDEAL_GAS = 5, /*!< \brief Incompressible ideal gas model. */ + INC_IDEAL_GAS_POLY = 6, /*!< \brief Inc. ideal gas, polynomial gas model. */ + MUTATIONPP = 7, /*!< \brief Mutation++ gas model for nonequilibrium flow. */ + SU2_NONEQ = 8, /*!< \brief User defined gas model for nonequilibrium flow. */ + FLUID_MIXTURE = 9, /*!< \brief Species mixture model. */ + COOLPROP = 10, /*!< \brief Thermodynamics library. */ + FLUID_FLAMELET = 11, /*!< \brief lookup table (LUT) method for premixed flamelets. */ + DATADRIVEN_FLUID = 12, /*!< \brief multi-layer perceptron driven fluid model. */ +}; +``` + +To register the model: + +1. Add the new value at the end of the enum. +2. Add a `MakePair("MY_FLUID", MY_FLUID)` line to `FluidModel_Map`. +3. Add the keyword to the comment above `FLUID_MODEL` in `config_template.cfg`. + +Read the value with `config->GetKind_FluidModel()`. + +The model parameters are normal options. The existing ones are next to `FLUID_MODEL` in `CConfig::SetConfig_Options`. Reuse them when they fit: + +- `GAMMA_VALUE`, `GAS_CONSTANT`, `CRITICAL_TEMPERATURE`, `CRITICAL_PRESSURE`, `ACENTRIC_FACTOR`; +- `FLUID_NAME` (CoolProp); +- `INTERPOLATION_METHOD` and `FILENAMES_INTERPOLATOR` (data-driven); +- `SPECIFIC_HEAT_CP`, `MOLECULAR_WEIGHT`, `THERMODYNAMIC_PRESSURE` (incompressible); +- ... + +## The CFluidModel class + +Your model derives from [`CFluidModel`](../../doxygen/classCFluidModel.html) (`SU2_CFD/include/fluid/CFluidModel.hpp`, `SU2_CFD/src/fluid/CFluidModel.cpp`). This class holds the thermodynamic state of **one** point. A solver sets the state with one of the `SetTDState_...` functions, then reads the other quantities with getters. + +The state is stored in protected members. A child class must fill them: + +- `Density`, `Pressure`, `Temperature`, `StaticEnergy`, `Enthalpy`, `Entropy`, `SoundSpeed2`, `Cp`, `Cv`; +- the derivatives `dPdrho_e`, `dPde_rho`, `dTdrho_e`, `dTde_rho`, used by the compressible solver; +- the derivatives `dhdrho_P`, `dhdP_rho`, `dsdrho_P`, `dsdP_rho`, used by the non-reflecting boundary conditions (NRBC); +- `Mu`, `Kt`, `mass_diffusivity` and their derivatives. The transport models fill these (see [below](#transport-property-models)). + +The getters (`GetPressure()`, `GetTemperature()`, `GetSoundSpeed()`, `GetSoundSpeed2()`, `GetdPdrho_e()`, ...) are not virtual. They return these members. + +The virtual functions that set the state all do nothing in the base class. Override the ones that your target solver calls: + +| Function | Inputs | +|---|---| +| `SetTDState_rhoe(su2double rho, su2double e)` | density, static energy | +| `SetTDState_PT(su2double P, su2double T)` | pressure, temperature | +| `SetTDState_Prho(su2double P, su2double rho)` | pressure, density | +| `SetEnergy_Prho(su2double P, su2double rho)` | pressure, density; only `StaticEnergy` is required | +| `SetTDState_hs(su2double h, su2double s)` | enthalpy, entropy | +| `SetTDState_rhoT(su2double rho, su2double T)` | density, temperature | +| `SetTDState_Ps(su2double P, su2double s)` | pressure, entropy | +| `ComputeDerivativeNRBC_Prho(su2double P, su2double rho)` | pressure, density; sets the state and `dhdrho_P`, `dhdP_rho`, `dsdrho_P`, `dsdP_rho` | +| `SetTDState_T(su2double val_Temperature, const su2double* val_scalars = nullptr)` | temperature (incompressible, with optional species or flamelet scalars) | +| `SetTDState_h(su2double val_enthalpy, const su2double* val_scalars = nullptr)` | enthalpy (incompressible) | + +Special models use other virtual functions: + +- `SetCpModel` (`CIncIdealGasPolynomial`); +- `EvaluateDataSet` (`CFluidFlamelet`); +- `GetNScalars` (not overridden by any model); +- `GetExtrapolation` (`CDataDrivenFluid`, `CFluidFlamelet`); +- `GetnIter_Newton` (`CDataDrivenFluid`); +- `GetEnthalpyDiffusivity` and `GetGradEnthalpyDiffusivity` (`CFluidScalar`); +- the transport getters `GetLaminarViscosity`, `GetThermalConductivity`, `GetMassDiffusivity`. + +A call that is not overridden does nothing and gives no error. So check the [table of callers](#what-the-solvers-call) before you leave a function out. + +## What the solvers call + +Your model must implement the functions that its solver calls. + +**Compressible solver** (`CEulerSolver`, `CNSSolver`). The main call is in the variable classes. `CEulerVariable::SetPrimVar` (`SU2_CFD/src/variables/CEulerVariable.cpp`) runs for every point at every iteration: + +```cpp +bool CEulerVariable::SetPrimVar(unsigned long iPoint, CFluidModel *FluidModel) { + + bool RightVol = true; + + SetVelocity(iPoint); // Computes velocity and velocity^2 + su2double density = GetDensity(iPoint); + su2double staticEnergy = GetEnergy(iPoint)-0.5*Velocity2(iPoint); + + /*--- Check will be moved inside fluid model plus error description strings ---*/ + + FluidModel->SetTDState_rhoe(density, staticEnergy); + + bool check_dens = SetDensity(iPoint); + bool check_press = SetPressure(iPoint, FluidModel->GetPressure()); + bool check_sos = SetSoundSpeed(iPoint, FluidModel->GetSoundSpeed2()); + bool check_temp = SetTemperature(iPoint, FluidModel->GetTemperature()); + + // ... +} +``` + +Other calls in the variable classes: + +- `CNSVariable::SetPrimVar` does the same, with the turbulent kinetic energy removed from the energy. It then calls `GetLaminarViscosity()`, `GetThermalConductivity()` and `GetCp()`. +- The `SetSecondaryVar` functions store `GetdPdrho_e()` and `GetdPde_rho()` (Euler). For Navier-Stokes they also store `GetdTdrho_e()`, `GetdTde_rho()`, `Getdmudrho_T()`, `GetdmudT_rho()`, `Getdktdrho_T()`, `GetdktdT_rho()`. +- The solver reaches the model with `GetFluidModel()`, which returns the object of the current OpenMP thread. + +So in `SetTDState_rhoe`, a compressible model must set at least: + +- `Density`, `StaticEnergy`, `Pressure`, `Temperature`, `SoundSpeed2`; +- `dPdrho_e`, `dPde_rho`, `dTdrho_e`, `dTde_rho`; +- `Entropy`; +- `Cp`, for viscous flows. + +The ideal gas sets `Cp`, `Cv` and the temperature derivatives only once, in its constructor. + +The other functions are called by: + +| Function | Callers (compressible) | +|---|---| +| `SetTDState_rhoe` | the variable classes (above), the `CEulerSolver` constructor, `BC_Riemann`, `BC_TurboRiemann`, `BC_Giles` | +| `SetTDState_PT` | `CEulerSolver::SetNondimensionalization` (free stream), `BC_Riemann`, `BC_TurboRiemann`, `BC_Giles`, `BC_ActDisk` | +| `SetTDState_Prho` | `SetNondimensionalization`, `ComputeConsistentExtrapolation` (MUSCL with non-ideal fluids), `BC_Riemann`, `BC_TurboRiemann`, `BC_Giles`, `PreprocessBC_Giles`, `MixedOut_Average`, `TurboAverageProcess`, `SetFreeStream_TurboSolution`, `CFVMFlowSolverBase::BC_Fluid_Interface`; also `CTurbSASolver` and `CTurbSSTSolver` | +| `SetEnergy_Prho` | `SetNondimensionalization` (non-dimensional free-stream energy), `LowMachPrimitiveCorrection` | +| `SetTDState_rhoT` | `SetNondimensionalization` (`INIT_OPTION= REYNOLDS`) | +| `SetTDState_hs` | `BC_Riemann`, `BC_TurboRiemann` | +| `SetTDState_Ps` | `CTurboOutput` (turbomachinery performance) | +| `ComputeDerivativeNRBC_Prho` | `CEulerSolver::MixedOut_Average`, `CNumerics::ComputeResJacobianGiles` | + +Some boundary conditions do not use the fluid model. For example, `CEulerSolver::BC_Inlet` uses ideal-gas relations with `Gamma` and `Gas_Constant`. For non-ideal fluids, the Riemann and Giles boundary conditions go through the fluid model. + +**Incompressible solver** (`CIncEulerSolver`, `CIncNSSolver`). The energy variable is the enthalpy. + +- `CIncEulerVariable::SetPrimVar` and `CIncNSVariable::SetPrimVar` call `SetTDState_h(Enthalpy, scalar)`. They then read `GetTemperature()`, `GetDensity()`, `GetCp()`, `GetCv()` and `GetEnthalpy()`. For viscous flows they also read `GetLaminarViscosity()` and `GetThermalConductivity()`, after `SetEddyViscosity`. +- Initialization and boundary conditions call `SetTDState_T`. +- The thermodynamic pressure is constant during the run. It is set in the constructor of the model. Only the setup changes it, with `SetPressure` (see [below](#non-dimensionalization-and-free-stream-values)). + +## Existing models + +Start from an existing model close to yours. All are in `SU2_CFD/include/fluid/` and `SU2_CFD/src/fluid/`. + +| Keyword | Class | Solver | Notes | +|---|---|---|---| +| `STANDARD_AIR`, `IDEAL_GAS` | [`CIdealGas`](../../doxygen/classCIdealGas.html) | compressible | Closed-form inverses for every `SetTDState_...` | +| `VW_GAS` | [`CVanDerWaalsGas`](../../doxygen/classCVanDerWaalsGas.html) | compressible | Child of `CIdealGas`; Newton iteration in `SetTDState_PT` | +| `PR_GAS` | [`CPengRobinson`](../../doxygen/classCPengRobinson.html) | compressible | Child of `CIdealGas` | +| `COOLPROP` | [`CCoolProp`](../../doxygen/classCCoolProp.html) | compressible | Calls the CoolProp library (`-Denable-coolprop=true`); dimensional only | +| `DATADRIVEN_FLUID` | [`CDataDrivenFluid`](../../doxygen/classCDataDrivenFluid.html) | compressible | Multi-layer perceptron, a small neural network (`-Denable-mlpcpp=true`), or look-up table; Newton solver for the inverse functions | +| `CONSTANT_DENSITY` | [`CConstantDensity`](../../doxygen/classCConstantDensity.html) | incompressible | Header only | +| `INC_IDEAL_GAS` | [`CIncIdealGas`](../../doxygen/classCIncIdealGas.html) | incompressible | Header only, density from the operating pressure | +| `INC_IDEAL_GAS_POLY` | [`CIncIdealGasPolynomial`](../../doxygen/classCIncIdealGasPolynomial.html) | incompressible | Header only, Cp polynomial in temperature | +| `FLUID_MIXTURE` | [`CFluidScalar`](../../doxygen/classCFluidScalar.html) | incompressible | Species mixture, one transport model per species | +| `FLUID_FLAMELET` | [`CFluidFlamelet`](../../doxygen/classCFluidFlamelet.html) | incompressible | Look-up table of a flamelet manifold (precomputed combustion states) | +| `SU2_NONEQ`, `MUTATIONPP` | `CSU2TCLib`, `CMutationTCLib` (children of [`CNEMOGas`](../../doxygen/classCNEMOGas.html)) | NEMO | See [NEMO](#nemo) | + +Good starting points: + +- `CVanDerWaalsGas` for a cubic equation of state; +- `CDataDrivenFluid` for a model that inverts its functions with a Newton solver; +- `CIncIdealGas` for an incompressible model. + +The state function of the ideal gas, from `SU2_CFD/src/fluid/CIdealGas.cpp`, shows how little a model needs: + +```cpp +void CIdealGas::SetTDState_rhoe(su2double rho, su2double e) { + Density = rho; + StaticEnergy = e; + dPdrho_e = Gamma_Minus_One * StaticEnergy; + dPde_rho = Gamma_Minus_One * Density; + Pressure = dPde_rho * StaticEnergy; + Temperature = dTde_rho * StaticEnergy; + SoundSpeed2 = Gamma * Gas_Constant * Temperature; + + if (ComputeEntropy) Entropy = Cv * log(Temperature) - Gas_Constant * log(Density); +} +``` + +## Add a compressible model + +Follow these steps to add a model for `CEulerSolver` and `CNSSolver`. + +1. **Option.** Add the enum value and the keyword ([above](#the-fluid_model-option)). Add options for new parameters: member, getter, `add...Option`, `config_template.cfg`. See [Adding a Config Option](../Adding-a-Config-Option/). +2. **Class.** Write `class CMyFluid final : public CFluidModel` in `SU2_CFD/include/fluid/CMyFluid.hpp`. You can also derive from `CIdealGas` to reuse its members, as `CVanDerWaalsGas` and `CPengRobinson` do. Implement the functions of the [table above](#what-the-solvers-call) that your cases need. The initialization alone needs `SetTDState_rhoe`, `SetTDState_PT`, `SetTDState_Prho` and `SetEnergy_Prho`. It also needs `SetTDState_rhoT` when the free stream is set from the Reynolds number (`INIT_OPTION= REYNOLDS`). +3. **Build.** Add `fluid/CMyFluid.cpp` to the `fluid/...` list in `SU2_CFD/src/meson.build`. +4. **Creation.** In `CEulerSolver::SetNondimensionalization` (`SU2_CFD/src/solvers/CEulerSolver.cpp`), add a `case` to **both** `switch (config->GetKind_FluidModel())` blocks: + - the first creates `auxFluidModel` with **dimensional** parameters, to compute the free-stream state; + - the second, inside `SU2_OMP_PARALLEL`, creates one model per thread with **non-dimensional** parameters, and stores it in `FluidModel[thread]`. + + This is the second block for the Van der Waals gas: + + ```cpp + case VW_GAS: + FluidModel[thread] = new CVanDerWaalsGas(Gamma, Gas_ConstantND, + config->GetPressure_Critical() / config->GetPressure_Ref(), + config->GetTemperature_Critical() / config->GetTemperature_Ref()); + break; + ``` + + Include the header at the top of `CEulerSolver.cpp`. `CEulerSolver::~CEulerSolver` deletes the models. The first `switch` ends with `SU2_MPI::Error("Unknown fluid model.", ...)`, so an incompressible keyword cannot reach the compressible solver. +5. **Optional.** Add the name to the `ModelTable` printed at the end of `SetNondimensionalization`, and to `CFlowOutput::WriteForcesBreakdown`. If the model must also work with the DG solver, repeat step 4 in `CFEM_DG_EulerSolver::SetNondimensionalization`. + +When the model is not `STANDARD_AIR` or `IDEAL_GAS`, the solver changes in three ways: + +- `CDriver::InitializeNumerics` creates the general numerics: `CUpwGeneralRoe_Flow`, `CUpwGeneralHLLC_Flow`, `CGeneralAvgGrad_Flow`, `CSourceGeneralAxisymmetric_Flow`. They read the secondary variables: `S_i[0]` = dP/drho at constant e, `S_i[1]` = dP/de at constant rho. +- `CEulerSolver::Upwind_Residual` uses the classic path, not the vectorized one. After the MUSCL reconstruction (the second-order left and right states of each edge), `ComputeConsistentExtrapolation` recomputes these states in a thermodynamically consistent way. It calls `SetTDState_Prho`. +- `CConfig::SetPostprocessing` allows only `ROE`, `HLLC` and `JST` (search for "Non-Ideal Compressible Fluids"). + +## Add an incompressible model + +Follow these steps to add a model for `CIncEulerSolver` and `CIncNSSolver`. + +1. Add the enum value, the keyword and the options, as for a compressible model. +2. Write the class with `SetTDState_T` and `SetTDState_h`. + - They must set `Temperature`, `Density` and `Enthalpy`. + - Set `Cp` and `Cv` too. Constant values can be set in the constructor, as in `CIncIdealGas`. + - The enthalpy is the sensible enthalpy relative to a reference temperature. The existing models take `STD_REF_TEMP / config->GetTemperature_Ref()` as the last constructor argument. +3. In `CIncEulerSolver::SetNondimensionalization` (`SU2_CFD/src/solvers/CIncEulerSolver.cpp`), add a `case` to both `switch` blocks. The incompressible model headers are included in this file. The two blocks are: + - the dimensional `auxFluidModel`; + - the per-thread `FluidModel` loop (`for (auto& fluidModel : FluidModel)`). After the second `switch`, this loop sets the transport models when the flow is viscous. +4. Check `CConfig::SetPostprocessing`: + - With `INC_DENSITY_MODEL= CONSTANT` or `BOUSSINESQ`, the code sets `Kind_FluidModel = CONSTANT_DENSITY`. + - With `INC_DENSITY_MODEL= VARIABLE`, it accepts only `INC_IDEAL_GAS`, `INC_IDEAL_GAS_POLY`, `FLUID_MIXTURE` and `FLUID_FLAMELET`. Add the new value there if the density varies. + - The same function rejects `DATADRIVEN_FLUID` for the incompressible solvers. + +## Non-dimensionalization and free-stream values + +`SetNondimensionalization` sets the free-stream state and the reference values. It creates each fluid model twice: + +- a dimensional copy, for the setup; +- non-dimensional copies, one per thread, used during the run. + +**Compressible free stream** (`CEulerSolver::SetNondimensionalization`): + +1. `Temperature_Ref`, `Viscosity_Ref` and `Conductivity_Ref` are set to 1. So the `...ND()` getters of `CConfig` return dimensional values for the auxiliary model. For example, `GetMu_ConstantND()` returns `Mu_Constant / Viscosity_Ref`. +2. `auxFluidModel` is created. + - With `FREESTREAM_OPTION= TEMPERATURE_FS`, `SetTDState_PT(Pressure_FreeStream, Temperature_FreeStream)` gives the density. + - With `DENSITY_FS`, `SetTDState_Prho` gives the temperature. +3. `GetSoundSpeed()` turns the Mach number into the free-stream velocity. +4. For viscous flows, `SetLaminarViscosityModel` and `SetThermalConductivityModel` are called on the auxiliary model. With `INIT_OPTION= REYNOLDS`, the density comes from the Reynolds number, and `SetTDState_rhoT` gives the pressure. `GetStaticEnergy()` gives the free-stream energy. +5. The reference values are set from `REF_DIMENSIONALIZATION`: `Pressure_Ref`, `Density_Ref`, `Temperature_Ref`, then `Velocity_Ref = sqrt(Pressure_Ref/Density_Ref)`, `Gas_Constant_Ref = Velocity_Ref^2/Temperature_Ref`, `Viscosity_Ref`, `Conductivity_Ref`, ... +6. The per-thread models are created with non-dimensional parameters. `SetEnergy_Prho(Pressure_FreeStreamND, Density_FreeStreamND)` gives the non-dimensional free-stream energy. `Energy_Ref` is the ratio of the dimensional and the non-dimensional energies. + +So the non-dimensional model must be consistent with these scales: + +| Quantity | Divided by | +|---|---| +| pressure | `Pressure_Ref` | +| density | `Density_Ref` | +| temperature | `Temperature_Ref` | +| energy and enthalpy | `Velocity_Ref^2` | +| gas constant | `Gas_Constant_Ref` | + +The simplest way is to give non-dimensional parameters to the constructor. `VW_GAS` and `PR_GAS` do this with `Gas_ConstantND` and with the critical values divided by the reference values. + +Some models cannot be scaled, for example tables or external libraries in physical units. For these, allow only `REF_DIMENSIONALIZATION= DIMENSIONAL`. CoolProp does this in `CConfig::SetPostprocessing`: + +```cpp + /*--- Check if CoolProp is used with non-dimensionalization. ---*/ + if (Kind_FluidModel == COOLPROP && Ref_NonDim != DIMENSIONAL) { + SU2_MPI::Error("CoolProp can not be used with non-dimensionalization.", CURRENT_FUNCTION); + } +``` + +**Incompressible** (`CIncEulerSolver::SetNondimensionalization`): + +- `auxFluidModel->SetTDState_T(Temperature_FreeStream, scalar_init)` gives the free-stream density. +- `INIT_OPTION_INC` decides what happens next. Either the thermodynamic pressure is scaled (`SetPressure`) to match the given density, or the density is taken from the model. +- The per-thread models get non-dimensional parameters: `Density_FreeStreamND`, `Specific_Heat_CpND`, `Gas_ConstantND`, `Pressure_ThermodynamicND`. Right after creation, most of these models get `SetTDState_T(Temperature_FreeStreamND)`. + +## Transport property models + +Viscosity, thermal conductivity and mass diffusivity come from separate objects owned by the fluid model: + +```cpp + unique_ptr LaminarViscosity; /*!< \brief Laminar Viscosity Model */ + unique_ptr ThermalConductivity; /*!< \brief Thermal Conductivity Model */ + unique_ptr MassDiffusivity; /*!< \brief Mass Diffusivity Model */ +``` + +| Option | Enum and map | Base class | Factory in `CFluidModel.cpp` | +|---|---|---|---| +| `VISCOSITY_MODEL` | `VISCOSITYMODEL`, `ViscosityModel_Map` | [`CViscosityModel`](../../doxygen/classCViscosityModel.html): `SetViscosity(t, rho)` | `MakeLaminarViscosityModel` | +| `CONDUCTIVITY_MODEL`, `TURBULENT_CONDUCTIVITY_MODEL` | `CONDUCTIVITYMODEL`, `ConductivityModel_Map`; `CONDUCTIVITYMODEL_TURB`, `TurbConductivityModel_Map` | [`CConductivityModel`](../../doxygen/classCConductivityModel.html): `SetConductivity(t, rho, mu_lam, mu_turb, cp, dmudrho_t, dmudt_rho)` | `MakeThermalConductivityModel` | +| `DIFFUSIVITY_MODEL` | `DIFFUSIVITYMODEL`, `Diffusivity_Model_Map` | [`CDiffusivityModel`](../../doxygen/classCDiffusivityModel.html): `SetDiffusivity(rho, mu_lam, cp, kt)` | `MakeMassDiffusivityModel` | + +Existing children: + +- viscosity: `CConstantViscosity`, `CSutherland`, `CPolynomialViscosity`, `CCoolPropViscosity`; +- conductivity: `CConstantConductivity`, `CConstantConductivityRANS`, `CConstantPrandtl`, `CConstantPrandtlRANS`, `CPolynomialConductivity`, `CPolynomialConductivityRANS`, `CCoolPropConductivity`; +- diffusivity: `CConstantDiffusivity`, `CConstantSchmidt`, `CConstantLewisDiffusivity`. + +For the `FLAMELET` value of each option, the factory returns `nullptr`, because `CFluidFlamelet` overrides the getters and reads the table. + +A model sets its value and its derivatives in protected members. `CSutherland`: + +```cpp + void SetViscosity(su2double t, su2double rho) override { + const su2double t_ref_inv = 1.0 / t_ref_; + const su2double t_nondim = t_ref_inv * t; + mu_ = mu_ref_ * t_nondim * sqrt(t_nondim) * ((t_ref_ + s_) / (t + s_)); + + /*--- Set Viscosity Derivatives. ---*/ + const su2double ts_inv = 1.0 / (t + s_); + dmudrho_t_ = 0.0; + dmudt_rho_ = mu_ref_ * (t_ref_ + s_) * ts_inv * sqrt(t_nondim) * (1.5 * t_ref_inv - t_nondim * ts_inv); + } +``` + +To add a transport model: + +1. Add a value to the enum and its map in `option_structure.hpp`, and options for its parameters. +2. Write the class in `SU2_CFD/include/fluid/`, derived from the base class of the table. A header alone is fine. +3. Add a `case` in the factory in `SU2_CFD/src/fluid/CFluidModel.cpp`, and include the header there. Take the parameters from non-dimensional getters of `CConfig`, such as `GetMu_RefND(iSpecies)`. These divide by the reference values, so the same factory serves both the dimensional and the non-dimensional model. The `iSpecies` argument is used by `CFluidScalar`, which creates one model per species. + +How the solvers use it: + +- In `SetNondimensionalization`, the solvers call `SetLaminarViscosityModel(config)`, `SetThermalConductivityModel(config)` and `SetMassDiffusivityModel(config)` on each fluid model. +- `CFluidModel::GetLaminarViscosity()` then calls `LaminarViscosity->SetViscosity(Temperature, Density)`. So the state must be set before. +- `GetThermalConductivity()` uses `Mu`, `Mu_Turb` and `Cp`. Only the incompressible solver sets `Mu_Turb`, with `SetEddyViscosity`. In the compressible solver it stays zero, and the viscous numerics add the turbulent part of the conductivity with `PRANDTL_TURB`. + +## NEMO + +Nothing on this page applies to non-equilibrium flows (`NEMO_EULER`, `NEMO_NAVIER_STOKES`). They use a separate interface, [`CNEMOGas`](../../doxygen/classCNEMOGas.html), a child of `CFluidModel`. Its children are `CSU2TCLib` and `CMutationTCLib`. They are created in `CNEMOEulerSolver::SetNondimensionalization` and in `CNEMONumerics`. + +## Discrete adjoint + +Write your model so that automatic differentiation (AD) works. The discrete adjoint records the primal iteration on the AD tape, the record of every operation it differentiates. The fluid model is called inside that iteration: in `SetPrimVar` for every point, in the boundary conditions, and in the reconstruction. So: + +- Write the model with `su2double`. A value converted to `double` has no derivative, whether through `SU2_TYPE::GetValue` or through a library that works on `double`. CoolProp is such a library: the error message of `CCoolProp` says that it cannot be used with `directdiff` or `autodiff`. +- Iterative inversions, such as the Newton loops in `CVanDerWaalsGas::SetTDState_PT` or in `CDataDrivenFluid`, are recorded with all their iterations. This makes the tape larger. +- Avoid hidden state between calls. For example, `CVanDerWaalsGas` keeps the compressibility factor `Zed` of the last call as the first guess of its Newton loop. So the result of `SetTDState_PT` depends on the previous call, within the tolerance. +- The hand-written derivatives (`dPdrho_e`, `dPde_rho`, ...) are recorded and differentiated like any other value. They enter the general Roe and HLLC fluxes (through the secondary variables), the viscous Jacobians and some boundary conditions. Compute them with `su2double` from the inputs, and check them with the unit tests below. + +An adjoint regression test with a non-ideal fluid is `transonic_stator` (`TestCases/disc_adj_turbomachinery/transonic_stator_2D/transonic_stator.cfg`, `PR_GAS`). It is in `serial_regression_AD.py` and `parallel_regression_AD.py`. + +## Tests + +Add unit tests and a regression test for the new model. + +**Unit tests.** Add a `TEST_CASE` for the new model to both files below. Build with `-Denable-tests=true`; see [Writing Unit Tests](../Writing-Unit-Tests/). + +- `UnitTests/SU2_CFD/fluid/CFluidModel_tests.cpp` has `FluidModelChecks(fluid_model, p, T)`. It checks that `SetTDState_rhoe` inverts `SetTDState_PT`. It also compares `dPdrho_e`, `dPde_rho`, `dTdrho_e`, `dTde_rho` with finite differences. +- `UnitTests/SU2_CFD/fluid/CFluidModel_tests_AD.cpp` has `FluidModelADChecks(fluid_model, p, T)`. It compares the same derivatives, `Cp`, and the NRBC entropy derivatives with AD. It is built as `test_driver_AD`. + +**Regression tests.** The non-ideal compressible cases are in `TestCases/nicf/`. They are in the "NICFD" sections of the regression scripts: + +| Test | Configuration | Fluid model | Script | +|---|---|---|---| +| `ls89_sa` | `nicf/LS89/turb_SA_PR.cfg` | `PR_GAS` | serial | +| `edge_VW` | `nicf/edge/edge_VW.cfg` | `VW_GAS` | serial, parallel, hybrid | +| `edge_PPR` | `nicf/edge/edge_PPR.cfg` | `PR_GAS` | serial, parallel, hybrid | +| `coolprop_fluidModel`, `coolprop_transportModel` | `nicf/coolprop/fluidModel.cfg`, `transportModel.cfg` | `COOLPROP` (only on `x86_64`) | parallel | +| `datadriven_fluidModel` | `nicf/datadriven/datadriven_nozzle.cfg` | `DATADRIVEN_FLUID` | parallel | + +Some `TestCases/turbomachinery/` cases also use `PR_GAS`: `centrifugal_blade`, `centrifugal_stage`, `transonic_stator_2D`. + +To add a regression test: + +1. Copy one of these cases and switch it to the new model. +2. Add it as a new `TestCase`; see [Running Regression Tests](../Running-Regression-Tests/). +3. Keep the test short (few iterations). +4. If the model is meant for optimization, add an AD test too. + +## Checklist + +- [ ] Enum value and keyword in `ENUM_FLUIDMODEL` / `FluidModel_Map`, keyword and options in `config_template.cfg`. +- [ ] The `SetTDState_...` functions that your solver calls, and all the state members and derivatives they must set. +- [ ] New `.cpp` in `SU2_CFD/src/meson.build`. +- [ ] Both `switch` blocks in `SetNondimensionalization` (dimensional and non-dimensional). +- [ ] Consistent non-dimensional parameters, or a check that forces `REF_DIMENSIONALIZATION= DIMENSIONAL`. +- [ ] Checks in `CConfig::SetPostprocessing` (density model list for incompressible models). +- [ ] Unit tests with `FluidModelChecks` and `FluidModelADChecks`, and a regression test. + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Adding-a-Numerical-Scheme.md b/Docs/website/pages/Adding-a-Numerical-Scheme.md new file mode 100644 index 000000000000..37b498518806 --- /dev/null +++ b/Docs/website/pages/Adding-a-Numerical-Scheme.md @@ -0,0 +1,533 @@ +--- +title: Adding a Numerical Scheme +permalink: /docs_v7/Adding-a-Numerical-Scheme/ +--- + +This page shows how to add a new convective scheme, upwind or centered, to the finite volume flow solvers of SU2. It also covers, more briefly, how to add a viscous flux or a source term. It follows the code of the compressible solver (`CEulerSolver`, `CNSSolver`) and points to the incompressible and NEMO versions. + +For the overall layout of the code, see [Code Structure](../Code-Structure/). Turbulence, transition and species schemes are covered in other pages: [Adding a Turbulence Model](../Adding-a-Turbulence-Model/), [Adding a Transition Model](../Adding-a-Transition-Model/) and [Adding a Solver](../Adding-a-Solver/). + +--- + +- [Files you will touch](#files-you-will-touch) +- [How the configuration picks the scheme](#how-the-configuration-picks-the-scheme) +- [Two code paths: classic and vectorized](#two-code-paths-classic-and-vectorized) +- [The CNumerics interface](#the-cnumerics-interface) +- [Add a classic upwind scheme](#add-a-classic-upwind-scheme) +- [MUSCL reconstruction and limiters](#muscl-reconstruction-and-limiters) +- [Add a vectorized scheme](#add-a-vectorized-scheme) +- [Centered schemes](#centered-schemes) +- [Jacobians](#jacobians) +- [Low-Mach and entropy fix options](#low-mach-and-entropy-fix-options) +- [Incompressible and NEMO schemes](#incompressible-and-nemo-schemes) +- [Viscous fluxes and source terms](#viscous-fluxes-and-source-terms) +- [Discrete adjoint](#discrete-adjoint) +- [Tests](#tests) +- [Checklist](#checklist) + +--- + +## Files you will touch + +| File | What goes there | +|---|---| +| `Common/include/option_structure.hpp` | A value in `enum class UPWIND` or `enum class CENTERED`, and its keyword in `Upwind_Map` or `Centered_Map` | +| `Common/src/CConfig.cpp` | Optional: checks in `CConfig::SetPostprocessing`, the screen summary in `CConfig::SetOutput` | +| `config_template.cfg` | The new keyword in the comment above `CONV_NUM_METHOD_FLOW` | +| `SU2_CFD/include/numerics/flow/convection/*.hpp`, `SU2_CFD/src/numerics/flow/convection/*.cpp` | The classic numerics class (one edge at a time) | +| `SU2_CFD/src/meson.build` | A new `.cpp` file in the `numerics/...` list | +| `SU2_CFD/src/drivers/CDriver.cpp` | The `new` of the numerics class in `CDriver::InitializeNumerics` | +| `SU2_CFD/include/numerics_simd/flow/convection/upwind.hpp` or `centered.hpp`, `SU2_CFD/include/numerics_simd/CNumericsSIMD.cpp` | Optional: the vectorized version and its factory entry | +| `SU2_CFD/src/solvers/CEulerSolver.cpp` | Only if the scheme needs new inputs, new sensors, or the vectorized path | +| `TestCases/parallel_regression.py`, `TestCases/serial_regression.py` (and the `_AD` lists) | A regression test | + +## How the configuration picks the scheme + +The option `CONV_NUM_METHOD_FLOW` is registered in `CConfig::SetConfig_Options` (`Common/src/CConfig.cpp`) with `addConvectOption`: + +```cpp + addConvectOption("CONV_NUM_METHOD_FLOW", Kind_ConvNumScheme_Flow, Kind_Centered_Flow, Kind_Upwind_Flow); +``` + +The parser is `COptionConvect` in `Common/include/option_structure.inl`. It looks up the keyword in `Centered_Map` first, then in `Upwind_Map`. It sets three values: + +- `Kind_ConvNumScheme_Flow`: `SPACE_CENTERED` or `SPACE_UPWIND` (`enum ENUM_SPACE`). Read it with `config->GetKind_ConvNumScheme_Flow()`. +- `Kind_Centered_Flow`: a `CENTERED` value. It is `CENTERED::NONE` for upwind schemes. Read it with `config->GetKind_Centered_Flow()`. +- `Kind_Upwind_Flow`: an `UPWIND` value. It is `UPWIND::NONE` for centered schemes. Read it with `config->GetKind_Upwind_Flow()`. + +The same kind of option is used for `CONV_NUM_METHOD_ADJFLOW`, `CONV_NUM_METHOD_TURB`, `CONV_NUM_METHOD_ADJTURB`, `CONV_NUM_METHOD_SPECIES` and `CONV_NUM_METHOD_HEAT`. + +The enums and maps are in `Common/include/option_structure.hpp`. For an upwind scheme, add a value to `enum class UPWIND` and a line to `Upwind_Map`: + +```cpp +static const MapType Upwind_Map = { + MakePair("NONE", UPWIND::NONE) + MakePair("ROE", UPWIND::ROE) + MakePair("TURKEL_PREC", UPWIND::TURKEL) + // ... + MakePair("LAX-FRIEDRICH", UPWIND::LAX_FRIEDRICH) + MakePair("UDS", UPWIND::UDS) +}; +``` + +For a centered scheme, do the same with `enum class CENTERED` and `Centered_Map`. + +Notes: + +- Give a new scheme a keyword that is in one map only. `Centered_Map` is searched first, so `CONV_NUM_METHOD_FLOW` reads a keyword that is in both maps as centered. `LAX-FRIEDRICH` is such a keyword. `UPWIND::LAX_FRIEDRICH` is reached only through `RIEMANN_SOLVER_FEM`, an `addEnumOption` with `Upwind_Map` that the DG solver uses. +- The comment above the two enums mentions an `ENUM_CONVECTIVE`. It no longer exists. The two maps are all you need. +- Add the keyword to the list in `config_template.cfg` above `CONV_NUM_METHOD_FLOW`. For the general rules on enums and maps, see [Adding a Config Option](../Adding-a-Config-Option/#4-add-an-enum). + +At run time, the solver picks the residual function as follows: + +1. Before each equation system is solved, `CConfig::SetGlobalParam` copies the values of that system into the generic ones (`SetKind_ConvNumScheme`). +2. `CIntegration::Space_Integration` (`SU2_CFD/src/integration/CIntegration.cpp`) then calls `Centered_Residual` if `config->GetKind_ConvNumScheme()` is `SPACE_CENTERED`, and `Upwind_Residual` if it is `SPACE_UPWIND`. + +## Two code paths: classic and vectorized + +The compressible solver `CEulerSolver` and its child `CNSSolver` can compute the convective fluxes on the edges in two ways: + +1. **Classic numerics.** A [`CNumerics`](../../doxygen/classCNumerics.html) object computes the flux of one edge. The solver does the MUSCL reconstruction and calls `ComputeResidual`. It then adds the result to the residual and the Jacobian. Files: `SU2_CFD/include/numerics/flow/convection/`. +2. **Vectorized numerics.** A [`CNumericsSIMD`](../../doxygen/classCNumericsSIMD.html) object computes the fluxes of several edges at once. It uses SIMD: one CPU instruction acts on several values together. The object also does the reconstruction and the viscous flux, and updates the residual and the Jacobian itself. Files: `SU2_CFD/include/numerics_simd/`. + +The table shows which path is used. See `CEulerSolver::Centered_Residual` and `CEulerSolver::Upwind_Residual` in `SU2_CFD/src/solvers/CEulerSolver.cpp`. + +| Case | Path | +|---|---| +| Centered schemes `JST`, `JST_KE`, `JST_MAT`, `LAX-FRIEDRICH` | Vectorized, always. Works with any fluid model. `LOW_MACH_CORR= YES` is rejected. Coarse multigrid levels use the Lax-Friedrich scheme. | +| Upwind `ROE` or `MSW`, `FLUID_MODEL` = `STANDARD_AIR` or `IDEAL_GAS`, `LOW_MACH_CORR= NO` | Vectorized | +| Any other upwind case (other schemes, other fluid models, `LOW_MACH_CORR= YES`) | Classic | +| Boundary conditions (`conv_bound_term`) | Classic, always | + +This is the start of `CEulerSolver::Upwind_Residual`: + +```cpp + const bool ideal_gas = (config->GetKind_FluidModel() == STANDARD_AIR) || + (config->GetKind_FluidModel() == IDEAL_GAS); + const bool low_mach_corr = config->Low_Mach_Correction(); + + /*--- Use vectorization if the scheme supports it. ---*/ + if (ideal_gas && !low_mach_corr) { + if (config->GetKind_Upwind_Flow() == UPWIND::ROE || config->GetKind_Upwind_Flow() == UPWIND::MSW) { + EdgeFluxResidual(geometry, solver_container, config); + return; + } + } +``` + +A new upwind scheme needs at least a classic version, because the boundary conditions use the `CONV_BOUND_TERM` numerics. A vectorized version is optional. Write one only for schemes that will be used a lot with an ideal gas. + +## The CNumerics interface + +Every classic flux class derives from [`CNumerics`](../../doxygen/classCNumerics.html) (`SU2_CFD/include/numerics/CNumerics.hpp`). A flux class: + +- takes `(unsigned short val_nDim, unsigned short val_nVar, const CConfig* config)` in its constructor, and passes them to `CNumerics(val_nDim, val_nVar, config)`; +- allocates its flux vector and its two Jacobian matrices once, in the constructor, and frees them in the destructor; +- gets the data of the edge through setters. The solver calls them before each call: `SetNormal`, `SetPrimitive`, `SetSecondary`, `SetGridVel`, `SetCoord`, `SetSensor`, `SetDissipation`, `SetVelocity2_Inf`, ...; +- overrides `ComputeResidual(const CConfig* config)`, which returns a `ResidualType<>`. + +`ResidualType` is a read-only view of the flux and of the two Jacobians. The data stays inside the numerics object. + +```cpp + template + struct ResidualType { + const Vector_t residual; + const Matrix_t jacobian_i; + const Matrix_t jacobian_j; + // ... + }; +``` + +The flow solvers use these conventions: + +- The normal (`Normal`) points from point i to point j. Its norm is the area of the face. The flux is the flux that leaves the control volume of i, integrated over the face. +- `jacobian_i` and `jacobian_j` are the derivatives of the flux with respect to the **conservative** variables of i and j. +- The primitive vector of the compressible solver is `(T, vx, vy, [vz,] P, rho, h, c, ...)`. `CEulerVariable::CIndices` gives the positions (`Pressure()`, `Density()`, `Enthalpy()`, ...). Older numerics use `V_i[nDim+1]` for the pressure, `V_i[nDim+2]` for the density and `V_i[nDim+3]` for the enthalpy. +- For a non-ideal fluid model, `S_i[0]` is dP/drho at constant e, and `S_i[1]` is dP/de at constant rho. See `CEulerVariable::SetSecondaryVar`. +- Each OpenMP thread has its own numerics object (`numerics[...][CONV_TERM + offset]`). So the class can keep work arrays as members. + +`CNumerics` also has older `ComputeResidual` overloads. They write into arrays passed by the caller, for example `ComputeResidual(su2double *val_residual, su2double **val_Jacobian_i, su2double **val_Jacobian_j, CConfig* config)`. The template classes in `SU2_CFD/include/numerics/template.hpp` still use them. The flow solvers call the `ResidualType<>` version, so use that one. + +`CNumerics` has helpers for the Euler flux and its eigensystem: `GetInviscidProjFlux`, `GetInviscidProjJac`, `GetPMatrix`, `GetPMatrix_inv`. `GetRoe_Dissipation` serves the low-dissipation Roe variants. + +### Example: the Roe family + +The Roe schemes in `SU2_CFD/include/numerics/flow/convection/roe.hpp` share an intermediate base class, [`CUpwRoeBase_Flow`](../../doxygen/classCUpwRoeBase__Flow.html). Its `ComputeResidual` works in two steps: + +1. It computes the central part of the flux. +2. It calls the pure virtual `FinalizeResidual`. Each child adds its own dissipation there: [`CUpwRoe_Flow`](../../doxygen/classCUpwRoe__Flow.html), `CUpwL2Roe_Flow`, `CUpwLMRoe_Flow`. + +The `AD::...Preacc` calls belong to the discrete adjoint. See [Discrete adjoint](#discrete-adjoint). + +Trimmed from `SU2_CFD/src/numerics/flow/convection/roe.cpp`: + +```cpp +CNumerics::ResidualType<> CUpwRoeBase_Flow::ComputeResidual(const CConfig* config) { + + implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + + unsigned short iVar, jVar, iDim; + su2double ProjGridVel = 0.0, Energy_i, Energy_j; + + AD::StartPreacc(); + AD::SetPreaccIn(V_i, nDim+4); AD::SetPreaccIn(V_j, nDim+4); AD::SetPreaccIn(Normal, nDim); + // ... + + /*--- Face area (norm of the normal vector) and unit normal ---*/ + + Area = GeometryToolbox::Norm(nDim, Normal); + + for (iDim = 0; iDim < nDim; iDim++) + UnitNormal[iDim] = Normal[iDim]/Area; + + /*--- Primitive variables at point i ---*/ + + for (iDim = 0; iDim < nDim; iDim++) + Velocity_i[iDim] = V_i[iDim+1]; + Pressure_i = V_i[nDim+1]; + Density_i = V_i[nDim+2]; + Enthalpy_i = V_i[nDim+3]; + Energy_i = Enthalpy_i - Pressure_i/Density_i; + + // ... + + /*--- Negative RoeSoundSpeed^2, the jump variables is too large, clear fluxes and exit. ---*/ + + if (RoeSoundSpeed2 <= 0.0) { + // ... + AD::SetPreaccOut(Flux, nVar); + AD::EndPreacc(); + + return ResidualType<>(Flux, Jacobian_i, Jacobian_j); + } + + // ... + + /*--- Apply Mavriplis' entropy correction to eigenvalues ---*/ + + su2double MaxLambda = fabs(ProjVelocity) + RoeSoundSpeed; + + for (iVar = 0; iVar < nVar; iVar++) + Lambda[iVar] = max(fabs(Lambda[iVar]), config->GetEntropyFix_Coeff()*MaxLambda); + + // ... + + /*--- Initialize residual (flux) and Jacobians ---*/ + + for (iVar = 0; iVar < nVar; iVar++) + Flux[iVar] = 0.5*(ProjFlux_i[iVar]+ProjFlux_j[iVar]); + + if (implicit) { + GetInviscidProjJac(Velocity_i, &Energy_i, Normal, 0.5, Jacobian_i); + GetInviscidProjJac(Velocity_j, &Energy_j, Normal, 0.5, Jacobian_j); + } + + /*--- Finalize in children class ---*/ + + FinalizeResidual(Flux, Jacobian_i, Jacobian_j, config); + + /*--- Correct for grid motion ---*/ + // ... + + AD::SetPreaccOut(Flux, nVar); + AD::SetPreaccOut(Jacobian_i, nVar, nVar); + AD::SetPreaccOut(Jacobian_j, nVar, nVar); + AD::EndPreacc(); + + return ResidualType<>(Flux, Jacobian_i, Jacobian_j); + +} +``` + +The AUSM+ and SLAU family in `ausm_slau.hpp` uses the same pattern. `CUpwAUSMPLUS_SLAU_Base_Flow::ComputeResidual` is `final`, and the children implement `ComputeMassAndPressureFluxes`. If your scheme belongs to one of these families, derive from the base class and implement only the part that is specific to your scheme. + +## Add a classic upwind scheme + +Follow these steps to add an upwind scheme that works with every fluid model and on the boundaries. + +1. Add the enum value and the keyword (see [above](#how-the-configuration-picks-the-scheme)). +2. Declare the class in a header in `SU2_CFD/include/numerics/flow/convection/`. Use an existing header or a new one. Derive the class from `CNumerics` or from a family base class. Override `ResidualType<> ComputeResidual(const CConfig* config)`. +3. Implement it in the matching `.cpp` in `SU2_CFD/src/numerics/flow/convection/`. If you add a new `.cpp` file, add it to the `numerics/...` list in `SU2_CFD/src/meson.build`. +4. Create the object in `SU2_CFD/src/drivers/CDriver.cpp`: + - Include the header. The convection headers are included at the top of the file. + - Add a `case` in the `switch (config->GetKind_Upwind_Flow())` of the compressible block of `CDriver::InitializeNumerics`. + - Create the object for every multigrid level, both as `conv_term` and as `conv_bound_term`. + + For example, the MSW case: + + ```cpp + case UPWIND::MSW: + for (iMGlevel = 0; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { + numerics[iMGlevel][FLOW_SOL][conv_term] = new CUpwMSW_Flow(nDim, nVar_Flow, config); + numerics[iMGlevel][FLOW_SOL][conv_bound_term] = new CUpwMSW_Flow(nDim, nVar_Flow, config); + } + break; + ``` + + `InitializeNumerics` runs this code once per OpenMP thread (`conv_term = CONV_TERM + offset`), so each thread gets its own objects. `CDriver::FinalizeNumerics` deletes them. + + If the scheme works only with an ideal gas, follow the `UPWIND::ROE` and `UPWIND::HLLC` cases. They test `ideal_gas`, and for other fluids they create `CUpwGeneralRoe_Flow` or `CUpwGeneralHLLC_Flow` instead. +5. The scheme may need data that `CEulerSolver::Upwind_Residual` does not set yet. In that case, add the `Set...` call there, in the edge loop, before `numerics->ComputeResidual(config)`. If it needs a new quantity per point (a sensor, for example), compute it in `CEulerSolver::CommonPreprocessing`, next to `SetCentered_Dissipation_Sensor` and `SetRoe_Dissipation`. +6. Optional: print the scheme name in `CConfig::SetOutput`, next to the other `Kind_Upwind_Flow` lines. Add checks in `CConfig::SetPostprocessing`. For example, non-ideal fluids accept only `ROE`, `HLLC` and `JST` (search for "Non-Ideal Compressible Fluids" in `CConfig.cpp`). + +Nothing else is needed. The solver gets the object from `numerics_container[CONV_TERM + omp_get_thread_num()*MAX_TERMS]`, calls it on every edge, and adds the result: + +```cpp + auto conv_residual = numerics->ComputeResidual(config); + // ... + if (ReducerStrategy) { + EdgeFluxes.SetBlock(iEdge, conv_residual); + } else { + LinSysRes.AddBlock(iPoint, conv_residual); + LinSysRes.SubtractBlock(jPoint, conv_residual); + } + + /*--- Viscous contribution, returns its Jacobians so that the matrix is updated once. ---*/ + + const auto visc_residual = Viscous_Residual( + iEdge, geometry, solver_container, numerics_container[VISC_TERM + omp_get_thread_num()*MAX_TERMS], config); + + if (implicit) UpdateJacobian(iEdge, iPoint, jPoint, conv_residual, visc_residual); +``` + +`UpdateJacobian` (`CFVMFlowSolverBase.hpp`) updates four blocks of the matrix, together with the viscous Jacobians: + +- `jacobian_i` is added to block (i,i) and subtracted from block (j,i); +- `jacobian_j` is added to block (i,j) and subtracted from block (j,j). + +## MUSCL reconstruction and limiters + +Your scheme does not reconstruct. The solver reconstructs the left and right states and passes them through `SetPrimitive` (and `SetSecondary`). Where this happens: + +- **Gradients and limiters.** `CEulerSolver::Preprocessing` computes them when `MUSCL_FLOW= YES`, on the finest grid. It calls `SetPrimitive_Gradient_GG` or `SetPrimitive_Gradient_LS` (option `NUM_METHOD_GRAD_RECON`), then `SetPrimitive_Limiter` (option `SLOPE_LIMITER_FLOW`). The limiter functions are in `SU2_CFD/include/limiters/` (`computeLimiters.hpp`, `CLimiterDetails.hpp`). +- **Reconstruction.** The edge loop of `CEulerSolver::Upwind_Residual` calls `CSolver::MUSCL_Reconstruction` (`SU2_CFD/include/solvers/CSolver.hpp`). It supports `MUSCL_KAPPA_FLOW` and the MUSCL ramp (`RAMP_MUSCL`). With `VAN_ALBADA_EDGE`, the limiter is computed per edge (`LimiterHelpers<>::vanAlbadaFunction`). +- **Non-ideal fluids and `LOW_MACH_CORR= YES`.** `CEulerSolver::ComputeConsistentExtrapolation` (and `LowMachPrimitiveCorrection`) makes the reconstructed states thermodynamically consistent. +- **Non-physical states.** If the reconstruction gives a negative pressure, density or Roe-averaged speed of sound, the edge falls back to first order (`UpdateNonPhysicalEdgeCounter`). + +In the vectorized path, `reconstructPrimitives` in `SU2_CFD/include/numerics_simd/flow/convection/common.hpp` does the same steps. `CUpwindBase::ComputeFlux` calls it. + +The incompressible solver (`CIncEulerSolver::Upwind_Residual`) and the NEMO solver (`CNEMOEulerSolver::Upwind_Residual`) do their own reconstruction in the same way. + +## Add a vectorized scheme + +The framework for vectorized (SIMD) schemes is in `SU2_CFD/include/numerics_simd/`: + +| File | Content | +|---|---| +| `CNumericsSIMD.hpp` | The interface: the pure virtual `ComputeFlux(Int iEdge, ...)` and the factory `CNumericsSIMD::CreateNumerics` | +| `CNumericsSIMD.cpp` | The factory functions `createUpwindIdealNumerics`, `createUpwindGeneralNumerics` (returns `nullptr`, no real-gas schemes yet), `createCenteredNumerics`, `createNumerics`. It is compiled through `SU2_CFD/src/meson.build`. | +| `flow/convection/upwind.hpp` | `CUpwindBase`, `CRoeScheme`, `CMSWScheme` | +| `flow/convection/centered.hpp` | `CCenteredBase`, `CJSTScheme`, `CJSTmatScheme`, `CJSTkeScheme`, `CLaxScheme` | +| `flow/convection/common.hpp` | `reconstructPrimitives`, `pMatrix`, `pMatrixInv`, `inviscidProjFlux`, `inviscidProjJac`, `roeDissipation`, ... | +| `flow/diffusion/viscous_fluxes.hpp` | The viscous "decorators" `CNoViscousFlux`, `CCompressibleViscousFlux`, `CGeneralCompressibleViscousFlux` | +| `flow/variables.hpp` | `CCompressiblePrimitives`, `CCompressibleConservatives`, `roeAveragedVariables` | + +The types `Double` and `Int` (`CNumericsSIMD.hpp`) are `simd::Array`s. Each operation on them acts on several edges at once. Helpers such as `gatherVariables`, `updateLinearSystem` and `stopPreacc` are in `SU2_CFD/include/numerics/util.hpp`. + +Each scheme is a class template. Its template parameter is a viscous decorator: a class that adds the viscous flux, or no flux for `CNoViscousFlux`. The work is split this way: + +- The base class (`CUpwindBase`) does the common work in `ComputeFlux`: geometry, reconstruction, conservative variables, viscous terms, and the update of the linear system. +- It then calls `finalizeFlux` of the derived class. The call is resolved at compile time through the template parameter, without virtual functions (static polymorphism). + +This is the start of `CRoeScheme`: + +```cpp +template +class CRoeScheme : public CUpwindBase, Decorator> { +private: + using Base = CUpwindBase, Decorator>; + // ... +public: + template + CRoeScheme(const CConfig& config, Ts&... args) : Base(config, args...), + kappa(config.GetRoe_Kappa()), + entropyFix(config.GetEntropyFix_Coeff()), + typeDissip(static_cast(config.GetKind_RoeLowDiss())) { + } + + template + FORCEINLINE void finalizeFlux(VectorDbl& flux, + MatrixDbl& jac_i, + MatrixDbl& jac_j, + const bool implicit, + const Double& area, + const VectorDbl& unitNormal, + const VectorDbl& normal, + const CPair& V, + const CPair& U, + const Int& iPoint, + const Int& jPoint, + const Double& nonPhysical, + const CEulerVariable& solution, + const CGeometry& geometry, + Ts&...) const { + // ... + } +}; +``` + +To add a vectorized upwind scheme for an ideal gas: + +1. In `upwind.hpp`, write `template class CMyScheme : public CUpwindBase, Decorator>`. Give it a constructor that forwards its arguments to the base, and a `finalizeFlux` with the signature above. Write all arithmetic with `Double`. Do not use `if` on `Double` values, because each one holds the values of several edges. Use `fmax`, `fmin`, `abs`, `sign` and masks instead, as `CMSWScheme` does. +2. Add a `case` in `createUpwindIdealNumerics` (`CNumericsSIMD.cpp`): + + ```cpp + switch (config.GetKind_Upwind_Flow()) { + case UPWIND::ROE: + obj = new CRoeScheme(config, iMesh, turbVars); + break; + case UPWIND::MSW: + obj = new CMSWScheme(config, iMesh, turbVars); + break; + default: + break; + } + ``` + +3. Add the scheme to the test at the start of `CEulerSolver::Upwind_Residual` (shown [above](#two-code-paths-classic-and-vectorized)). Otherwise the classic path is used. +4. Keep the classic version in `CDriver::InitializeNumerics` for `conv_bound_term`. + +How the solver calls it: + +1. `CFVMFlowSolverBase::EdgeFluxResidual` (`SU2_CFD/include/solvers/CFVMFlowSolverBase.inl`) creates the object on first use (`CEulerSolver::InstantiateEdgeNumerics`). +2. It loops over the edge colors in chunks of `Double::Size` edges, and calls `edgeNumerics->ComputeFlux(...)`. Edge colors are groups of edges that threads can process in parallel. +3. If the factory returns `nullptr`, the run stops with "The numerical scheme + gas model in use do not support vectorization.". + +With OpenMP, `EDGE_COLORING_GROUP_SIZE` must be a multiple of the SIMD length. The SIMD length depends on the build option `cpu-arch`. + +The decorator adds the viscous flux in the same edge loop. So a viscous run with a vectorized scheme does not use `CAvgGrad_Flow` for the interior edges. + +## Centered schemes + +All compressible centered schemes are vectorized, so a new one goes into the vectorized framework. `CEulerSolver::Centered_Residual` only calls `EdgeFluxResidual`. `CDriver::InitializeNumerics` does not create a `conv_term` for these schemes. It creates only `CUpwRoe_Flow` as `conv_bound_term`. + +To add a centered scheme: + +1. Add the value to `enum class CENTERED` and `Centered_Map`. +2. Write the class in `SU2_CFD/include/numerics_simd/flow/convection/centered.hpp`, derived from `CCenteredBase, Decorator>`. Follow `CJSTScheme` or `CLaxScheme`. +3. Add a `case` in `createCenteredNumerics` (`CNumericsSIMD.cpp`). On coarse grids (`iMesh != MESH_0`) the factory always uses `CENTERED::LAX`. The `switch` lists every `CENTERED` value and has no `default`. So add the new value there even if it is not vectorized. +4. The scheme may need the pressure sensor, the undivided Laplacian or the spectral radius. In that case, add it to the booleans `center_jst`, `center_jst_ke`, `center_jst_mat` in `CEulerSolver::CommonPreprocessing`. These booleans trigger the calls to `SetCentered_Dissipation_Sensor`, `SetUndivided_Laplacian` and `SetMax_Eigenvalue`. + +The existing centered schemes use these options: `JST_SENSOR_COEFF`, `LAX_SENSOR_COEFF`, `CENTRAL_JACOBIAN_FIX_FACTOR`. + +`CENTERED::LD2` and `CENTERED::CDS` exist only for the incompressible solver. For the compressible solver, they make the factory return `nullptr`. + +## Jacobians + +With `TIME_DISCRE_FLOW= EULER_IMPLICIT`, the solver needs the Jacobians of the flux. They go into the block matrix `Jacobian` (`CSysMatrix`). `ImplicitEuler_Iteration` solves this matrix together with the time-step term. + +- The Jacobians do not need to be exact. Most schemes use an approximation: + - the Roe schemes use `GetInviscidProjJac` and the frozen dissipation matrix; + - the AUSM+ and SLAU family uses the Roe Jacobian (`ApproximateJacobian`). With `USE_ACCURATE_FLUX_JACOBIANS= YES` it uses `AccurateJacobian` instead, which mixes finite differences and manual differentiation. + + A more accurate Jacobian usually lets you use a higher CFL number. +- The matrix stores passive values (`PassiveAssign` in `CSysMatrix::UpdateBlocks`). Passive values are plain numbers that automatic differentiation (AD) does not track. So AD does not differentiate the Jacobians. +- Fill the Jacobians only when `implicit` is true. This saves time in explicit runs. +- With `NEWTON_KRYLOV= YES`, `CNewtonIntegration` uses matrix-free products with the true Jacobian, computed by finite differences of the residual. It uses this matrix only to build the preconditioner. Expect less accurate finite differences if your flux is not smooth. + +## Low-Mach and entropy fix options + +These options change the convective fluxes. All are in `Common/src/CConfig.cpp`. + +| Option | Getter | Used by | +|---|---|---| +| `ENTROPY_FIX_COEFF` | `GetEntropyFix_Coeff()` | Roe schemes (Mavriplis' correction of the eigenvalues) | +| `ROE_KAPPA` | `GetRoe_Kappa()` | Roe schemes (weight of the dissipation) | +| `ROE_LOW_DISSIPATION` | `GetKind_RoeLowDiss()` | `CUpwRoe_Flow`, `CUpwSLAU_Flow`, `CUpwSLAU2_Flow`, `CRoeScheme`; values in `RoeLowDiss_Map` | +| `LOW_MACH_CORR` | `Low_Mach_Correction()` | Correction of the reconstructed velocity in `CEulerSolver::LowMachPrimitiveCorrection`; not supported by the vectorized path | +| `LOW_MACH_PREC` | `Low_Mach_Preconditioning()` | Low-Mach preconditioning of the implicit time term in `CEulerSolver::PrepareImplicitIteration` (always on with `TURKEL_PREC`) | +| `MSW_ALPHA` | `GetMSW_Alpha()` | MSW scheme | +| `USE_ACCURATE_FLUX_JACOBIANS` | `GetUse_Accurate_Jacobians()` | AUSM+-up, AUSM+-up2, SLAU, SLAU2 | + +How `ROE_LOW_DISSIPATION` works: + +1. The solver computes the blending in `CEulerSolver::CommonPreprocessing` (`SetRoe_Dissipation`, `SetUpwind_Ducros_Sensor`). +2. It passes the result to the numerics with `SetDissipation` and `SetSensor`. +3. The numerics combine them with `CNumerics::GetRoe_Dissipation`. + +A new scheme that supports this option takes a `bool val_low_dissipation` in its constructor, as `CUpwRoe_Flow` does. `CDriver` passes `roe_low_dissipation` for `conv_term` and `false` for `conv_bound_term`. + +## Incompressible and NEMO schemes + +The other finite volume solvers have their own scheme classes. + +**Incompressible, density based** (`CIncEulerSolver`): + +- Schemes: upwind `FDS` (`CUpwFDSInc_Flow`, `SU2_CFD/include/numerics/flow/convection/fds.hpp`); centered `JST`/`LD2` (`CCentJSTInc_Flow`) and `LAX-FRIEDRICH` (`CCentLaxInc_Flow`) in `centered.hpp`. +- They are created in the `incompressible` part of the convective `switch` in `CDriver::InitializeNumerics`. +- `CIncEulerSolver::Upwind_Residual` and `CIncEulerSolver::Centered_Residual` call them. +- There is no vectorized path. + +**Incompressible, pressure based** (`INCOMP_SYSTEM::PRESSURE_BASED`): `UDS` (`CPBConvection_Upwind`) and `CDS` (`CPBConvection_Central`) in `pressure_based.hpp`. + +**NEMO** (`CNEMOEulerSolver`): + +- The schemes derive from [`CNEMONumerics`](../../doxygen/classCNEMONumerics.html) and are in `SU2_CFD/include/numerics/NEMO/convection/`: `CUpwRoe_NEMO`, `CUpwAUSM_NEMO`, `CUpwAUSMPLUSUP2_NEMO`, `CUpwAUSMPLUSM_NEMO`, `CUpwMSW_NEMO`, `CCentLax_NEMO`. +- They are created in the `NEMO_euler || NEMO_ns` block of `CDriver::InitializeNumerics`. +- Their constructors also take `nPrimVar` and `nPrimVarGrad`. + +**FEM (DG)**: `RIEMANN_SOLVER_FEM` chooses the Riemann solver. `ROE` and `LAX-FRIEDRICH` are hard coded in `CFEM_DG_EulerSolver`. The others reuse the classic finite volume classes. + +## Viscous fluxes and source terms + +**Viscous flux.** + +- The classic classes are in `SU2_CFD/include/numerics/flow/flow_diffusion.hpp`: `CAvgGrad_Base`, `CAvgGrad_Flow` (ideal gas), `CGeneralAvgGrad_Flow` (other fluid models), `CAvgGradInc_Flow`. +- They are created as `visc_term` and `visc_bound_term`, after the convective `switch` in `CDriver::InitializeNumerics`. +- The flow solvers call them per edge, from the convective loop, through `Viscous_Residual(iEdge, ...)` (`CFVMFlowSolverBase::Viscous_Residual_impl`). The residual is subtracted from i and added to j. +- In the compressible solver, the vectorized path uses the decorators in `numerics_simd/flow/diffusion/viscous_fluxes.hpp` instead. So make any change to the viscous flux in both places. + +**Source term.** The flow source terms are in `SU2_CFD/include/numerics/flow/flow_sources.hpp`. They derive from `CSourceBase_Flow`, which allocates `residual` and `jacobian`. To add one: + +1. Write the class with `ResidualType<> ComputeResidual(const CConfig* config) override`, returning `ResidualType<>(residual, jacobian, nullptr)`. The residual is minus the source integrated over the control volume (see `CSourceBodyForce::ComputeResidual`). +2. Create it in `CDriver::InitializeNumerics`, in the `if`/`else if` chain for `source_first_term` or `source_second_term`. Each solver has only these two source slots. `CSourceNothing` (an alias of `CNumerics`) fills an unused slot. +3. Call it in the `Source_Residual` of the solver (`CEulerSolver::Source_Residual`, `CIncEulerSolver::Source_Residual`): + - loop over the points; + - set the inputs (`SetConservative`, `SetPrimitive`, `SetVolume`, `SetCoord`, ...); + - call `ComputeResidual`; + - call `LinSysRes.AddBlock(iPoint, residual)` and, if implicit, `Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i)`. + +## Discrete adjoint + +A new scheme does not need adjoint code, but you must write it so that AD works. The discrete adjoint drivers record one primal iteration, with all its flux evaluations, on the AD tape. The tape is the record of every operation. Evaluating the tape gives the adjoint. `CDiscAdjSolver` holds the adjoint variables. + +Rules for the scheme: + +- Use `su2double` for every value that depends on the solution or on the mesh. `passivedouble` and `SU2_TYPE::GetValue` cut the derivative. +- Use preaccumulation correctly. Preaccumulation condenses the operations of one function into a small Jacobian from its inputs to its outputs, so the tape stays smaller. + - Start with `AD::StartPreacc()`. + - Declare **all** inputs with `AD::SetPreaccIn`: primitives, normal, grid velocity, sensors, secondary variables, ... + - End **every** return path with `AD::SetPreaccOut` and `AD::EndPreacc()`, as in the early return of `CUpwRoeBase_Flow::ComputeResidual` above. + + A missing input gives wrong gradients without any error. In the vectorized framework, `gatherVariables` registers the inputs and `stopPreacc` ends the section. +- Branches on solution values (`max`, `fabs`, `if`) are recorded as taken at the converged state. The derivative is the one of the active branch. Avoid branches whose outcome changes between iterations at convergence. +- A tape debug build helps to find preaccumulation errors. Build with `-Dcodi-tape=Tag` and use the options `CHECK_TAPE_TYPE` and `CHECK_TAPE_VARIABLES`. See [Advanced AD Techniques](../Advanced-AD-Techniques/). + +## Tests + +Add these tests for a new scheme: + +- **Regression test.** Add a short case to `TestCases/parallel_regression.py` and `TestCases/serial_regression.py`. Existing scheme tests you can copy: `wedge` (`euler/wedge/inv_wedge_HLLC.cfg`) and `ramp_msw` (`euler/ramp/inv_ramp_msw.cfg`). If the scheme is meant for the discrete adjoint, also add a case to `parallel_regression_AD.py` / `serial_regression_AD.py` (for example, the `disc_adj_euler` cases). See [Running Regression Tests](../Running-Regression-Tests/). +- **Order of accuracy.** Use the method of manufactured solutions (MMS) cases in `TestCases/mms/`. The `mms_fvm_*` tests in `parallel_regression.py` use them: + - `mms/fvm_euler/inv_mms_vortex.cfg` (`ROE`, `KIND_VERIFICATION_SOLUTION= INVISCID_VORTEX`) + - `mms/fvm_navierstokes/lam_mms_roe.cfg` (`ROE`, `MMS_NS_UNIT_QUAD`) + - `mms/fvm_incomp_euler/inv_mms_jst.cfg` (`JST`, `MMS_INC_EULER`) + - `mms/fvm_incomp_navierstokes/lam_mms_fds.cfg` (`FDS`, `MMS_INC_NS`) + + Change `CONV_NUM_METHOD_FLOW` and run them on a sequence of meshes. The exact solutions are in `Common/include/toolboxes/MMS/`. `CFVMFlowSolverBase::ComputeVerificationError` computes the error norms. +- **Unit test.** You can test numerics alone with Catch2. See `UnitTests/SU2_CFD/numerics/CNumerics_tests.cpp` and [Writing Unit Tests](../Writing-Unit-Tests/). +- **OpenMP.** If you changed the solver loops, run at least one case with OpenMP threads (`hybrid_regression.py`). + +## Checklist + +- [ ] Enum value and keyword in `option_structure.hpp`, keyword in only one of `Centered_Map` / `Upwind_Map`. +- [ ] Keyword listed in `config_template.cfg`. +- [ ] Classic class with `ResidualType<> ComputeResidual(const CConfig*)`, flux and Jacobians w.r.t. conservative variables. +- [ ] New `.cpp` in `SU2_CFD/src/meson.build`. +- [ ] `conv_term` and `conv_bound_term` created for every multigrid level in `CDriver::InitializeNumerics`. +- [ ] Vectorized version (optional): class in `numerics_simd`, factory `case`, test in `CEulerSolver::Upwind_Residual`. +- [ ] AD: all inputs in preaccumulation, every return path closes it. +- [ ] Regression test, and MMS runs for the order of accuracy. + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Adding-a-Solver.md b/Docs/website/pages/Adding-a-Solver.md new file mode 100644 index 000000000000..f5da0350c44b --- /dev/null +++ b/Docs/website/pages/Adding-a-Solver.md @@ -0,0 +1,719 @@ +--- +title: Adding a Solver +permalink: /docs_v7/Adding-a-Solver/ +--- + +This page shows how to add a new solver to `SU2_CFD`. It covers three cases: + +- a new set of equations with its own `SOLVER=` keyword; +- a new equation that runs next to the flow solver, like the species or turbulence solvers; +- a new `CSolver` class for physics that already exist. + +It follows the code path from the configuration file to the restart file. Read [Code Structure](../Code-Structure/) first; this page does not repeat the class overview. + +## Files you will touch + +| File | What goes there | +|---|---| +| `Common/include/option_structure.hpp` | `enum class MAIN_SOLVER` and `Solver_Map`, the position in the solver container (`enum SOLVER_TYPE`, `MAX_SOLS`), the equation system (`enum RUNTIME_TYPE`), `enum class MPI_QUANTITIES` if you need new communications | +| `Common/include/CConfig.hpp`, `Common/src/CConfig.cpp` | new options, `CConfig::GetContainerPosition`, `CConfig::SetGlobalParam`, the discrete adjoint mapping in `CConfig::SetPostprocessing`, problem helpers such as `GetHeatProblem` | +| `SU2_CFD/include/solvers/CSolverFactory.hpp`, `SU2_CFD/src/solvers/CSolverFactory.cpp` | `enum class SUB_SOLVER_TYPE`, the solver creation and its `INTEGRATION_TYPE` | +| `SU2_CFD/include/solvers/CMySolver.hpp`, `SU2_CFD/src/solvers/CMySolver.cpp` | the new `CSolver` child | +| `SU2_CFD/include/variables/CMyVariable.hpp`, `SU2_CFD/src/variables/CMyVariable.cpp` | the new `CVariable` child | +| `SU2_CFD/src/drivers/CDriver.cpp` | numerics objects in `CDriver::InitializeNumerics` (if the solver uses `CNumerics`) | +| `SU2_CFD/src/iteration/CIterationFactory.cpp` and an iteration class | which `CIteration` runs the solver | +| `SU2_CFD/src/drivers/CSinglezoneDriver.cpp`, `SU2_CFD/src/drivers/CMultizoneDriver.cpp` | the call to `SetInitialCondition` | +| `SU2_CFD/src/output/COutputFactory.cpp` and an output class | screen, history and volume output | +| `SU2_CFD/src/meson.build` | the new `.cpp` files | +| `config_template.cfg`, `TestCases/` | documentation of new options, a regression test | + +--- + +- [Choose the kind of solver and a template](#choose-the-kind-of-solver-and-a-template) +- [1. The SOLVER keyword](#1-the-solver-keyword) +- [2. Position in the solver container and equation system](#2-position-in-the-solver-container-and-equation-system) +- [3. Create the solver in CSolverFactory](#3-create-the-solver-in-csolverfactory) +- [4. The CVariable class](#4-the-cvariable-class) +- [5. The CSolver class](#5-the-csolver-class) +- [6. Numerics](#6-numerics) +- [7. Integration, iteration and initial condition](#7-integration-iteration-and-initial-condition) +- [8. Output](#8-output) +- [9. Restart files](#9-restart-files) +- [10. MPI communication](#10-mpi-communication) +- [11. OpenMP](#11-openmp) +- [12. Discrete adjoint](#12-discrete-adjoint) +- [13. Build and test](#13-build-and-test) +- [Non-conservative systems of equations](#non-conservative-systems-of-equations) +- [Checklist](#checklist) + +--- + +## Choose the kind of solver and a template + +First find which of three cases you are in. Each case needs a different subset of the steps below. + +| Case | Example in the code | Steps | +|---|---|---| +| New main solver with its own `SOLVER=` keyword | `HEAT_EQUATION` (`CHeatSolver`) | all | +| New equation that runs after the flow solver in the same zone | species transport (`CSpeciesSolver`, option `KIND_SCALAR_MODEL`) | 2 to 13, with a model option instead of a new keyword | +| New class for existing physics | a turbulence model chosen by `KIND_TURB_MODEL` in `CSolverFactory::CreateTurbSolver` | 3 to 6, 8, 13 | + +Then pick a class to copy: + +- **`CTemplateSolver`** (`SU2_CFD/include/solvers/CTemplateSolver.hpp`) lists the main virtual functions with empty bodies. It is a list, not a working solver. Its constructor does not create `nodes`. `SOLVER= TEMPLATE_SOLVER` stops during set-up, at the latest in `CIterationFactory::CreateIteration` ("No iteration found for specified solver."). Use it only to see which functions exist. +- **[`CHeatSolver`](../../doxygen/classCHeatSolver.html)** is the smallest complete main solver. It has its own keyword (`HEAT_EQUATION`), iteration (`CHeatIteration`), output (`CHeatOutput`), restart and discrete adjoint (`DISC_ADJ_HEAT`). It also runs as a sub-solver of the incompressible solver (`WEAKLY_COUPLED_HEAT_EQUATION= YES`). Copy it for a new set of scalar equations. +- **[`CSpeciesSolver`](../../doxygen/classCSpeciesSolver.html)** is the template for an equation that reads the flow solution. +- Both derive from [`CScalarSolver`](../../doxygen/classCScalarSolver.html)``. This base class already implements the implicit and explicit updates, dual time stepping, periodic boundaries, OpenMP and the scalar edge loop. Derive from it if your equations are transport equations for scalars. Derive from `CSolver` directly only for something different. Examples are `CRadSolver` and `CRadP1Solver`, or the flow solvers through [`CFVMFlowSolverBase`](../../doxygen/classCFVMFlowSolverBase.html). + +## 1. The SOLVER keyword + +Add a keyword so that users can select your solver with `SOLVER=`. The option `SOLVER` is read into `CConfig::Kind_Solver`: + +```cpp + addEnumOption("SOLVER", Kind_Solver, Solver_Map, MAIN_SOLVER::NONE); +``` + +Add a value to `enum class MAIN_SOLVER` and a keyword to `Solver_Map` in `Common/include/option_structure.hpp`: + +```cpp +enum class MAIN_SOLVER { + NONE, /*!< \brief Definition of no solver. */ + EULER, /*!< \brief Definition of the Euler's solver. */ + // ... + HEAT_EQUATION, /*!< \brief Definition of the finite volume heat solver. */ + // ... + TEMPLATE_SOLVER, /*!< \brief Definition of template solver. */ + // ... + DISC_ADJ_HEAT, /*!< \brief Definition of the discrete adjoint heat solver. */ + // ... +}; +static const MapType Solver_Map = { + // ... + MakePair("HEAT_EQUATION", MAIN_SOLVER::HEAT_EQUATION) + MakePair("ELASTICITY", MAIN_SOLVER::FEM_ELASTICITY) + MakePair("TEMPLATE_SOLVER", MAIN_SOLVER::TEMPLATE_SOLVER) + // ... +}; +``` + +**Discrete adjoint.** The `DISC_ADJ_*` and `ADJ_*` values are not in the map. Users write the primal keyword and `MATH_PROBLEM= DISCRETE_ADJOINT`. `CConfig::SetPostprocessing` then replaces the value at its end: + +```cpp + /*--- Note that this is deliberately done at the end of this routine! ---*/ + switch(Kind_Solver) { + // ... + case MAIN_SOLVER::HEAT_EQUATION: + Kind_Solver = MAIN_SOLVER::DISC_ADJ_HEAT; + break; + default: + break; + } +``` + +If your solver gets a discrete adjoint, add a `DISC_ADJ_` value and a case here. + +**Problem helpers.** Many places ask which kind of problem runs. They use helpers in `Common/include/CConfig.hpp`: `GetFluidProblem()`, `GetStructuralProblem()`, `GetHeatProblem()`, `GetFEMSolver()`. Search for them and decide whether the new value belongs in one of them. + +**Sub-models.** A new sub-model of an existing solver does not get a `SOLVER=` value. It gets its own enum option instead, for example: + +- `KIND_TURB_MODEL` (`enum class TURB_MODEL`, `Turb_Model_Map`, grouped by `TurbModelFamily()`); +- `KIND_TRANS_MODEL` (`enum class TURB_TRANS_MODEL`); +- `KIND_SCALAR_MODEL` (`enum class SPECIES_MODEL`). + +See [Adding a Config Option](../Adding-a-Config-Option/) for options and enums. + +## 2. Position in the solver container and equation system + +Give the solver a slot in the solver container and an equation system value. + +The solver container is the array that holds the solvers of a zone. There is one array per multigrid level: `solver_container[iZone][iInst][iMesh][iSol]`. The positions `iSol` are in `enum SOLVER_TYPE` in `Common/include/option_structure.hpp`: + +```cpp + enum SOLVER_TYPE : const int { + FLOW_SOL=0, /*!< \brief Position of the mean flow solution in the solver container array. */ + ADJFLOW_SOL=1, /*!< \brief Position of the continuous adjoint flow solution in the solver container array. */ + TURB_SOL=2, /*!< \brief Position of the turbulence model solution in the solver container array. */ + // ... + SPECIES_SOL=11, /*!< \brief Position of the species solver. */ + ADJSPECIES_SOL=12, /*!< \brief Position of the adjoint of the species solver. */ + FEA_SOL=0, /*!< \brief Position of the Finite Element flow solution in the solver container array. */ + ADJFEA_SOL=1, /*!< \brief Position of the continuous adjoint Finite Element flow solution in the solver container array. */ + POISSON_SOL=13, /*!< \brief Position of the poisson solution in the solver container array */ + TEMPLATE_SOL=0, /*!< \brief Position of the template solution. */ + }; +``` + +The array size is `MAX_SOLS`: + +```cpp +enum: unsigned int{MAX_SOLS = 14}; /*!< \brief Maximum number of solutions at the same time (dimension of solution container array). */ +``` + +Positions 0 to 13 are taken. You have two options: + +- Reuse a slot, if your solver is never created together with the solver that holds it. `FEA_SOL` and `TEMPLATE_SOL` reuse 0. +- Otherwise, add a new position (and one for its adjoint) and increase `MAX_SOLS`. + +The drivers, integrations and outputs loop over `MAX_SOLS` and skip null entries. + +The solver also needs an **equation system** value in `enum RUNTIME_TYPE`, for example `RUNTIME_HEAT_SYS = 21`. The integrations receive this value. They turn it into the container position with `CConfig::GetContainerPosition` (`Common/src/CConfig.cpp`): + +```cpp +unsigned short CConfig::GetContainerPosition(unsigned short val_eqsystem) { + + switch (val_eqsystem) { + case RUNTIME_FLOW_SYS: return FLOW_SOL; + // ... + case RUNTIME_HEAT_SYS: return HEAT_SOL; + // ... + } + return 0; +} +``` + +An unknown value returns 0, the flow solver, without an error. Add your case. + +Then set the schemes of your system. `CIntegration::Space_Integration` and `CIntegration::Time_Integration` read the convective scheme (`GetKind_ConvNumScheme()`) and the time scheme (`GetKind_TimeIntScheme()`) of the *current* system. `CConfig::SetGlobalParam(MAIN_SOLVER, RUNTIME_...)` sets them; the iteration and the solver's `Preprocessing` call it before each system. Add a case there for your solver and system: + +```cpp + case MAIN_SOLVER::HEAT_EQUATION: + case MAIN_SOLVER::DISC_ADJ_HEAT: + if (val_system == RUNTIME_HEAT_SYS) { + SetKind_ConvNumScheme(NONE, CENTERED::NONE, UPWIND::NONE, LIMITER::NONE, NONE, 0.0, NONE); + SetKind_TimeIntScheme(Kind_TimeIntScheme_Heat); + } + break; +``` + +With `NONE` as convective scheme, `Space_Integration` calls neither `Centered_Residual` nor `Upwind_Residual`. The heat solver does all its work in `Viscous_Residual`. + +A sub-solver of the flow solver needs a case in the branch of each flow `MAIN_SOLVER` value it runs with. See the `SetSpeciesParam` and `SetHeatParam` lambdas in `SetGlobalParam`. + +## 3. Create the solver in CSolverFactory + +Make `CSolverFactory` create your solver and choose its integration class. `CDriver::InitializeSolver` calls `CSolverFactory::CreateSolverContainer` once per multigrid level, so each level has its own solver object: + +```cpp + for (iMesh = 0; iMesh <= config->GetnMGLevels(); iMesh++){ + solver[iMesh] = CSolverFactory::CreateSolverContainer(kindSolver, config, geometry[iMesh], iMesh); + } +``` + +In `SU2_CFD/include/solvers/CSolverFactory.hpp`, add a value to `enum class SUB_SOLVER_TYPE`. Then, in `SU2_CFD/src/solvers/CSolverFactory.cpp`: + +1. In `CSolverFactory::CreateSolverContainer`, add a case for the `MAIN_SOLVER` value. The case fills the positions of your solvers: + + ```cpp + case MAIN_SOLVER::HEAT_EQUATION: + solver[HEAT_SOL] = CreateSubSolver(SUB_SOLVER_TYPE::HEAT, solver, geometry, config, iMGLevel); + break; + // ... + case MAIN_SOLVER::DISC_ADJ_HEAT: + solver[HEAT_SOL] = CreateSubSolver(SUB_SOLVER_TYPE::HEAT, solver, geometry, config, iMGLevel); + solver[ADJHEAT_SOL] = CreateSubSolver(SUB_SOLVER_TYPE::DISC_ADJ_HEAT, solver, geometry, config, iMGLevel); + break; + ``` + + For a sub-solver, add the line to the case of each flow solver it can run with (look at `SUB_SOLVER_TYPE::SPECIES`). After the switch, the mesh solvers are always added (`MESH_SOL`, `ADJMESH_SOL`). + +2. In `CSolverFactory::CreateSubSolver`, create the object and set its `INTEGRATION_TYPE`: + + ```cpp + case SUB_SOLVER_TYPE::HEAT: + genericSolver = CreateHeatSolver(solver, geometry, config, iMGLevel, false); + metaData.integrationType = INTEGRATION_TYPE::SINGLEGRID; + break; + case SUB_SOLVER_TYPE::DISC_ADJ_HEAT: + genericSolver = CreateHeatSolver(solver, geometry, config, iMGLevel, true); + metaData.integrationType = INTEGRATION_TYPE::DEFAULT; + break; + ``` + + The sub-solver may return `nullptr`, for example when the model is off. Only non-null solvers are stored in the metadata map, and only they get an integration. + +3. Add the new `#include` at the top of `SU2_CFD/src/solvers/CSolverFactory.cpp`. + +`INTEGRATION_TYPE` (same header) picks the integration class in `CIntegrationFactory::CreateIntegration`: + +| `INTEGRATION_TYPE` | Class | Used by | +|---|---|---| +| `MULTIGRID` | `CMultiGridIntegration` | finite volume flow solvers, continuous adjoint flow | +| `NEWTON` | `CNewtonIntegration` | flow solvers with `NEWTON_KRYLOV= YES` (primal only) | +| `SINGLEGRID` | `CSingleGridIntegration` | turbulence, transition, species, heat, radiation, Poisson, template | +| `STRUCTURAL` | `CStructuralIntegration` | `CFEASolver` | +| `FEM_DG` | `CFEM_DG_Integration` | DG flow solvers | +| `DEFAULT` | `CIntegration` | discrete adjoint solvers, baseline solvers | +| `NONE` | none | mesh solver | + +## 4. The CVariable class + +Write a `CVariable` child to hold the per-point data of your solver. [`CVariable`](../../doxygen/classCVariable.html) stores the data of all points of one solver (see [Code Structure](../Code-Structure/#variables)). + +For scalar equations, derive from [`CScalarVariable`](../../doxygen/classCScalarVariable.html). It allocates gradients, limiters, the local time step, the under-relaxation and the local CFL. `CHeatVariable` is the shortest example: + +```cpp +class CHeatVariable final : public CScalarVariable { +public: + static constexpr size_t MAXNVAR = 1; /*!< \brief Max number of variables, for static arrays. */ + // ... + CHeatVariable(su2double heat, unsigned long npoint, unsigned long ndim, unsigned long nvar, CConfig *config); + // ... + inline su2double GetTemperature(unsigned long iPoint) const final { return Solution(iPoint, 0); } +}; +``` + +`CScalarSolver` needs `MAXNVAR`: it sizes its static arrays with `VariableType::MAXNVAR`. + +The constructor (`SU2_CFD/src/variables/CHeatVariable.cpp`) sets `Solution` and `Solution_Old`. It allocates only what the solver uses: + +- `Res_TruncError`; +- `Solution_time_n` and `Solution_time_n1`, for dual time stepping; +- `Max_Lambda_Visc`; +- `Set_BGSSolution_k()`, for multizone problems. + +Store extra per-point quantities as members of the variable class (`su2activevector`, `su2activematrix`). Give them getters and setters that take the point index. + +## 5. The CSolver class + +The solver class holds the residual loops, the solution update and the restart. + +### Constructor + +Follow the order of `CHeatSolver::CHeatSolver` (`SU2_CFD/src/solvers/CHeatSolver.cpp`): + +1. Call the base constructor. For `CScalarSolver` it is `CScalarSolver(geometry, config, flow_solver, conservative, bounded_scalar)`. `flow_solver` may be `nullptr` when there is no flow. +2. Set `nVar`, `nPrimVar`, `nPoint`, `nPointDomain`, `nVarGrad`, and size `Residual_RMS`, `Residual_Max`, `Point_Max`, `Point_Max_Coord`. +3. Initialize the linear system. `ReducerStrategy` is one of the two OpenMP strategies for edge loops (see [step 11](#11-openmp)); it needs per-edge flux storage: + + ```cpp + Jacobian.Initialize(nPoint, nPointDomain, nVar, nVar, true, geometry, config, ReducerStrategy); + LinSysSol.Initialize(nPoint, nPointDomain, nVar, 0.0); + LinSysRes.Initialize(nPoint, nPointDomain, nVar, 0.0); + if (ReducerStrategy) { + EdgeFluxes.Initialize(geometry->GetnEdge(), geometry->GetnEdge(), nVar, nullptr); + EdgeFluxesDiff.Initialize(geometry->GetnEdge(), geometry->GetnEdge(), nVar, nullptr); + } + ``` + +4. Create the variables and set the base class pointer. `GetBaseClassPointerToNodes()` is the only pure virtual function of `CSolver`, and `CScalarSolver` implements it. `CSolver::GetNodes()` asserts that the pointer was set. + + ```cpp + nodes = new CHeatVariable(Solution_Inf[0], nPoint, nDim, nVar, config); + + SetBaseClassPointerToNodes(); + ``` + +5. Set up the periodic and MPI communication: + - `InitiatePeriodicComms` / `CompletePeriodicComms` with `PERIODIC_VOLUME` and `PERIODIC_NEIGHBORS`; + - `SetImplicitPeriodic(...)`; + - `InitiateComms` / `CompleteComms` with `MPI_QUANTITIES::SOLUTION`. +6. Set the local CFL of every point, and `SolverName` (used in the output). + +### Virtual functions + +The virtual functions are declared in `SU2_CFD/include/solvers/CSolver.hpp` with an empty default body, except `GetBaseClassPointerToNodes`. The integration calls them in the order of this table (see `CSingleGridIntegration::SingleGrid_Iteration` and `CIntegration::Space_Integration`): + +| Function | Called by | What it does | In `CScalarSolver`? | +|---|---|---|---| +| `Preprocessing(geometry, solver_container, config, iMesh, iRKStep, RunTime_EqSystem, Output)` | integration, restart, adjoint recording | call `config->SetGlobalParam`, compute gradients and limiters, clear residual and Jacobian (`CScalarSolver::CommonPreprocessing`) | no, but the helper `CommonPreprocessing` does most of it | +| `Set_OldSolution()` | `SingleGrid_Iteration` | copy `Solution` to `Solution_Old` | yes (`CSolver`) | +| `SetTime_Step(geometry, solver_container, config, iMesh, Iteration)` | integration | local time step | copies the flow time step; override for a standalone solver (`CHeatSolver` does) | +| `Centered_Residual` / `Upwind_Residual` | `Space_Integration`, by `GetKind_ConvNumScheme()` | convective edge loop | no, implement it | +| `Viscous_Residual(geometry, solver_container, numerics_container, config, iMesh, iRKStep)` | `Space_Integration`, always | diffusive edge loop | no | +| `Source_Residual(geometry, solver_container, numerics_container, config, iMesh)` | `Space_Integration`, always | point source terms | no | +| `SetResidual_DualTime(...)` | `Space_Integration`, dual time only | time derivative terms | yes | +| `BC_...` | `Space_Integration` | boundary conditions, see [Adding a Boundary Condition](../Adding-a-Boundary-Condition/) | some | +| `ImplicitEuler_Iteration` / `ExplicitEuler_Iteration` / `ExplicitRK_Iteration` / `ClassicalRK4_Iteration` | `Time_Integration`, by `GetKind_TimeIntScheme()` | update the solution | implicit and explicit Euler | +| `Postprocessing(geometry, solver_container, config, iMesh)` | integration | quantities derived from the new solution | no | +| `SetInitialCondition(geometry, solver_container, config, TimeIter)` | driver, each time step | initial or analytic condition | no | +| `LoadRestart(geometry, solver, config, val_iter, val_update_geo)` | `CDriver::RestartSolver` | read the restart file | pure virtual in `CScalarSolver` | +| `GetHasHybridParallel()` | integration, restart | whether the solver supports OpenMP | `true` | + +`SingleGrid_Iteration` also contains code for specific systems. Examples are the heat fluxes for `RUNTIME_HEAT_SYS`, and the restriction of the turbulence solution to the coarse levels for `RUNTIME_TURB_SYS`. Check it if your solver needs something similar. + +### Implicit iteration + +A solver derived from `CSolver` directly must implement `ImplicitEuler_Iteration`. The function must: + +1. add the time term to the diagonal of `Jacobian`; +2. copy `-LinSysRes` into the right-hand side; +3. call `System.Solve(Jacobian, LinSysRes, LinSysSol, geometry, config)` (see the existing implementations); +4. update the solution; +5. communicate the solution. + +`CScalarSolver` splits this into `PrepareImplicitIteration` and `CompleteImplicitIteration` (both `final`). Use them as the reference. + +Check `config->GetKind_TimeIntScheme() == EULER_IMPLICIT` inside each function, not once in the constructor. The reason: `CNewtonIntegration::ComputeResiduals` temporarily switches the scheme to `EULER_EXPLICIT`, to evaluate residuals without the Jacobian. + +## 6. Numerics + +The numerics compute the fluxes and source terms. There is no numerics factory. The code uses two styles: + +- `CNumerics` objects, for the flow solvers, source terms and radiation; +- scalar edge kernels, for all `CScalarSolver` children. + +**CNumerics objects.** `CDriver::InitializeNumerics` (`SU2_CFD/src/drivers/CDriver.cpp`) creates them, one per thread and per multigrid level. It stores them in `numerics[iMGlevel][iSol][iTerm + offset]`, with `offset = thread * MAX_TERMS`. The term indices are `CONV_TERM`, `VISC_TERM`, `SOURCE_FIRST_TERM`, `SOURCE_SECOND_TERM`, `CONV_BOUND_TERM`, `VISC_BOUND_TERM` and `GRAD_TERM`. The radiation solver is a short example: + +```cpp + if (config->AddRadiation()) { + /*--- Definition of the viscous scheme for each equation and mesh level ---*/ + numerics[MESH_0][RAD_SOL][VISC_TERM] = new CAvgGradCorrected_P1(nDim, nVar_Rad, config); + + /*--- Definition of the source term integration scheme for each equation and mesh level ---*/ + numerics[MESH_0][RAD_SOL][SOURCE_FIRST_TERM] = new CSourceP1(nDim, nVar_Rad, config); + + /*--- Definition of the boundary condition method ---*/ + numerics[MESH_0][RAD_SOL][VISC_BOUND_TERM] = new CAvgGradCorrected_P1(nDim, nVar_Rad, config); + } +``` + +To add your solver: + +- `InitializeNumerics` first sets booleans from `config->GetKind_Solver()` (`template_solver`, `heat`, `turbulent`, ...) and reads `nVar` from the solvers. Add your solver there. +- The helpers `InstantiateTurbulentNumerics`, `InstantiateTransitionNumerics` and `InstantiateSpeciesNumerics` create the turbulence, transition and species source terms. +- The solver receives the array `numerics_container` for its position. It picks its object with `numerics_container[CONV_TERM + omp_get_thread_num()*MAX_TERMS]`. +- Template numerics classes are in `SU2_CFD/include/numerics/template.hpp` (`CConvective_Template`, `CViscous_Template`, `CSource_Template`). + +**Scalar edge kernels.** An edge kernel is a small class that computes the flux of one edge. In all `CScalarSolver` children, a kernel computes the convection, diffusion and boundary fluxes, not `CNumerics`. The kernel derives from `CUpwScalarBase` (`SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp`). `InitializeNumerics` creates only the source terms of these solvers. `CONV_TERM`, `VISC_TERM`, `CONV_BOUND_TERM` and `VISC_BOUND_TERM` stay `nullptr`. The solver calls its kernel from `Upwind_Residual` or `Viscous_Residual`: + +```cpp +void CHeatSolver::Upwind_Residual(CGeometry *geometry, CSolver **solver_container, + CNumerics **numerics_container, CConfig *config, unsigned short iMesh) { + // ... + const auto opt = ScalarFluxOptions::Interior(*config, false); + + DispatchScheme(config, [&](auto tag) { + EdgeFluxResidual(geometry, solver_container, config, opt); + }); +} +``` + +`DispatchScheme` picks, at compile time, the compressible or incompressible flow indices, the dimension and the number of equations. + +Kernels exist in `SU2_CFD/include/numerics` for: + +- SA (`turbulent/turb_sa_edge_flux.hpp`); +- SST (`turbulent/turb_sst_edge_flux.hpp`); +- LM (`turbulent/transition/trans_edge_flux.hpp`); +- species and flamelet (`species/`); +- heat (`heat_edge_flux.hpp`); +- Poisson (`poisson_edge_flux.hpp`). + +See [Non-conservative systems of equations](#non-conservative-systems-of-equations) for how a kernel fills the residual. + +## 7. Integration, iteration and initial condition + +Choose the iteration class that runs your solver, and set up its initial condition. + +The integration is set by `INTEGRATION_TYPE` (step 3). `CIterationFactory::CreateIteration` (`SU2_CFD/src/iteration/CIterationFactory.cpp`) creates the iteration; `CDriver::PreprocessIteration` calls it. Add a case for the new `MAIN_SOLVER` value: + +```cpp + case MAIN_SOLVER::HEAT_EQUATION: + if (rank == MASTER_NODE) + cout << "Heat iteration (finite volume method)." << endl; + iteration = new CHeatIteration(config); + break; +``` + +`NONE`, `TEMPLATE_SOLVER`, `MULTIPHYSICS` and `POISSON_EQUATION` end in `SU2_MPI::Error("No iteration found for specified solver.", ...)`. The switch has no `default`. So a new value without a case returns a null iteration, without an error. Add the case. + +**New iteration class.** Copy [`CHeatIteration`](../../doxygen/classCHeatIteration.html), a child of `CFluidIteration`. Override `Iterate` and `Update`: + +```cpp +void CHeatIteration::Iterate(COutput* output, CIntegration**** integration, CGeometry**** geometry, CSolver***** solver, + CNumerics****** numerics, CConfig** config, CSurfaceMovement** surface_movement, + CVolumetricMovement*** grid_movement, CFreeFormDefBox*** FFDBox, unsigned short val_iZone, + unsigned short val_iInst) { + SU2_ZONE_SCOPED + + /*--- Update global parameters ---*/ + + config[val_iZone]->SetGlobalParam(MAIN_SOLVER::HEAT_EQUATION, RUNTIME_HEAT_SYS); + + integration[val_iZone][val_iInst][HEAT_SOL]->SingleGrid_Iteration(geometry, solver, numerics, config, + RUNTIME_HEAT_SYS, val_iZone, val_iInst); +} +``` + +For dual time stepping, `CHeatIteration::Update` calls `SetDualTime_Solver` and `SetDualTime_Geometry` of the integration on every level. + +**Sub-solver.** A sub-solver of the flow solver needs no new iteration. Add it to `CFluidIteration::CommonAuxiliarySolvers` (`SU2_CFD/src/iteration/CFluidIteration.cpp`), which runs after the flow solver: + +```cpp + if (config[val_iZone]->GetKind_Species_Model() != SPECIES_MODEL::NONE) { + config[val_iZone]->SetGlobalParam(main_solver, RUNTIME_SPECIES_SYS); + integration[val_iZone][val_iInst][SPECIES_SOL]->SingleGrid_Iteration(geometry, solver, numerics, config, + RUNTIME_SPECIES_SYS, val_iZone, val_iInst); + // ... + } +``` + +Also look at `CFluidIteration::Update` for the dual time update of each solver. + +**Initial condition.** The drivers call `SetInitialCondition` of each solver at every time step. The calls are in `CSinglezoneDriver::Preprocess` and `CMultizoneDriver::Preprocess`, and for harmonic balance in `CFluidDriver::Preprocess` (in `CDriver.cpp`). They test `GetFluidProblem()`, `GetHeatProblem()` and the species model. Add a call for your solver there if its initial condition goes beyond the free-stream values set in its constructor. + +## 8. Output + +Add the output class that writes the screen, history and volume output. `CDriver::PreprocessOutput` calls `COutputFactory::CreateOutput(kindSolver, config, nDim)` (`SU2_CFD/src/output/COutputFactory.cpp`). Add a case: + +```cpp + case MAIN_SOLVER::HEAT_EQUATION: + output = new CHeatOutput(config, nDim); + break; +``` + +A value without a case gets a plain `COutput(config, nDim, false)`, which writes no solver fields. + +- For a main solver, write an output class (copy `CHeatOutput`). +- For a sub-solver of the flow, add its fields to `CFlowOutput`. + +[Adding Output Fields](../Adding-Output-Fields/) explains the hooks, the groups, and which groups go into the restart file. + +## 9. Restart files + +Implement `LoadRestart` so that the solver can read its solution back. + +`CDriver::RestartSolver` calls `LoadRestart` for every non-null, non-adjoint solver when: + +- `RESTART_SOL= YES`, or +- `config->GetRestart_Flow()` is true. Adjoint runs set it, because they read the primal solution. + +The call runs inside an OpenMP region if `GetHasHybridParallel()` is true. A second loop loads the adjoint solvers. + +The output class writes the restart file. The file holds the coordinates, then the volume fields of the restart groups, in the order of the `AddVolumeOutput` calls (see [Adding Output Fields](../Adding-Output-Fields/#groups-and-restart-files)). SU2 reads the file **by position**. So `LoadRestart` must skip the columns in front of its own variables. `CHeatSolver::LoadRestart` shows the pattern: + +```cpp + string restart_filename = config->GetSolution_FileName(); + + BEGIN_SU2_OMP_SAFE_GLOBAL_ACCESS { + /*--- Skip coordinates ---*/ + + unsigned short skipVars = nDim; + + if (flow) { + // P, vx, vy (,vz) + skipVars += 1 + nDim; + } + // ... + if (config->GetRead_Binary_Restart()) { + restart_filename = config->GetFilename(restart_filename, ".dat", val_iter); + Read_SU2_Restart_Binary(geometry[MESH_0], config, restart_filename); + } else { + restart_filename = config->GetFilename(restart_filename, ".csv", val_iter); + Read_SU2_Restart_ASCII(geometry[MESH_0], config, restart_filename); + } + // ... copy Restart_Data[counter*Restart_Vars[1] + skipVars + iVar] into nodes, check counter == nPointDomain + } + END_SU2_OMP_SAFE_GLOBAL_ACCESS +``` + +After reading the file, `CHeatSolver::LoadRestart`: + +1. communicates the solution; +2. calls `Preprocessing`; +3. restricts the solution to the coarse levels with `MultigridRestriction`; +4. clears `Restart_Vars` and `Restart_Data`. + +The file name comes from `SOLUTION_FILENAME`. The format comes from `READ_BINARY_RESTART`: SU2 binary (`.dat`) or ASCII (`.csv`). If the layout is simple, `CSolver::BasicLoadRestart` copies `nVar` values per point after `skipVars`. + +Your solver may add fields to the restart file of another solver, for example a sub-solver that writes after the flow fields. Then the `LoadRestart` of the solvers that come after it must skip more columns. + +## 10. MPI communication + +Each rank stores halo points: copies of points that a neighbouring rank owns. After you change a per-point quantity that other ranks read, update the halos with this pair: + +```cpp + InitiateComms(geometry, config, MPI_QUANTITIES::SOLUTION); + CompleteComms(geometry, config, MPI_QUANTITIES::SOLUTION); +``` + +`enum class MPI_QUANTITIES` (`Common/include/option_structure.hpp`) lists what can be sent (`SOLUTION`, `SOLUTION_OLD`, `SOLUTION_GRADIENT`, `SOLUTION_LIMITER`, `PRIMITIVE_GRADIENT`, `SOLUTION_EDDY`, ...). To send a new quantity: + +1. Add a value to `MPI_QUANTITIES`. +2. Add a case to `CSolver::GetCommCountAndType` (`SU2_CFD/src/solvers/CSolver.cpp`) with the count per point. Without it, the `default` case stops with "Unrecognized quantity for point-to-point MPI comms." +3. Pack it in the `switch (commType)` of `CSolver::InitiateComms`, and unpack it in `CSolver::CompleteComms`. + +Periodic boundaries use `InitiatePeriodicComms` / `CompletePeriodicComms`, with their own enum values (`PERIODIC_VOLUME`, `PERIODIC_NEIGHBORS`, ...). For any other communication, use the `SU2_MPI` wrapper (see [Code Structure](../Code-Structure/#parallelism-mpi-openmp-and-vectorization)). + +## 11. OpenMP + +`CSolver::GetHasHybridParallel()` returns `false` by default. The integration then opens its parallel region with a single thread, and the solver works as a pure MPI code. Start like this. Return `true` only when every function is thread safe. `CScalarSolver`, `CEulerSolver` and `CIncEulerSolver` return `true`. + +When it is `true`, the **whole** iteration runs inside one parallel region (`SU2_OMP_PARALLEL_(if(...GetHasHybridParallel()))` in `SingleGrid_Iteration` and `MultiGrid_Iteration`). Every thread calls every solver function. So: + +- Split loops with work-sharing macros, and close them. Use `SU2_OMP_FOR_STAT(omp_chunk_size)` for point loops and `SU2_OMP_FOR_DYN(OMP_MIN_SIZE)` for vertex loops. Close each one with `END_SU2_OMP_FOR`. +- Function-local variables are private to each thread; members are shared. Change shared state (members, `config`) inside `SU2_OMP_SAFE_GLOBAL_ACCESS(...)` or `BEGIN_SU2_OMP_SAFE_GLOBAL_ACCESS { ... } END_SU2_OMP_SAFE_GLOBAL_ACCESS`. `CHeatSolver::Preprocessing` does this for `config->SetGlobalParam`. +- Edge loops write to both points of an edge. There are two ways to avoid conflicts between threads: + - Loop over `EdgeColoring`. Edges of one color share no point. + - Support the reducer strategy (`ReducerStrategy`, `EdgeFluxes`, `SumEdgeFluxes`). The solver switches to it by itself when the coloring is poor (`parallelEff < COLORING_EFF_THRESH`). The edge loop then stores one flux per edge in `EdgeFluxes`. `SumEdgeFluxes` then adds them to the points. + + `CScalarSolver::EdgeFluxResidual` does both. + +The macro list is in [Code Structure](../Code-Structure/#parallelism-mpi-openmp-and-vectorization). + +## 12. Discrete adjoint + +The discrete adjoint of a solver is [`CDiscAdjSolver`](../../doxygen/classCDiscAdjSolver.html), created on top of the primal solver: + +```cpp + heatSolver = new CDiscAdjSolver(geometry, config, solver[HEAT_SOL], RUNTIME_HEAT_SYS, iMGLevel); +``` + +The adjoint uses the AD tape: the record of the primal operations that automatic differentiation runs backwards. `CDiscAdjSolver` registers the primal solution on the tape: + +- `direct_solver->GetNodes()->RegisterSolution(true)` for the inputs; +- `RegisterSolution(false)` for the outputs. + +These call `AD::RegisterInput` / `AD::RegisterOutput` for every entry of `Solution`. Extra state that must be part of the fixed point goes through `RegisterSolutionExtra`, `ExtractAdjoint_SolutionExtra` and `SetAdjoint_SolutionExtra`. + +To add the adjoint: + +1. Add a `DISC_ADJ_` value of `MAIN_SOLVER` and its mapping in `CConfig::SetPostprocessing` (step 1). +2. Add an adjoint position (like `ADJHEAT_SOL`), and the `CDiscAdjSolver` in `CreateSolverContainer` with `INTEGRATION_TYPE::DEFAULT` (step 3). +3. Set up the recording. + - Standalone solver: the constructor of `CDiscAdjSinglezoneDriver` picks `direct_iteration`, `direct_output`, `MainVariables`, `SecondaryVariables` and `MainSolver` for each `DISC_ADJ_` value. `CIterationFactory::CreateIteration` creates the adjoint iteration (`CDiscAdjHeatIteration` for `DISC_ADJ_HEAT`). + - Sub-solver of the flow: follow `SPECIES_SOL` / `ADJSPECIES_SOL` through `CDiscAdjFluidIteration` (`Preprocess`, `LoadUnsteady_Solution`, `IterateDiscAdj`, `InitializeAdjoint`, `RegisterInput`, `SetDependencies`, `RegisterOutput`). +4. Add an adjoint output class (`CAdjHeatOutput` for heat) in `COutputFactory::CreateOutput`. + +Follow these rules in the primal code, so that the tape is correct: + +- Use `su2double` for everything that depends on the solution or on design variables. `passivedouble` and `SU2_TYPE::GetValue` cut the derivative. +- The Jacobian assembly is not recorded; the scalar kernels wrap it in `AD::BeginPassive` / `AD::EndPassive`. Only the residual matters for the adjoint. +- Kernels may use preaccumulation (`AD::StartPreacc`, `AD::SetPreaccIn`, `AD::SetPreaccOut`, `AD::EndPreacc`). Preaccumulation stores the local derivatives of a short code section on the tape, instead of all its operations. In edge loops with the reducer strategy, preaccumulation is paused with `AD::PausePreaccumulation`. With coloring, `AD::StartNoSharedReading` is used. See `CScalarSolver::EdgeFluxResidual`. + +See [Advanced AD Techniques](../Advanced-AD-Techniques/). + +## 13. Build and test + +1. Add the `.cpp` files to the source list in `SU2_CFD/src/meson.build` (next to `'solvers/CHeatSolver.cpp'`, `'variables/CHeatVariable.cpp'`, ...). +2. Add the new options to `config_template.cfg` (see [Adding a Config Option](../Adding-a-Config-Option/)). +3. Do a dry run (`SU2_CFD -d case.cfg`). It builds the driver on a dummy geometry and lists the output fields. +4. Run serial, with MPI and, if `GetHasHybridParallel()` is true, with OpenMP (`SU2_CFD -t `). The results must agree. +5. Add a regression test (and an `_AD` one for the adjoint) as in [Running Regression Tests](../Running-Regression-Tests/#add-a-new-test). + +## Non-conservative systems of equations + +A term is conservative when an edge adds a flux to the row of one point and the opposite flux to the row of the other point. It is non-conservative when the two row contributions differ. + +In short: + +- **Scalar solvers already support non-conservative edge terms.** Every `CScalarSolver` child can have them. The SA and SST kernels use them today (see the [table below](#scalar-solvers-two-row-contributions-per-edge)). +- **To use this path**, derive your solver from `CScalarSolver` and write a kernel derived from `CUpwScalarBase`. In the kernel, make the two row contributions differ. The steps are below. +- **The flow solvers assume one conservative flux per edge.** Their edge loops and helpers are not written for two different row contributions. If your system does not fit the scalar model, write your own edge loop (see [Advice](#advice)). + +### Scalar solvers: two row contributions per edge + +The scalar edge kernels return the contributions to the two rows separately, in an `EdgeResidual` (`SU2_CFD/include/numerics/util.hpp`): + +```cpp +/*! + * \brief Residual of one edge, accumulated by the convective and the diffusive terms. + * \note flux_i and flux_j are the contributions to the rows of i and j. They are opposite + * for a conservative term and independent for a non-conservative one. The Jacobians + * map onto the ii, ij, ji and jj blocks of the edge. A dynamic model sizes the storage + * with the maximum and iterates to nVar, which the scheme sets from the solver. + */ +template +struct EdgeResidual { + // ... + Vector flux_i, flux_j; + Matrix jac_ii, jac_ij, jac_ji, jac_jj; + const size_t nVar; + // ... +}; +``` + +The second `updateLinearSystem` overload in the same file writes it into the linear system. It handles both OpenMP strategies of [step 11](#11-openmp): + +- **Coloring:** `flux_i` is added to the row of `iPoint`, and `flux_j` to the row of `jPoint`. `CSysMatrix::SetBlocks(iEdge, iPoint, jPoint, jac_ii, jac_ij, jac_ji, jac_jj)` adds the two diagonal blocks and **sets** the two off-diagonal blocks. +- **Reducer:** `flux_i` goes to `EdgeFluxes`, and `flux_j` to `EdgeFluxesDiff`. Only the off-diagonal blocks are set (`SetOffDiagBlocks`). `CScalarSolver::SumEdgeFluxes` then adds `EdgeFluxes` to the first point of each edge, and `EdgeFluxesDiff` to the second. `Jacobian.SetDiagonalAsColumnSum()` builds the diagonal. + + The note on that overload says this diagonal equals `jac_ii` and `jac_jj` only for a conservative flux. With a non-conservative model, the solver then converges along a slightly different path, to the same solution. + +To use this path in a new scalar solver: + +1. In its constructor, allocate both `EdgeFluxes` and `EdgeFluxesDiff` when `ReducerStrategy` is true. The `CScalarSolver` constructor does not do it. All existing scalar solvers do, for example `CHeatSolver`, `CTurbSASolver`, `CTurbSSTSolver`, `CTransLMSolver`, `CSpeciesSolver`, `CPoissonSolver`. +2. Call `EdgeFluxResidual` with a kernel derived from `CUpwScalarBase` (`SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp`), through `DispatchScheme` as in [step 6](#6-numerics). +3. In the kernel, make the two rows differ where the term is non-conservative. + +How the base kernel fills the two rows: + +- The convective term is conservative: `finalizeFlux` does `flux_i += flux` and `flux_j -= flux`. +- The diffusive term is written per row: + +```cpp + res.flux_i(iVar) -= D.i(iVar) * projGrad(iVar); + // ... + res.flux_j(iVar) += D.j(iVar) * projGrad(iVar); +``` + +`D` comes from the model's `coefficients()` function, which returns a pair `{D_i, D_j}`: + +- if the two are equal, the diffusion is conservative; +- if they differ, it is not. + +The Jacobian blocks follow the same split: `jac_ii` and `jac_ij` come from `D.i`, `jac_ji` and `jac_jj` from `D.j`. + +The kernel has more hooks: + +- `finalizeFlux`: your own convective term; +- `coefficientJacobians`: the derivatives of the coefficients; +- `extraDiffusionTerms`: extra fluxes (the flamelet kernel uses it). + +Models that use this today: + +| Kernel | `coefficients()` | Discretization | +|---|---|---| +| `CScalarFlux_SA` (`SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp`) | `D_i` and `D_j` use the `nu_tilde` of the row's own point (the `cb2` term) | non-conservative | +| `CScalarFlux_SST` (`SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp`) | the cross-diffusion part reads `omega` of the row's own point | non-conservative | +| `CScalarFlux_Heat` (`SU2_CFD/include/numerics/heat_edge_flux.hpp`), LM (`SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp`), species (`SU2_CFD/include/numerics/species/species_edge_flux.hpp`) | `return {D, D};` | conservative | + +The kernel flag `Conservative` means something else. It says whether the transported variable is weighted by the density. SST, LM and species set it to `true`; SA and heat set it to `false`. + +### Flow solvers: one flux per edge + +The flow solvers assemble each edge flux conservatively: the same flux is added to the row of `iPoint` and subtracted from the row of `jPoint`. In the non-vectorized loop of `CEulerSolver::Upwind_Residual`: + +```cpp + if (ReducerStrategy) { + EdgeFluxes.SetBlock(iEdge, conv_residual); + } else { + LinSysRes.AddBlock(iPoint, conv_residual); + LinSysRes.SubtractBlock(jPoint, conv_residual); + } +``` + +The other parts follow the same rule: + +- **Jacobian:** `CFVMFlowSolverBase::UpdateJacobian` updates it with `CSysMatrix::UpdateBlocks`, which takes two blocks. `block_i` is added to `ii` and subtracted from `ji`. `block_j` is added to `ij` and subtracted from `jj`. With the reducer strategy it calls `CSysMatrix::SetBlocks` instead. That writes only the off-diagonal blocks `ij` and `ji`; the diagonal blocks are computed afterwards from them (`SetDiagonalAsColumnSum`). +- **Vectorized path:** `CFVMFlowSolverBase::EdgeFluxResidual` processes several edges at once with SIMD instructions. It calls the first `updateLinearSystem` overload in `SU2_CFD/include/numerics/util.hpp` ("one conservative flux"). That overload uses `CSysVector::UpdateBlocks`: add to `iPoint`, subtract from `jPoint`. +- **Reducer strategy:** `CFVMFlowSolverBase::SumEdgeFluxes` adds `EdgeFluxes` for the first point of the edge and subtracts it for the second. `Jacobian.SetDiagonalAsColumnSum()` builds the diagonal blocks from the off-diagonal ones. + +### Advice + +- **Your system fits the scalar model** (transport of scalars, upwind convection, gradient diffusion, point sources). Derive the solver from `CScalarSolver`. Copy the SA kernel for asymmetric coefficients, or the heat kernel as the simplest one. +- **Your system does not fit.** Write your own edge loop that adds separate `i` and `j` contributions to `LinSysRes`, and the four Jacobian blocks. + - Do not reuse the conservative helpers (`CSysVector::UpdateBlocks`, `CSysMatrix::UpdateBlocks`, the flow `SumEdgeFluxes`). + - Write all four blocks of an edge in one call. In quantized Jacobian mode, the matrix stores the off-diagonal blocks in a compressed 8-bit form. These blocks can only be overwritten, not accumulated (see the note on `CFVMFlowSolverBase::UpdateJacobian`). +- **Check every part that relies on fluxes cancelling between neighbours:** + - the reducer-strategy diagonal (above); + - the multigrid forcing term: `CMultiGridIntegration::SetForcing_Term` sums the fine residuals of the children of a coarse cell (a coarse-grid cell made of several fine cells); + - integrals or outputs built from boundary fluxes; + - the periodic and MPI handling of edges and residuals. +- **A point source is an alternative.** You can also write a non-conservative term as a point source in `Source_Residual`, using the gradients stored at the point. SA used this form before. An older `CSourceBase_TurbSA` had a `ComputeCrossProduction` function for the `cb2` term. Commit `bcb0f94f5a` removed it. Today the term is in `CScalarFlux_SA::coefficients`. + +## Checklist + +1. `MAIN_SOLVER` value and `Solver_Map` keyword (or a sub-model enum and option); `DISC_ADJ_` mapping in `CConfig::SetPostprocessing`; problem helpers in `Common/include/CConfig.hpp`. +2. Container position (and `MAX_SOLS`), `RUNTIME_TYPE` value, `CConfig::GetContainerPosition`, `CConfig::SetGlobalParam`. +3. `SUB_SOLVER_TYPE` value, `CreateSolverContainer` and `CreateSubSolver` cases with the right `INTEGRATION_TYPE`. +4. `CVariable` child with `MAXNVAR` (for `CScalarSolver`). +5. `CSolver` child: constructor (linear system, `nodes`, `SetBaseClassPointerToNodes`, comms), residual loops, time step, BCs, update, `LoadRestart`. +6. Numerics in `CDriver::InitializeNumerics`, or a scalar edge kernel. +7. Iteration (`CIterationFactory::CreateIteration` or `CFluidIteration::CommonAuxiliarySolvers`), `SetInitialCondition` calls in the drivers. +8. Output class in `COutputFactory::CreateOutput`, restart columns in `LoadRestart`. +9. MPI and OpenMP correct; `GetHasHybridParallel()` only `true` when thread safe. +10. Adjoint position, recording and output, if needed. +11. `SU2_CFD/src/meson.build`, `config_template.cfg`, regression tests. + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Adding-a-Transition-Model.md b/Docs/website/pages/Adding-a-Transition-Model.md new file mode 100644 index 000000000000..ef3dbbeb0236 --- /dev/null +++ b/Docs/website/pages/Adding-a-Transition-Model.md @@ -0,0 +1,502 @@ +--- +title: Adding a Transition Model +permalink: /docs_v7/Adding-a-Transition-Model/ +--- + +This page shows how the transition model of SU2 is built, how it is coupled with the turbulence models, and what to change to add a correlation, a variant or a new transition model. The code examples are copied from the SU2 source. + +SU2 has one transport-equation transition model: the Langtry-Menter (LM) model. It works with SA and with SST. It has two equations: + +- the intermittency, which is near 0 in laminar regions and 1 in turbulent regions; +- the transition momentum thickness Reynolds number (`Re_theta_t`), the Reynolds number at which transition starts, based on the momentum thickness of the boundary layer. + +The SA option `SA_OPTIONS= BCM` (Bas-Cakmakcioclu) is different: it is an algebraic transition term inside the SA source term. It is described in [Adding a Turbulence Model](../Adding-a-Turbulence-Model/), not here. + +Read [Adding a Turbulence Model](../Adding-a-Turbulence-Model/) first. The transition solver uses the same base classes, edge kernels and source term structure. + +## Files you will touch + +| File | What goes there | +|---|---| +| `Common/include/option_structure.hpp` | `TURB_TRANS_MODEL` and `Trans_Model_Map`, `LM_OPTIONS` and `LM_Options_Map`, `TURB_TRANS_CORRELATION`, `LM_ParsedOptions`, `ParseLMOptions` | +| `Common/include/CConfig.hpp`, `Common/src/CConfig.cpp` | `KIND_TRANS_MODEL`, `LM_OPTIONS`, `HROUGHNESS`, the parsing and checks in `CConfig::SetPostprocessing`, the summary in `CConfig::SetOutput` | +| `SU2_CFD/include/solvers/CTransLMSolver.hpp`, `SU2_CFD/src/solvers/CTransLMSolver.cpp` | The solver | +| `SU2_CFD/include/variables/CTransLMVariable.hpp`, `SU2_CFD/src/variables/CTransLMVariable.cpp` | The variables, with the separation and effective intermittency | +| `SU2_CFD/include/numerics/turbulent/transition/trans_sources.hpp` | The source terms | +| `SU2_CFD/include/numerics/turbulent/transition/trans_correlations.hpp` | The correlations (`TransLMCorrelations`) | +| `SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp` | Convection and diffusion (`CScalarFlux_TransLM`) | +| `SU2_CFD/src/solvers/CTurbSASolver.cpp`, `CTurbSSTSolver.cpp`, `SU2_CFD/src/variables/CTurbSSTVariable.cpp`, `SU2_CFD/include/numerics/turbulent/turb_sources.hpp` | The coupling with the turbulence model | +| `SU2_CFD/src/solvers/CSolverFactory.cpp`, `SU2_CFD/src/drivers/CDriver.cpp`, `SU2_CFD/src/iteration/CFluidIteration.cpp` | Creation of the solver and numerics, order of the solvers | +| `SU2_CFD/src/output/CFlowOutput.cpp` | Output fields | + +--- + +- [Overview](#overview) +- [Options](#options) +- [Solver and variable classes](#solver-and-variable-classes) +- [Source terms](#source-terms) +- [Correlations](#correlations) +- [Convection and diffusion](#convection-and-diffusion) +- [Coupling with the turbulence model](#coupling-with-the-turbulence-model) +- [Order of the solvers](#order-of-the-solvers) +- [Boundary conditions](#boundary-conditions) +- [Free-stream and initial values](#free-stream-and-initial-values) +- [Output and restart](#output-and-restart) +- [Discrete adjoint](#discrete-adjoint) +- [Tests](#tests) +- [How to add a correlation, a variant or a new model](#how-to-add-a-correlation-a-variant-or-a-new-model) +- [Checklist](#checklist) + +--- + +## Overview + +| Item | LM in SU2 | +|---|---| +| Solver | [`CTransLMSolver`](../../doxygen/classCTransLMSolver.html), at position `TRANS_SOL` of the solver container, equation system `RUNTIME_TRANS_SYS` | +| Variables | [`CTransLMVariable`](../../doxygen/classCTransLMVariable.html): intermittency (index 0) and transition momentum thickness Reynolds number (index 1) | +| Source term | [`CSourcePieceWise_TransLM`](../../doxygen/classCSourcePieceWise__TransLM.html) | +| Correlations | [`TransLMCorrelations`](../../doxygen/classTransLMCorrelations.html) | +| Convection and diffusion | [`CScalarFlux_TransLM`](../../doxygen/classCScalarFlux__TransLM.html) | +| Numerical settings | the turbulence ones: `CONV_NUM_METHOD_TURB`, `MUSCL_TURB`, `SLOPE_LIMITER_TURB`, `TIME_DISCRE_TURB`, `CFL_REDUCTION_TURB` | + +The transition model has no numerical options of its own. `CConfig::SetGlobalParam` applies the turbulence settings for `RUNTIME_TRANS_SYS`. + +## Options + +Two options control the model: + +- `KIND_TRANS_MODEL` chooses the model. Its enum is `TURB_TRANS_MODEL`, its map is `Trans_Model_Map`, and its values are `NONE` and `LM`. +- `LM_OPTIONS` chooses the variants and the correlation. It is an enum list option, so the user can give several keywords: + +```cpp +enum class LM_OPTIONS { + NONE, /*!< \brief No option / default. */ + LM2015, /*!< \brief Cross-flow corrections. */ + MALAN, /*!< \brief Kind of transition correlation model (Malan). */ + SULUKSNA, /*!< \brief Kind of transition correlation model (Suluksna). */ + KRAUSE, /*!< \brief Kind of transition correlation model (Krause). */ + KRAUSE_HYPER, /*!< \brief Kind of transition correlation model (Krause hypersonic). */ + MEDIDA_BAEDER,/*!< \brief Kind of transition correlation model (Medida-Baeder). */ + MEDIDA, /*!< \brief Kind of transition correlation model (Medida). */ + MENTER_LANGTRY, /*!< \brief Kind of transition correlation model (Menter-Langtry). */ + DEFAULT /*!< \brief Kind of transition correlation model (Menter-Langtry if SST, MALAN if SA). */ +}; +``` + +`LM_Options_Map` has the keywords `NONE`, `LM2015`, `MALAN`, `SULUKSNA`, `KRAUSE`, `KRAUSE_HYPER`, `MEDIDA_BAEDER`, `MENTER_LANGTRY` and `DEFAULT`. `MEDIDA` is in the enum but not in the map, so users cannot select it. + +The correlation has its own enum, `TURB_TRANS_CORRELATION`, with the same names as the correlation keywords. The parsed options struct holds the result of reading the list: + +```cpp +struct LM_ParsedOptions { + LM_OPTIONS version = LM_OPTIONS::NONE; /*!< \brief LM base model. */ + bool LM2015 = false; /*!< \brief Use cross-flow corrections. */ + TURB_TRANS_CORRELATION Correlation = TURB_TRANS_CORRELATION::DEFAULT; +}; +``` + +`ParseLMOptions(LM_Options, nLM_Options, rank, Kind_Turb_Model)` does four things: + +1. It sets `LM2015`. +2. It sets `Correlation` from the correlation keyword. +3. It stops if more than one correlation is given. +4. It replaces `DEFAULT` by the default of the turbulence model: + +```cpp + if (LMParsedOptions.Correlation == TURB_TRANS_CORRELATION::DEFAULT){ + if (Kind_Turb_Model == TURB_MODEL::SST) { + LMParsedOptions.Correlation = TURB_TRANS_CORRELATION::MENTER_LANGTRY; + } else if (Kind_Turb_Model == TURB_MODEL::SA) { + LMParsedOptions.Correlation = TURB_TRANS_CORRELATION::MALAN; + } + } +``` + +`ParseLMOptions` does not set `version`. It stays `LM_OPTIONS::NONE`. + +`CConfig::SetPostprocessing` calls the parser and checks the options: + +```cpp + if (Kind_Turb_Model == TURB_MODEL::NONE && Kind_Trans_Model != TURB_TRANS_MODEL::NONE) { + SU2_MPI::Error("KIND_TURB_MODEL cannot be NONE to use a transition model", CURRENT_FUNCTION); + } + // ... + /*--- Postprocess LM_OPTIONS into structure. ---*/ + if (Kind_Trans_Model == TURB_TRANS_MODEL::LM) { + lmParsedOptions = ParseLMOptions(LM_Options, nLM_Options, rank, Kind_Turb_Model); + + /*--- Check if problem is 2D and LM2015 has been selected ---*/ + if (lmParsedOptions.LM2015 && val_nDim == 2) { + SU2_MPI::Error("LM2015 is available only for 3D problems", CURRENT_FUNCTION); + } + } +``` + +The code reads the result with `config->GetLMParsedOptions()`. `CConfig::SetOutput` prints the model (2009 or 2015) and the correlation. `HROUGHNESS` (`GethRoughness()`) is the wall roughness used by the `LM2015` cross-flow term. + +New variants of the LM model should use the same mechanism: a keyword in `LM_OPTIONS` and a field in `LM_ParsedOptions`. + +## Solver and variable classes + +The transition model has a solver class and a variable class, built like those of the turbulence models. + +`CTransLMSolver` derives from `CTurbSolver` with `conservative = true`. The transported quantities are the density times the intermittency and the density times the Reynolds number. Its constructor (`SU2_CFD/src/solvers/CTransLMSolver.cpp`) follows the turbulence solvers: `nVar = 2`, the linear system, the limits, `Solution_Inf`, `nodes` and `ghostNodes`, `Inlet_TurbVars`, the local CFL. It also stores the options, the correlations and the family of the turbulence model: + +```cpp + /*--- Define variables needed for transition from config file */ + options = config->GetLMParsedOptions(); + TransCorrelations.SetOptions(options); + TurbFamily = TurbModelFamily(config->GetKind_Turb_Model()); +``` + +The implicit update limits the intermittency to `[1e-4, 5.0]` and the Reynolds number to `[1e-4, 1e15]`. + +`CTransLMVariable` derives from `CTurbVariable`. It adds two per-point fields: the separation intermittency `Intermittency_Sep` and the effective intermittency `Intermittency_Eff`. The effective value is computed when it is set: + +```cpp +void CTransLMVariable::SetIntermittencyEff(unsigned long iPoint, su2double val_Intermittency_sep) { + + /*--- Effective intermittency ---*/ + Intermittency_Eff(iPoint) = max(Solution(iPoint,0), val_Intermittency_sep); + +} +``` + +`CTransLMSolver::Postprocessing` first computes the gradients. Then, at each point, it computes the separation intermittency from the flow, the turbulence and the correlations (`ReThetaC_Correlations`). It stores both values: + +```cpp + su2double Intermittency_Sep = 2.0*max(0.0, Re_v/(3.235*Corr_Rec)-1.0)*f_reattach; + Intermittency_Sep = min(Intermittency_Sep,2.0)*f_theta; + Intermittency_Sep = min(max(0.0, Intermittency_Sep), 2.0); + nodes->SetIntermittencySep(iPoint, Intermittency_Sep); + nodes->SetIntermittencyEff(iPoint, Intermittency_Sep); +``` + +The solver also implements `Preprocessing` (which calls `CommonPreprocessing`), `Upwind_Residual`, `Source_Residual`, the boundary conditions and `LoadRestart`. + +`CSolverFactory::CreateTransSolver` creates the solver for `RANS` and `INC_RANS`. It then runs the flow `Preprocessing`, the transition `Postprocessing` (which sets the effective intermittency) and the flow `Preprocessing` again: + +```cpp + case TURB_TRANS_MODEL::LM : + transSolver = new CTransLMSolver(geometry, config, solver[FLOW_SOL], iMGLevel); + solver[FLOW_SOL]->Preprocessing(geometry, solver, config, iMGLevel, NO_RK_ITER, RUNTIME_FLOW_SYS, true); + transSolver->Postprocessing(geometry, solver, config, iMGLevel); + solver[FLOW_SOL]->Preprocessing(geometry, solver, config, iMGLevel, NO_RK_ITER, RUNTIME_FLOW_SYS, true); + break; +``` + +## Source terms + +`CSourcePieceWise_TransLM` (`trans_sources.hpp`) computes the sources of both equations at one point. `CDriver::InstantiateTransitionNumerics` creates it for every multigrid level: + +```cpp + if (LM) trans_source_first_term = new CSourcePieceWise_TransLM(nDim, nVar_Trans, config); +``` + +`CTransLMSolver::Source_Residual` sets its inputs. The turbulence solution goes in the "scalar" slots, and the transition solution goes in the "trans" slots: + +```cpp + numerics->SetScalarVar(turbNodes->GetSolution(iPoint), nullptr); + numerics->SetScalarVarGradient(turbNodes->GetGradient(iPoint), nullptr); + + /*--- Transition variables w/o reconstruction, and its gradient ---*/ + + numerics->SetTransVar(nodes->GetSolution(iPoint), nullptr); + numerics->SetTransVarGradient(nodes->GetGradient(iPoint), nullptr); +``` + +It also sets: + +- the flow primitives and their gradients; +- the volume, the wall distance and the coordinates; +- the vorticity and the strain rate magnitude; +- for `LM2015`, the local grid length (`SetLocalGridLength(geometry->nodes->GetMaxLength(iPoint))`). + +The result is subtracted from the residual, as for the turbulence models. + +`ComputeResidual` follows the LM model of Langtry and Menter (2009), with the `LM2015` cross-flow terms. Some terms depend on the turbulence model. They branch on `TurbFamily`: + +| Quantity | SST (`TURB_FAMILY::KW`) | SA (`TURB_FAMILY::SA`) | +|---|---|---| +| Turbulence intensity `Tu` (in percent) | from the local k and velocity, at least 0.027 | `FREESTREAM_TURBULENCEINTENSITY` | +| `R_t` | `rho*k/(mu*omega)` | `mu_t/mu` | +| `F_length` | correlation blended with 40 near the wall (`f_sub`) | correlation | +| `F_onset2`, `F_onset3` | limits 2.0 and `1 - (R_t/2.5)^3` | limits 4.0 and `2 - (R_t/2.5)^3` | +| `f_wake` | from `re_omega` | 1 | + +The source of the intermittency is production `Pg` minus destruction `Dg`. The source of the Reynolds number is `PRethetat` minus `DRethetat`. `DRethetat` is non-zero only with `LM2015`: + +```cpp + /*--- Source ---*/ + Residual[0] += (Pg - Dg) * Volume; + Residual[1] += (PRethetat - DRethetat) * Volume; +``` + +`ComputeResidual` also computes the equilibrium Reynolds number `Corr_Ret`. It uses a fixed-point loop on the pressure gradient parameter `lambda`, with at most 100 iterations. The Jacobian is diagonal: `Jacobian_i[0][0]` and `Jacobian_i[1][1]`. + +## Correlations + +The correlations give two quantities from the turbulence intensity and the transported Reynolds number: + +- `ReThetaC_Correlations` gives the critical Reynolds number; +- `FLength_Correlations` gives the transition length function. + +Both are functions of `TransLMCorrelations` (`trans_correlations.hpp`). This class holds a copy of `LM_ParsedOptions` (`SetOptions`), and both functions switch on `options.Correlation`: + +```cpp + su2double ReThetaC_Correlations(const su2double Tu, const su2double Re_theta_t) const { + // ... + su2double FLength_Correlations(const su2double Tu, const su2double Re_theta_t) const { +``` + +`CSourcePieceWise_TransLM::ComputeResidual` uses them. So does `CTransLMSolver::Postprocessing`, for the separation intermittency. The `DEFAULT` case stops with an error, because `ParseLMOptions` always replaces `DEFAULT`. + +## Convection and diffusion + +The edge kernel of the transition model is the smallest one of the scalar models. It inherits the conservative upwind convection of `CUpwScalarFlux`. It only gives the diffusion coefficients: `mu + mu_t` for the intermittency, and `2(mu + mu_t)` for the Reynolds number: + +```cpp +template +class CScalarFlux_TransLM + : public CUpwScalarBase, FlowIndices, nDim, nVar> { + public: + static constexpr bool Conservative = true; + static constexpr bool DiagonalDiffusion = true; + // ... + const Double diff_i_gamma = mu_i + muT_i; + const Double diff_j_gamma = mu_j + muT_j; + const Double diff_i_ReThetaT = 2.0 * (mu_i + muT_i); + const Double diff_j_ReThetaT = 2.0 * (mu_j + muT_j); + + Vector D; + D(0) = 0.5 * (diff_i_gamma + diff_j_gamma); + D(1) = 0.5 * (diff_i_ReThetaT + diff_j_ReThetaT); + return {D, D}; +``` + +`CTransLMSolver::Upwind_Residual` runs it with `DispatchScheme` and `ScalarFluxOptions::Interior(*config, config->GetBounded_Turb())`. [Adding a Turbulence Model](../Adding-a-Turbulence-Model/#5-convection-and-diffusion-the-edge-kernel) explains the kernel structure. + +## Coupling with the turbulence model + +The transition and turbulence models exchange data in both directions. + +**From the turbulence model to the transition model:** + +- the turbulence solution: k and omega for SST, through `SetScalarVar` in `Source_Residual` and `turbNodes->GetSolution` in `Postprocessing`; +- the eddy viscosity. `Postprocessing` reads it with `turbNodes->GetmuT`. The source term reads it from the flow primitives (`V_i[idx.EddyViscosity()]`), and so does the edge kernel. + +**From the transition model to the turbulence model:** the effective intermittency. SA also gets the transported intermittency. + +SST, in `CTurbSSTSolver::Source_Residual`: + +```cpp + /*--- Effective Intermittency ---*/ + if (config->GetKind_Trans_Model() == TURB_TRANS_MODEL::LM) { + numerics->SetIntermittencyEff(solver_container[TRANS_SOL]->GetNodes()->GetIntermittencyEff(iPoint)); + } +``` + +Then `CSourcePieceWise_TurbSST::ComputeResidual` scales the production and destruction of k: + +```cpp + /*--- LM model coupling with production and dissipation term for k transport equation---*/ + if (config->GetKind_Trans_Model() == TURB_TRANS_MODEL::LM) { + pk = pk * eff_intermittency; + dk = min(max(eff_intermittency, 0.1), 1.0) * dk; + } +``` + +SST also changes the blending function `F1` in `CTurbSSTVariable::SetBlendingFunc`. `CTurbSSTSolver::Postprocessing` calls this function with the transition model: + +```cpp + /*--- LM model for F1 ---*/ + if (trans_model == TURB_TRANS_MODEL::LM) { + su2double Ry = val_density*val_dist*sqrt(Solution(iPoint,0))/val_viscosity; + su2double F3 = exp(-pow(Ry/120.0, 8.0)); + F1(iPoint) = max(F1(iPoint), F3); + } +``` + +SA, in `CTurbSASolver::Source_Residual`: + +```cpp + if (config->GetKind_Trans_Model() != TURB_TRANS_MODEL::NONE) { + numerics->SetIntermittencyEff(solver_container[TRANS_SOL]->GetNodes()->GetIntermittencyEff(iPoint)); + numerics->SetIntermittency(solver_container[TRANS_SOL]->GetNodes()->GetSolution(iPoint, 0)); + } +``` + +Then, in `CSourceBase_TurbSA::ComputeResidual`, the effective intermittency multiplies the production. The transported intermittency limits the destruction: + +```cpp + } else if (transition_LM){ + + var.intermittency = intermittency_eff_i; + // Is wrong the reference from NASA? + // Original max(min(gamma, 0.5), 1.0) always gives 1 as result. + var.interDestrFactor = min(max(intermittency_i, 0.5), 1.0); +``` + +Both turbulence solvers store the effective intermittency they used, with `nodes->SetIntermittency`. With a transition model, their `Postprocessing` also computes the turbulence index at the walls (`SetTurbIndex`). It is written as the output field `TURB_INDEX`. + +The flow solver does not read the transition variables. The transition model acts on the flow only through the eddy viscosity of the turbulence model. + +## Order of the solvers + +In each inner iteration, the transition model runs after the flow and **before** the turbulence model. `CFluidIteration::Iterate` solves the flow. Then `CFluidIteration::CommonAuxiliarySolvers` solves the transition model and the turbulence model: + +```cpp + /*--- Solve transition model ---*/ + + if (config[val_iZone]->GetKind_Trans_Model() == TURB_TRANS_MODEL::LM) { + config[val_iZone]->SetGlobalParam(main_solver, RUNTIME_TRANS_SYS); + integration[val_iZone][val_iInst][TRANS_SOL]->SingleGrid_Iteration(geometry, solver, numerics, config, + RUNTIME_TRANS_SYS, val_iZone, val_iInst); + } + + /*--- Solve the turbulence model ---*/ +``` + +So: + +1. The flow uses the eddy viscosity of the previous iteration. +2. The transition model uses the new flow and the turbulence of the previous iteration. At the end of its iteration, `CTransLMSolver::Postprocessing` updates the effective intermittency. +3. The turbulence model uses the new effective intermittency. It computes the eddy viscosity for the next flow iteration. + +The transition model is solved on the finest grid only. Unlike the turbulence solution, its solution is not restricted (transferred) to the coarse grids: `CSingleGridIntegration::SingleGrid_Iteration` restricts only `RUNTIME_TURB_SYS`. For unsteady problems, `CFluidIteration::Update` calls `SetDualTime_Solver` for `TRANS_SOL` after `TURB_SOL`. + +## Boundary conditions + +`CTransLMSolver` implements the conditions in the table. All of them write the outside state into `ghostNodes`. Then they call the shared boundary flux (`BoundaryFlux`) with the convective term only. + +| Function | Outside state | +|---|---| +| `BC_HeatFlux_Wall` (and `BC_Isothermal_Wall`, which calls it) | the free-stream state; the comment in the code calls it "wall-normal zero flux, convective only" | +| `BC_Inlet` | `Inlet_TurbVars` (initialized to the free-stream state) | +| `BC_Outlet` | calls `BC_Far_Field` | +| `BC_Far_Field` | the free-stream state | +| `BC_Fluid_Interface` | the donor state (`FluidInterfaceFluxResidual`) | + +The other conditions come from the base classes: + +- symmetry planes, Euler walls, periodic and Riemann boundaries from `CScalarSolver`; +- the Giles and turbomachinery Riemann boundaries from `CTurbSolver`. + +The transition solver does not override `SetInletAtVertex` or `SetUniformInlet`. For new boundary conditions, see [Adding a Boundary Condition](../Adding-a-Boundary-Condition/). + +## Free-stream and initial values + +`CTransLMSolver` sets the free-stream state in its constructor and initializes all points with it. The intermittency is fixed at 1. The Reynolds number comes from the free-stream turbulence intensity (`FREESTREAM_TURBULENCEINTENSITY`, in percent here): + +```cpp + const su2double Intensity = config->GetTurbulenceIntensity_FreeStream()*100.0; + + const su2double Intermittency_Inf = 1.0; + su2double ReThetaT_Inf = 100.0; + + /*--- Momentum thickness Reynolds number, initialized from freestream turbulent intensity*/ + if (Intensity <= 1.3) { + if(Intensity >=0.027) { + ReThetaT_Inf = (1173.51-589.428*Intensity+0.2196/(Intensity*Intensity)); + } + else { + ReThetaT_Inf = (1173.51-589.428*Intensity+0.2196/(0.27*0.27)); + } + } + else if(Intensity>1.3) { + ReThetaT_Inf = 331.5*pow(Intensity-0.5658,-0.671); + } + + Solution_Inf[0] = Intermittency_Inf; + Solution_Inf[1] = ReThetaT_Inf; +``` + +`CEulerSolver::SetNondimensionalization` computes the same Reynolds number and stores it with `config->SetReThetaT_FreeStream`. This value and `FREESTREAM_INTERMITTENCY` (`GetIntermittency_FreeStream()`) are only printed in the start-up table. The solver does not read them. + +The turbulence model sets its own free-stream values (see [Adding a Turbulence Model](../Adding-a-Turbulence-Model/#free-stream-initial-and-inlet-values)). + +## Output and restart + +The output fields are in `CFlowOutput` (`SU2_CFD/src/output/CFlowOutput.cpp`). They are in the `switch (config->GetKind_Trans_Model())` blocks of the scalar output functions: + +| Kind | Fields | +|---|---| +| History | `RMS_INTERMITTENCY`, `RMS_RE_THETA_T`, `MAX_INTERMITTENCY`, `MAX_RE_THETA_T`, `BGS_INTERMITTENCY`, `BGS_RE_THETA_T`, `LINSOL_ITER_TRANS`, `LINSOL_RESIDUAL_TRANS` | +| Volume, `SOLUTION` group | `INTERMITTENCY`, `RE_THETA_T` | +| Volume, `PRIMITIVE` group | `INTERMITTENCY_SEP`, `INTERMITTENCY_EFF`, `TURB_INDEX` | +| Volume, `RESIDUAL` group | `RES_INTERMITTENCY`, `RES_RE_THETA_T` | + +To add more, see [Adding Output Fields](../Adding-Output-Fields/). + +**Restart.** In the `SOLUTION` group, the transition fields come after the turbulence fields (`CFlowOutput::SetVolumeOutputFieldsScalarSolution`). `CTransLMSolver::LoadRestart` skips the coordinates, the flow variables and the turbulence variables: + +```cpp + unsigned short skipVars = nDim + solver[MESH_0][FLOW_SOL]->GetnVar() + solver[MESH_0][TURB_SOL] ->GetnVar(); +``` + +Then, when no species model is active, it runs the flow `Preprocessing`, the turbulence `Postprocessing` and the transition `Postprocessing`. `CTurbSolver::LoadRestart` skips this step when a transition model is active. This way the step runs once, after both solutions are read. + +## Discrete adjoint + +The transition model has no discrete adjoint. For `DISC_ADJ_RANS`, `CSolverFactory::CreateSolverContainer` creates no `TRANS_SOL`. `CDiscAdjFluidIteration` handles only the flow, turbulence and species solvers. + +Adding the adjoint would need: + +- the transition solver in those two places; +- a `CDiscAdjSolver` for it; +- all preaccumulation inputs in `CSourcePieceWise_TransLM` and in the coupling terms of the turbulence sources. Preaccumulation is explained in [Adding a Turbulence Model](../Adding-a-Turbulence-Model/#discrete-adjoint). + +## Tests + +These tests cover the LM model: + +| Case | Where | Script | +|---|---|---| +| T3A and T3A- flat plates (`transitional_LM_model_ConfigFile.cfg`), E387 airfoil with SA-LM and SST-LM | `Tutorials` repository, `compressible_flow/Transitional_Flat_Plate/Langtry_and_Menter` and `compressible_flow/Transitional_Airfoil/Langtry_and_Menter` | `tutorials.py` | +| E387 SST-LM, 2 iterations, sanitizer runs only (`enabled_with_regular = False`) | same configuration as the tutorial | `serial_regression.py` (`tutorial_trans_e387_sst_asan`) | + +The folder `TestCases/transition` holds SA-BCM cases (`transitional_BC_model_ConfigFile.cfg`), not LM. `TestCases/rans/s809/trans_s809.cfg` uses SA-LM, but no test script runs it. + +To add a test, follow [Running Regression Tests](../Running-Regression-Tests/#add-a-new-test). For a new correlation or variant, add an entry to `parallel_regression.py`. Use a small LM case with the new keyword in `LM_OPTIONS`, and one configuration file per test. Before you open the pull request, check the transition location against experiments, for example the T3A flat plate. + +## How to add a correlation, a variant or a new model + +### A new correlation + +1. Add the value to `LM_OPTIONS`, to `LM_Options_Map` and to `TURB_TRANS_CORRELATION` in `option_structure.hpp`. +2. In `ParseLMOptions`, add an `if (IsPresent(...))` block that sets `Correlation` and increments `NFoundCorrelations`. +3. Add a `case` to `TransLMCorrelations::ReThetaC_Correlations` and `TransLMCorrelations::FLength_Correlations`. +4. Add the name to the `switch (lmParsedOptions.Correlation)` in `CConfig::SetOutput`. +5. Add the keyword to the comment of `LM_OPTIONS` in `config_template.cfg`. + +### A variant of the LM model + +1. Add the keyword to `LM_OPTIONS` and `LM_Options_Map`. Add a field to `LM_ParsedOptions`, and set it in `ParseLMOptions` (as for `LM2015`). +2. Add the checks in `CConfig::SetPostprocessing` (as the 3D check of `LM2015`). +3. Use the field in `CSourcePieceWise_TransLM::ComputeResidual`, which keeps a copy in `options`. If needed, also use it in `CTransLMSolver`, which also has `options`. If the variant changes the number of equations, these parts must follow: `nVar` in the solver, the variable class, the edge kernel dispatch (`DispatchScheme`), the boundary conditions, the restart and the output. +4. If the variant changes how the turbulence model uses the intermittency, change the coupling code listed in [Coupling with the turbulence model](#coupling-with-the-turbulence-model). +5. Print it in `CConfig::SetOutput`. + +### A new transition model + +1. Add a value to `TURB_TRANS_MODEL` and `Trans_Model_Map`. Every `switch (config->GetKind_Trans_Model())` in `CFlowOutput` must handle it. +2. Write four classes, as for a turbulence model: + - the variable class, derived from `CTurbVariable`; + - the solver, derived from `CTurbSolver`; + - the source class, in `SU2_CFD/include/numerics/turbulent/transition/`; + - the edge kernel. +3. Create the solver in `CSolverFactory::CreateTransSolver`, and the numerics in `CDriver::InstantiateTransitionNumerics`. +4. Many places test `TURB_TRANS_MODEL::LM` directly: `CDriver::InitializeNumerics` (the `transition` flag), `CFluidIteration::CommonAuxiliarySolvers` and `CFluidIteration::Update`, the turbulence solvers and sources, and `CTurbSSTVariable::SetBlendingFunc`. Find them with `git grep -n "TURB_TRANS_MODEL::LM"`. +5. Add the new `.cpp` files to `SU2_CFD/src/meson.build`. + +## Checklist + +1. Keyword in `LM_OPTIONS` (and `TURB_TRANS_CORRELATION`) or in `TURB_TRANS_MODEL`, with its map entry. +2. Parsing in `ParseLMOptions`, checks in `CConfig::SetPostprocessing`, summary in `CConfig::SetOutput`, keyword in `config_template.cfg`. +3. Source terms in `CSourcePieceWise_TransLM`, correlations in `TransLMCorrelations`. +4. Coupling terms in the SA and SST sources and in `CTurbSSTVariable::SetBlendingFunc` reviewed. +5. Output fields, and the order of the `SOLUTION` fields for the restart. +6. A regression test with the new keyword. + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Adding-a-Turbulence-Model.md b/Docs/website/pages/Adding-a-Turbulence-Model.md new file mode 100644 index 000000000000..608415418cfe --- /dev/null +++ b/Docs/website/pages/Adding-a-Turbulence-Model.md @@ -0,0 +1,751 @@ +--- +title: Adding a Turbulence Model +permalink: /docs_v7/Adding-a-Turbulence-Model/ +--- + +This page is for developers. It shows how the RANS (Reynolds-averaged Navier-Stokes) turbulence models of SU2 are built. It then explains what to change in two cases: a variant of an existing model, and a new model with its own equations. The code examples are copied from the SU2 source. + +Read [Code Structure](../Code-Structure/) first. The parts that are not specific to turbulence have their own pages: [Adding a Config Option](../Adding-a-Config-Option/), [Adding Output Fields](../Adding-Output-Fields/), [Adding a Solver](../Adding-a-Solver/), [Adding a Boundary Condition](../Adding-a-Boundary-Condition/) and [Running Regression Tests](../Running-Regression-Tests/). Transition models are covered in [Adding a Transition Model](../Adding-a-Transition-Model/). + +## Files you will touch + +| File | What goes there | +|---|---| +| `Common/include/option_structure.hpp` | `TURB_MODEL` and `Turb_Model_Map`, `TURB_FAMILY` and `TurbModelFamily`, the model options (`SA_OPTIONS`, `SST_OPTIONS`), their maps, parsed structs and parse functions | +| `Common/include/CConfig.hpp`, `Common/src/CConfig.cpp` | The options, the parsing in `CConfig::SetPostprocessing`, the number of turbulence variables, the start-up summary in `CConfig::SetOutput` | +| `SU2_CFD/include/numerics/turbulent/turb_sources.hpp` | Source terms and their Jacobians | +| `SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp`, `turb_sst_edge_flux.hpp` | Convection and diffusion, in the edge kernels. An edge kernel is a class that computes the fluxes of one mesh edge. | +| `SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp` | The shared base of the edge kernels. You use it, but you rarely change it. | +| `SU2_CFD/include/solvers/CTurb*Solver.hpp`, `SU2_CFD/src/solvers/CTurb*Solver.cpp` | The solver: residuals, boundary conditions, eddy viscosity | +| `SU2_CFD/include/variables/CTurb*Variable.hpp`, `SU2_CFD/src/variables/CTurb*Variable.cpp` | The data stored at each point | +| `SU2_CFD/src/solvers/CSolverFactory.cpp` | Creates the turbulence solver (`CSolverFactory::CreateTurbSolver`) | +| `SU2_CFD/src/drivers/CDriver.cpp` | Creates the source numerics (`CDriver::InstantiateTurbulentNumerics`) | +| `SU2_CFD/src/output/CFlowOutput.cpp`, `SU2_CFD/src/output/CAdjFlowOutput.cpp` | Screen, history and volume fields | +| `SU2_CFD/src/meson.build` | New source files | +| `config_template.cfg`, `TestCases/` | Option documentation and tests | + +--- + +- [How the turbulence equations are solved](#how-the-turbulence-equations-are-solved) +- [Class hierarchy](#class-hierarchy) +- [Case A: a variant of an existing model](#case-a-a-variant-of-an-existing-model) +- [Case B: a new model with its own equations](#case-b-a-new-model-with-its-own-equations) +- [Coupling with the flow solver](#coupling-with-the-flow-solver) +- [Walls, wall functions and wall distance](#walls-wall-functions-and-wall-distance) +- [Free-stream, initial and inlet values](#free-stream-initial-and-inlet-values) +- [Output and restart](#output-and-restart) +- [Discrete adjoint](#discrete-adjoint) +- [Hybrid RANS/LES](#hybrid-ransles) +- [Tests](#tests) +- [Checklist](#checklist) + +--- + +## How the turbulence equations are solved + +The turbulence model has its own solver object. It sits at position `TURB_SOL` of the solver container, on every multigrid level. The coupling with the flow is segregated: the flow and the turbulence model are solved one after the other, not as one system. In each inner iteration, `CFluidIteration::Iterate` (`SU2_CFD/src/iteration/CFluidIteration.cpp`) first runs the flow (`MultiGrid_Iteration`). Then `CFluidIteration::CommonAuxiliarySolvers` runs the transition model and the turbulence model: + +```cpp + if (config[val_iZone]->GetKind_Turb_Model() != TURB_MODEL::NONE && !frozen_visc) { + + /*--- Solve transition model ---*/ + + if (config[val_iZone]->GetKind_Trans_Model() == TURB_TRANS_MODEL::LM) { + config[val_iZone]->SetGlobalParam(main_solver, RUNTIME_TRANS_SYS); + integration[val_iZone][val_iInst][TRANS_SOL]->SingleGrid_Iteration(geometry, solver, numerics, config, + RUNTIME_TRANS_SYS, val_iZone, val_iInst); + } + + /*--- Solve the turbulence model ---*/ + + config[val_iZone]->SetGlobalParam(main_solver, RUNTIME_TURB_SYS); + integration[val_iZone][val_iInst][TURB_SOL]->SingleGrid_Iteration(geometry, solver, numerics, config, + RUNTIME_TURB_SYS, val_iZone, val_iInst); + } +``` + +`SetGlobalParam(..., RUNTIME_TURB_SYS)` selects the turbulence settings: `CONV_NUM_METHOD_TURB`, `MUSCL_TURB`, `SLOPE_LIMITER_TURB` and `TIME_DISCRE_TURB`. Then `CSingleGridIntegration::SingleGrid_Iteration` (`SU2_CFD/src/integration/CSingleGridIntegration.cpp`) runs these steps on the finest grid: + +1. `Preprocessing`: gradients and limiters of the turbulence variables (`CScalarSolver::CommonPreprocessing`). +2. `SetTime_Step`: the local time step. It is the flow time step, scaled by the ratio of the turbulence and flow CFL numbers. +3. `CIntegration::Space_Integration`: `Upwind_Residual` (convection **and** diffusion, see below), `Viscous_Residual` (empty for the turbulence solvers), `Source_Residual`, then the boundary conditions. +4. `CIntegration::Time_Integration`: implicit or explicit update. +5. `Postprocessing`: gradients and the eddy viscosity. +6. For `RUNTIME_TURB_SYS` only: restriction of the solution and of the eddy viscosity to the coarse grids (`SetRestricted_Solution`, `SetRestricted_EddyVisc`). Restriction transfers values from the fine grid to the coarse grids. The flow multigrid uses these values. + +The flow solver reads the new eddy viscosity in its next `Preprocessing` (see [Coupling with the flow solver](#coupling-with-the-flow-solver)). + +## Class hierarchy + +Each model is a set of classes: a solver, a variable class, a source term and an edge kernel. The table lists the base classes and the SA and SST classes for each role. + +| Role | Base classes | SA | SST | +|---|---|---|---| +| Solver | [`CSolver`](../../doxygen/classCSolver.html) → [`CScalarSolver`](../../doxygen/classCScalarSolver.html)`` → [`CTurbSolver`](../../doxygen/classCTurbSolver.html) | [`CTurbSASolver`](../../doxygen/classCTurbSASolver.html) | [`CTurbSSTSolver`](../../doxygen/classCTurbSSTSolver.html) | +| Variables | [`CVariable`](../../doxygen/classCVariable.html) → [`CScalarVariable`](../../doxygen/classCScalarVariable.html) → [`CTurbVariable`](../../doxygen/classCTurbVariable.html) | [`CTurbSAVariable`](../../doxygen/classCTurbSAVariable.html) | [`CTurbSSTVariable`](../../doxygen/classCTurbSSTVariable.html) | +| Source term | [`CNumerics`](../../doxygen/classCNumerics.html) | [`CSourceBase_TurbSA`](../../doxygen/classCSourceBase__TurbSA.html), made by `SAFactory` | [`CSourcePieceWise_TurbSST`](../../doxygen/classCSourcePieceWise__TurbSST.html) | +| Convection and diffusion | [`CAvgGradScalarBase`](../../doxygen/classCAvgGradScalarBase.html) → [`CUpwScalarFlux`](../../doxygen/classCUpwScalarFlux.html) → [`CUpwScalarBase`](../../doxygen/classCUpwScalarBase.html) | [`CScalarFlux_SA`](../../doxygen/classCScalarFlux__SA.html) | [`CScalarFlux_SST`](../../doxygen/classCScalarFlux__SST.html) | + +The transition solver [`CTransLMSolver`](../../doxygen/classCTransLMSolver.html) also derives from `CTurbSolver`. + +The base classes already do most of the work: + +- `CScalarSolver` (`SU2_CFD/include/solvers/CScalarSolver.hpp` and `.inl`) provides: + - gradients and limiters; + - the generic edge loop `EdgeFluxResidual` and the generic boundary pass `BoundaryFluxResidual`; + - the local time step, the implicit and explicit updates, and dual time stepping; + - periodic boundaries; + - the clipping of the solution between `lowerlimit` and `upperlimit`, in `CompleteImplicitIteration`. + + Its constructor argument `conservative` says if the transported quantity is the solution times the density. It is true for SST and false for SA. +- `CTurbSolver` (`SU2_CFD/src/solvers/CTurbSolver.cpp`) provides `LoadRestart`, `Impose_Fixed_Values` (option `TURB_FIXED_VALUES`), `RegisterSolutionExtra`, `BC_Giles`, `BC_TurboRiemann`, the inlet storage `Inlet_TurbVars` and `ComputeUnderRelaxationFactorHelper`. `RegisterSolutionExtra` registers the eddy viscosity for the discrete adjoint. +- `CTurbVariable` (`SU2_CFD/include/variables/CTurbVariable.hpp`) holds the eddy viscosity `muT` (`GetmuT`, `SetmuT`), `turb_index` and `intermittency`. It sets `MAXNVAR = 4`, the size of the static arrays of the scalar solver. + +## Case A: a variant of an existing model + +A variant is a correction or another version of SA or SST. It does not need a new solver. It needs three things: + +1. A keyword in `SA_OPTIONS` or `SST_OPTIONS`. +2. A field in the parsed options struct. This struct holds the result of reading the option list. +3. A change in the source term. Some variants change the diffusion, the eddy viscosity or the wall values instead. + +### How the model options are parsed + +`SA_OPTIONS` and `SST_OPTIONS` are enum list options. So is `LM_OPTIONS`, for transition. The user can give several keywords, for example `SA_OPTIONS= NEGATIVE, WITHFT2`. `Common/include/option_structure.hpp` defines four things for each option: an enum, a map from keywords to enum values, a struct with the parsed result, and a parse function: + +```cpp +enum class SA_OPTIONS { + NONE, /*!< \brief No option / default. */ + NEG, /*!< \brief Negative SA. */ + EDW, /*!< \brief Edwards version. */ + FT2, /*!< \brief Use FT2 term. */ + // ... + EXP, /*!< \brief Allow experimental combinations of options (according to TMR). */ +}; +static const MapType SA_Options_Map = { + MakePair("NONE", SA_OPTIONS::NONE) + MakePair("NEGATIVE", SA_OPTIONS::NEG) + MakePair("EDWARDS", SA_OPTIONS::EDW) + MakePair("WITHFT2", SA_OPTIONS::FT2) + // ... +}; + +struct SA_ParsedOptions { + SA_OPTIONS version = SA_OPTIONS::NONE; /*!< \brief SA base model. */ + bool ft2 = false; /*!< \brief Use ft2 term. */ + bool qcr2000 = false; /*!< \brief Use QCR-2000. */ + bool comp = false; /*!< \brief Use compressibility correction. */ + bool rot = false; /*!< \brief Use rotation correction. */ + bool bc = false; /*!< \brief BC transition. */ +}; +``` + +`ParseSAOptions` turns the list into the struct. It checks for conflicts with a small lambda: + +```cpp + auto IsPresent = [&](SA_OPTIONS option) { + const auto sa_options_end = SA_Options + nSA_Options; + return std::find(SA_Options, sa_options_end, option) != sa_options_end; + }; +``` + +Then it sets `version` (`NONE`, `NEG` or `EDW`) and the booleans. Some combinations are not standard versions of SA. The parser stops on them, unless `EXPERIMENTAL` is in the list. Its error message refers to the NASA Turbulence Modeling Resource. + +`SST_ParsedOptions` and `ParseSSTOptions` work the same way. The struct has these fields: + +- `version`: `V1994` or `V2003`. +- `production`: one production modifier, `V`, `KL`, `UQ`, `COMP_Wilcox` or `COMP_Sarkar`. +- the booleans `sust`, `uq`, `modified`, `compWilcox`, `compSarkar` and `dll`. + +With no version keyword, the parser selects `V2003` with `modified = true`. The keywords `V1994` and `V2003` (the unmodified versions) are commented out in `SST_Options_Map`, so users cannot select them. + +`CConfig::SetConfig_Options` registers the options: + +```cpp + addEnumListOption("SST_OPTIONS", nSST_Options, SST_Options, SST_Options_Map); + // ... + addEnumListOption("SA_OPTIONS", nSA_Options, SA_Options, SA_Options_Map); +``` + +`CConfig::SetPostprocessing` parses them, only for the selected model: + +```cpp + /*--- Postprocess SST_OPTIONS into structure. ---*/ + if (Kind_Turb_Model == TURB_MODEL::SST) { + sstParsedOptions = ParseSSTOptions(SST_Options, nSST_Options, rank); + } else if (Kind_Turb_Model == TURB_MODEL::SA) { + saParsedOptions = ParseSAOptions(SA_Options, nSA_Options, rank); + } +``` + +The rest of the code reads the result with `config->GetSAParsedOptions()` and `config->GetSSTParsedOptions()`. + +### Steps for a new option + +1. Add the value to `enum class SA_OPTIONS` (or `SST_OPTIONS`). Add a `MakePair` line to `SA_Options_Map` (or `SST_Options_Map`). +2. Add a field to `SA_ParsedOptions` (or `SST_ParsedOptions`). Set it in `ParseSAOptions` (or `ParseSSTOptions`) with `IsPresent`. Add the conflict checks. For SA, decide if the new option is allowed without `EXPERIMENTAL`. +3. Use the field where the model changes. This is usually the source term (see the next sections). Sometimes it is the diffusion, the eddy viscosity, the limits or the wall values. +4. Add the checks that involve other options in `CConfig::SetPostprocessing`. Example from the code: + + ```cpp + if (Kind_Solver == MAIN_SOLVER::INC_RANS && sstParsedOptions.compSarkar){ + SU2_MPI::Error("COMPRESSIBILITY-SARKAR only supported for SOLVER= RANS", CURRENT_FUNCTION); + } + ``` + +5. Print the variant in the start-up summary in `CConfig::SetOutput`. This is the `switch (Kind_Turb_Model)` that prints `Spalart-Allmaras-noft2-R...` or `Menter's k-omega SST-2003m ...`. +6. Add the keyword to the comment of `SA_OPTIONS` or `SST_OPTIONS` in `config_template.cfg`. +7. Add a regression test (see [Tests](#tests)). + +### Where an SA variant changes the source term + +The SA source term is a class template in `SU2_CFD/include/numerics/turbulent/turb_sources.hpp`. Each piece of the model that changes between versions is a template parameter: + +```cpp +template +class CSourceBase_TurbSA : public CNumerics { +``` + +| Parameter | Computes | Implementations in `namespace detail` | +|---|---|---| +| `Omega` | the vorticity (or strain) magnitude `var.Omega` | `Omega::Bsl`, `Omega::Edw` | +| `ft2` | `var.ft2` and `var.d_ft2` | `ft2::Zero`, `ft2::Nonzero` | +| `ModVort` | the modified vorticity `var.Shat` and `var.d_Shat` | `ModVort::Bsl`, `ModVort::Edw`, `ModVort::Neg` | +| `rFunc` | `var.r` and `var.d_r` | `r::Bsl`, `r::Edw` | +| `SourceTerms` | production, destruction and the Jacobian | `SourceTerms::Bsl`, `SourceTerms::Neg` | + +Each implementation is a struct with a static `get` function. It reads and writes the shared struct `CSAVariables`. This struct holds constants such as `cb1`, `cw2` and `cv1_3`. It also holds the auxiliary values and their derivatives with respect to the SA variable, named `d_...`. For example: + +```cpp +/*! \brief Non-zero ft2 term according to the literature. */ +struct Nonzero { + static void get(CSAVariables& var) { + const su2double xsi2 = pow(var.Ji, 2); + var.ft2 = var.ct3 * exp(-var.ct4 * xsi2); + var.d_ft2 = -2.0 * var.ct4 * var.Ji * var.ft2 * var.d_Ji; + } +}; +``` + +`CSourceBase_TurbSA::ComputeResidual` calls them in this order: `Omega::get`, `ModVort::get`, `ft2::get`, `rFunc::get`, the `fw` function, then `SourceTerms::get`. It returns the residual and the 1x1 Jacobian: + +```cpp + /*--- Compute production, destruction and jacobian ---*/ + su2double Production = 0.0, Destruction = 0.0; + SourceTerms::get(ScalarVar_i[0], var, Production, Destruction, Jacobian_i[0][0]); + // ... + Residual[0] = (Production - Destruction) * Volume; + // ... + Jacobian_i[0][0] *= Volume; +``` + +Helper structs and `SAFactory` instantiate the template. The driver calls `SAFactory`: + +```cpp +template +struct BaselineSA { + template + using type = CSourceBase_TurbSA; +}; +// ... NegativeSA, EdwardsSA + +template +CNumerics* SAFactory(unsigned short nDim, const CConfig* config) { + const auto options = config->GetSAParsedOptions(); + + switch (options.version) { + case SA_OPTIONS::NONE: + return detail::SAFactoryImpl>(options.ft2, options.comp, nDim, config); + case SA_OPTIONS::NEG: + return detail::SAFactoryImpl>(options.ft2, options.comp, nDim, config); + case SA_OPTIONS::EDW: + return detail::SAFactoryImpl>(options.ft2, options.comp, nDim, config); + default: + break; + } + return nullptr; +} +``` + +`SAFactoryImpl` picks `ft2::Nonzero` or `ft2::Zero`. When `options.comp` is set, `AddCompressibilityCorrection` wraps the class in the decorator `CCompressibilityCorrection`. A decorator is a class that derives from the source class and adds to its result. + +There are three ways to add an SA variant. Choose the one that matches the change: + +1. **A different version of one piece**, for example another modified vorticity: + - write a new struct with a static `get` in the matching group (`ModVort`, `r`, ...); + - set the value **and** its derivative; + - add a helper like `BaselineSA` that uses it; + - add a `case` to `SAFactory`. +2. **An extra term added to the residual**, like SA-comp. Write a decorator like `CCompressibilityCorrection`. Its `ComputeResidual` calls `ParentClass::ComputeResidual(config)`. It then changes `this->Residual[0]` and `this->Jacobian_i[0][0]`. Select the decorator in a factory helper like `AddCompressibilityCorrection`. +3. **A small switch inside the common code.** Test the option in `CSourceBase_TurbSA::ComputeResidual`, which keeps its own copy of the options (`options`). The rotation correction does this: + + ```cpp + /*--- Dacles-Mariani et. al. rotation correction ("-R"). ---*/ + if (options.rot) { + var.Prod += var.CRot * min(0.0, StrainMag_i - var.Omega); + /*--- Do not allow negative production for SA-neg. ---*/ + if (ScalarVar_i[0] < 0) var.Prod = abs(var.Prod); + } + ``` + +Some SA options also act outside the source term: + +- `NEGATIVE` changes three things: + - the diffusion coefficient (`fn` in `CScalarFlux_SA`, `turb_sa_edge_flux.hpp`); + - the eddy viscosity, which `CTurbSASolver::Postprocessing` sets to zero for a negative SA variable; + - the lower limit. The `CTurbSASolver` constructor sets `lowerlimit[0] = EPS` only for the other versions. +- `QCR2000` changes the viscous stresses of the flow, in `SU2_CFD/src/numerics/flow/flow_diffusion.cpp` and `SU2_CFD/include/numerics_simd/flow/diffusion/viscous_fluxes.hpp`. +- `BCM` changes the source term (`options.bc` in `ComputeResidual`). It also changes the free-stream and inlet values, with the factor `0.005` in the `CTurbSASolver` constructor and in `CTurbSASolver::BC_Inlet`. + +### Where an SST variant changes the source term + +The SST source term is `CSourcePieceWise_TurbSST`, in the same file. It reads the parsed options from `sstParsedOptions`. This is a member of `CNumerics`, set by the `CNumerics` constructor with `config->GetSSTParsedOptions()`. The production modifiers are a `switch`: + +```cpp + /*--- Apply production term modifications ---*/ + switch (sstParsedOptions.production) { + case SST_OPTIONS::UQ: + ComputePerturbedRSM(nDim, Eig_Val_Comp, uq_permute, uq_delta_b, uq_urlx, PrimVar_Grad_i + idx.Velocity(), + Density_i, Eddy_Viscosity_i, ScalarVar_i[0], MeanPerturbedRSM); + P_Base = PerturbedStrainMag(ScalarVar_i[0]); + break; + + case SST_OPTIONS::V: + P_Base = VorticityMag; + break; + // ... KL, COMP_Wilcox, COMP_Sarkar + + default: + /*--- Base production term for SST-1994 and SST-2003 ---*/ + P_Base = StrainMag_i; + break; + } +``` + +`production` holds one value. `ParseSSTOptions` first sets it from `VORTICITY`, `KATO-LAUNDER` or `UQ`. Then it sets it from the compressibility keywords, which overwrite the first value. + +The other SST options act in these places: + +| Option field | Where | +|---|---| +| `version` | The constants `constants[8]`, `constants[9]`, `constants[10]` in the `CTurbSSTSolver` constructor. The `pw` term in `CSourcePieceWise_TurbSST::ComputeResidual`. The eddy viscosity, which uses the vorticity for `V1994` and the strain rate otherwise, both in the source term (`zeta`) and in `CTurbSSTSolver::Postprocessing`. | +| `sust` | The sustaining terms (`kAmb`, `omegaAmb`) in `ComputeResidual` | +| `dll` | The lower limits `lowerlimit[0]`, `lowerlimit[1]` in the `CTurbSSTSolver` constructor (options `LOWER_LIMIT_K_FACTOR`, `LOWER_LIMIT_OMEGA_FACTOR`) | +| `uq` | The Reynolds stress of the flow viscous flux (`viscous_fluxes.hpp`) | + +The source Jacobian of SST has only the dissipation terms: `Jacobian_i[0][0]`, `Jacobian_i[0][1]` and `Jacobian_i[1][1]`. The production is treated explicitly. `CTurbSSTSolver` is conservative, so the Jacobian is with respect to the conservative variables: density times k, and density times omega. + +To add an SST production modifier: + +1. Add the keyword. +2. Set `production` in `ParseSSTOptions`, and update the error message that lists the modifiers. +3. Add a `case` to the `switch`. +4. Print it in `CConfig::SetOutput`. + +## Case B: a new model with its own equations + +A new model needs its own solver, variable, source and edge kernel classes. Start from a copy of `CTurbSSTSolver` (2 equations, conservative) or `CTurbSASolver` (1 equation, not conservative). For the general steps of a new solver, see [Adding a Solver](../Adding-a-Solver/). + +### 1. Model keyword and family + +In `Common/include/option_structure.hpp`, add a value to `TURB_MODEL` and to `Turb_Model_Map`. Then map it to a family in `TurbModelFamily`: + +```cpp +inline TURB_FAMILY TurbModelFamily(TURB_MODEL model) { + switch (model) { + case TURB_MODEL::NONE: + return TURB_FAMILY::NONE; + case TURB_MODEL::SA: + return TURB_FAMILY::SA; + case TURB_MODEL::SST: + return TURB_FAMILY::KW; + } + return TURB_FAMILY::NONE; +} +``` + +`TURB_FAMILY` has three values: `NONE`, `SA` (SA variants) and `KW` (k-omega models). Most of the code switches on the family, not on the model. If the new model does not fit an existing family, add a value to `TURB_FAMILY`. The compiler then warns at each `switch (TurbModelFamily(...))` that does not handle it. These switches are in: + +- `CConfig::SetPostprocessing`: the number of turbulence variables `nTurbVar`; +- `CSolverFactory::CreateTurbSolver`: which solver class is created; +- `CFlowOutput` and `CAdjFlowOutput`: the output fields; +- `CSolver::LoadInletProfile`: the column names of the inlet profile template file. + +Some code tests `TURB_MODEL::SST` or `TURB_MODEL::SA` directly. Check each place with `git grep -n "TURB_MODEL::SST"` and `git grep -n "TURB_MODEL::SA\b"`. The main ones are: + +- `tkeNeeded`. The flow solvers read the turbulent kinetic energy for `TURB_MODEL::SST`, including its contribution to the compressible total energy. Check `CNSSolver::SetPrimitive_Variables`, `CIncNSSolver`, `CFVMFlowSolverBase::Viscous_Residual_impl`, and the boundary conditions in `CEulerSolver.cpp` and `CIncEulerSolver.cpp` for a new model that transports k. Passing k to the viscous numerics does not mean it enters the stress tensor: ordinary SST uses the "m" form, which omits the isotropic k term. With `SST_OPTIONS= UQ`, the perturbed Reynolds stress tensor includes k. See [Turbulence Models](../Turbulence-Models/#sst). +- `CConfig::GetInlet_TurbVal`, and the check of the number of values of `MARKER_INLET_TURBULENT` in `CConfig::SetPostprocessing`. +- `CDriver::InstantiateTurbulentNumerics`. +- The turbomachinery averaging in `CEulerSolver::PreprocessAverage` and `CEulerSolver::TurboAverageProcess`. + +### 2. Configuration + +Add the checks of the new model in `CConfig::SetPostprocessing`, and its name in `CConfig::SetOutput`. If users can change options or constants of the model, add them as described in [Adding a Config Option](../Adding-a-Config-Option/). For variants, follow the `SA_OPTIONS` pattern above. + +### 3. Variable class + +Derive from `CTurbVariable`. The constructor sets the initial solution and allocates `muT`. `CTurbVariable` does not allocate it. From `CTurbSSTVariable`: + +```cpp +CTurbSSTVariable::CTurbSSTVariable(su2double kine, su2double omega, su2double mut, unsigned long npoint, unsigned long ndim, unsigned long nvar, const su2double* constants, CConfig *config) + : CTurbVariable(npoint, ndim, nvar, config) { + // ... + for(unsigned long iPoint=0; iPointGetCFL(MGLevel)*config->GetCFLRedCoeff_Turb()`) and `SolverName`. + +Then implement these functions: + +| Function | What it does in SST | +|---|---| +| `Preprocessing` | `CommonPreprocessing(geometry, config, Output)` | +| `Postprocessing` | gradients, blending functions, eddy viscosity (see [Coupling with the flow solver](#coupling-with-the-flow-solver)) | +| `Upwind_Residual` | the edge loop with the model kernel (step 5) | +| `Source_Residual` | the source term at each point (step 6) | +| `BC_HeatFlux_Wall`, `BC_Isothermal_Wall` | wall values (see [Walls](#walls-wall-functions-and-wall-distance)) | +| `BC_Inlet`, `BC_Outlet`, `BC_Far_Field`, `BC_Inlet_MixingPlane`, `BC_Inlet_Turbo`, `BC_Fluid_Interface` | ghost state, then the shared boundary flux | +| `SetInletAtVertex`, `GetInletAtVertex`, `SetUniformInlet` | inlet profile files | +| `ComputeUnderRelaxationFactor` | calls `ComputeUnderRelaxationFactorHelper` with `MAX_UPDATE_SST` (SA uses `MAX_UPDATE_SA`) | +| `GetConstants`, `GetTke_Inf`, `GetOmega_Inf` | used by the driver to build the source numerics | + +The solver `.cpp` file includes `SU2_CFD/include/solvers/CScalarSolver.inl`. This is needed because `EdgeFluxResidual` and `BoundaryFluxResidual` are member templates, instantiated with the kernel of the model. + +### 5. Convection and diffusion: the edge kernel + +Each model has an edge kernel. It computes the convection and the diffusion of one edge, with their Jacobians, in one pass. The turbulence solvers do not use `CNumerics` objects for these terms. The interior loop is one call: + +```cpp +void CTurbSSTSolver::Upwind_Residual(CGeometry* geometry, CSolver** solver_container, CNumerics**, + CConfig* config, unsigned short iMesh) { + SU2_ZONE_SCOPED + + const auto opt = ScalarFluxOptions::Interior(*config, config->GetBounded_Turb(), + config->GetUse_Accurate_Turb_Jacobians()); + + DispatchScheme(config, [&](auto tag) { + EdgeFluxResidual(geometry, solver_container, config, opt); + }); +} +``` + +`DispatchScheme` turns run-time settings into a compile-time kernel type. It picks the flow indices (compressible or incompressible), the dimension and the number of equations at run time. It then calls the lambda with the matching kernel type. The template arguments after the kernel are the allowed numbers of equations. SA uses `DispatchScheme`. `ScalarFluxOptions` has named constructors for the interior (`Interior`) and for the boundaries (`BoundaryConvective`, `BoundaryFull`, `BoundaryDiffusive`). + +A kernel derives from `CUpwScalarBase` (`SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp`). It provides: + +- `static constexpr bool Conservative`: the convected quantity is density times the solution; +- `static constexpr bool DiagonalDiffusion`: `coefficients` returns a vector (one coefficient per equation) instead of a matrix; +- `coefficients(...)`: the diffusion coefficients for both orientations of the edge. + +It can also provide: + +- `coefficientJacobians(...)`: extra Jacobian terms when the coefficients depend on the solution, used with `USE_ACCURATE_TURB_JACOBIANS`; +- `finalizeFlux(...)`: a convective flux different from the default upwind one, as SA does; +- `extraDiffusionTerms(...)`: diffusion terms beyond the standard one, as the flamelet kernel does; +- `DiffusionReadsDensity`: set it to true when the diffusion coefficients need the density, as SA does. The base class then reads the density. + +The SST kernel declares: + +```cpp +template +class CScalarFlux_SST + : public CUpwScalarBase, FlowIndices, nDim, nVar> { + public: + static constexpr bool Conservative = true; + static constexpr bool DiagonalDiffusion = false; +``` + +Its `coefficients` returns a 2x2 matrix per side. The matrix holds the blended diffusion of k and omega and the cross-diffusion term. The coefficients read, through `gatherVariables`: + +- the flow primitives (`GetPrimitive`); +- the turbulence containers (`GetSolution`, `GetF1blending`). + +So any variable container that a kernel reads must be stored at every point, like `CTurbVariable::GetF1blending()`. `CTransLMSolver` has the smallest kernel, `CScalarFlux_TransLM`. See [Adding a Transition Model](../Adding-a-Transition-Model/). + +### 6. Source term + +Write a class in `SU2_CFD/include/numerics/turbulent/turb_sources.hpp`. It derives from `CNumerics` and is a template on `FlowIndices`. `FlowIndices` is the index helper of the flow primitives: `idx.Density()`, `idx.Velocity()`, `idx.LaminarViscosity()`, ... Follow `CSourcePieceWise_TurbSST`: + +- Call `CNumerics(nDim, nVar, config)` in the constructor. Keep the residual and the Jacobian in fixed arrays: `Residual[2]`, and `Jacobian_i[2]` pointing into `Jacobian_Buffer[4]`. +- In `ComputeResidual(const CConfig* config)`, fill `Residual` and `Jacobian_i`, and return `ResidualType<>(Residual, Jacobian_i, nullptr)`. `Residual` is the source integrated over the volume, positive for production. `Jacobian_i` is its derivative with respect to the conservative variables of the model. +- Register every input with `AD::SetPreaccIn`, between `AD::StartPreacc()` and `AD::SetPreaccOut(Residual, nVar); AD::EndPreacc();` (see [Discrete adjoint](#discrete-adjoint)). + +The solver sets the inputs point by point in `Source_Residual`. It adds the result with a minus sign, because the residual is "fluxes minus sources": + +```cpp + /*--- Compute the source term ---*/ + + auto residual = numerics->ComputeResidual(config); + // ... + /*--- Subtract residual and the Jacobian ---*/ + + LinSysRes.SubtractBlock(iPoint, residual); + if (implicit) Jacobian.SubtractBlock2Diag(iPoint, residual.jacobian_i); +``` + +The inputs are set with the setters of `CNumerics`: `SetPrimitive`, `SetPrimVarGradient`, `SetScalarVar`, `SetScalarVarGradient`, `SetVolume`, `SetDistance`, `SetVorticity`, `SetStrainMag`, `SetCoord` and `SetIntermittencyEff`. The SST setters are virtual: `SetF1blending`, `SetF2blending`, `SetCrossDiff`. If the model needs a new input, add a virtual setter to `CNumerics` (`SU2_CFD/include/numerics/CNumerics.hpp`). + +### 7. Create the source numerics + +`CDriver::InstantiateTurbulentNumerics` (`SU2_CFD/src/drivers/CDriver.cpp`) creates one source object per multigrid level. It also creates one per OpenMP thread, through `offset`. Add the model to the `switch` on `GetKind_Turb_Model()` at the top, and to the loop: + +```cpp + for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { + auto& turb_source_first_term = numerics[iMGlevel][TURB_SOL][source_first_term]; + + if (spalart_allmaras) + turb_source_first_term = SAFactory(nDim, config); + else if (menter_sst) + turb_source_first_term = new CSourcePieceWise_TurbSST(nDim, nVar_Turb, constants, kine_Inf, omega_Inf, + config); + + numerics[iMGlevel][TURB_SOL][source_second_term] = new CSourceNothing(nDim, nVar_Turb, config); + } +``` + +The function is a template on the flow indices. At the end of its definition, it is instantiated for `CEulerVariable::CIndices` and `CIncEulerVariable::CIndices`. It also checks that `CONV_NUM_METHOD_TURB` is an upwind scheme. It creates no convective or viscous numerics, because the edge kernels replace them. + +### 8. Create the solver + +`CSolverFactory::CreateTurbSolver` (`SU2_CFD/src/solvers/CSolverFactory.cpp`) switches on the family: + +```cpp + case TURB_FAMILY::KW: + turbSolver = new CTurbSSTSolver(geometry, config, solver[FLOW_SOL], iMGLevel); + solver[FLOW_SOL]->Preprocessing(geometry, solver, config, iMGLevel, NO_RK_ITER, RUNTIME_FLOW_SYS, true); + turbSolver->Postprocessing(geometry, solver, config, iMGLevel); + solver[FLOW_SOL]->Preprocessing(geometry, solver, config, iMGLevel, NO_RK_ITER, RUNTIME_FLOW_SYS, true); + break; +``` + +The calls after the constructor compute the initial eddy viscosity and pass it to the flow. When `adjoint` is true, the same function also creates the adjoint solver (`CDiscAdjSolver`). This part needs no change. + +### 9. Build + +Add the new `.cpp` files to `SU2_CFD/src/meson.build`, next to `solvers/CTurbSSTSolver.cpp` and `variables/CTurbSSTVariable.cpp`. + +## Coupling with the flow solver + +The flow reads two things from the turbulence solver: the eddy viscosity and, for SST, the turbulent kinetic energy. `CNSSolver::SetPrimitive_Variables` (`SU2_CFD/src/solvers/CNSSolver.cpp`) copies them into the primitive variables of each point: + +```cpp + if (turb_model != TURB_MODEL::NONE && solver_container[TURB_SOL] != nullptr) { + eddy_visc = solver_container[TURB_SOL]->GetNodes()->GetmuT(iPoint); + if (tkeNeeded) turb_ke = solver_container[TURB_SOL]->GetNodes()->GetSolution(iPoint,0); + // ... + } + // ... + bool physical = ns_nodes->SetPrimVar(iPoint, eddy_visc, turb_ke, GetFluidModel()); +``` + +`CIncNSSolver::SetPrimitive_Variables` does the same. `CNEMONSSolver` reads the eddy viscosity in the same way. `CFVMFlowSolverBase::Viscous_Residual_impl` passes k to the viscous numerics with `SetTurbKineticEnergy`. + +The turbulence solver computes the eddy viscosity in `Postprocessing` and stores it with `nodes->SetmuT`. For SST: + +```cpp + const auto& eddy_visc_var = sstParsedOptions.version == SST_OPTIONS::V1994 ? VorticityMag : StrainMag; + const su2double muT = max(0.0, rho * a1 * kine / max(a1 * omega, eddy_visc_var * F2)); + + nodes->SetmuT(iPoint, muT); +``` + +SA (`CTurbSASolver::Postprocessing`) computes `muT = rho*fv1*nu_hat`, with the roughness correction of `Ji`. With `SA_OPTIONS= NEGATIVE`, it sets `muT = 0` for a negative SA variable. + +In the other direction, the turbulence solver reads the flow through `solver_container[FLOW_SOL]`: + +- density, laminar viscosity and velocity gradients; +- vorticity and strain rate magnitude (`GetVorticity`, `GetStrainMag`); +- in the edge kernels, the velocity and the eddy viscosity from the primitive variables. + +## Walls, wall functions and wall distance + +**Wall distance.** `CGeometry::ComputeWallDistance` (`Common/src/geometry/CGeometry.cpp`) computes the distance to the nearest viscous wall. It does this for the `RANS` and `INC_RANS` solvers and their adjoints, and for `FEM_RANS` and `FEM_LES`. The drivers call it at start-up. `CSinglezoneDriver` calls it again when the mesh moves or deforms. Read it with `geometry->nodes->GetWall_Distance(iPoint)`. SA adds `0.03` times the roughness height (`GetRoughnessHeight`) to it. + +**Wall values.** `BC_HeatFlux_Wall` imposes the wall values directly, as Dirichlet conditions. `BC_Isothermal_Wall` calls it. The pattern has three steps: + +1. Set the value in the solution. +2. Zero the residual row. +3. Replace the Jacobian row by the identity. + +From `CTurbSSTSolver::BC_HeatFlux_Wall` (smooth wall): + +```cpp + solution[0] = 0.0; + solution[1] = 60.0*laminar_viscosity/(density*beta_1*pow(wall_dist,2)); + } + + /*--- Set the solution values and zero the residual ---*/ + nodes->SetSolution_Old(iPoint, solution); + nodes->SetSolution(iPoint, solution); + LinSysRes.SetBlock_Zero(iPoint); + + if (implicit) { + /*--- Change rows of the Jacobian (includes 1 in the diagonal) ---*/ + Jacobian.DeleteValsRowi(iPoint, 0); + Jacobian.DeleteValsRowi(iPoint, 1); + } +``` + +Rough walls are read with `config->GetWallRoughnessProperties(Marker_Tag)` (option `WALL_ROUGHNESS`). SST chooses the rough wall values with `KIND_ROUGHSST_MODEL` (`ROUGHSST_MODEL` in `option_structure.hpp`). On rough walls, SA imposes a flux instead of a value. The wall-normal derivative of the SA variable equals the SA variable divided by 0.03 times the roughness height. + +**Wall functions.** With `MARKER_WALL_FUNCTIONS`, the flow solver computes the wall shear stress and the eddy viscosity at the wall (`CNSSolver::SetTau_Wall_WF`). `BC_HeatFlux_Wall` then calls `SetTurbVars_WF`. This function fixes the turbulence variables at the first point off the wall (`GetNormal_Neighbor()`). It uses `GetYPlus`, `GetUTau` and `GetEddyViscWall`, and acts only when y+ is above `WALLMODEL_MINYPLUS`. A new model needs its own `SetTurbVars_WF`. + +The other boundary conditions (inlet, outlet, far field, ...) write the outside state into `ghostNodes` and call the shared boundary flux. See [Adding a Boundary Condition](../Adding-a-Boundary-Condition/). + +## Free-stream, initial and inlet values + +Three options set the free-stream turbulence. They are registered in `CConfig::SetConfig_Options`: + +| Option | Getter | Used by | +|---|---|---| +| `FREESTREAM_TURBULENCEINTENSITY` | `GetTurbulenceIntensity_FreeStream()` | SST (k), LM, SA-BCM | +| `FREESTREAM_TURB2LAMVISCRATIO` | `GetTurb2LamViscRatio_FreeStream()` | SST (omega) | +| `FREESTREAM_NU_FACTOR` | `GetNuFactor_FreeStream()` | SA | + +Each turbulence solver computes its free-stream state `Solution_Inf` in its constructor. It uses the non-dimensional free-stream flow: `GetDensity_FreeStreamND()`, `GetVelocity_FreeStreamND()`, `GetViscosity_FreeStreamND()`. It then initializes all points with this state. For SST: + +```cpp + su2double kine_Inf = 3.0/2.0*(VelMag2*Intensity*Intensity); + su2double omega_Inf = rhoInf*kine_Inf/(muLamInf*viscRatio); + + Solution_Inf[0] = kine_Inf; + Solution_Inf[1] = omega_Inf; +``` + +`CEulerSolver::SetNondimensionalization` also computes the dimensional and non-dimensional free-stream k and omega. The incompressible and NEMO solvers do the same in their own `SetNondimensionalization`. The values are stored in `CConfig` (`SetTke_FreeStream`, `SetOmega_FreeStream`, ...). They are printed in the start-up table and used to write the inlet profile template. + +`Solution_Inf` has two uses: it is the far-field state (`BC_Far_Field`), and it is the value imposed by `TURB_FIXED_VALUES`. `Inlet_TurbVars` holds the inlet values for each vertex. Where they come from depends on the inlet type: + +- Without an inlet file, the values come from `MARKER_INLET_TURBULENT` or from the free-stream options, through `CConfig::GetInlet_TurbVal`. +- With an inlet file (`SPECIFIED_INLET_PROFILE= YES`), `SetInletAtVertex` reads the turbulence columns after the flow columns. `SetUniformInlet` fills the default values. + +## Output and restart + +The turbulence fields are declared and set in `CFlowOutput` (`SU2_CFD/src/output/CFlowOutput.cpp`). The functions that handle them switch on `TurbModelFamily(config->GetKind_Turb_Model())`: + +- `AddHistoryOutputFields_ScalarRMS_RES`, `AddHistoryOutputFields_ScalarMAX_RES`, `AddHistoryOutputFields_ScalarBGS_RES`; +- `LoadHistoryDataScalar`; +- `SetVolumeOutputFieldsScalarSolution`, `SetVolumeOutputFieldsScalarResidual`, `SetVolumeOutputFieldsScalarLimiter`; +- `LoadVolumeDataScalar`. + +The SST fields are `RMS_TKE`, `RMS_DISSIPATION`, `TKE`, `DISSIPATION`, `RES_TKE`, ... The SA fields are `RMS_NU_TILDE`, `NU_TILDE`, ... The fields `EDDY_VISCOSITY`, `TURB_DELTA_TIME` and `TURB_CFL` exist for every model. The adjoint fields are in `CAdjFlowOutput` (`RMS_ADJ_TKE`, `ADJ_TKE`, ...). To add fields, see [Adding Output Fields](../Adding-Output-Fields/). + +**Restart.** The restart file is the `SOLUTION` group of the volume output. `CTurbSolver::LoadRestart` reads the turbulence variables right after the coordinates and the flow variables: + +```cpp + /*--- Skip flow variables ---*/ + + unsigned short skipVars = nDim + solver[MESH_0][FLOW_SOL]->GetnVar(); +``` + +So declare the `SOLUTION` fields of a new model in `SetVolumeOutputFieldsScalarSolution`, in the order of the solution vector. `CFlowCompOutput::SetVolumeOutputFields` calls it right after the flow `SOLUTION` fields. + +After reading, `LoadRestart` restricts the solution to the coarse grids. When no species or transition model is active, it also computes the eddy viscosity: flow `Preprocessing`, then turbulence `Postprocessing`. Otherwise, the restart of those solvers does it. + +## Discrete adjoint + +A new model needs no adjoint equations. The discrete adjoint differentiates the primal code with CoDiPack, an automatic differentiation (AD) tool. Check these points: + +- `CSolverFactory::CreateTurbSolver` creates a `CDiscAdjSolver` for `TURB_SOL`, unless `FROZEN_VISC_DISC= YES`. `CDiscAdjFluidIteration` registers, records and extracts the turbulence solution together with the flow. +- `CTurbSolver::RegisterSolutionExtra` registers `muT` as an input and output of the recording. Keep it if the model computes the eddy viscosity in `Postprocessing`. +- The numerics use preaccumulation. Preaccumulation stores only the local derivatives of a code section's outputs with respect to its inputs on the tape (the record of operations that AD replays backwards). So every value that the result depends on must be declared as an input with `AD::SetPreaccIn`. A missing input gives wrong gradients without any error. For example, `CSourcePieceWise_TurbSST::ComputeResidual` registers the intermittency only when the LM model is on: + + ```cpp + if (config->GetKind_Trans_Model() == TURB_TRANS_MODEL::LM) { + AD::SetPreaccIn(intermittency_eff_i); + eff_intermittency = intermittency_eff_i; + } + ``` + +- Loops over points use `AD::StartNoSharedReading()` / `AD::EndNoSharedReading()`. These mark a section where no two OpenMP threads read the same value, which makes the reverse sweep faster. +- Add a test to `serial_regression_AD.py`. See `discadj_rans_naca0012_sa` and `discadj_rans_naca0012_sst`. + +## Hybrid RANS/LES + +Detached eddy simulation (DES) is implemented for SA only. `HYBRID_RANSLES` takes `SA_DES`, `SA_DDES`, `SA_ZDES` or `SA_EDDES` (`HybridRANSLES_Map`). `DES_CONST` sets the constant. The hooks are in `CTurbSASolver`: + +1. `CTurbSASolver::Preprocessing` calls `SetDES_LengthScale`. This stores the length scale and the LES sensor in the variable (`SetDES_LengthScale`, `SetLES_Mode`). +2. `CTurbSASolver::Source_Residual` passes the DES length scale to the source term instead of the wall distance: + + ```cpp + /*--- Set DES length scale ---*/ + + numerics->SetDistance(nodes->GetDES_LengthScale(iPoint), 0.0); + ``` + +3. `CNSSolver::SetPrimitive_Variables` copies the length scale and the LES sensor into the flow variables. The output reads them there (`DES_LENGTHSCALE`, `LES_SENSOR`). + +The stochastic backscatter model (`STOCHASTIC_BACKSCATTER`) adds three Langevin (stochastic) equations to the SA solver. The solver then has 4 variables. This is why `CScalarFlux_SA` is dispatched with 1 or 4 equations. A new model with a DES variant needs the same hooks in its own solver. + +## Tests + +These regression tests cover the turbulence models. The scripts are in `TestCases/`. + +| Model or feature | Test cases | Script | +|---|---|---| +| SA versions (baseline, negative, Edwards, compressibility, QCR) | `turbmod_sa_*_rae2822` in `TestCases/turbulence_models/sa/rae2822` | `parallel_regression.py` | +| SA, SST on airfoils and wings | `rae2822_sa`, `rae2822_sst`, `rae2822_sst_sust`, `turb_naca0012_sa`, `turb_naca0012_sst*`, `turb_oneram6` | `parallel_regression.py`, `serial_regression.py` | +| Flat plate, compressibility corrections, roughness | `turb_flatplate`, `turb_flatplate_CC_Wilcox`, `turb_flatplate_CC_Sarkar`, `turb_sst_flatplate_rough*` in `TestCases/rans/flatplate` | `parallel_regression.py`, `serial_regression.py` | +| Wall functions | `turb_sst_wallfunction_flatplate`, `turb_sa_wallfunction_flatplate`, `inc_turb_sst_wallfunction_flatplate`, ... in `TestCases/wallfunctions` | `serial_regression.py`, `parallel_regression.py` | +| SST-UQ | `turb_naca0012_1c`, ... in `TestCases/rans_uq` | `serial_regression.py`, `parallel_regression.py`, `hybrid_regression.py` | +| DDES, stochastic backscatter | `ddes_flatplate`, `sbs_backward_step` | `parallel_regression.py`, `hybrid_regression.py` | +| SA-BCM transition | `Schubauer_Klebanoff` in `TestCases/transition` | `serial_regression.py`, `parallel_regression.py` | +| Discrete adjoint | `discadj_rans_naca0012_sa`, `discadj_rans_naca0012_sst`, `discadj_incomp_turb_NACA0012_sa`, `discadj_incomp_turb_NACA0012_sst` | `serial_regression_AD.py`, `parallel_regression_AD.py` | + +**Verification and validation.** `TestCases/vandv.py` runs cases from `TestCases/vandv/rans`: + +- the flat plate (`flatplate_sst1994m`); +- the bump in channel (`bump_sst1994m`); +- a shock/boundary layer interaction (`swbli_sa`, `swbli_sst`); +- `dsma661_sa` and `dsma661_sst`; +- the 30P30N airfoil. + +The script runs each case for a few iterations only, like a regression test. For a new model, run the flat plate and the bump on several grid levels. Compare the skin friction and the eddy viscosity profiles with published results before you open the pull request. + +To add tests, follow [Running Regression Tests](../Running-Regression-Tests/#add-a-new-test). For a new model or option, add at least one entry to `parallel_regression.py`, for example a copy of a `turbulence_models/sa/rae2822` case with the new option. If the adjoint must work, also add one entry to `serial_regression_AD.py`. Use one configuration file per test. + +## Checklist + +1. Keyword: `TURB_MODEL` + `Turb_Model_Map` (+ `TurbModelFamily`, `TURB_FAMILY`), or `SA_OPTIONS`/`SST_OPTIONS` + map + parsed struct + parse function. +2. Checks in `CConfig::SetPostprocessing`, summary in `CConfig::SetOutput`, keyword in `config_template.cfg`. +3. Variable class derived from `CTurbVariable`, with `muT` allocated. +4. Solver class derived from `CTurbSolver`: constructor, `Preprocessing`, `Postprocessing` (eddy viscosity), `Upwind_Residual`, `Source_Residual`, wall and other boundary conditions, inlet functions. +5. Edge kernel derived from `CUpwScalarBase`. Source class derived from `CNumerics`, with the Jacobian and all preaccumulation inputs. +6. `CDriver::InstantiateTurbulentNumerics` and `CSolverFactory::CreateTurbSolver`. +7. Every `switch (TurbModelFamily(...))` and every test on `TURB_MODEL::SST` / `TURB_MODEL::SA` reviewed (`tkeNeeded`, inlet values, turbomachinery). +8. Output fields in `CFlowOutput` and `CAdjFlowOutput`. `SOLUTION` fields in the order the restart expects. +9. New files in `SU2_CFD/src/meson.build`. +10. Regression tests (primal and adjoint) and a verification case. + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Automatic-Differentiation.md b/Docs/website/pages/Automatic-Differentiation.md new file mode 100644 index 000000000000..f8925f0fab62 --- /dev/null +++ b/Docs/website/pages/Automatic-Differentiation.md @@ -0,0 +1,745 @@ +--- +title: Automatic Differentiation (AD) +permalink: /docs_v7/Automatic-Differentiation/ +--- + +SU2 computes derivatives of its results with automatic differentiation (AD), also called algorithmic +differentiation. This page explains how AD works in the code, and what you must do so that new code gives correct +derivatives. + +This page covers: + +- [What AD is in SU2](#what-ad-is-in-su2) +- [Building the AD executables](#building-the-ad-executables) +- [How the discrete adjoint runs](#how-the-discrete-adjoint-runs) +- [Forward mode (direct differentiation)](#forward-mode-direct-differentiation) +- [Writing differentiable code](#writing-differentiable-code) +- [Debugging AD](#debugging-ad) +- [Testing gradients](#testing-gradients) +- [Checklist for a new feature](#checklist-for-a-new-feature) + +For a short overview, see [Code Structure](../Code-Structure/#algorithmic-differentiation). For the theory of +expression templates, see [Advanced AD Techniques](../Advanced-AD-Techniques/). + +## What AD is in SU2 + +AD applies the chain rule to every operation that the program runs, so the derivatives are exact up to rounding. +SU2 uses AD in two modes. The forward mode (SU2 calls it "direct differentiation") carries one derivative along +with each value, and gives the derivative of all outputs with respect to one input per run. The reverse mode gives +the derivative of one output with respect to all inputs at once; SU2 uses it for the discrete adjoint. +SU2 gets AD from [CoDiPack](https://github.com/SciCompKL/CoDiPack), an operator-overloading tool: the type +`su2double` replaces `double`, and its operators also record derivative information. In reverse mode this +information goes to the tape: a stored list of all operations and their local derivatives. CoDiPack later replays +the tape backwards to get the adjoints, the derivatives of the output with respect to each recorded value. + +The type `su2double` is chosen at compile time in `Common/include/code_config.hpp`: + +```cpp +#if defined(CODI_REVERSE_TYPE) // reverse mode AD +#include "codi.hpp" +// ... +#if defined(HAVE_OMP) +using su2double = codi::RealReverseIndexOpenMPGen; +#else +#if defined(CODI_JACOBIAN_LINEAR_TAPE) +using su2double = codi::RealReverse; +// ... +#elif defined(CODI_TAG_TAPE) +using su2double = codi::RealReverseTag; +// ... +#elif defined(CODI_FORWARD_TYPE) // forward mode AD +#include "codi.hpp" +using su2double = codi::RealForward; +#else // primal / direct / no AD +using su2double = passivedouble; +#endif +``` + +`passivedouble` is always a plain `double` (or `float` in a full single-precision build). Use it for values that +must never carry derivatives. + +Two more libraries complete the AD support: + +- [MeDiPack](https://github.com/SciCompKL/MeDiPack) (`externals/medi`) differentiates MPI communication. +- [OpDiLib](https://github.com/SciCompKL/OpDiLib) (`externals/opdi`) differentiates OpenMP parallel regions. + +The `meson.py` script downloads CoDiPack, MeDiPack and OpDiLib at the commits set in `meson_scripts/init.py`. + +## Building the AD executables + +The general build steps are on the [Build SU2 on Linux/MacOS](../Build-SU2-Linux-MacOS/) page. These meson options +(`meson_options.txt`) control AD: + +| Option | Default | Effect | +|---|---|---| +| `-Denable-autodiff=true` | `false` | Builds the reverse mode executables `SU2_CFD_AD` and `SU2_DOT_AD` (and the Python module `pysu2ad` with `-Denable-pywrapper=true`). Defines `CODI_REVERSE_TYPE`. | +| `-Denable-directdiff=true` | `false` | Builds the forward mode executable `SU2_CFD_DIRECTDIFF`. Defines `CODI_FORWARD_TYPE`. | +| `-Denable-normal=false` | `true` | Skips the normal executables (`SU2_CFD`, `SU2_DEF`, `SU2_DOT`, ...). Useful to build only the AD versions. | +| `-Dcodi-tape=` | `JacobianLinear` | The CoDiPack tape of the reverse build, see below. | +| `-Dopdi-backend=auto\|macro\|ompt` | `auto` | How OpDiLib follows the OpenMP regions (reverse build with OpenMP only). | +| `-Dopdi-shared-read-opt=false` | `true` | Turns off the faster "no shared reading" sections, see [OpenMP](#openmp). | + +There is no `SU2_DEF_AD` executable. The AD build of `SU2_DEF` is only a library (`SU2core_DEF_AD`), which +`SU2_DOT_AD` and `pysu2ad` link. + +A typical reverse build, without the normal executables: + +``` +./meson.py setup build_ad -Denable-autodiff=true -Denable-normal=false --prefix=$PWD/install +./ninja -C build_ad install +``` + +`SU2_CFD_AD` runs the discrete adjoint by default: in a reverse build, `MATH_PROBLEM` defaults to +`DISCRETE_ADJOINT`. The normal `SU2_CFD` stops with an error if you ask for `MATH_PROBLEM= DISCRETE_ADJOINT` or +`DIRECT_DIFF`. + +### Tape types + +The option `-Dcodi-tape` selects the reverse type. Each value defines one macro in `meson.build`, and +`code_config.hpp` turns it into a CoDiPack type: + +| `-Dcodi-tape=` | `su2double` is | What it is for | +|---|---|---| +| `JacobianLinear` | `codi::RealReverse` | The default, and the only tape tested regularly. It stores the partial derivatives of each statement. Identifiers (the indices of the adjoint values) grow with every statement. | +| `JacobianReuse` | `codi::RealReverseIndexGen>` | Same derivative data, but identifiers of deleted variables are reused. The adjoint vector is smaller. | +| `JacobianMultiUse` | `codi::RealReverseIndex` | Like `JacobianReuse`, and a copy shares the identifier of the original instead of adding a statement. | +| `PrimalLinear`, `PrimalReuse`, `PrimalMultiUse` | `codi::RealReversePrimal`, `codi::RealReversePrimalIndexGen<...>`, `codi::RealReversePrimalIndex` | Primal value tapes. They store the values and the operations, and compute the partial derivatives during the reverse sweep. | +| `Tag` | `codi::RealReverseTag` | Computes no derivatives. It gives each variable a tag, to find errors in the recording. See [Debugging AD](#debugging-ad). | + +Notes: + +- `meson.build` prints a warning for every tape except `JacobianLinear`. +- With `-Dwith-omp=true`, `meson.build` ignores `-Dcodi-tape`. The type is always the thread-safe + `codi::RealReverseIndexOpenMPGen`, which reuses identifiers. So the `Tag` tape needs a build + without OpenMP. +- The tapes that reuse identifiers define `CODI_INDEX_REUSE`. With them, `CDiscAdjMultizoneDriver::ComputeAdjoints` + prints "WARNING: AD types that reuse indices do not support multiple zones." +- With `--buildtype=debug` or `--buildtype=debugoptimized`, meson also defines `CODI_EnableAssert`, which turns on + the internal checks of CoDiPack. +- With MPI, `SU2_MPI` becomes `CMediMPIWrapper`, built on MeDiPack. With OpenMP, a configure step runs + `externals/opdi/syntax/check.py` with `su2omp.syntax.json`. It stops the configuration if an `SU2_OMP_*` macro has + no matching `END_SU2_OMP_*`. + +The CI builds `Reverse` and `Forward` versions with MPI, without MPI and with OpenMP, plus `ReverseTagNoMPI` +(`-Dcodi-tape=Tag`). See [Running Regression Tests](../Running-Regression-Tests/#builds). + +## How the discrete adjoint runs + +The discrete adjoint solves a fixed-point problem. One primal iteration maps the solution `U` to a new solution +`G(U)`. At the converged solution, the adjoint `Ū` satisfies + +`Ū = (∂G/∂U)^T Ū + (∂J/∂U)^T` + +where `J` is the objective function. SU2 records one primal iteration on the tape. Each evaluation of the tape +gives `(∂G/∂U)^T Ū + (∂J/∂U)^T` for the current `Ū`, so repeated evaluations are a fixed-point iteration for `Ū`. +No hand-written adjoint equations are needed. + +The drivers are `CDiscAdjSinglezoneDriver` and `CDiscAdjMultizoneDriver` (`SU2_CFD/src/drivers`). The single-zone +driver is described first. + +### Recording the tape + +`CDiscAdjSinglezoneDriver::SetRecording` records one iteration. Shortened: + +```cpp +void CDiscAdjSinglezoneDriver::SetRecording(RECORDING kind_recording){ + AD::Reset(); + + /*--- Prepare for recording by resetting the solution to the initial converged solution. ---*/ + // ... solver->SetRecording(geometry, config) for every adjoint solver ... + + /*---Enable recording and register input of the iteration --- */ + if (kind_recording != RECORDING::CLEAR_INDICES){ + AD::StartRecording(); + iteration->RegisterInput(solver_container, geometry_container, config_container, ZONE_0, INST_0, kind_recording); + } + + /*--- Set the dependencies of the iteration ---*/ + iteration->SetDependencies(solver_container, geometry_container, numerics_container, config_container, ZONE_0, + INST_0, kind_recording); + + /*--- Do one iteration of the direct solver ---*/ + DirectRun(kind_recording); + // ... + /*--- Register Output of the iteration ---*/ + iteration->RegisterOutput(solver_container, geometry_container, config_container, ZONE_0, INST_0); + + /*--- Extract the objective function and store it --- */ + SetObjFunction(); + // ... + AD::StopRecording(); +} +``` + +The steps are: + +1. `AD::Reset()` deletes the old tape. `CDiscAdjSolver::SetRecording` copies the converged primal solution + (`GetSolution_Direct`) back into the primal solver. Then it removes the old tape identifiers from the solution + and from the other registered variables (`AD::ResetInput`). +2. `AD::StartRecording()` switches the tape on, and `RegisterInput` marks the inputs. What is registered depends + on the `RECORDING` value (`Common/include/option_structure.hpp`): + + | `RECORDING::` | Inputs registered by `CDiscAdjFluidIteration::RegisterInput` | + |---|---| + | `CLEAR_INDICES` | Nothing. The tape stays off; this pass only removes old identifiers. | + | `SOLUTION_VARIABLES` | The solution of each adjoint solver (`RegisterSolution`), the solutions at time `n` and `n-1` for dual time stepping, extra variables (`RegisterSolutionExtra`), and the free-stream or boundary values (`RegisterVariables`). | + | `MESH_COORDS` | The mesh coordinates (`CGeometry::RegisterCoordinates`). | + | `MESH_DEFORM` | The undeformed mesh and the boundary displacements, through the adjoint mesh solver `ADJMESH_SOL`. Used when the mesh is deformed (`DEFORM_MESH= YES`). | + | `SOLUTION_AND_MESH` | Both the solution and the coordinates. | + +3. `SetDependencies` recomputes the values that depend on the inputs but are normally computed only once or + outside the iteration: the geometry (normals, volumes, wall distance) when the coordinates are inputs, and the + primitive variables, eddy viscosity and halo values. Without this step the tape would miss these dependencies. +4. `DirectRun` runs one primal iteration (`Preprocess`, `Iterate`, `Postprocess` of the direct iteration). Every + operation on active `su2double` values goes to the tape. +5. `RegisterOutput` marks the new solution as output, and `SetObjFunction` computes `ObjFunc` and registers it + (only on the master rank). + +`MainRecording` records with the solution as input (`RECORDING::SOLUTION_VARIABLES`). +`SecondaryRecording` records with the mesh as input (`MESH_COORDS` or `MESH_DEFORM`). Both first call +`SetRecording(RECORDING::CLEAR_INDICES)`. + +The adjoint solver keeps the tape identifiers of the inputs and outputs, so it can later read and write their +adjoints. `CVariable::RegisterContainer` (`SU2_CFD/include/variables/CVariable.hpp`) registers a whole matrix and +stores the identifiers: + +```cpp +void RegisterContainer(bool input, su2activematrix& variable, su2matrix* ad_index = nullptr) { + const auto nPoint = variable.rows(); + SU2_OMP_FOR_STAT(roundUpDiv(nPoint,omp_get_num_threads())) + for (unsigned long iPoint = 0; iPoint < nPoint; ++iPoint) { + for(unsigned long iVar=0; iVarInitializeAdjoint(solver_container, geometry_container, config_container, ZONE_0, INST_0); + + /*--- Initialize the adjoint of the objective function with 1.0. ---*/ + SetAdjObjFunction(); + + /*--- Interpret the stored information by calling the corresponding routine of the AD tool. ---*/ + AD::ComputeAdjoint(); + + /*--- Extract the computed adjoint values of the input variables and store them for the next iteration. ---*/ + iteration->IterateDiscAdj(geometry_container, solver_container, + config_container, ZONE_0, INST_0, false); + // ... Monitor ... + /*--- Clear the stored adjoint information to be ready for a new evaluation. ---*/ + AD::ClearAdjoints(); + // ... Output, quasi-Newton correction ... +} +``` + +- `InitializeAdjoint` calls `CDiscAdjSolver::SetAdjoint_Output`. It writes the current adjoint solution `Ū` into + the adjoints of the outputs (`CVariable::SetAdjointSolution`, which uses `AD::SetDerivative` with the stored + `AD_OutputIndex`). +- `SetAdjObjFunction` seeds the adjoint of `ObjFunc` with 1 (with a time-window weight for unsteady problems). +- `AD::ComputeAdjoint()` evaluates the tape backwards. +- `IterateDiscAdj` calls `CDiscAdjSolver::ExtractAdjoint_Solution`. It reads the adjoints of the inputs + (`CVariable::GetAdjointSolution`, which uses `AD::GetDerivative` with `AD_InputIndex`), applies the relaxation + factor and computes the adjoint residuals. `ExtractAdjoint_Variables` reads the sensitivities to the registered + free-stream values (Mach, angle of attack, pressure, temperature, ...). +- `AD::ClearAdjoints()` sets all adjoints to zero, but keeps the tape. + +For a steady problem the tape is recorded once. `Preprocess` records again only when the last recording was of +another kind, for example after a sensitivity recording. + +### Extracting sensitivities + +After the loop, `Postprocess` calls `SecondaryRecording`. It records again with the mesh as input, seeds the +outputs with the converged adjoint, evaluates the tape once and extracts the mesh sensitivities: + +```cpp +if (SecondaryVariables == RECORDING::MESH_COORDS) { + solver[MainSolver]->SetSensitivity(geometry, config); +} +else { // MESH_DEFORM + solver[ADJMESH_SOL]->SetSensitivity(geometry, config, solver[MainSolver]); +} +``` + +`CDiscAdjSolver::SetSensitivity` reads the adjoint of each coordinate and stores it as the sensitivity. +`SU2_DOT_AD` then projects the surface sensitivities onto the design variables. +`CDiscAdjDeformationDriver::SetProjection_AD` (`SU2_DEF/src/drivers`) records the surface deformation with the +design variables as inputs (`AD::RegisterInput(config->GetDV_Value(iDV, iDV_Value))`), seeds the moved surface +coordinates with the sensitivities, and runs `AD::ComputeAdjoint()`. + +### Free-stream sensitivities + +`CDiscAdjSolver::RegisterVariables` shows how to add a scalar input. It copies the value from the config into a +member, registers the member, and writes it back so that the primal code uses the registered copy: + +```cpp +if (!reset) { + AD::RegisterInput(Mach); + AD::RegisterInput(Alpha); + AD::RegisterInput(Temperature); + AD::RegisterInput(Pressure); +} + +/*--- Set the free stream velocity (now using the registered values for Alpha, Beta and Mach). ---*/ +if (nDim == 2) { + config->GetVelocity_FreeStreamND()[0] = cos(Alpha)*Mach*SoundSpeed/Velocity_Ref; + config->GetVelocity_FreeStreamND()[1] = sin(Alpha)*Mach*SoundSpeed/Velocity_Ref; +} +// ... +``` + +`ExtractAdjoint_Variables` then reads `SU2_TYPE::GetDerivative(Mach)` and sums it over the ranks. + +### Multizone problems + +`CDiscAdjMultizoneDriver` records all zones on one tape and marks positions in it with `AD::Push_TapePosition()` +(`START`, `REGISTERED`, `DEPENDENCIES`, `OBJECTIVE_FUNCTION`, `TRANSFER`, then one pair per zone). It evaluates +only parts of the tape with `AD::ComputeAdjoint(enter, leave)`: + +```cpp +const unsigned short enter_izone = iZone*2+1 + ITERATION_READY; +const unsigned short leave_izone = iZone*2 + ITERATION_READY; + +AD::ComputeAdjoint(enter_izone, leave_izone); +// ... +if (eval_transfer || config_container[iZone]->GetStructuralProblem()) + AD::ComputeAdjoint(TRANSFER, OBJECTIVE_FUNCTION); +// ... +AD::ComputeAdjoint(DEPENDENCIES, START); +``` + +It uses two kinds of tape (`Kind_Tape`): `FULL_SOLVER_TAPE` for the iterations, and `OBJECTIVE_FUNCTION_TAPE`, +which contains only the objective function. It solves the coupled adjoint with a block Gauss-Seidel loop over the +zones, with optional quasi-Newton or Krylov acceleration. A single-zone case also runs with this driver when its +configuration sets `MULTIZONE= YES`. + +## Forward mode (direct differentiation) + +`SU2_CFD_DIRECTDIFF` carries one derivative with each `su2double`. There is no tape. You choose the input with +`DIRECT_DIFF` (`DirectDiff_Var_Map`): `MACH`, `AOA`, `SIDESLIP`, `REYNOLDS`, `TURB2LAM`, `PRESSURE`, +`TEMPERATURE`, `DENSITY`, `VISCOSITY`, `DESIGN_VARIABLES`, and structural values (`YOUNG_MODULUS`, +`POISSON_RATIO`, `STRUCTURAL_DENSITY`, `STRUCTURAL_DEAD_LOAD`, `ELECTRIC_FIELD`). + +`CConfig` seeds some of them, for example `SU2_TYPE::SetDerivative(Mach, 1.0)`. The solvers seed the others. +For `DESIGN_VARIABLES`, `CSurfaceMovement::SetSurface_Derivative` seeds every design variable with a non-zero value. +So run one design variable at a time. The history output then has a column `D_` for every coefficient +field, filled with `SU2_TYPE::GetDerivative`. `direct_differentiation.py` runs one `SU2_CFD_DIRECTDIFF` per design +variable. + +In forward mode, the `AD::` functions of the reverse mode (recording, preaccumulation, ...) do nothing. + +## Writing differentiable code + +Most code needs no AD work: if it computes with `su2double`, CoDiPack records it. You must take care of the +cases below. + +### The AD API + +All functions are in the namespace [`AD`](../../doxygen/namespaceAD.html) (`Common/include/basic_types/ad_structure.hpp`) +or `SU2_TYPE` (`Common/include/basic_types/datatype_structure.hpp`). In a build without reverse AD, the `AD::` +functions are empty, so you can call them in any build. + +| Function | Use | +|---|---| +| `AD::StartRecording()`, `AD::StopRecording()`, `AD::TapeActive()` | Switch the tape on and off; ask if it is on. | +| `AD::RegisterInput(x)`, `AD::RegisterOutput(y)` | Mark the inputs and outputs of the recording. | +| `AD::ComputeAdjoint()`, `AD::ComputeAdjoint(enter, leave)` | Evaluate the whole tape, or the part between two positions stored with `AD::Push_TapePosition()`. | +| `AD::ClearAdjoints()`, `AD::Reset()` | Set all adjoints to zero; delete the tape. | +| `SU2_TYPE::SetDerivative(x, v)`, `SU2_TYPE::GetDerivative(x)` | Set or read the adjoint (reverse) or the tangent (forward) of a variable. | +| `AD::SetIndex(index, x)`, `AD::GetDerivative(index)`, `AD::SetDerivative(index, v)` | Store the identifier of `x`, then read or write its adjoint later by identifier. Use `AD::ResizeAdjoints()` first and wrap the calls in `AD::BeginUseAdjoints()` / `AD::EndUseAdjoints()`: these functions do no bounds checks and no locking. | +| `AD::ResetInput(x)` | Make `x` passive again (keeps the value, drops the identifier). | +| `AD::StartPreacc()`, `AD::SetPreaccIn(...)`, `AD::SetPreaccOut(...)`, `AD::EndPreacc()` | Preaccumulation, see below. | +| `AD::PausePreaccumulation()`, `AD::ResumePreaccumulation(wasActive)` | Switch preaccumulation off for a code section. | +| `AD::BeginPassive()`, `AD::EndPassive(wasActive)`; `AD::PauseRecording()`, `AD::ResumeRecording(wasActive)` | Run a section without recording. The two pairs do the same thing. | +| `AD::StartNoSharedReading()`, `AD::EndNoSharedReading()` | Mark an OpenMP section where no two threads read the same active value. | +| `AD::SetExtFuncIn`, `AD::SetExtFuncOut`, `AD::FuncHelper` | External functions: code that is not recorded, with a hand-written reverse function. | +| `AD::PrintStatistics(comm, printingRank)` | Print the tape size. The drivers call it when `WRT_AD_STATISTICS= YES`. | +| `SU2_TYPE::GetValue(x)`, `SU2_TYPE::PassiveValue(x)` | The plain value of `x`, as `passivedouble`. | + +The unit test `UnitTests/Common/simple_ad_test.cpp` shows the basic sequence: + +```cpp +su2double x = 4.0; + +AD::StartRecording(); +AD::RegisterInput(x); + +su2double y = func(x); + +AD::RegisterOutput(y); +AD::StopRecording(); +SU2_TYPE::SetDerivative(y, 1.0); +AD::ComputeAdjoint(); + +CHECK(SU2_TYPE::GetValue(y) == Approx(64)); +CHECK(SU2_TYPE::GetDerivative(x) == Approx(48)); +``` + +### Preaccumulation + +The tape stores every operation, so it is large: often several times the memory of the primal solver. +Preaccumulation reduces it. For a code section with few inputs and outputs but many operations, CoDiPack computes +the local Jacobian (outputs with respect to inputs) at the end of the section. It then replaces the recorded +operations with this Jacobian. The derivatives do not change. + +`CFVMFlowSolverBase::ComputeVorticityAndStrainMag` (`SU2_CFD/include/solvers/CFVMFlowSolverBase.inl`) is a small +example: + +```cpp +const su2double vy = nodes->GetVelocity(iPoint, 1); +const su2double y = geometry->nodes->GetCoord(iPoint, 1); +AD::StartPreacc(); +AD::SetPreaccIn(VelocityGradient, nDim, nDim); +AD::SetPreaccIn(vy, y); + +StrainMag(iPoint) = 0.0; + +/*--- Add diagonal part ---*/ + +for (unsigned long iDim = 0; iDim < nDim; iDim++) { + StrainMag(iPoint) += pow(VelocityGradient(iDim, iDim), 2); +} +if (config.GetAxisymmetric() && y > EPS) { + StrainMag(iPoint) += pow(vy / y, 2); +} +// ... off-diagonal terms ... +StrainMag(iPoint) = sqrt(2.0*StrainMag(iPoint)); +AD::SetPreaccOut(StrainMag(iPoint)); +AD::EndPreacc(); +``` + +Rules: + +- Declare **every** active value that the outputs depend on with `AD::SetPreaccIn`. A value that is not declared + is treated as a constant. The gradient is then wrong, and there is no error message. Inputs can be declared + anywhere between `StartPreacc` and their first use, as `CSourcePieceWise_TurbSST::ComputeResidual` does for the + intermittency. +- Declare every output with `AD::SetPreaccOut` and close the section with `AD::EndPreacc()` on **every** return + path. `CUpwRoeBase_Flow::ComputeResidual` does this before its early return, see + [Adding a Numerical Scheme](../Adding-a-Numerical-Scheme/#discrete-adjoint). +- `SetPreaccIn` takes lvalues of type `su2double`, single values, 1D arrays (`SetPreaccIn(data, size)`) or 2D + arrays (`SetPreaccIn(data, size_x, size_y)`). An expression or a temporary does not compile ("rvalues cannot be + registered"). Store it in a local `su2double` first, as `vy` and `y` above. +- Do not nest sections, and do not call code that starts its own section. +- The section is skipped when the tape is off, or when `PREACC= NO`. So the code must give the same values with + and without preaccumulation. +- Preaccumulation helps when the section has many operations per input and output. Use as few inputs as possible. + `CPhysicalGeometry` recomputes intermediate values inside the section for this reason. +- In the vectorized numerics (`SU2_CFD/include/numerics_simd`), `gatherVariables` declares the inputs and + `stopPreacc` (`SU2_CFD/include/numerics/util.hpp`) ends the section. + +### Passive sections + +Some values must not be differentiated, for example the Jacobian of the implicit scheme, a monitored maximum, or +a frozen limiter. Wrap them in a passive section, so that they cost no tape memory: + +```cpp +/*--- The derivative with respect to strainMax and omegaMax is not required. ---*/ +bool wa = AD::PauseRecording(); +strainMax = max(strainMax, StrainMag(iPoint)); +omegaMax = max(omegaMax, GeometryToolbox::Norm(3, Vorticity)); +AD::ResumeRecording(wa); +``` + +A value computed in a passive section is a constant on the tape. Use this only when that is what you want. + +### Values, branches and iteration counters + +- Do not use `SU2_TYPE::GetValue` (or a cast to `passivedouble`, `int`, ...) on a value that the result depends on. + It cuts the dependency. Use it only for output, for logic that has no derivative (indices, counters), or for + values that must be constant on purpose. +- A branch on an active value is recorded as the branch that was taken. The derivative is the derivative of that + branch. This is correct away from the switch point. At the switch point (`max`, `min`, `fabs`, limiters) the + derivative is one-sided. Avoid branches whose outcome changes between the converged primal and the recording, + because then the tape describes another function. +- The tape is recorded once, at one inner iteration. Code that runs only at some iterations + (`config->GetInnerIter() == 0`, every n iterations, only when a residual is small, ...) must also run during the + recording if it affects the result. For example, `CIncEulerSolver::Preprocessing`: + + ```cpp + /*--- InnerIter is not reset while recording the discrete adjoint tape. ---*/ + if (dual_time && (config->GetInnerIter() == 0 || AD::TapeActive())) { + RecomputeDensity_time_n(solver_container, config); + } + ``` + +- A value that the iteration uses must be computed from the inputs during the recording. If it is computed + earlier (in a postprocessing step of the previous iteration, at start-up, ...), the tape sees it as a constant. + Recompute it in `SetDependencies`, or register it with `RegisterSolutionExtra`, as `CTurbSolver` does for the + eddy viscosity. +- `static` and global `su2double` variables survive between recordings. Their tape identifiers then point to an old + tape. Avoid them, or reset them with `AD::ResetInput`. + +### MPI + +Use the `SU2_MPI` wrapper (`Common/include/parallelization/mpi_structure.hpp`). In AD builds it is +`CMediMPIWrapper`, and `MPI_DOUBLE` means `su2double`: MeDiPack records the communication. Other MPI types +(`MPI_INT`, `MPI_UNSIGNED_LONG`, ...) are passed through. A type that the wrapper does not know stops with +"Conversion not implemented". + +Do not send `passivedouble` buffers through `SU2_MPI` with `MPI_DOUBLE`. Use `SelectMPIWrapper::W`, +which in the reverse build is the plain MPI wrapper: + +```cpp +using CPassiveMPI = SelectMPIWrapper::W; +vector dirOut(nRecvTotal * nDim, 0.0), dirIn(nSendTotal * nDim, 0.0); +// ... +CPassiveMPI::Sendrecv(dirOut.data() + hp.offR * nDim, hp.nVertexR * nDim, MPI_DOUBLE, hp.receive_from, 1, + dirIn.data() + hp.offS * nDim, hp.nVertexS * nDim, MPI_DOUBLE, hp.send_to, 1, + SU2_MPI::GetComm(), MPI_STATUS_IGNORE); +``` + +Halo updates of solver data (`InitiateComms` / `CompleteComms`) are recorded like any other operation. Halo +points are local copies of points owned by other MPI ranks. + +### OpenMP + +In a reverse build with OpenMP, the `SU2_OMP_*` macros of `omp_structure.hpp` become OpDiLib macros, which record +the parallel structure for the reverse sweep. Rules: + +- Use only the `SU2_OMP_*` macros, never `#pragma omp` directly, and close each one with its `END_SU2_OMP_*` macro. +- In the reverse sweep, the adjoint of a value is updated by every thread that read the value in the primal run. + By default OpDiLib makes these updates atomic, which is safe but slow. +- Between `AD::StartNoSharedReading()` and `AD::EndNoSharedReading()`, OpDiLib uses plain updates. Use these + sections only where no two threads read the same active value, for example a loop over points that reads only + data of its own point. `CTurbSASolver::Postprocessing` puts the eddy viscosity loop in such a section. +- `SU2_OMP_SAFE_GLOBAL_ACCESS(...)` runs code on the master thread, with barriers. It also opens a no-shared-reading + section, because the other threads wait. +- Preaccumulation is thread-local (`PreaccHelper` and `PreaccActive` are `threadprivate`). But in an edge loop + whose threads share points, it is not safe. This happens with the reducer strategy, which SU2 uses when edge + coloring is not efficient: the fluxes are stored per edge and summed per point afterwards. + `CScalarSolver::EdgeFluxResidual` therefore pauses preaccumulation in that case: + + ```cpp + bool pausePreacc = false; + if (ReducerStrategy) pausePreacc = AD::PausePreaccumulation(); + else AD::StartNoSharedReading(); + ``` + +- `SU2_OMP_SIMD_IF_NOT_AD` is a SIMD pragma only in builds without AD. Use it on loops over `su2double`. +- A solver that supports hybrid parallel adjoints returns `true` from `CSolver::GetHasHybridParallel()`. The + adjoint iterations then open their parallel regions with `SU2_OMP_PARALLEL_(if(...GetHasHybridParallel()))`. + +### External functions + +Some algorithms are cheaper to differentiate by hand than by recording, mainly linear solvers. `CSysSolve::Solve` +(`Common/src/linear_algebra/CSysSolve.cpp`) declares the right-hand side as input and the solution as output, runs +the primal solve without recording, and adds a reverse function to the tape: + +```cpp +BEGIN_SU2_OMP_SAFE_GLOBAL_ACCESS { + AD::SetExtFuncIn(&LinSysRes[0], LinSysRes.GetLocSize()); + AD::SetExtFuncOut(&LinSysSol[0], LinSysSol.GetLocSize()); + AD::FuncHelper.addUserData(&LinSysRes_tmp); + // ... +} +END_SU2_OMP_SAFE_GLOBAL_ACCESS +// ... +AD::FuncHelper.callPrimalFuncWithADType(externalFunction); + +AD::FuncHelper.addToTape(CSysSolve_b::Solve_b); +``` + +`CSysSolve_b::Solve_b` solves the transposed system in the reverse sweep. + +### Memory + +The tape grows with the number of active operations in one iteration. To keep it small: + +- Use `passivedouble` for values that never need derivatives (counters, timers, geometric search structures). +- Preaccumulate small functions with many operations. +- Put code that must not be differentiated in a passive section. +- Check the effect with `WRT_AD_STATISTICS= YES`. The drivers then print the tape statistics after each recording, + summed over MPI ranks and OpenMP threads. + +## Debugging AD + +### Tape debug build + +A build with `-Dcodi-tape=Tag` (and without OpenMP) uses the tag tape. It computes no derivatives. It gives every +variable a tag and reports when a statement uses a variable with an unexpected tag. + +The check runs only in `CDiscAdjMultizoneDriver`, so the configuration needs `MULTIZONE= YES` (one zone is fine). +In a tag build, `CConfig` sets the debug mode, and `CDiscAdjMultizoneDriver::StartSolver` calls `TapeTest` instead +of the solver. `TapeTest` records the tape twice: + +1. The **initial** recording gives the inputs the tag 1 (in multizone problems, `10*(iZone+1) + 1`). Errors in + this recording are mostly preaccumulation errors. Inside a preaccumulation section the current tag is 1337. +2. The **check** recording repeats the recording with the tag 2. A variable that still has the tag 1 was not + recomputed from the inputs in this recording. It was computed before the inputs were registered, for example + in a postprocessing step, and the tape treats it as a constant. + +Two options select what is checked: + +| Option | Values | Meaning | +|---|---|---| +| `CHECK_TAPE_TYPE` | `FULL_SOLVER_TAPE` (default), `OBJECTIVE_FUNCTION_TAPE` | Check the tape of the whole iteration, or only the tape of the objective function. | +| `CHECK_TAPE_VARIABLES` | `SOLVER_VARIABLES` (default), `MESH_COORDINATES` | Register the solution, or the mesh coordinates (plus the solution) as inputs. | + +Each rank writes its errors to `debug_run_process.out`. The screen shows the total, "Total number of tape +inconsistencies: N". A typical line is: + +``` +Use of variable with bad tag '01', should be '1337'. +``` + +How to read it: + +- `should be '1337'`: a value used inside a preaccumulation section is not declared with `AD::SetPreaccIn`. +- `bad tag '1337'`: a value computed inside a preaccumulation section is used later, but it is not declared with + `AD::SetPreaccOut`. +- `bad tag '01', should be '02'` (in the check recording): a value is used that was not computed from the inputs + in this recording. + +The file does not say where the error is. Use a debugger with a breakpoint in `AD::tagErrorCallback` +(`ad_structure.hpp`) to get the call stack. + +The functions `AD::SetTag`, `AD::ComputeTag`, `AD::SetTapeDebugOption` (with a `AD::TAPE_DEBUG_OPTION` value) and +`AD::SetTagOnVariable` control the tags. `CInterface` uses `TAPE_DEBUG_OPTION::ALLOW_ZONE` to allow values of the +donor zone during the transfer between zones. + +### Tape tests in the regression scripts + +`TestCase.py` has the argument `--tapetests`. It runs only the tests with `enabled_with_tapetests = True`, and +compares the number of tape inconsistencies with `tapetest_vals`: + +```python +discadj_naca0012_via_mz = TestCase('discadj_naca0012_via_mz') +discadj_naca0012_via_mz.cfg_dir = "cont_adj_euler/naca0012" +discadj_naca0012_via_mz.cfg_file = "inv_NACA0012_discadj_multizone.cfg" +# ... +discadj_naca0012_via_mz.enabled_with_tapetests = True +discadj_naca0012_via_mz.tapetest_vals = [0] +``` + +The CI job "Tape Tests" runs `serial_regression_AD.py --tapetests` with the `ReverseTagNoMPI` build. To run it +yourself, build `SU2_CFD_AD` with `-Dcodi-tape=Tag -Dwith-mpi=disabled` and run +`python3 serial_regression_AD.py --tapetests`. See [Running Regression Tests](../Running-Regression-Tests/). + +### Other checks + +- **`PREACC= NO`.** Preaccumulation does not change the derivatives. If the gradient changes when you turn it off, + a preaccumulation section is wrong. +- **Assertions.** A debug build defines `CODI_EnableAssert` and enables the `assert` checks of SU2. +- **Thread sanitizer.** The CI runs `hybrid_regression_AD.py --tsan` with an OpenMP reverse build, to find data + races in the parallel reverse sweep. + +### Common errors + +| Symptom | Likely cause | +|---|---| +| Gradient is zero for an input | The input is not registered, or the dependency is cut by `SU2_TYPE::GetValue`, a cast, a passive section or `passivedouble`. | +| Gradient is wrong, no error | A missing `AD::SetPreaccIn`, a value computed outside the recording, or a branch that differs between primal and recording. Run the tape test and compare with finite differences. | +| Adjoint diverges, primal converges | Often the same causes. Also check that the primal is well converged, and try a smaller `CFL_NUMBER` or the quasi-Newton option `QUASI_NEWTON_NUM_SAMPLES`. | +| Compile error "rvalues cannot be registered" | An expression passed to `AD::SetPreaccIn`. Store it in a variable first. | +| "Conversion not implemented" | An MPI type that `CMediMPIWrapper::convertDatatype` does not know. | +| Configure error from `check.py` | An `SU2_OMP_*` macro without its `END_SU2_OMP_*` (OpenMP reverse build). | +| Crash or nonsense in the reverse sweep with OpenMP | A no-shared-reading section where threads do share values, or preaccumulation in a loop whose threads share points. | +| Out of memory | The tape is too big. Check `WRT_AD_STATISTICS= YES`, preaccumulate, use passive sections. | + +## Testing gradients + +### Compare with finite differences + +Compare the AD gradient with finite differences on a small case. The Python scripts in `SU2_PY` compute the +gradient of the objective with respect to the design variables in the configuration (`DEFINITION_DV`): + +| Script | Method | Needs | +|---|---|---| +| `discrete_adjoint.py -f ` | Discrete adjoint: `SU2_CFD`, `SU2_CFD_AD`, then `SU2_DOT_AD`. | Normal and reverse builds. | +| `direct_differentiation.py -f ` | Forward mode, one `SU2_CFD_DIRECTDIFF` run per design variable. The result is in `DIRECTDIFF/of_grad_directdiff.dat`. | Forward build. `HISTORY_OUTPUT` must contain a derivative group (`D_...`). | +| `finite_differences.py -f ` | One primal run per design variable, step `FIN_DIFF_STEP`. The result is in the folder `FINDIFF`. | Normal build. | + +All three use the same configuration file. The case `TestCases/cont_adj_euler/naca0012` has configurations for +them (`inv_NACA0012.cfg`, `inv_NACA0012_FD.cfg`). + +Tips: + +- Converge the primal and the adjoint tightly. Most mismatches come from poor convergence. +- Try two or three finite-difference steps. A small step suffers from rounding, a large one from truncation. +- Forward mode and the discrete adjoint should agree closely when the primal, forward sensitivities and adjoint + are sufficiently converged and use the same model and limiter settings. Forward AD through a finite number of + primal iterations can differ from the fixed-point adjoint sensitivity without an AD error. Check convergence + and frozen-model or frozen-limiter settings before investigating the recording. This comparison has no + finite-difference step-size error. + +### Unit tests + +For a self-contained piece of code (a fluid model, a numerics class), a Catch2 unit test is faster. The test +driver `test_driver_AD` (`UnitTests/meson.build`, list `su2_cfd_tests_ad`) is built with `-Denable-tests=true` +and `-Denable-autodiff=true`. `UnitTests/SU2_CFD/fluid/CFluidModel_tests_AD.cpp` checks the derivatives that a +fluid model computes by hand against AD: + +```cpp +AD::Reset(); +AD::StartRecording(); +AD::RegisterInput(val_rho); +AD::RegisterInput(val_e); + +fluid_model->SetTDState_rhoe(val_rho, val_e); +su2double val_p_pred = fluid_model->GetPressure(); +AD::RegisterOutput(val_p_pred); +AD::StopRecording(); + +SU2_TYPE::SetDerivative(val_p_pred, 1.0); +AD::ComputeAdjoint(); + +/*--- Extract pressure derivatives from fluid model and AD. ---*/ +const su2double dpdrho_e_AD = SU2_TYPE::GetDerivative(val_rho); +// ... +CHECK(SU2_TYPE::GetValue(dpdrho_e_AD) == Approx(SU2_TYPE::GetValue(dpdrho_e_pred))); +``` + +See [Writing Unit Tests](../Writing-Unit-Tests/). + +### Regression tests + +The AD regression scripts are `serial_regression_AD.py`, `parallel_regression_AD.py` and +`hybrid_regression_AD.py`. They compare adjoint residuals and sensitivities after a few iterations, and they run +the Python gradient scripts above (`discadj_euler_py`, `directdiff_euler_py`, ...) against stored gradient files. +See [Running Regression Tests](../Running-Regression-Tests/). + +When a change breaks the AD tests: + +1. Check the primal tests first. If the primal values changed on purpose, the adjoint values change too. +2. If only the AD tests fail, find out why before you change any value. Run the tape test, try `PREACC= NO`, and + compare with finite differences or forward mode on a small case. +3. If the new values are correct, update them as explained in + [Update the stored values](../Running-Regression-Tests/#update-the-stored-values), and say in the pull request + why they changed. + +If your feature is meant for the discrete adjoint, add an AD regression test. For a feature with new recorded +code, consider a multizone test with `enabled_with_tapetests = True` and `tapetest_vals = [0]`. + +## Checklist for a new feature + +- [ ] All values that can carry derivatives are `su2double`. `passivedouble` only for values that never need them. +- [ ] No `SU2_TYPE::GetValue`, casts or passive sections on values that the result depends on. +- [ ] New inputs of the iteration (for example a new boundary value) are registered in `RegisterVariables` and + extracted in `ExtractAdjoint_Variables`, or registered with `RegisterSolutionExtra`. +- [ ] Values computed outside the iteration are recomputed in `SetDependencies`. +- [ ] Code that runs only at some iterations also runs during the recording (`AD::TapeActive()`), if it affects the + result. +- [ ] Every preaccumulation section declares all inputs and outputs, and every return path calls `AD::EndPreacc()`. +- [ ] MPI goes through `SU2_MPI` (active data) or `SelectMPIWrapper::W` (passive data). +- [ ] OpenMP uses the `SU2_OMP_*` macros; no-shared-reading sections contain no shared reads. +- [ ] The feature compiles in the reverse, forward and `Tag` builds. +- [ ] The tape test shows no new inconsistencies. +- [ ] The gradient agrees with finite differences (or forward mode) on a small case. +- [ ] An AD regression test covers the feature. + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Code-Reference.md b/Docs/website/pages/Code-Reference.md new file mode 100644 index 000000000000..50be36bdd8b2 --- /dev/null +++ b/Docs/website/pages/Code-Reference.md @@ -0,0 +1,25 @@ +--- +title: Code Reference (Doxygen) +permalink: /docs_v7/Code-Reference/ +--- + +The code reference describes the classes and files of SU2. [Doxygen](https://www.doxygen.nl) makes it from the comments in the source code. +It is rebuilt each time the code changes, so it always matches the code. + +**[Open the code reference]({{ "/doxygen/index.html" | relative_url }})** + +Good places to start: + +- [Class list]({{ "/doxygen/annotated.html" | relative_url }}): all classes, with their parents and children. +- [File list]({{ "/doxygen/files.html" | relative_url }}): all source files. +- `CDriver`: runs the simulation and creates all the other objects. +- `CConfig`: reads the configuration file. The [Option Reference](../Option-Reference/) is made from it. +- `CSolver`, `CNumerics`, `CVariable`, `CGeometry`: the solvers, the numerical schemes, the stored variables and the mesh. + +To build it on your computer, run this command in the SU2 folder. Then open `Docs/html/index.html`. + +``` +doxygen doxyfile +``` + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Code-Structure.md b/Docs/website/pages/Code-Structure.md new file mode 100644 index 000000000000..31c70625dfab --- /dev/null +++ b/Docs/website/pages/Code-Structure.md @@ -0,0 +1,369 @@ +--- +title: Code Structure +permalink: /docs_v7/Code-Structure/ +--- + +This page is a map of the SU2 source code for new developers. It shows: + +- where things live; +- how a simulation runs through the classes of `SU2_CFD`; +- which files to change for common tasks. + +For the full class documentation, see the [Code Reference](../Code-Reference/) (Doxygen). Class names on this page link to their Doxygen page. + +--- + +- [Top-level folders](#top-level-folders) +- [Inside SU2_CFD](#inside-su2_cfd) +- [How SU2_CFD runs](#how-su2_cfd-runs) +- [Main classes](#main-classes) +- [Linear algebra](#linear-algebra) +- [Containers and toolboxes](#containers-and-toolboxes) +- [Parallelism: MPI, OpenMP and vectorization](#parallelism-mpi-openmp-and-vectorization) +- [Algorithmic differentiation](#algorithmic-differentiation) +- [Where do I change...](#where-do-i-change) + +--- + +## Top-level folders + +| Folder or file | What it holds | +|---|---| +| `Common` | Code shared by all programs: configuration (`CConfig`), mesh and dual grid (`CGeometry`), linear algebra, containers, MPI/OpenMP wrappers, AD (automatic differentiation) types, grid movement, interface interpolation. Headers are in `Common/include`, sources in `Common/src`. | +| `SU2_CFD` | The solver program `SU2_CFD`: drivers, iterations, integrations, solvers, variables, numerics, fluid models and output. | +| `SU2_DEF` | The mesh deformation program `SU2_DEF` (`SU2_DEF/src/SU2_DEF.cpp`). Its drivers `CDeformationDriver` and `CDiscAdjDeformationDriver` are in `SU2_DEF/include/drivers`. | +| `SU2_DOT` | The gradient projection program `SU2_DOT` (`SU2_DOT/src/SU2_DOT.cpp`). It uses `CDiscAdjDeformationDriver` from `SU2_DEF`. | +| `SU2_GEO` | The geometry evaluation program `SU2_GEO`. It computes sections, thickness and volume, which shape design uses as constraints. | +| `SU2_SOL` | The program `SU2_SOL`. It writes solution files from restart files, with `CBaselineSolver` and `CBaselineOutput`. | +| `SU2_PY` | Python scripts (`parallel_computation.py`, `shape_optimization.py`, `discrete_adjoint.py`, ...), the `SU2` Python package (`SU2_PY/SU2`) and the SWIG interface of the Python wrapper (`SU2_PY/pySU2`). | +| `UnitTests` | Unit tests with Catch2 (`externals/catch2`), split into `UnitTests/Common` and `UnitTests/SU2_CFD`. Build them with `-Denable-tests=true`. | +| `TestCases` | Regression tests. The test lists are `serial_regression.py`, `parallel_regression.py`, `hybrid_regression.py` and their `_AD` versions, plus `tutorials.py` and `vandv.py`. All of them use the shared runner `TestCase.py`. | +| `externals` | Third-party code: CGNS, METIS, ParMETIS, TecIO, CLI11, Catch2. Also the git submodules CoDiPack (`codi`), MeDiPack (`medi`), OpDiLib (`opdi`), MEL (`mel`), Eigen (`eigen`), FADO, meson and ninja. | +| `subprojects` | Optional libraries built with meson: CoolProp, Mutation++ (`Mutationpp`), MLPCpp. | +| `meson.build`, `meson_options.txt` | The build system. `meson_options.txt` lists the build options (`with-mpi`, `with-omp`, `enable-autodiff`, `enable-directdiff`, `enable-pywrapper`, `enable-tests`, ...). Each program folder has its own `meson.build` in `src`. | +| `meson.py`, `preconfigure.py`, `meson_scripts` | `meson.py` runs the meson version bundled in `externals/meson`. Before that, `preconfigure.py` and `meson_scripts/init.py` download the submodules and build ninja. | +| `SU2_IDE`, `docker`, `QuickStart` | IDE project files, container files, and the quick start case. | +| `Docs` | The Doxygen main page, and the website pages that are kept with the code (`Docs/website`). | +| `config_template.cfg` | Every configuration option, with a short comment. | + +## Inside SU2_CFD + +Headers are in `SU2_CFD/include/` and sources are in `SU2_CFD/src/`. The folders `gradients` and `numerics_simd` exist only under `include`. + +| Folder | Content | +|---|---| +| `drivers` | `CDriver` and its children. They create all objects and run the time loop. | +| `iteration` | `CIteration` and its children. Each one runs one iteration of one physics (fluid, heat, structure, adjoint). | +| `integration` | `CIntegration` and its children: multigrid, single grid, Newton-Krylov, structural and DG (discontinuous Galerkin) time integration. | +| `solvers` | `CSolver` and its children. A solver computes the residuals, applies the boundary conditions and holds the linear system of one set of equations. Also `CSolverFactory`. | +| `variables` | `CVariable` and its children. They store the data at the mesh points. | +| `numerics` | `CNumerics` and its children. They compute the fluxes for one edge or the source terms for one point. Subfolders: `flow` (`convection/`, `flow_diffusion`, `flow_sources`), `turbulent`, `species`, `scalar`, `NEMO`, `elasticity`, `continuous_adjoint`. | +| `numerics_simd` | Vectorized flux schemes (`CNumericsSIMD`), used by `CEulerSolver`. | +| `fluid` | Fluid models (`CFluidModel`, `CIdealGas`, `CPengRobinson`, `CCoolProp`, ...), viscosity and conductivity models. | +| `gradients`, `limiters` | Green-Gauss and least squares gradients, slope limiters. | +| `interfaces` | Data transfer between zones (`CInterface`; `cfd`, `cht`, `fsi`). | +| `output` | `COutput` and its children, the file writers (`output/filewriter`) and the data sorters. See [Adding Output Fields](../Adding-Output-Fields/). | + +`SU2_CFD/src/SU2_CFD.cpp` holds `main()`. `SU2_CFD/src/python_wrapper_structure.cpp` holds the driver functions that the Python wrapper uses. + +## How SU2_CFD runs + +The tree below shows the calls of one `SU2_CFD` run, from `main()` down to the numerics. + +``` +main() SU2_CFD/src/SU2_CFD.cpp + |-- CConfig: read the config file (number of zones, solver kind) + |-- create the driver: + | -d (dry run) -> CDummyDriver + | single zone -> CSinglezoneDriver / CDiscAdjSinglezoneDriver + | multizone -> CMultizoneDriver / CDiscAdjMultizoneDriver + | harmonic balance -> CHBDriver + | The CDriver constructor creates everything: + | PreprocessInput, PreprocessOutput, InitializeGeometry, InitializeSolver, + | InitializeNumerics, InitializeIntegration, PreprocessIteration, ... + | + |-- driver->StartSolver() + | loop over time steps (CSinglezoneDriver): + | Preprocess -> Run -> Postprocess -> Update -> Monitor -> Output + | Run() calls CIteration::Solve() + | loop over inner iterations: + | CIteration::Iterate() e.g. CFluidIteration + | CIntegration::MultiGrid_Iteration() flow (CMultiGridIntegration) + | CIntegration::SingleGrid_Iteration() turbulence, species, ... (CSingleGridIntegration) + | CSolver::Preprocessing() primitives, gradients, limiters + | CSolver::SetTime_Step() + | CIntegration::Space_Integration() + | CSolver::Upwind_Residual() or Centered_Residual() + | CSolver::Viscous_Residual(), Source_Residual() + | -> CNumerics::ComputeResidual() on each edge or point + | CSolver::BC_...() on each marker + | CIntegration::Time_Integration() + | CSolver::ImplicitEuler_Iteration() -> CSysSolve with CSysMatrix, CSysVector + | CSolver::Postprocessing() + | CIteration::Monitor() -> COutput::SetHistoryOutput() + | CIteration::Output() -> COutput::SetResultFiles() + | + |-- driver->Finalize() +``` + +Notes: + +- `CMultiGridIntegration::MultiGrid_Iteration` runs the multigrid cycle. On each grid level it does the same steps as above: preprocessing, space and time integration, postprocessing. +- `CFluidIteration::Iterate` solves the flow first. Then `CommonAuxiliarySolvers` runs `SingleGrid_Iteration` for the transition, turbulence and species solvers. The coupling is segregated: each solver is solved on its own, one after the other. +- `CIntegration::Space_Integration` and `CIntegration::Time_Integration` are in `SU2_CFD/src/integration/CIntegration.cpp`. Read them to see exactly which solver functions are called, and in which order. +- The discrete adjoint drivers (`CDiscAdjSinglezoneDriver`, `CDiscAdjMultizoneDriver`) record one primal iteration on the AD tape. The tape is the stored list of operations that AD later runs backwards to get derivatives. The drivers then evaluate the tape many times (`AD::ComputeAdjoint`) to solve the adjoint fixed-point problem. + +### The containers in the driver + +The driver holds arrays of pointers to all objects. It passes them down to the iterations and integrations. + +| Member | Indices | +|---|---| +| `config_container` | `[iZone]` | +| `geometry_container` | `[iZone][iInst][iMesh]` (iMesh = multigrid level, `MESH_0` is the finest) | +| `solver_container` | `[iZone][iInst][iMesh][iSol]` (iSol = `FLOW_SOL`, `TURB_SOL`, `TRANS_SOL`, `SPECIES_SOL`, `HEAT_SOL`, ...) | +| `numerics_container` | `[iZone][iInst][iMesh][iSol][iTerm]` (iTerm = `CONV_TERM`, `VISC_TERM`, `SOURCE_FIRST_TERM`, `CONV_BOUND_TERM`, ..., one set per OpenMP thread) | +| `integration_container` | `[iZone][iInst][iSol]` | +| `iteration_container` | `[iZone][iInst]` | +| `output_container` | `[iZone]` | + +The solver indices and term indices are defined in `Common/include/option_structure.hpp`. + +## Main classes + +### Drivers + +- [`CDriverBase`](../../doxygen/classCDriverBase.html): common base of the `SU2_CFD` and `SU2_DEF` drivers. It also holds the functions that the Python wrapper uses. +- [`CDriver`](../../doxygen/classCDriver.html): creates geometry, solvers, numerics, integrations, iterations and outputs. +- [`CSinglezoneDriver`](../../doxygen/classCSinglezoneDriver.html), [`CMultizoneDriver`](../../doxygen/classCMultizoneDriver.html): primal problems. The multizone driver couples the zones with block Gauss-Seidel or Jacobi iterations. It transfers data between zones through `CInterface` objects. +- [`CDiscAdjSinglezoneDriver`](../../doxygen/classCDiscAdjSinglezoneDriver.html), [`CDiscAdjMultizoneDriver`](../../doxygen/classCDiscAdjMultizoneDriver.html): discrete adjoint problems. +- [`CHBDriver`](../../doxygen/classCHBDriver.html) (child of [`CFluidDriver`](../../doxygen/classCFluidDriver.html)): harmonic balance. +- [`CDummyDriver`](../../doxygen/classCDummyDriver.html): dry run (`SU2_CFD -d`). It prints the available output fields. + +### Iterations + +[`CIteration`](../../doxygen/classCIteration.html) runs one iteration of one physics in one zone (`Preprocess`, `Iterate`, `Postprocess`, `Monitor`, `Output`, `Solve`). `CIterationFactory` creates the children: + +- [`CFluidIteration`](../../doxygen/classCFluidIteration.html), with the children [`CTurboIteration`](../../doxygen/classCTurboIteration.html), [`CPBFluidIteration`](../../doxygen/classCPBFluidIteration.html) (pressure-based incompressible), [`CFEMFluidIteration`](../../doxygen/classCFEMFluidIteration.html), [`CHeatIteration`](../../doxygen/classCHeatIteration.html) and [`CAdjFluidIteration`](../../doxygen/classCAdjFluidIteration.html) (continuous adjoint). +- [`CFEAIteration`](../../doxygen/classCFEAIteration.html): structural analysis. +- [`CDiscAdjFluidIteration`](../../doxygen/classCDiscAdjFluidIteration.html), [`CDiscAdjHeatIteration`](../../doxygen/classCDiscAdjHeatIteration.html), [`CDiscAdjFEAIteration`](../../doxygen/classCDiscAdjFEAIteration.html): discrete adjoint. + +### Integrations + +[`CIntegration`](../../doxygen/classCIntegration.html) holds `Space_Integration` and `Time_Integration`. `CIntegrationFactory` picks the child from the solver type (`INTEGRATION_TYPE` in `SU2_CFD/include/solvers/CSolverFactory.hpp`): + +- [`CMultiGridIntegration`](../../doxygen/classCMultiGridIntegration.html): finite volume flow solvers (also with `MGLEVEL= 0`). +- [`CSingleGridIntegration`](../../doxygen/classCSingleGridIntegration.html): turbulence, transition, species, heat and radiation solvers. +- [`CNewtonIntegration`](../../doxygen/classCNewtonIntegration.html): flow solvers with `NEWTON_KRYLOV= YES` (primal only). +- [`CStructuralIntegration`](../../doxygen/classCStructuralIntegration.html): structural (FEA) solver. +- [`CFEM_DG_Integration`](../../doxygen/classCFEM__DG__Integration.html): discontinuous Galerkin flow solver. +- The discrete adjoint solvers use the base `CIntegration`. + +### Solvers + +[`CSolver`](../../doxygen/classCSolver.html) solves one set of equations on one grid level. It: + +- computes the residuals; +- applies the boundary conditions (`BC_...` functions); +- owns the linear system (`Jacobian`, `LinSysRes`, `LinSysSol`, `System`). + +`CSolverFactory` (`SU2_CFD/src/solvers/CSolverFactory.cpp`) decides which solvers to create for each `SOLVER` option. + +Main families: + +- Finite volume flow: [`CFVMFlowSolverBase`](../../doxygen/classCFVMFlowSolverBase.html) is a template with the shared code. Its children are [`CEulerSolver`](../../doxygen/classCEulerSolver.html) / [`CNSSolver`](../../doxygen/classCNSSolver.html) (compressible), [`CIncEulerSolver`](../../doxygen/classCIncEulerSolver.html) / [`CIncNSSolver`](../../doxygen/classCIncNSSolver.html) (incompressible) and [`CNEMOEulerSolver`](../../doxygen/classCNEMOEulerSolver.html) / [`CNEMONSSolver`](../../doxygen/classCNEMONSSolver.html) (non-equilibrium). +- Scalar transport: [`CScalarSolver`](../../doxygen/classCScalarSolver.html) (template), with: + - [`CTurbSolver`](../../doxygen/classCTurbSolver.html) → [`CTurbSASolver`](../../doxygen/classCTurbSASolver.html), [`CTurbSSTSolver`](../../doxygen/classCTurbSSTSolver.html), [`CTransLMSolver`](../../doxygen/classCTransLMSolver.html); + - [`CSpeciesSolver`](../../doxygen/classCSpeciesSolver.html) → [`CSpeciesFlameletSolver`](../../doxygen/classCSpeciesFlameletSolver.html); + - [`CHeatSolver`](../../doxygen/classCHeatSolver.html); + - [`CPoissonSolver`](../../doxygen/classCPoissonSolver.html). +- Others: [`CRadP1Solver`](../../doxygen/classCRadP1Solver.html), [`CFEASolver`](../../doxygen/classCFEASolver.html), [`CMeshSolver`](../../doxygen/classCMeshSolver.html) (mesh deformation), [`CFEM_DG_EulerSolver`](../../doxygen/classCFEM__DG__EulerSolver.html) / [`CFEM_DG_NSSolver`](../../doxygen/classCFEM__DG__NSSolver.html), [`CBaselineSolver`](../../doxygen/classCBaselineSolver.html). +- Adjoint: [`CDiscAdjSolver`](../../doxygen/classCDiscAdjSolver.html) (discrete adjoint of any solver), [`CDiscAdjFEASolver`](../../doxygen/classCDiscAdjFEASolver.html), [`CDiscAdjMeshSolver`](../../doxygen/classCDiscAdjMeshSolver.html), and the continuous adjoint solvers [`CAdjEulerSolver`](../../doxygen/classCAdjEulerSolver.html), [`CAdjNSSolver`](../../doxygen/classCAdjNSSolver.html), [`CAdjTurbSolver`](../../doxygen/classCAdjTurbSolver.html). +- [`CTemplateSolver`](../../doxygen/classCTemplateSolver.html): an empty starting point for a new solver. + +### Variables + +[`CVariable`](../../doxygen/classCVariable.html) stores the data of **all** points of one solver in one object (`solver->GetNodes()`). Each quantity is its own container, indexed by point: `Solution`, `Solution_Old`, `Gradient`, `Limiter`, `Delta_Time`, ... The matrices are row-major (`su2activematrix`), so the values of one point sit next to each other in memory. You access a value by point index, for example `nodes->GetSolution(iPoint, iVar)`. + +The children follow the solvers: + +- [`CFlowVariable`](../../doxygen/classCFlowVariable.html) → [`CEulerVariable`](../../doxygen/classCEulerVariable.html) → [`CNSVariable`](../../doxygen/classCNSVariable.html); +- [`CIncEulerVariable`](../../doxygen/classCIncEulerVariable.html) → [`CIncNSVariable`](../../doxygen/classCIncNSVariable.html); +- [`CScalarVariable`](../../doxygen/classCScalarVariable.html) → [`CTurbVariable`](../../doxygen/classCTurbVariable.html) → [`CTurbSAVariable`](../../doxygen/classCTurbSAVariable.html), [`CTurbSSTVariable`](../../doxygen/classCTurbSSTVariable.html); +- and so on. + +### Numerics + +[`CNumerics`](../../doxygen/classCNumerics.html) computes a flux across one edge, or a source term at one point. `ComputeResidual(config)` returns a `ResidualType` with the residual and the Jacobians for the two points of the edge. + +The solver uses it in this way: + +1. It loops over the edges or points. +2. It sets the inputs of the numerics object. +3. It calls `ComputeResidual`. +4. It adds the result to `LinSysRes` and `Jacobian`. + +`CDriver::InitializeNumerics` (`SU2_CFD/src/drivers/CDriver.cpp`) creates the numerics objects. It uses the helpers `InstantiateTurbulentNumerics`, `InstantiateTransitionNumerics` and `InstantiateSpeciesNumerics`. + +### Geometry + +[`CGeometry`](../../doxygen/classCGeometry.html) holds the mesh and the dual grid. The dual grid is the set of control volumes, one around each mesh point, that the edge-based finite volume method uses. The main members are: + +- `nodes` ([`CPoint`](../../doxygen/classCPoint.html)): coordinates, volumes and neighbors of all points. +- `edges` ([`CEdge`](../../doxygen/classCEdge.html)): the two points and the normal of each edge. +- `vertex[iMarker][iVertex]` ([`CVertex`](../../doxygen/classCVertex.html)): boundary points and their normals. +- `InitiateComms` / `CompleteComms`: halo exchange between MPI ranks (see [MPI](#parallelism-mpi-openmp-and-vectorization) below). + +Children: + +- [`CPhysicalGeometry`](../../doxygen/classCPhysicalGeometry.html): the mesh read from file, partitioned with ParMETIS. The file is in SU2 or CGNS format; the readers are in `Common/include/geometry/meshreader`. +- [`CMultiGridGeometry`](../../doxygen/classCMultiGridGeometry.html): the coarse levels. Each one is made by agglomeration, which merges neighboring control volumes of the finer level into larger ones. +- [`CDummyGeometry`](../../doxygen/classCDummyGeometry.html): empty geometry for the dry run. +- [`CMeshFEM_DG`](../../doxygen/classCMeshFEM__DG.html): geometry of the DG solver (`Common/include/fem`). + +Grid movement (`CSurfaceMovement`, `CVolumetricMovement`, `CFreeFormDefBox`) is in `Common/include/grid_movement`. Interpolation between non-matching zone interfaces (`CInterpolator` and its children) is in `Common/include/interface_interpolation`. + +### Configuration + +[`CConfig`](../../doxygen/classCConfig.html) (`Common/include/CConfig.hpp`, `Common/src/CConfig.cpp`) reads the configuration file and stores every option. There is one `CConfig` per zone. There is also a `driver_config`, read from the main configuration file. It holds the settings of the driver, for example for multizone problems. + +- `CConfig::SetConfig_Options` declares the options with `addDoubleOption`, `addBoolOption`, `addEnumOption`, `addStringListOption`, ... +- The enums, and the maps from option strings to enum values, are in `Common/include/option_structure.hpp`. The option parser classes are in `Common/include/option_structure.inl`. +- `CConfig::SetPostprocessing` and `CConfig::SetMarkers` compute the derived settings. + +See [Adding a Config Option](../Adding-a-Config-Option/) and the [Option Reference](../Option-Reference/). + +### Output + +[`COutput`](../../doxygen/classCOutput.html) writes the screen output, the history file, and the volume and surface files. Each solver kind has a child class, created by `COutputFactory`. Examples are [`CFlowCompOutput`](../../doxygen/classCFlowCompOutput.html), [`CFlowIncOutput`](../../doxygen/classCFlowIncOutput.html), [`CHeatOutput`](../../doxygen/classCHeatOutput.html) and [`CElasticityOutput`](../../doxygen/classCElasticityOutput.html). [`CMultizoneOutput`](../../doxygen/classCMultizoneOutput.html) combines the zones of a multizone run. The file writers (Paraview, Tecplot, CGNS, CSV, SU2 restart, ...) are in `SU2_CFD/include/output/filewriter`. + +See [Adding Output Fields](../Adding-Output-Fields/) for the details. + +## Linear algebra + +Files: `Common/include/linear_algebra` and `Common/src/linear_algebra`. + +- [`CSysMatrix`](../../doxygen/classCSysMatrix.html): block sparse matrix, with one block per pair of neighbor points. Implicit solvers use it for the Jacobian. +- [`CSysVector`](../../doxygen/classCSysVector.html): block vector, for the residual and the solution update. +- [`CSysSolve`](../../doxygen/classCSysSolve.html): Krylov solvers (`FGMRES_LinSolver`, `RFGMRES_LinSolver`, `FGCRODR_LinSolver`, `BCGSTAB_LinSolver`, `CG_LinSolver`, `Smoother_LinSolver`). `Solve` reads the method and the preconditioner from the configuration. For the primal flow these are `LINEAR_SOLVER` and `LINEAR_SOLVER_PREC`. The mesh, adjoint and other problems have their own options. +- [`CPreconditioner`](../../doxygen/classCPreconditioner.html) and its children [`CJacobiPreconditioner`](../../doxygen/classCJacobiPreconditioner.html), [`CILUPreconditioner`](../../doxygen/classCILUPreconditioner.html), [`CLU_SGSPreconditioner`](../../doxygen/classCLU__SGSPreconditioner.html), [`CLineletPreconditioner`](../../doxygen/classCLineletPreconditioner.html), [`CPastixPreconditioner`](../../doxygen/classCPastixPreconditioner.html). `CPreconditioner::Create` builds the chosen one. +- [`CMatrixVectorProduct`](../../doxygen/classCMatrixVectorProduct.html): the product used inside the Krylov solvers. + +Each solver has the members `Jacobian` (`CSysMatrix`), `LinSysRes`, `LinSysSol` and `System` (`CSysSolve`). `su2mixedfloat` is `float` when SU2 is built with `-Denable-mixedprec=true`. See also [Linear Solvers and Preconditioners](../Linear-Solvers-and-Preconditioners/). + +## Containers and toolboxes + +Containers (`Common/include/containers`): + +- [`C2DContainer`](../../doxygen/classC2DContainer.html) (`C2DContainer.hpp`): aligned 2D storage. The same file defines the common type aliases: + - `su2vector`, `su2matrix` (row-major), `ColMajorMatrix`; + - `su2activevector`, `su2activematrix` (of `su2double`); + - `su2passivevector`, `su2passivematrix` (of `passivedouble`). +- `container_decorators.hpp`: `C3DDoubleMatrix` (also called `CVectorOfMatrix`, used for gradients) and `CMatrixView`. +- `CLookUpTable`, `CFileReaderLUT`, `CTrapezoidalMap`: look-up tables. The flamelet fluid model (`CFluidFlamelet`) and the data-driven fluid model (`CDataDrivenFluid`) use them. +- `CVertexMap`: maps indices from a large range (for example all points) to a smaller one (for example the boundary vertices). +- `CFastFindAndEraseQueue`: a queue that can also remove items from the middle quickly. +- `CPyWrapperMatrixView`: gives the Python wrapper access to solver data such as `su2activematrix`. + +Toolboxes (`Common/include/toolboxes`): + +- `geometry_toolbox.hpp` (vector operations); +- `graph_toolbox.hpp` (sparse patterns, coloring); +- `printing_toolbox.hpp` (`PrintingToolbox::CTablePrinter`); +- `ndflattener.hpp`, `CSquareMatrixCM`, `CSymmetricMatrix`, `C1DInterpolation`, `CLinearPartitioner`, `CQuasiNewtonInvLeastSquares`; +- the verification solutions in `toolboxes/MMS`. + +## Parallelism: MPI, OpenMP and vectorization + +Files: `Common/include/parallelization`. + +**MPI.** Use the `SU2_MPI` wrapper from `mpi_structure.hpp`, never MPI directly. `SU2_MPI` is `CBaseMPIWrapper`, or `CMediMPIWrapper` (MeDiPack) in AD builds. This way MPI calls work with `su2double`. + +SU2 partitions the mesh at start-up. Each rank also stores halo points: copies of the points next to its partition that belong to other ranks. To update the halo values, call `InitiateComms` / `CompleteComms` of `CGeometry` and `CSolver`. An `MPI_QUANTITIES` value says what to send. + +**OpenMP.** Do not include `omp.h`. Include `omp_structure.hpp` and use its macros. They compile to nothing when OpenMP is off (`-Dwith-omp=false`), and to OpDiLib calls in AD builds: + +- `SU2_OMP_PARALLEL` ... `END_SU2_OMP_PARALLEL`, `SU2_OMP_PARALLEL_(ARGS)` +- `SU2_OMP_FOR_STAT(CHUNK)`, `SU2_OMP_FOR_DYN(CHUNK)`, `SU2_OMP_FOR_(ARGS)` ... `END_SU2_OMP_FOR` +- `SU2_OMP_MASTER` ... `END_SU2_OMP_MASTER`, `SU2_OMP_CRITICAL` ... `END_SU2_OMP_CRITICAL`, `SU2_OMP_BARRIER`, `SU2_OMP_ATOMIC` +- `SU2_OMP_SAFE_GLOBAL_ACCESS(...)`: run code on one thread, with barriers around it +- `SU2_OMP_SIMD` +- helpers: `omp_get_num_threads()`, `omp_get_thread_num()`, `roundUpDiv`, `atomicAdd` + +When the solver supports it (`CSolver::GetHasHybridParallel()`), the integrations open one parallel region for a whole iteration. + +Edge loops write to both points of an edge. Two threads could then write to the same point at the same time (a race condition). To avoid this, SU2 groups the edges by color (`CGeometry::GetEdgeColoring`, option `EDGE_COLORING_GROUP_SIZE`). If the coloring is poor, the flow and scalar solvers first store the edge fluxes, then sum them for each point (`ReducerStrategy`, `EdgeFluxes`). + +Set the number of threads per rank with `SU2_CFD -t `. + +**Vectorization.** SIMD (single instruction, multiple data) instructions let the CPU apply one operation to several values at once. + +- `vectorization.hpp` defines [`simd::Array`](../../doxygen/classsimd_1_1Array.html), a short fixed-size array with arithmetic operators. The compiler turns its loops into SIMD instructions. `special_vectorization.hpp` has explicit versions for some instruction sets. +- [`CNumericsSIMD`](../../doxygen/classCNumericsSIMD.html) (`SU2_CFD/include/numerics_simd`) uses it to compute the fluxes of several edges at once. +- `CEulerSolver` uses the vectorized versions for all centered schemes. It also uses them for the `ROE` and `MSW` upwind schemes, with an ideal gas and no low-Mach correction. See `CEulerSolver::Centered_Residual` and `CEulerSolver::Upwind_Residual`. +- The build option `cpu-arch` (default `native`) sets the target instruction set. + +## Algorithmic differentiation + +SU2 uses operator overloading AD with [CoDiPack](https://github.com/scicompkl/CoDiPack) (`externals/codi`). + +- `su2double` (`Common/include/code_config.hpp`) is the type for all values that may need derivatives. What it is depends on the build: + - in a normal build, it is `passivedouble` (normally `double`); + - with `-Denable-autodiff=true` (builds `SU2_CFD_AD`, `SU2_DOT_AD`), it is a CoDiPack reverse type; + - with `-Denable-directdiff=true` (builds `SU2_CFD_DIRECTDIFF`), it is a forward type. +- `passivedouble` is never differentiated. Use `SU2_TYPE::GetValue(x)` to get the plain value of a `su2double`. +- The [`AD`](../../doxygen/namespaceAD.html) namespace (`Common/include/basic_types/ad_structure.hpp`) wraps the tape: + - `StartRecording`, `StopRecording`, `RegisterInput`, `RegisterOutput`, `ComputeAdjoint`, `ClearAdjoints`; + - preaccumulation (`StartPreacc`, `SetPreaccIn`, `SetPreaccOut`, `EndPreacc`). Preaccumulation stores only the Jacobian of a code section on the tape, instead of all its operations. This makes the tape smaller. + + The functions do nothing in non-AD builds, so the code can always call them. +- MeDiPack (`externals/medi`) differentiates MPI calls. OpDiLib (`externals/opdi`) differentiates OpenMP regions. + +See [Advanced AD Techniques](../Advanced-AD-Techniques/) for more. + +## Where do I change... + +### New boundary condition + +1. Add the kind to `enum BC_TYPE` in `Common/include/option_structure.hpp`. +2. In `Common/src/CConfig.cpp`, add the marker option in `CConfig::SetConfig_Options`, and set `Marker_CfgFile_KindBC` in `CConfig::SetMarkers`. +3. Add a virtual `BC_...` function to `CSolver` (`SU2_CFD/include/solvers/CSolver.hpp`). +4. Implement it in the solvers that need it, for example `CEulerSolver`, `CIncEulerSolver`, and the scalar solvers through `CScalarSolver`. +5. Call it from the `switch` on `GetMarker_All_KindBC` in `CIntegration::Space_Integration`. + +### New convective scheme + +1. Add the value to `enum class UPWIND` (or `CENTERED`) and to `Upwind_Map` in `option_structure.hpp`. +2. Write the numerics class in `SU2_CFD/include/numerics/flow/convection/` and `SU2_CFD/src/numerics/flow/convection/`. +3. Create it in `CDriver::InitializeNumerics` (switch on `GetKind_Upwind_Flow()`). +4. Optional, for a compressible ideal gas only: write a vectorized version in `SU2_CFD/include/numerics_simd/flow/convection/upwind.hpp`. Create it in `createUpwindIdealNumerics` (`CNumericsSIMD.cpp`) and select it in `CEulerSolver::Upwind_Residual`. + +### New turbulence model source term + +- The source terms are in `SU2_CFD/include/numerics/turbulent/turb_sources.hpp`: `CSourceBase_TurbSA` and `SAFactory` for the Spalart-Allmaras (SA) model, `CSourcePieceWise_TurbSST` for the SST (shear stress transport) model. +- `CDriver::InstantiateTurbulentNumerics` creates them. `CTurbSASolver::Source_Residual` / `CTurbSSTSolver::Source_Residual` use them. +- Users choose the model variants with `SA_OPTIONS` / `SST_OPTIONS` (`enum class SA_OPTIONS`, `SST_OPTIONS` in `option_structure.hpp`). + +### New output field + +Change the output class of the solver in `SU2_CFD/src/output/`, or `CFlowOutput` for turbulence and species fields. See [Adding Output Fields](../Adding-Output-Fields/). + +### New config option + +Add the option in `CConfig::SetConfig_Options`, a member and a getter in `CConfig.hpp`, and an entry in `config_template.cfg`. See [Adding a Config Option](../Adding-a-Config-Option/). + +### New solver + +Start from `CTemplateSolver` (`SU2_CFD/include/solvers/CTemplateSolver.hpp`) and the template numerics (`CConvective_Template`, `CViscous_Template`, `CSource_Template` in `SU2_CFD/include/numerics/template.hpp`). Then add: + +1. a value in `enum class MAIN_SOLVER` and `Solver_Map` (`option_structure.hpp`); +2. the solver in `CSolverFactory::CreateSolverContainer` and `CreateSubSolver`, with its `INTEGRATION_TYPE`; +3. its numerics in `CDriver::InitializeNumerics`; +4. an iteration in `CIterationFactory::CreateIteration`; +5. an output class in `COutputFactory::CreateOutput`. + +### Tests and style + +When you change code, add or update a regression test (see [Running Regression Tests](../Running-Regression-Tests/)) or a unit test (see [Writing Unit Tests](../Writing-Unit-Tests/)). Follow the [Style Guide](../Style-Guide/). + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Debugging.md b/Docs/website/pages/Debugging.md new file mode 100644 index 000000000000..e489e4d7ef26 --- /dev/null +++ b/Docs/website/pages/Debugging.md @@ -0,0 +1,408 @@ +--- +title: Debugging SU2 +permalink: /docs_v7/Debugging/ +--- + +This page shows practical ways to find bugs in SU2: crashes, wrong results, divergence, data races and slow code. + +Tips marked **(advice)** are general practice, not features of SU2. Words such as "zone", "halo point" or +"reducer strategy" are explained in the [Glossary](../Glossary/). + +--- + +- [Build for debugging](#build-for-debugging) +- [Check the config file first](#check-the-config-file-first) +- [Reproduce with a small case](#reproduce-with-a-small-case) +- [Use a debugger](#use-a-debugger) +- [Sanitizers](#sanitizers) +- [OpenMP data races](#openmp-data-races) +- [Floating point exceptions](#floating-point-exceptions) +- [Find where a NaN comes from](#find-where-a-nan-comes-from) +- [Algorithmic differentiation](#algorithmic-differentiation) +- [Profiling](#profiling) +- [Valgrind](#valgrind) + +--- + +## Build for debugging + +Make a separate build folder for debugging, so your normal build stays fast **(advice)**: + +``` +./meson.py setup build_debug --buildtype=debug --prefix=$PWD/install_debug +./ninja -C build_debug install +``` + +The build type changes what the compiler does: + +| Build type | Optimization | Debug symbols (`-g`) | `assert` checks | +|---|---|---|---| +| `release` (default) | yes | no | off | +| `debugoptimized` | yes (`-O2`) | yes | off | +| `debug` | no | yes | on | + +Notes: + +- SU2 sets `b_ndebug=true` in `meson.build`, which turns `assert` off. Only `--buildtype=debug` turns it back on + (SU2 adds `-UNDEBUG`). To keep the checks with optimization, add the meson option `-Db_ndebug=false` to a + `debugoptimized` build **(advice)**. +- With `assert` on, SU2 also runs some extra checks that are inside `#ifndef NDEBUG`. For example + `CSysMatrix` checks the sizes of the vectors in the matrix-vector product, and prints a message when it + regularizes a small pivot. +- `debugoptimized` is a good default for gdb and profilers: the code is fast enough for real cases, and the stack + traces have function names and lines **(advice)**. Some variables show as "optimized out"; use `debug` when you + need them. +- `--warnlevel=3` turns on most compiler warnings. Check your change with `--warnlevel=3 --werror` locally; + `--werror` makes warnings fail the build **(advice)**. Most CI configurations use warning level 3, but the + current `.github/workflows/regression.yml` exports the CPU flags as its `werror` output by mistake, so those + builds do not currently receive `--werror`. +- `-Dcpu-arch` (default `native`) sets `-march`. `-Dcpu-arch=` (empty) removes it. This helps with tools that do + not know the newest instructions, see [Valgrind](#valgrind). +- SU2 always compiles with `-ffast-math -fno-finite-math-only` (when the compiler accepts these flags). NaN checks + with `std::isnan` still work. Other effects of fast math are described in + [Floating point exceptions](#floating-point-exceptions). + +Build options in `meson_options.txt` that matter when debugging: + +| Option | Use | +|---|---| +| `-Dwith-mpi=disabled` | Serial build. Simpler to debug, and needed for the address sanitizer jobs of the CI. | +| `-Dwith-omp=true` | OpenMP build, for thread problems. | +| `-Denable-tests=true` | Builds the unit tests, see [Running Unit Tests](../Running-Unit-Tests/). | +| `-Denable-mixedprec=true` | Linear algebra in single precision. Try without it if a linear solver fails. | +| `-Denable-autodiff=true`, `-Denable-directdiff=true` | AD builds, see [Algorithmic differentiation](#algorithmic-differentiation). | +| `-Dtracy_enable=true` | Tracy profiler, see [Profiling](#profiling). | +| `-Denable-gprof=true` | gprof profiling, see [Profiling](#profiling). | + +To change the options of an existing build folder, use `./meson.py setup build_debug --reconfigure `. +See [Build SU2 on Linux/MacOS](../Build-SU2-Linux-MacOS/) for all build steps. + +## Check the config file first + +Many problems are config problems. Check the config before you look at the code. + +### The dry run + +``` +SU2_CFD -d case.cfg +``` + +`-d` (or `--dryrun`) starts `CDummyDriver`. SU2 reads and checks the whole config file, sets up the solvers on an +empty geometry, prints the available volume and history output fields, and stops. It does not read the mesh +points, so it is fast even for large cases. The mesh file must still exist, because SU2 reads the number of +dimensions from it. + +### Read the error box + +When SU2 stops on an error, it prints a box like this (from `SU2_MPI::Error`): + +``` +Error in "void CConfig::SetConfig_Parsing(std::istream&)": +------------------------------------------------------------------------- +Line 12 KIND_TURB_MODLE: invalid option name. Check current SU2 options in config_template.cfg. +Did you mean KIND_TURB_MODEL? +------------------------------ Error Exit ------------------------------- +``` + +- The first line names the C++ function that stopped. Search for the message in that function to see the exact + check. +- The parser collects all errors of the file, up to 30, before it stops. Fix them from the top. +- Errors about the config file come in three kinds: an unknown option name (with a "Did you mean" guess), an + option that appears twice, and a bad value (`invalid option value`, with the list of allowed values). +- Other errors come later, from the checks in `CConfig::SetPostprocessing` and from the solvers. + +The [Error Messages](../Error-Messages/) page lists every `SU2_MPI::Error` message with a link to its code. If an +old config file stops with "invalid option name", look for the option in +[Renamed and Removed Options](../Renamed-Options/). The [Option Reference](../Option-Reference/) lists all +current options. + +## Reproduce with a small case + +Debugging is faster on a small case that runs in seconds **(advice)**. Good sources are the regression cases in +`TestCases/`. [Running Regression Tests](../Running-Regression-Tests/#run-one-test) explains how to run one test. + +When the test script runs a case, it writes a copy of the config file with the number of iterations changed, +named `.cfg.autotest`, and the screen output to `.log`. Both stay in the case folder. To repeat +exactly what the test did, run the `.autotest` file by hand, for example in gdb: + +``` +cd euler/naca0012 +gdb --args SU2_CFD inv_NACA0012_Roe.cfg.autotest +``` + +Lower `ITER` (or `INNER_ITER`, `OUTER_ITER`, `TIME_ITER`) to the first iteration that shows the problem +**(advice)**. + +## Use a debugger + +### Serial runs + +``` +gdb --args SU2_CFD case.cfg +(gdb) run +(gdb) bt +``` + +After a crash, `bt` prints the call stack. To stop where SU2 reports an error, set a breakpoint before `run` +**(advice)**. `SU2_MPI::Error` ends with `MPI_Abort` in an MPI build, and with `exit` in a build without MPI: + +``` +(gdb) break MPI_Abort # build with MPI +(gdb) break exit # build without MPI +``` + +To stop at a solver function, use its full name, for example `break CSolver::SetResidual_RMS` **(advice)**. + +### Runs with MPI + +Some bugs only show in parallel, for example at the halo points. Three ways to use gdb with MPI **(advice)**: + +- One terminal per rank (needs a display): + + ``` + mpirun -np 2 xterm -e gdb --args SU2_CFD case.cfg + ``` + +- Only one rank in gdb, the others run normally (Open MPI syntax): + + ``` + mpirun -np 1 gdb --args SU2_CFD case.cfg : -np 1 SU2_CFD case.cfg + ``` + +- Attach to a running rank: start the run, find the process id with `ps` or `pgrep SU2_CFD`, then + `gdb -p `. This is useful for a run that hangs. Run `bt` in each rank to see where they wait. + +Only one rank prints the error box: the lowest rank that called `SU2_MPI::Error`. Other ranks may have failed +too. + +### The Python wrapper + +The Python wrapper (built with `-Denable-pywrapper=true`) runs the same C++ code. Debug it with +`gdb --args python3 your_script.py` **(advice)**. + +## Sanitizers + +Sanitizers are compiler tools that check the program while it runs: + +- the address sanitizer (asan) finds memory errors, such as reading past the end of an array, use after free and + leaks; +- the thread sanitizer (tsan) finds data races between threads. + +The program runs slower, and stops with a report on the first error. + +### How the CI uses them + +The workflow `.github/workflows/regression.yml` has these jobs: + +| Build job | Configurations | Main build flags | Test job | +|---|---|---|---| +| Build SU2 (asan) | `BaseNoMPI-asan`, `ReverseNoMPI-asan` | `--buildtype=debugoptimized -Dwith-mpi=disabled`; the reverse build also has `--optimization=1 -Denable-autodiff=true -Denable-normal=false` | Address Sanitizer Tests: `serial_regression.py`, `serial_regression_AD.py` with `--asan` | +| Build SU2 (tsan) | `BaseOMP-tsan`, `ReverseOMP-tsan` | `--buildtype=debugoptimized -Dwith-omp=true -Denable-mixedprec=true -Denable-tecio=false`; the reverse build also has `-Denable-autodiff=true -Denable-normal=false` | Thread Sanitizer Tests: `hybrid_regression.py`, `hybrid_regression_AD.py` with `--tsan` | + +The sanitizer flags are not in the table, because the build containers set them: +`ghcr.io/su2code/su2/build-su2-asan` and `build-su2-tsan`, made from the +[Docker-Builds](https://github.com/su2code/Docker-Builds) repository. The tsan container has a compiler and +OpenMP library built for the thread sanitizer. + +### What `--asan` and `--tsan` do + +The test scripts read these arguments with `parse_args()` in `TestCase.py`. With one of them: + +- tests with `enabled_with_asan = False` (or `enabled_with_tsan = False`) are skipped; +- the number of iterations is cut to at most 2 after the restart iteration, because sanitized runs are slow; +- a test fails only if SU2 returns an error. The results are not compared with the stored values. + +### Run a sanitizer on your computer + +The easiest way is the CI container, see [Container Development](../Container-Development/). It also explains how +to work inside the tsan container. + +You can also use the meson option `b_sanitize` **(advice)**: + +``` +./meson.py setup build_asan --buildtype=debugoptimized -Dwith-mpi=disabled -Db_sanitize=address +./meson.py setup build_tsan --buildtype=debugoptimized -Dwith-omp=true -Db_sanitize=thread +``` + +Use a build without MPI for asan, like the CI: MPI libraries give many reports that are not SU2 bugs +**(advice)**. For tsan with OpenMP, the OpenMP library must also be built for tsan. Otherwise you get false +reports, which is why the CI uses a special container **(advice)**. + +## OpenMP data races + +A data race happens when two threads write to the same memory, or one writes while another reads, without +synchronization. The result then changes from run to run. + +How to look for one: + +1. Run the case with threads: `SU2_CFD -t 2 case.cfg` (`-t` sets the number of OpenMP threads per MPI rank). The + hybrid regression tests use `-t 2`. +2. Run it several times with the same number of threads, and compare the history files. Results that change + between identical runs point to a race **(advice)**. Small differences between different thread counts can + also come from a different order of sums, and are not always a bug **(advice)**. +3. Set `EDGE_COLORING_GROUP_SIZE= 0`. This forces the reducer strategy in the flow and scalar solvers: the edge + fluxes are stored first, then summed for each point, so the edge loop has no shared writes. If the problem + disappears, look at the edge loops and the edge coloring. +4. Run the case in a tsan build, see [Sanitizers](#sanitizers). + +At start-up, SU2 prints either "All ranks use edge coloring." or a warning that some ranks use the fallback (the +reducer strategy), with the coloring efficiency. + +Write thread code only with the macros of `Common/include/parallelization/omp_structure.hpp` +(`SU2_OMP_PARALLEL`, `SU2_OMP_FOR_STAT`, `END_SU2_OMP_FOR`, `SU2_OMP_MASTER`, `BEGIN_SU2_OMP_SAFE_GLOBAL_ACCESS`, ...). +A block such as `SU2_OMP_PARALLEL` or `SU2_OMP_FOR_STAT` needs its `END_SU2_OMP_...` macro. When you configure a build with both +`-Dwith-omp=true` and `-Denable-autodiff=true`, meson runs the OpDiLib syntax checker (rules in +`su2omp.syntax.json`) and stops if the macros are not used correctly. + +See [Parallelism](../Code-Structure/#parallelism-mpi-openmp-and-vectorization) for how the threads share the work. + +## Floating point exceptions + +SU2 has no config option or build option to stop on floating point exceptions (division by zero, overflow, +invalid operations that make a NaN). + +`SU2_CFD/src/SU2_CFD.cpp` has two commented lines for this. To use them, remove the comment marks from both, +and rebuild: + +```cpp +#include // near the top of the file +feenableexcept(FE_INVALID | FE_OVERFLOW | FE_DIVBYZERO ); // in main() +``` + +The program then stops with the signal `SIGFPE` at the first such operation. Run it in gdb to see where. + +Limits **(advice)**: + +- `feenableexcept` is a GNU extension (Linux with glibc). +- With `-ffast-math` the compiler assumes that math operations do not trap. It may move or add operations, so a + trap can come from code that is correct. A `debug` build has no optimization, so it gives cleaner results. +- On x86, `-ffast-math` may also link code that sets denormal numbers to zero. Results with very small numbers can + then differ from a build without fast math. +- Libraries called before or by SU2 (MPI, BLAS) can also trigger the trap. + +## Find where a NaN comes from + +### Where SU2 stops + +SU2 checks for NaN in a few places and stops with `SU2 has diverged (NaN detected).`: + +- `CSolver::SetResidual_RMS`, on the root mean square residual of each equation. It also stops when the residual + is larger than 10^20 (`SU2 has diverged (Residual > 10^20 detected).`). +- `COutput` (`SU2_CFD/src/output/COutput.cpp`), on the history fields used for the convergence check. Here SU2 + stops without saving the solution. +- The DG (discontinuous Galerkin) solver (`CFEM_DG_EulerSolver`) has its own checks. + +The NEMO (non-equilibrium) solvers also check each flux and source term with `CNumerics::CheckResidualNaNs`. + +The check happens after the iteration is done, so the NaN can come from anywhere in that iteration. The steps +below narrow it down. + +### Write the solution before it breaks + +Write the volume files at every iteration, and keep one file per iteration: + +``` +OUTPUT_WRT_FREQ= 1 +WRT_VOLUME_OVERWRITE= NO +VOLUME_OUTPUT= SOLUTION, PRIMITIVE, RESIDUAL, LIMITER, MESH_QUALITY, MPI +``` + +The last file written before the error shows the state just before the NaN. Useful volume output groups: + +| Group | Fields | What to look for | +|---|---|---| +| `RESIDUAL` | `RES_DENSITY`, `RES_MOMENTUM-X`, ..., `RES_TKE`, ... | Where the residual grows first. | +| `LIMITER` | `LIMITER_PRESSURE`, ..., `LIMITER_TKE`, ... | Points where the limiter goes to 0 (strong gradients, shocks). | +| `PRIMITIVE` | `PRESSURE`, `TEMPERATURE`, `MACH`, `EDDY_VISCOSITY`, ... | Negative or extreme values. | +| `MESH_QUALITY` | `ORTHOGONALITY`, `ASPECT_RATIO`, `VOLUME_RATIO` | Bad cells near the problem. | +| `MPI` | `RANK` | Problems on a partition border point to halo exchange bugs. | + +The groups that exist depend on the solver. Run `SU2_CFD -d case.cfg` to print them, or see the +[Output Fields Reference](../Output-Fields-Reference/). + +In a long run, restart from a solution saved a few iterations before the failure, and write the files at every +iteration only from there **(advice)**. + +### History fields to watch + +- `RMS_RES` and `MAX_RES` groups: which equation grows first. +- `NONPHYSICAL_POINTS`: the number of points where the flow solver found a non-physical state (for example a + negative pressure or density). At these points the compressible solver goes back to the solution of the previous + iteration. A growing number often comes before a NaN. At the end of the run + SU2 also prints a warning with this number. +- `MIN_CFL`, `MAX_CFL`, `AVG_CFL` (group `CFL_NUMBER`, with `CFL_ADAPT= YES`): a CFL number that keeps growing + while the residual grows. +- `LINSOL` group (for example `LINSOL_ITER`, `LINSOL_RESIDUAL`): a linear solver that does not converge. + +Add them with `HISTORY_OUTPUT` and `SCREEN_OUTPUT`. + +### Common checks + +These help to tell a bug from a hard case **(advice)**: + +- Lower `CFL_NUMBER`, or use `CFL_ADAPT= YES`. +- Run first order (`MUSCL_FLOW= NO`, `MUSCL_TURB= NO`). If that works, look at the gradients and limiters. +- Run serial and in parallel. If only the parallel run fails, look at the halo exchange. +- Run with a `debug` build, so `assert` checks are on. + +## Algorithmic differentiation + +The discrete adjoint has its own debugging tools: the tape test mode `--tapetests` of the test scripts, the +options `CHECK_TAPE_TYPE` and `CHECK_TAPE_VARIABLES` (for builds with `-Dcodi-tape=Tag`), the tape statistics of +`WRT_AD_STATISTICS`, and the CoDiPack build options. A `debug` or `debugoptimized` AD build also turns on the +internal checks of CoDiPack (`CODI_EnableAssert`). + +See [Automatic Differentiation](../Automatic-Differentiation/) for how to use them. + +## Profiling + +### Built-in timing + +`WRT_PERFORMANCE= YES` prints a performance summary at the end of `SU2_CFD`: the time spent in preprocessing, +computation and output, the time per iteration, points per core and ghost points per core. It also prints the +write speed of each output file. The history fields `ITER_TIME` and `WALL_TIME` (group `WALL_TIME`) give the time +per iteration. + +### Tracy + +[Tracy](../Tracy-Integration/) shows a timeline of the instrumented code sections. The build option is +`-Dtracy_enable=true`; use it with `--buildtype=debugoptimized` (meson prints a warning otherwise). Mark code +sections with `SU2_ZONE_SCOPED` or `SU2_ZONE_SCOPED_N("name")` from `Common/include/tracy_structure.hpp`. These +macros are empty when Tracy is off. + +### perf + +On Linux, `perf` samples the whole program without changes to the code **(advice)**: + +``` +perf record -g SU2_CFD case.cfg +perf report +``` + +Use a `debugoptimized` build, so the report has function names and lines **(advice)**. For an MPI run, profile +one rank, for example with `mpirun -np 1 perf record -g SU2_CFD case.cfg : -np 3 SU2_CFD case.cfg` (Open MPI +syntax) **(advice)**. + +### gprof + +`-Denable-gprof=true` adds `-pg` to the `SU2_CFD` executables (see +[Build SU2 on Linux/MacOS](../Build-SU2-Linux-MacOS/)). Only the main file `SU2_CFD.cpp` and the link step get the +flag, not the library `SU2core` with the solver code. So the call graph of gprof is mostly empty. Prefer perf or +Tracy **(advice)**. + +## Valgrind + +Valgrind (memcheck) finds memory errors like the address sanitizer, without a special build, but it is much +slower **(advice)**. Notes: + +- The default build uses `-march=native`. Valgrind may not know the newest vector instructions (for example + AVX-512) and then stops with "unrecognised instruction". Build with `-Dcpu-arch=` (empty) or an older + architecture for valgrind runs **(advice)**. +- Use a build without MPI, or expect many reports from the MPI library **(advice)**. +- Use a very small case and few iterations **(advice)**. + +``` +valgrind --track-origins=yes SU2_CFD case.cfg +``` + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Documentation-Status.md b/Docs/website/pages/Documentation-Status.md new file mode 100644 index 000000000000..6e31ce268a47 --- /dev/null +++ b/Docs/website/pages/Documentation-Status.md @@ -0,0 +1,145 @@ +--- +title: Documentation Improvements and Status +permalink: /docs_v7/Documentation-Status/ +--- + +This page tracks the documentation overhaul: what has been implemented, what still needs review, and the next work to undertake. Update it when a batch of work is completed, with links to its changes and checks. + +**Status date: 26 October 2026.** This work is implemented on the `docs_site` branches of [rois1995/SU2](https://github.com/rois1995/SU2/tree/docs_site) and [rois1995/su2code.github.io](https://github.com/rois1995/su2code.github.io/tree/docs_site). It is a development preview; adoption by the official repositories remains a separate review. + +For the current build, see [Docs Check](../Docs-Check/), the [source and tool manifest]({{ '/build-info.json' | relative_url }}) and the [automated check results]({{ '/docs-check.json' | relative_url }}). Those reports complement this maintained account of the work; a successful build does not establish the scientific accuracy of every explanation or recommendation. + +- [What has improved](#what-has-improved) +- [Corrections from the content and code review](#corrections-from-the-content-and-code-review) +- [Evidence and current limits](#evidence-and-current-limits) +- [Guide and model review](#guide-and-model-review) +- [What is still missing](#what-is-still-missing) +- [How to go further](#how-to-go-further) +- [Change history](#change-history) +- [Keeping this page current](#keeping-this-page-current) + +## What has improved + +**Implemented** means that the feature or text is present in the fork preview. **Draft / review needed** means that content exists but the stated review is incomplete. **Planned** means that the work has not been completed. These labels do not imply numerical validation. + +| Area | Status | Improvement and where to find it | +| --- | --- | --- | +| Documentation structure | Implemented | [Using the Documentation](../Using-the-Documentation/) explains where to find setup choices, worked examples, model definitions, numerical evidence and implementation details. | +| Configuration reference | Implemented | The [Option Reference](../Option-Reference/) is generated from option registrations, enum maps and source comments, with template and regression examples. Unknown defaults and additional applicability restrictions are stated explicitly. | +| Output reference | Implemented | The [Output Fields Reference](../Output-Fields-Reference/) collects registered screen, history and volume fields. Runtime-generated names and solver-specific availability still require attention to the stated limits. | +| Errors and migration | Implemented | [Error Messages](../Error-Messages/), [Renamed and Removed Options](../Renamed-Options/) and [Changes in v8](../Guide-to-v8/) help interpret diagnostics and update old configurations. | +| Developer explanations | Implemented; ongoing maintenance | [Code Structure](../Code-Structure/), [regression testing](../Running-Regression-Tests/), [Automatic Differentiation](../Automatic-Differentiation/), [Debugging](../Debugging/), [Glossary](../Glossary/) and eight **How to Add...** guides connect explanations to the implementation. | +| Code reference | Implemented | [Doxygen](../Code-Reference/) is built with the site. [Docs Check](../Docs-Check/) reports approximate source-documentation coverage and Doxygen warnings. Publishing it does not mean every class is adequately documented. | +| Physical model explanations | Source/theory follow-up completed; expert validation pending | Dedicated pages cover [turbulence](../Turbulence-Models/), [transition](../Transition-Models/), [hybrid RANS/LES](../Hybrid-RANS-LES/) and [wall treatment](../Wall-Treatment/), including variants, implementation differences and known limits. Scientific completeness still needs expert review. | +| Case setup guidance | Nine drafts; source review completed | Guides cover external RANS, incompressible flow, unsteady/DES, transition, turbomachinery, CHT, species/combustion, shape optimization and NICFD. Shared introductions explain evidence tags and review status. | +| Worked cases and numerical comparisons | Existing collections retained | [Tutorials]({{ '/tutorials/home/' | relative_url }}) remain the home for complete worked examples; [V&V]({{ '/vandv/home/' | relative_url }}) remains the home for numerical comparisons. Setup guides link to those collections rather than creating a second case collection. | +| Search and navigation | Implemented | [Search]({{ '/search/' | relative_url }}) indexes the current documentation, Tutorials and V&V without the former per-page text truncation. Old v6 pages remain available for existing links but are excluded from this search. | +| Website maintenance | Implemented | Feed and sitemap generation were restored by consolidating the plugin configuration. Added/modified menu sections, entries and page titles remain red in the fork preview. | +| Automated checks | Implemented | Generator regression tests, local files/anchors, documented option names, enum keywords in configuration blocks, descriptions for new options and search coverage are checked during builds. Existing findings are recorded separately from new failures. | +| Local and online builds | Implemented | One build command stages both sources, generates references, builds Doxygen/Jekyll and checks the assembled site. Ruby and gem versions are fixed; each artifact records both source revisions, build flags and actual tool versions. | +| Review and publishing | Implemented on the forks | Both repositories have push and pull-request checks. Pull requests produce previews without deploying. Successful SU2 branch builds publish to the fork's Pages site; website-only changes are checked separately and can be published by selecting their commit SHA in the SU2 workflow. | + +## Corrections from the content and code review + +The 22 October review corrected 16 documentation or generation findings. The main changes were: + +- **Allowed keywords:** commented-out enum entries no longer appear as accepted settings. Migration examples now show accepted SA option combinations and explain the exceptional combinations. +- **Defaults:** array storage variables are no longer presented as default values. Template examples are distinguished from parser defaults; values initialized elsewhere are left explicitly unextracted. +- **Physical explanations:** turbulent heat transport is described separately for compressible and incompressible implementations; SA inlet values are distinguished from eddy-viscosity ratios; the SST stress explanation identifies the implemented variant and exceptions. +- **Support and diagnostics:** transition pages state the lack of LM discrete-adjoint support and the unreliable BCM intermittency output. These are documented limitations, not solver fixes delivered by this documentation work. +- **Examples and troubleshooting:** flamelet option/default names, CGNS format guidance, migration anchors and misleading transition-tutorial explanations were corrected. +- **Developer guidance:** AD comparison advice now includes convergence and matching settings; regression coverage and the existing warnings-as-errors workflow wiring are described accurately. +- **Search:** removal of the text limit restored searchable output names near the ends of long reference pages. + +The changes are recorded in SU2 commit [7862504f9d](https://github.com/rois1995/SU2/commit/7862504f9d77ee708e002f125b52b3c1eaf6f641) and website commit [be90edc](https://github.com/rois1995/su2code.github.io/commit/be90edcd0c520f57c7381f19a2cc18433916c22c). + +## Evidence and current limits + +The latest completed build-system milestone before this status page used SU2 [a14b4df35d](https://github.com/rois1995/SU2/commit/a14b4df35d3863fc79b48c781fc83b8cb222d912) and website [602189b](https://github.com/rois1995/su2code.github.io/commit/602189b07ccee63ed1a99dae1ecc3b792d8ed782). Its dated results were: + +- Nine regression checks passed; 25,607 local references and 178 search entries were checked, with zero new findings. +- Both the [SU2 build/deployment](https://github.com/rois1995/SU2/actions/runs/39788838345) and [website checks](https://github.com/rois1995/su2code.github.io/actions/runs/39788836318) passed. The published artifact was independently checked against the local source revisions, dependency lock and highlighting settings. +- Ruby 3.3.12, Bundler 2.4.22 and Jekyll 3.9.5 matched locally and in CI. Doxygen and Graphviz versions are recorded rather than pinned, so identical source revisions do not promise byte-identical output across environments. + +These numbers describe that milestone, not a permanent total. Use the current generated reports linked above for this build's source revisions and counts. + +The checks cover internal references, source extraction and selected configuration facts. They do not check external URL availability, prove convergence, validate physics or guarantee that every solver-specific option combination is supported. Source extraction uses text parsing and can miss facts established later at runtime. Draft recommendations and model explanations still need review against their stated assumptions, implementation and references. + +## Guide and model review + +**Review record, 22 October 2026:** Codex performed a documentation/source comparison of all nine setup guides and four physical-model pages against [SU2 `41ea6a219a`](https://github.com/rois1995/SU2/tree/41ea6a219ac038574f3d4a5b130c0c1d8af66457). Option spellings and enum values were checked automatically; support, units, equations and selected examples were followed into the implementation. This is an assisted source and reference review, not an independent CFD expert sign-off. No new CFD cases, gradient comparisons or mesh/time-refinement campaigns were run. + +| Page | Reviewed and corrected where needed | Evidence and remaining work | +| --- | --- | --- | +| [External RANS](../Setup-Guide-RANS-External-Aero/) | SST ambient terms, inlet defaults, model sensitivity, log10 linear residuals, wall and far-field advice. | `CConfig`, `ParseSSTOptions`, `turb_sources.hpp`, `CFlowCompOutput`; compare the selected SA/SST variant with TMR and test mesh/domain sensitivity. | +| [Incompressible](../Setup-Guide-Incompressible/) | Density/pressure coupling, thermal feedback through viscosity, enthalpy versus temperature outputs, scaling, inlet direction, 2D mass flow and FDS reconstruction. | `CIncEulerSolver`, `CConfig`; independently run the chosen density-based or pressure-based case and check conservation. | +| [Unsteady/DES](../Setup-Guide-Unsteady-DES/) | Original versus delayed DES length scales, grid measures, shielding and statistical convergence. | `CTurbSASolver::SetDES_LengthScale` and the DDES paper; a 2D regression does not validate 3D DES. Time-step, span and sampling studies remain. | +| [Transition setup](../Setup-Guide-Transition/) | Base-model coupling, turbulence decay, supported correlations and interpretation of wall shear. | `CTransLMSolver`, transition source terms and TMR; calibration ranges, roughness scaling and moving-frame use still need specialist review. | +| [Turbomachinery](../Setup-Guide-Turbomachinery/) | Axis conventions for axial/radial and 2D examples, boundary kinds, averaging and ramps. | Turbomachinery geometry, `CEulerSolver` boundaries and linked stage configurations; validate radial/3D performance and interface conservation. | +| [Heat transfer/CHT](../Setup-Guide-Heat-Transfer-CHT/) | Heat-flux sign, steady versus transient material roles, coupling and heat balance. | `CHeatSolver`, `CIncNSSolver` and CHT transfer routines; quantify non-matching-interface heat conservation and transient convergence. | +| [Species/combustion](../Setup-Guide-Species-Combustion/) | Mixture properties, Lewis-model limits, raw wall-flux scaling, clipping and source-inclusive species balances. | `CSpeciesSolver::BC_Wall_Generic`, fluid/diffusion models, `CSpeciesFlameletSolver`; the modified fresh-start flame example and manifold accuracy still require runs. | +| [Shape optimization](../Setup-Guide-Shape-Optimization/) | Adjoint cost/derivative scope, legacy versus new mesh deformation, physical boundary preservation and disabled validation flag. | `CDeformationDriver`, `CLinearElasticity`, `CMeshSolver`, `discrete_adjoint.py`; perform converged finite-difference comparisons and mesh-validity checks. | +| [NICFD](../Setup-Guide-NICFD/) | Phase stability, critical/acentric definitions, EOS accuracy limits and modified-example status. | SU2 fluid models and CoolProp primary documentation; validate the actual fluid/state range, transport closure and surrogate derivatives. | +| [Turbulence models](../Turbulence-Models/) | SA/SST variants and restrictions, Kato–Launder production, ambient terms and inlet definitions. | `option_structure.hpp`, turbulence source/solver files and TMR; implementation correspondence is not a complete numerical verification. | +| [Transition models](../Transition-Models/) | LM/SA coupling, BCM coefficient convention and documented output/adjoint limitations. | Transition source/solver files and TMR; solver limitations remain open, and specialist assessment is pending. | +| [Hybrid RANS/LES](../Hybrid-RANS-LES/) | Shielded length scale, grid measures, backscatter caveats and averaging limits. | SA length-scale and backscatter implementations plus original papers; full 3D validation and the noted backscatter unit discrepancy remain. | +| [Wall treatment](../Wall-Treatment/) | Wall-function restrictions, roughness/heat-transfer scope and conditional y+ guidance. | Flow/turbulence wall routines, TMR SA roughness and cited papers; low-y+ fallback and mixed-wall limitations remain solver work. | + +Primary equation references checked include the TMR pages for [SA](https://tmbwg.github.io/turbmodels/spalart.html), [SST](https://tmbwg.github.io/turbmodels/sst.html), [LM-SST](https://tmbwg.github.io/turbmodels/langtrymenter_4eqn.html), [LM-SA](https://tmbwg.github.io/turbmodels/sa_gamma_retheta_3eqn.html) and [SA-BCM](https://tmbwg.github.io/turbmodels/sa-bc_1eqn.html), the [Spalart et al. DDES paper](https://doi.org/10.1007/s00162-006-0015-0), and [CoolProp EOS documentation](https://coolprop.org/fluid_properties/PurePseudoPure.html). All 18 TMR links/anchors on the reviewed pages resolved at this review. These checks do not endorse every empirical range in the guides; **Draft** and **to be reviewed** labels remain. + +The Python configuration helper still writes the unsupported buffet-monitoring key in `SU2_PY/SU2/io/config.py`; that caller needs a separate fix and regression check before using its buffet path. Removing the template assignment does not repair that helper. + +The same follow-up repaired all 180 recorded link findings: image/media paths, two image enlargement links, old page names and anchors, version paths, two tutorial file links, and 59 archived downloads. The latter now point to their official archive; all 59 downloads and both replacement case files returned HTTP 200. It also fixed four template spellings (`DNETTHRUST_DBCTHRUST`, `FLAME_INIT_IGNITION`, `HOLD_GRID_FIXED_COORD`, `DEFORM_POISSONS_RATIO`), removed four unsupported assignments, explained solid diffusivity through material properties, and changed the structural example to `PARAVIEW_LEGACY` to retain its stated `.vtk` output. The enum checker now handles hyphenated keywords, with a reproducing test. + +## What is still missing + +The current [accepted-findings list](https://github.com/rois1995/SU2/blob/docs_site/Docs/website/tools/check-baseline.json) retains 24 missing source descriptions. The link, template-name and enum baselines are empty after the follow-up above. Historical build counts earlier on this page remain dated evidence; use the generated reports for current counts. + +| Priority | Remaining work | What completion requires | +| --- | --- | --- | +| Next | Improve option descriptions: 24 registrations lacked source descriptions; approximately 296 descriptions were very short. Enum choices also need explanations. | Describe effect, units, applicability, restrictions, interactions and conditional defaults in the source, prioritizing options used by the setup guides. Check the generated reference afterwards. | +| Next | Obtain independent expert review and numerical evidence for the source-reviewed guides and model pages. | Use the review table above to select representative cases, check convergence/conservation and compare with appropriate theory/data. Retain draft labels until the stated review is complete. | +| Then | Refresh older tutorials, version claims and contribution instructions as they are checked. | Confirm linked case files, accepted settings, run instructions and stated results for the declared version. Keep worked examples in Tutorials and numerical comparisons in V&V. | +| Then | Fill missing model explanations and keep known support limitations current. | Link exact variants and equation references to the SU2 implementation, define symbols/units, and distinguish verified behavior from proposed or unsupported capabilities. Extend the existing model pages where appropriate. | +| Then | Strengthen extraction and build checks when a new mismatch is demonstrated. | Add a focused reproducing test and fix its cause. Solver-specific configuration compatibility still needs appropriate SU2 checks; a general parser or framework rewrite is not an agreed requirement. | +| Then | Decide whether the remaining build tools need tighter version control. | Demonstrate that an unpinned tool causes a relevant difference, then document or pin it. Continue recording actual versions and both source SHAs. | +| Separate work | Verify and fix underlying solver defects noted during documentation review. | Reproduce the behavior, add an appropriate failing check, fix the implementation and update the affected documentation. A documentation caveat alone is not a solver repair. | +| Separate review | Propose the improvements to the official repositories. | Submit focused, reviewable changes with evidence and coordinated source/website revisions. Fork deployment does not establish upstream approval. | + +## How to go further + +1. **Keep the new checks passing.** Keep the resolved link/template/enum baselines empty. Correct new findings at their source; do not add them to an accepted-debt list. +2. **Improve the source descriptions used by the guides.** Work through one option family at a time. This improves the reference and explanations together without maintaining a competing option catalog. +3. **Review the existing guidance and evidence.** Use the linked Tutorials and V&V cases. Distinguish source checking, a successful run, numerical verification and physical validation; record which has actually been performed. +4. **Resolve implementation issues separately.** Update known-limit sections only when the relevant code change and its checks support doing so. +5. **Prepare upstream changes in coherent batches.** Keep tooling, corrected explanations and reviewed guidance understandable on their own, and identify the matching changes in the other repository. + +These are proposed next priorities. The page records completed work; it does not imply that every backlog item is already assigned or underway. + +## Change history + +| Date | Delivered | Evidence | +| --- | --- | --- | +| 21 October 2026 | Generated references and Doxygen integration; developer and extension guides; model pages; nine draft setup guides; search, navigation and feed/sitemap repairs. | The linked pages above and the history of the two `docs_site` branches. Draft guidance remained explicitly marked for review. | +| 22 October 2026 | Corrected 16 documentation/generation findings from comparison with source and model references. | SU2 `7862504f9d` and website `be90edc`, linked above. Solver limitations were documented, not silently changed. | +| 22 October 2026 | Clarified page roles, introduced permanent checks and made local/CI builds use a shared entry point and locked Ruby/gem dependencies. | SU2 `a14b4df35d`, website `602189b` and the successful milestone workflows linked above. | +| 22 October 2026 | Added this maintained status page under Developer Docs and contributor guidance for keeping the record current. Fixed report self-link ordering with a reproducing regression check. | This page's source history. The current build manifest identifies the revisions that include it. | +| 22 October 2026 | Repaired all 180 historical link findings, eight template names and the obsolete structural output keyword; reviewed nine setup guides and four model pages; added a hyphenated-enum regression check. | Review scope and primary references above; current build manifest and check results identify this batch. Eleven tooling tests; CFD validation remains pending. | +| 23 October 2026 | Added the V&V case "NACA 0021 Beyond Stall: Hybrid RANS/LES" (SA and SST DDES/EDDES against experiment and published DES). Described the SST DES variants and the new `SA_EDDES` cell size of SU2 PR #2150 on the hybrid RANS/LES, turbulence model and unsteady setup pages. | Website `7279f4d`. Page content checked against PR #2150 at `e9e85686ad`. The V&V data come from local runs. Website commits through `c71d420` add mesh/flow figures, equal 520 c/U sample windows and the reference averaging times. | +| 26 October 2026 | Reviewed the NACA 0021 case for PR preparation: corrected convergence monitoring, restored the physical-time limit, excluded the transient from output averages, clarified SA/SST free-stream inputs, provided steady-restart instructions and unrounded result values, and qualified the statistical interpretation. | The mesh exists locally; its public repository/location will be agreed with reviewers before a separate data PR. Table values match the archived 520 c/U summary; SU2 preprocessing passed for all four hybrid configurations and the steady-start conversion. No new CFD time steps were run. | +| 26 October 2026 | Corrected generated references: ignore C++ comments and quoted examples, keep commas and parentheses inside strings, handle member calls in argument lists, and show string default values instead of constructor expressions. Fixed the renamed-option regex and made intentional text joins explicit. | [SU2 PR #2992](https://github.com/su2code/SU2/pull/2992). Eighteen tooling tests include reproductions of the extraction and regex failures. All 770 current option registrations retain their argument lists; source error messages and output descriptions are checked against the implementation. Current build results are recorded in the manifest and check report. | + +## Keeping this page current + +Edit [Documentation-Status.md in the SU2 repository](https://github.com/rois1995/SU2/blob/docs_site/Docs/website/pages/Documentation-Status.md). The build copies it into the site and adds it under **Developer Docs**. Do not create a second maintained copy in the website repository. + +For each completed batch: + +1. Update the status date and the relevant implemented/backlog row. State what remains unreviewed or unsupported. +2. Add a dated history entry with source and website commit or pull-request links, the checks performed, and any numerical evidence actually obtained. +3. Remove resolved historical findings individually. Keep current generated counts in the generated reports; label any numbers quoted here with their milestone and date. +4. Run the shared documentation build and the relevant checks. Use **Docs Website** with the reviewed website commit SHA when publishing website-only changes. + +See [Contribute](../contribute/) for page ownership and editing steps. Build commands and contributor details are in [Docs/website/README.md](https://github.com/rois1995/SU2/blob/docs_site/Docs/website/README.md). + +Maintained against the two `docs_site` branches. Current source revisions and check totals are recorded in this build’s manifest and check report. diff --git a/Docs/website/pages/Glossary.md b/Docs/website/pages/Glossary.md new file mode 100644 index 000000000000..a39d4dc4cee9 --- /dev/null +++ b/Docs/website/pages/Glossary.md @@ -0,0 +1,274 @@ +--- +title: Glossary +permalink: /docs_v7/Glossary/ +--- + +This page explains the words that SU2 users and developers meet in the code, the configuration file and the +discussions on GitHub. Where it helps, an entry names the class or option, and links to more. + +For a map of the source code, see [Code Structure](../Code-Structure/). For all options, see the +[Option Reference](../Option-Reference/). Class names link to the [Code Reference](../Code-Reference/) (Doxygen). + +--- + +- [Mesh and geometry](#mesh-and-geometry) +- [Solvers and numerics](#solvers-and-numerics) +- [Multizone and coupling](#multizone-and-coupling) +- [Output](#output) +- [Parallelism](#parallelism) +- [Algorithmic differentiation](#algorithmic-differentiation) +- [Configuration](#configuration) + +--- + +## Mesh and geometry + +**Agglomeration, multigrid level.** +Multigrid solves the flow on a sequence of coarser grids to speed up convergence. SU2 builds each coarse grid by +agglomeration: it merges neighboring control volumes of the finer grid into larger ones +([`CMultiGridGeometry`](../../doxygen/classCMultiGridGeometry.html)). The index `iMesh` selects the level. +`MESH_0` is the finest grid, the one read from the file. The option `MGLEVEL` sets the number of coarse levels +(default 0, no multigrid). + +**Dual grid, control volume.** +SU2 solves the equations on control volumes, one around each mesh point. Together they form the dual grid. +[`CGeometry`](../../doxygen/classCGeometry.html) builds it from the mesh elements. The volume of each control +volume is stored in [`CPoint`](../../doxygen/classCPoint.html) (`nodes->GetVolume(iPoint)`). + +**Edge.** +An edge joins two neighboring mesh points. Each edge carries the normal of the face that the two control volumes +share. Flux loops in the finite volume solvers run over edges. +See [`CEdge`](../../doxygen/classCEdge.html) (`geometry->edges`, `geometry->GetnEdge()`). + +**Halo point, ghost point.** +After the mesh is split between MPI ranks, each rank also stores copies of the points next to its part that +belong to other ranks. These are halo (or ghost) points. The rank does not compute them; it receives their values +from the owner. `geometry->GetnPointDomain()` counts the points the rank owns, `geometry->GetnPoint()` counts all +points, halos included. The owned points come first, so a loop up to `nPointDomain` visits only owned points. +`nodes->GetDomain(iPoint)` is `false` for a halo point. See +[Parallelism](../Code-Structure/#parallelism-mpi-openmp-and-vectorization). + +**Interface (marker).** +A marker where two zones meet. `MARKER_ZONE_INTERFACE` lists the interface markers in pairs, one marker from each +zone. `MARKER_FLUID_INTERFACE` and `MARKER_CHT_INTERFACE` set the kind of boundary condition on such markers. +When the meshes do not match at the interface, an interpolator +([`CInterpolator`](../../doxygen/classCInterpolator.html), option `KIND_INTERPOLATION`) maps one side to the +other. See [Multizone](../Multizone/). + +**Marker.** +A named group of boundary faces, for example a wall or a far field. The mesh file defines the markers. The +`MARKER_*` options of the configuration file set their boundary conditions. In the code, `iMarker` indexes them, +and `config->GetMarker_All_TagBound(iMarker)` gives the name. See [Markers and BC](../Markers-and-BC/). + +**Periodic pair.** +Two markers that SU2 treats as the same surface, moved by a rotation or a translation. `MARKER_PERIODIC` lists, +for each pair, the periodic marker, its donor marker, the rotation center, the rotation angles and the +translation. SU2 adds the points of the other side as extra halo points. The solvers exchange data across the pair +with `InitiatePeriodicComms` / `CompletePeriodicComms` of [`CSolver`](../../doxygen/classCSolver.html). + +**Vertex.** +A boundary point seen from one marker. `geometry->vertex[iMarker][iVertex]` is a +[`CVertex`](../../doxygen/classCVertex.html). It holds the index of the mesh point (`GetNode()`) and the boundary +normal (`GetNormal()`). A mesh point at a corner belongs to several markers, so it has one vertex on each. + +**Zone.** +A part of the domain with its own mesh, its own configuration and its own solvers, for example the fluid and the +solid in a conjugate heat transfer problem. `iZone` indexes the zones. The driver holds one +[`CConfig`](../../doxygen/classCConfig.html) per zone. Set `MULTIZONE= YES` and `CONFIG_LIST` to use several zones. +See [Multizone](../Multizone/). + +## Solvers and numerics + +**CFL adaptation.** +The CFL (Courant-Friedrichs-Lewy) number sets the size of the local pseudo time step. With `CFL_ADAPT= YES`, SU2 raises the CFL number when +the iterations go well, and lowers it when they do not (`CSolver::AdaptCFLNumber`). `CFL_ADAPT_PARAM` sets the +factors and the limits. The history fields `MIN_CFL`, `MAX_CFL` and `AVG_CFL` show the result. + +**Dual time stepping.** +The method that SU2 uses for most unsteady problems. Each physical time step is solved as a steady problem in a +pseudo time, with inner iterations. Set it with `TIME_MARCHING= DUAL_TIME_STEPPING-1ST_ORDER` or +`DUAL_TIME_STEPPING-2ND_ORDER`. `CSolver::SetResidual_DualTime` adds the time terms to the residual. + +**Edge kernel.** +The code that computes the flux across one edge from the states of its two points. Examples are +`CNumerics::ComputeResidual`, the vectorized `CNumericsSIMD::ComputeFlux`, and the scalar flux classes such as +`CScalarFlux_SA` and `CScalarFlux_SST` (`SU2_CFD/include/numerics/**/*_edge_flux.hpp`). The solver calls the +kernel inside its edge loop. + +**Inner and outer iterations.** +Inner iterations (`INNER_ITER`) solve one zone, or one time step, towards convergence. Outer iterations +(`OUTER_ITER`) repeat the exchange of data between zones in a multizone problem. Time iterations (`TIME_ITER`) +count the physical time steps. In a single-zone steady problem `ITER` is the same as `INNER_ITER`. SU2 stops with +an error if you use `ITER` in a multizone or unsteady problem. + +**Jacobian.** +The matrix of the derivatives of the residual with respect to the solution. Implicit solvers use it to compute +the update of each iteration. In SU2 it is a block sparse matrix, with one block per pair of neighbor points: +`Jacobian` of [`CSolver`](../../doxygen/classCSolver.html), of type +[`CSysMatrix`](../../doxygen/classCSysMatrix.html). It is often approximate: it only has to make the iterations +converge. + +**Limiter.** +A factor between 0 and 1 that reduces the reconstructed gradient near shocks and strong changes. It prevents new +extrema and oscillations. Set it with `SLOPE_LIMITER_FLOW`, `SLOPE_LIMITER_TURB`, ... (for example +`VENKATAKRISHNAN`). The code is in `SU2_CFD/include/limiters` (`computeLimiters`). The volume output group +`LIMITER` writes the values. See [Slope Limiters](../Slope-Limiters-and-Shock-Resolution/). + +**MUSCL.** +Monotonic Upstream-centered Scheme for Conservation Laws: a way to reach second order accuracy with an upwind scheme. Before the flux is computed, the states at the two +ends of an edge are extrapolated to the middle of the edge with the gradients. Set it with `MUSCL_FLOW`, +`MUSCL_TURB`, ... The gradient method is `NUM_METHOD_GRAD_RECON` (or `NUM_METHOD_GRAD` if that is not set). See +[Convective Schemes](../Convective-Schemes/). + +**Numerics object.** +An object of a [`CNumerics`](../../doxygen/classCNumerics.html) child class. It computes one term, such as the +convective flux of an edge or the source term of a point, with its Jacobians. The driver creates them in +`CDriver::InitializeNumerics`. They are stored in `numerics_container[iZone][iInst][iMesh][iSol][iTerm]`, where +`iTerm` is `CONV_TERM`, `VISC_TERM`, `SOURCE_FIRST_TERM`, ... See +[Adding a Numerical Scheme](../Adding-a-Numerical-Scheme/). + +**Residual.** +The sum of the fluxes and source terms of a control volume. It is zero when the steady solution is reached. The +solvers store it in `LinSysRes`. The history fields in the groups `RMS_RES` and `MAX_RES` show the base 10 +logarithm of its root mean square and maximum over all points, one value per equation. + +**Runtime system (`RUNTIME_*_SYS`).** +A value of `enum RUNTIME_TYPE` (`Common/include/option_structure.hpp`), such as `RUNTIME_FLOW_SYS`, +`RUNTIME_TURB_SYS` or `RUNTIME_HEAT_SYS`. The iterations pass it to the integrations to say which set of equations +to advance. `CConfig::GetContainerPosition` turns it into the solver position, for example +`RUNTIME_TURB_SYS` into `TURB_SOL`. + +**Solver container.** +The array of all solver objects of a run: `solver_container[iZone][iInst][iMesh][iSol]` in the driver. Inside the +solvers and the integrations you mostly see the last level, `CSolver** solver_container`, indexed by the solver +position. See [Code Structure](../Code-Structure/#the-containers-in-the-driver). + +**Solver position (`FLOW_SOL`, ...).** +The index of one solver in the solver container: `FLOW_SOL`, `TURB_SOL`, `TRANS_SOL`, `SPECIES_SOL`, `HEAT_SOL`, +`RAD_SOL`, `MESH_SOL`, `ADJFLOW_SOL`, ... (`enum SOLVER_TYPE` in `Common/include/option_structure.hpp`). Some +values are shared: `FEA_SOL` is also 0, like `FLOW_SOL`, because a zone never has both. + +## Multizone and coupling + +**Driver.** +The object that runs the whole simulation. It creates the geometry, solvers, numerics and outputs, and runs the +time loop. [`CSinglezoneDriver`](../../doxygen/classCSinglezoneDriver.html) and +[`CMultizoneDriver`](../../doxygen/classCMultizoneDriver.html) are the main ones; all derive from +[`CDriver`](../../doxygen/classCDriver.html). See [Code Structure](../Code-Structure/#drivers). + +**Instance.** +A copy of the solvers of one zone. Only harmonic balance uses more than one: one instance per time instance (a sampled time of the period). +`iInst` indexes them. In all other cases there is one instance. + +**Interface (object).** +An object of a [`CInterface`](../../doxygen/classCInterface.html) child class. It knows what to send from one +zone to another, for example the tractions from a fluid to a structure +([`CFlowTractionInterface`](../../doxygen/classCFlowTractionInterface.html)), or the temperature and heat flux in +conjugate heat transfer. The driver stores them in `interface_container[donorZone][targetZone]`. + +**Iteration.** +An object of a [`CIteration`](../../doxygen/classCIteration.html) child class. It runs one iteration of one +physics in one zone: `Preprocess`, `Iterate`, `Postprocess`, `Monitor`, `Output`. For example, +[`CFluidIteration`](../../doxygen/classCFluidIteration.html) advances the flow and then the turbulence, transition +and species solvers. + +**Transfer.** +The exchange of boundary data between two zones, through an interface object. `CDriver::TransferData` (and +`CMultizoneDriver::TransferData`) call it. `CInterface::BroadcastData` reads the donor values +(`GetDonor_Variable`), maps them with the interpolator and sets them in the target solver +(`SetTarget_Variable`). The kind of transfer is a value of `enum ENUM_TRANSFER`, such as `FLOW_TRACTION` or +`CONJUGATE_HEAT_FS`. + +## Output + +**History field.** +A single number per iteration, such as a residual, a force coefficient or the time. History fields go to the +history file (`history.csv` by default). Choose them with `HISTORY_OUTPUT`. In the code they are added with +`COutput::AddHistoryOutput`. See [Custom Output](../Custom-Output/) and the +[Output Fields Reference](../Output-Fields-Reference/). + +**Output group.** +A name that stands for several fields, for example `RMS_RES`, `AERO_COEFF`, `SOLUTION` or `PRIMITIVE`. Each field +belongs to one group (an argument of `AddHistoryOutput` and `AddVolumeOutput`). Writing the group name in +`SCREEN_OUTPUT`, `HISTORY_OUTPUT` or `VOLUME_OUTPUT` selects all its fields. + +**Screen field.** +A history field that is also printed on the screen. Choose them with `SCREEN_OUTPUT`. + +**Volume field.** +A value at each mesh point, such as the pressure or the residual. Volume fields go to the volume and surface files +(Paraview, Tecplot, CGNS, ...). Choose them with `VOLUME_OUTPUT`. In the code they are added with +`COutput::AddVolumeOutput`. See [Adding Output Fields](../Adding-Output-Fields/). + +## Parallelism + +**Edge coloring (OpenMP).** +An edge loop writes to both points of each edge. If two threads handle edges that share a point, they write to +the same memory at the same time. To avoid this, SU2 groups the edges by color: edges of one color share no +point, so the threads can work on them safely (`CGeometry::GetEdgeColoring`). The option +`EDGE_COLORING_GROUP_SIZE` sets the size of the groups of edges. + +**MPI rank.** +One of the processes of a parallel run started with `mpirun`. Each rank works on one part of the mesh. Get its +number with `SU2_MPI::GetRank()`. Rank 0 is `MASTER_NODE`, which prints the screen output. Use the `SU2_MPI` +wrapper, never MPI directly. + +**Partition.** +The part of the mesh that one rank owns. SU2 splits the mesh at start-up with ParMETIS +(`CPhysicalGeometry::SetColorGrid_Parallel`). The volume field `RANK` (group `MPI`) shows the partitions. + +**Reducer strategy.** +The fallback when the edge coloring is poor, that is, when its expected parallel efficiency is below +`COLORING_EFF_THRESH` (0.875). The solver first stores the flux of every edge (`EdgeFluxes`), then sums them for +each point in a point loop. The member `ReducerStrategy` of the flow and scalar solvers says if it is in use. +`EDGE_COLORING_GROUP_SIZE= 0` forces it. + +**SIMD.** +Single instruction, multiple data: CPU instructions that apply one operation to several values at once. SU2 uses +them through [`simd::Array`](../../doxygen/classsimd_1_1Array.html) and +[`CNumericsSIMD`](../../doxygen/classCNumericsSIMD.html), which compute the fluxes of several edges together. The +build option `cpu-arch` sets which instructions the compiler may use. + +## Algorithmic differentiation + +See [Automatic Differentiation](../Automatic-Differentiation/) for the full guide. + +**Preaccumulation.** +A way to make the tape smaller. For a code section with few inputs and outputs, SU2 stores only the Jacobian of +the section on the tape, not all its operations (`AD::StartPreacc`, `AD::SetPreaccIn`, `AD::SetPreaccOut`, +`AD::EndPreacc`). + +**`su2double`.** +The type of every value that may need derivatives. In a normal build it is a plain `double`. In an AD build it is +a CoDiPack type that records its operations. `passivedouble` is never differentiated. + +**Tape.** +The record of all operations done on `su2double` values while recording is on. The discrete adjoint runs the tape +backwards to get derivatives (`AD::StartRecording`, `AD::StopRecording`, `AD::ComputeAdjoint`). + +## Configuration + +**Config file.** +The text file with the options of a run, one `NAME= value` per line. `%` starts a comment. A line that ends with +`\` continues on the next line. [`CConfig`](../../doxygen/classCConfig.html) reads it. `config_template.cfg` in +the SU2 folder lists every option. See [Configuration File](../Configuration-File/). + +**Keyword map.** +The table that turns the text value of an option into a value of a C++ enum, for example `Solver_Map` turns +`RANS` into `MAIN_SOLVER::RANS`. The maps are in `Common/include/option_structure.hpp`, built with `MapType` and +`MakePair`. They exist only where `ENABLE_MAPS` is defined, such as in `CConfig.cpp`; elsewhere they compile to +nothing, which keeps the build fast. + +**Option.** +One setting of the config file. `CConfig::SetConfig_Options` declares each option with `addDoubleOption`, +`addEnumOption`, `addStringListOption`, ... and stores it in `option_map`. Each option is an object of a +`COptionBase` child class (`Common/include/option_structure.inl`). See +[Adding a Config Option](../Adding-a-Config-Option/). + +**Sub-config.** +A config file for one zone of a multizone problem, listed in `CONFIG_LIST` of the main config file. It holds only +the options that differ in that zone. The zone takes the other options from the main file +(`CConfig::SetDefaultFromConfig`), except `SCREEN_OUTPUT` and `HISTORY_OUTPUT`. See +[Multizone](../Multizone/#sub-config-files). + +Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/pages/Running-Regression-Tests.md b/Docs/website/pages/Running-Regression-Tests.md new file mode 100644 index 000000000000..abee93353908 --- /dev/null +++ b/Docs/website/pages/Running-Regression-Tests.md @@ -0,0 +1,317 @@ +--- +title: Running Regression Tests +permalink: /docs_v7/Running-Regression-Tests/ +--- + +The regression tests run SU2 on many small cases. For each case, they compare a few numbers of the output with +stored values. This catches changes in the results that nobody intended. Every pull request runs the tests, and a +pull request is merged only when they pass. + +This page explains: + +- where the tests are; +- how the CI (continuous integration: the automatic tests on GitHub) runs them; +- how to run them on your computer; +- how to add a test; +- how to update the stored values. + +For the C++ unit tests, see [Running Unit Tests](../Running-Unit-Tests/) and +[Writing Unit Tests](../Writing-Unit-Tests/). + +## Where the tests are + +The tests use three repositories: + +- **SU2** (`su2code/SU2`): the test scripts and the configuration files, in `TestCases/`. +- **TestCases** (`su2code/TestCases`): the meshes, restart files and other large input files. Its folders match + the folders of `TestCases/` in SU2, and the two sets of files do not overlap. See [Test Cases](../Test-Cases/). +- **Tutorials** (`su2code/Tutorials`): the configuration files and meshes of the tutorials. `tutorials.py` uses them. + +The scripts in `TestCases/` are: + +| Script | What it runs | Default command | +|---|---|---| +| `serial_regression.py` | Main tests without MPI, plus `SU2_GEO`, `SU2_DEF`, Python scripts and Python wrapper tests | `SU2_CFD` | +| `parallel_regression.py` | Main tests with MPI, plus `SU2_SOL`, `SU2_DEF` and output file tests | `mpirun -n 2 SU2_CFD` | +| `serial_regression_AD.py` | Discrete adjoint and direct differentiation tests without MPI | `SU2_CFD_AD` | +| `parallel_regression_AD.py` | Discrete adjoint tests with MPI | `mpirun -n 2 SU2_CFD_AD` | +| `hybrid_regression.py` | Tests with OpenMP threads | `SU2_CFD -t 2` | +| `hybrid_regression_AD.py` | Discrete adjoint tests with OpenMP threads | `SU2_CFD_AD -t 2` | +| `tutorials.py` | Cases of the Tutorials repository | `mpirun -n 2 SU2_CFD` | +| `vandv.py` | A few verification and validation cases (`TestCases/vandv/`) | `mpirun -n 2 SU2_CFD` | + +All scripts use the class `TestCase` from `TestCases/TestCase.py`. + +## How a test works + +Each test is a `TestCase` object. This entry is copied from `TestCases/serial_regression.py`: + +```python + # NACA0012 + naca0012 = TestCase('naca0012') + naca0012.cfg_dir = "euler/naca0012" + naca0012.cfg_file = "inv_NACA0012_Roe.cfg" + naca0012.test_iter = 20 + naca0012.test_vals = [-4.031446, -3.524530, 0.273056, 0.009703] + test_list.append(naca0012) +``` + +`TestCase.run_test()` does the following: + +1. It copies `cfg_file` to `.autotest` in `cfg_dir`, and sets the number of iterations to `test_iter + 1`. + The option it changes is: + - `ITER` by default; + - `TIME_ITER` if `unsteady` is set; + - `OUTER_ITER` if `multizone` is set and `unsteady` is not. +2. It runs the command in `cfg_dir`, and writes the screen output to `.log` + (here `inv_NACA0012_Roe.cfg.log`). +3. It reads the screen output after the line `Begin Solver`. It finds the first row whose first column is `test_iter`. +4. It takes the **last** `len(test_vals)` columns of that row and compares them with `test_vals`. + +The test fails if: + +- one absolute difference is larger than `tol`; +- the row is not found; +- the run takes longer than `timeout` seconds; +- the command returns an error. + +The configuration file of this test has `SCREEN_OUTPUT = (INNER_ITER, RMS_DENSITY, RMS_ENERGY, LIFT, DRAG)`. +So the four values are the last four columns of the screen output: + +``` +| Inner_Iter| rms[Rho]| rms[RhoE]| CL| CD| +``` + +### Main attributes of TestCase + +| Attribute | Meaning | Default in `TestCase.py` | +|---|---|---| +| `cfg_dir`, `cfg_file` | Folder (relative to `TestCases/`) and configuration file | `"."`, `"default.cfg"` | +| `test_iter` | Iteration whose row is checked | `1` | +| `test_vals` | Stored values, compared with the last columns of the row | `[]` | +| `test_vals_aarch64` | Stored values used instead on ARM64 (`aarch64`, `arm64`) | `[]` | +| `tol` | Allowed absolute difference: one number, or a list with one number per value | `0.0` | +| `tol_aarch64` | Tolerance used instead on ARM64 | `0.0` | +| `timeout` | Maximum run time in seconds | `0` | +| `command` | `TestCase.Command(launch, exec, param)`, e.g. `TestCase.Command("mpirun -n 2", "SU2_CFD")` | empty | +| `unsteady`, `multizone` | Choose which iteration option is changed (see above) | `False` | +| `no_restart` | Writes `RESTART_SOL= NO` in the copied configuration file | `False` | +| `enabled_on_cpu_arch` | CPU types on which the test runs | `["x86_64","amd64","aarch64","arm64"]` | +| `enabled_with_tsan`, `enabled_with_asan` | Run the test in the thread / address sanitizer jobs | `True` | +| `enabled_with_tapetests`, `tapetest_vals` | Run the test in the tape test job, and the expected number of tape errors | `False`, `[]` | +| `enabled_with_regular` | Set to `False` for a test that runs only under a sanitizer | `True` | + +In `TestCase.py`, the defaults of `command`, `timeout` and `tol` are empty or zero. Each script fills them in a loop +before it runs its `test_list` (the block `RUN TESTS`). For example, `serial_regression.py` uses `SU2_CFD`, +1600 s and `0.00001`. + +Some scripts overwrite values that you set, for all tests in `test_list`: + +- `hybrid_regression.py` sets `tol = 1e-4`; +- `hybrid_regression_AD.py` sets the command `SU2_CFD_AD -t 2` and `timeout = 600`; +- `vandv.py` sets `timeout = 300` and `tol = 1e-5`. + +### Other kinds of tests + +`TestCase` has more methods, for tests that do not check the screen output of `SU2_CFD`: + +- `run_filediff()`: runs the command and compares the file `test_file` with `reference_file`. + - With `tol_file_percent = 0`, the two files must be equal. + - Otherwise each number may differ by `tol_file_percent` percent. Numbers smaller than `comp_threshold` are not + compared. + - On ARM64 it uses `reference_file_aarch64`. +- `run_opt()`: for `shape_optimization.py`. It sets `OPT_ITERATIONS` to `test_iter`. +- `run_geo()`: for `SU2_GEO`. +- `run_def()`: for `SU2_DEF`. + +The scripts call these methods one test at a time, after the loop that fills in the defaults. So these tests must +set `command`, `timeout` and `tol` themselves. + +## How the CI runs the tests + +The workflow is `.github/workflows/regression.yml`. It runs on every push to `develop` and `master`, and on every +pull request to them. `.github/workflows/regression-arm64.yml` runs the same workflow on an ARM64 runner, but only on +pushes to `develop`. + +### Builds + +The job **Build SU2** compiles SU2 in a container (`ghcr.io/su2code/su2/build-su2`), in several configurations: + +- `Base` is the normal build. +- `Reverse` is the discrete adjoint build (`-Denable-autodiff=true`). +- `Forward` is the direct differentiation build (`-Denable-directdiff=true`). + +Each one is built with MPI (`MPI`), without MPI (`NoMPI`) and with OpenMP (`OMP`). `ReverseTagNoMPI` is a discrete +adjoint build with `-Dcodi-tape=Tag`, for the Tape Tests job. + +The jobs **Build SU2 (tsan)** and **Build SU2 (asan)** make the builds for the thread and address sanitizers. These +are compiler tools that detect data races between threads (tsan) and memory errors (asan) while the program runs. + +### Test jobs + +Each test job runs one script. It uses the binaries of one build type (column **Binaries**): the `Base`, `Reverse` +and `Forward` builds of that type, when they exist. + +| Job | Script | Binaries | Extra argument | +|---|---|---|---| +| Regression Tests | `serial_regression.py`, `serial_regression_AD.py` | `NoMPI` | | +| Regression Tests | `parallel_regression.py`, `parallel_regression_AD.py`, `tutorials.py`, `vandv.py` | `MPI` | | +| Regression Tests | `hybrid_regression.py`, `hybrid_regression_AD.py` | `OMP` | | +| Tape Tests | `serial_regression_AD.py` | `TagNoMPI` | `--tapetests` | +| Thread Sanitizer Tests | `hybrid_regression.py`, `hybrid_regression_AD.py` | `OMP-tsan` | `--tsan` | +| Address Sanitizer Tests | `serial_regression.py`, `serial_regression_AD.py` | `NoMPI-asan` | `--asan` | +| Unit Tests | `test_driver`, `test_driver_AD`, `test_driver_DD` | `MPI` | | + +Details: + +- The Tape Tests and Thread Sanitizer Tests jobs do not run on the ARM64 runner. +- `parse_args()` in `TestCase.py` reads the extra arguments. Only `serial_regression.py`, `serial_regression_AD.py`, + `hybrid_regression.py` and `hybrid_regression_AD.py` read them. +- With a sanitizer, a test fails only if the run returns an error. The output values are not checked. +- The Unit Tests job with `test_driver` also runs `UnitTests/SU2_CFD/output/check_nemo_convergence_migration.py`. + +### Where the meshes come from + +The test jobs run the container `ghcr.io/su2code/su2/test-su2`. Its entry script is `test/runTests.sh` in the +repository `su2code/Docker-Builds`. The jobs call it with these arguments: + +``` +-b -t develop -c develop -s \n' + if tag not in text: + with open(layout, "w", encoding="utf-8") as f: + f.write(text.replace("", tag + "", 1)) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--su2", required=True, help="root of the SU2 source tree") + parser.add_argument( + "--site", required=True, help="checkout of su2code.github.io, changed in place" + ) + parser.add_argument( + "--light", action="store_true", help="remove the download folders (test site)" + ) + parser.add_argument( + "--doxygen-log", help="file with the Doxygen warnings (WARN_LOGFILE)" + ) + parser.add_argument( + "--compare-site", help="original website checkout used for red highlighting" + ) + parser.add_argument( + "--mark-changes", + action="store_true", + help="debug: show in red the pages that differ from the official website", + ) + args = parser.parse_args() + + try: + commit = subprocess.check_output( + ["git", "-C", args.su2, "rev-parse", "--short=10", "HEAD"], text=True + ).strip() + except (OSError, subprocess.CalledProcessError): + commit = "unknown" + + # Pages kept in this repository replace or extend the ones of the website. + pages_dir = os.path.join(args.su2, "Docs", "website", "pages") + dev_pages = [] + for path in sorted(glob.glob(os.path.join(pages_dir, "*.md"))): + shutil.copy(path, os.path.join(args.site, DOCS)) + dev_pages.append(os.path.splitext(os.path.basename(path))[0]) + for folder in ("img", "files"): + src = os.path.join(pages_dir, folder) + if os.path.isdir(src): + shutil.copytree( + src, + os.path.join(args.site, "docs_files", "website"), + dirs_exist_ok=True, + ) + + # Links to the code point to the repository that is built (a fork during tests). + repo = os.environ.get("GITHUB_REPOSITORY", "su2code/SU2") + + user_pages = copy_code_notes(args.su2, args.site) + maps = read_maps(args.su2) + options = read_options(args.su2) + ref_pages = write_reference( + args.su2, args.site, options, maps, commit, repo, set(dev_pages) + ) + found = check_docs( + args.site, {o["name"] for o in options}, read_code_names(args.su2) + ) + blob = "https://github.com/{}/blob/{}/".format(repo, commit) + output_page = write_output_page(args.site, read_outputs(args.su2), commit, blob) + renamed_page = write_renamed_page( + args.su2, args.site, {o["name"] for o in options}, commit + ) + errors_page = write_errors_page( + args.site, read_error_messages(args.su2), options, commit, blob + ) + template_found = check_template(args.su2, {o["name"] for o in options}) + extra = [ + "", + "## Options in config_template.cfg that do not exist", + "", + 'Users copy this file, so a wrong name there stops their run with an "invalid option name" error.', + "", + "Found: {}".format(len(template_found)), + "", + ] + extra += ["- line {}: `{}`".format(num, name) for num, name in template_found] + extra += quality_sections( + find_stale_pages(args.su2, pages_dir), + doc_coverage(args.su2), + read_doxygen_log(args.doxygen_log), + ) + write_check_page(args.site, found, commit, options, extra) + patch_nav( + args.site, + ref_pages + [renamed_page], + dev_pages + ["Docs-Check"], + user_pages + [output_page, errors_page], + ) + + if args.mark_changes: + generated = ( + ref_pages + + dev_pages + + user_pages + + [output_page, renamed_page, errors_page, "Docs-Check"] + ) + urls = ["/{}/{}/".format(URL, p) for p in generated] + changed_site_pages( + args.compare_site or args.site + ) + mark_changes(args.site, urls, ["Option Reference", "How to Add..."]) + + if args.light: + for folder in HEAVY_FOLDERS: + shutil.rmtree(os.path.join(args.site, folder), ignore_errors=True) + + print( + "Options: {}, groups: {}, unknown options named in the docs: {}, in config_template.cfg: {}".format( + len(options), len(ref_pages) - 1, len(found), len(template_found) + ) + ) + for path, num, name, _ in found: + print(" {}:{}: {}".format(path, num, name)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Docs/website/tools/check-baseline.json b/Docs/website/tools/check-baseline.json new file mode 100644 index 000000000000..17230ebf0cfd --- /dev/null +++ b/Docs/website/tools/check-baseline.json @@ -0,0 +1,32 @@ +{ + "note": "Existing findings at SU2 7862504f9d / website be90edc, before automated checks. Fix individual entries and remove them; do not regenerate this baseline to accept regressions. Link entries are scoped to a page and normalized destination. External links are not checked. The 22 October 2026 follow-up resolved the 180 links, eight template names and one enum finding; their arrays are now empty. The 24 missing source descriptions remain.", + "template_options": [], + "missing_descriptions": [ + "ACTDISK_SECONDARY_FLOW", + "CONDUCTIVITY_MODEL", + "ENGINE_NU_FACTOR", + "FIX_AZIMUTHAL_LINE", + "FREESTREAM_INTERMITTENCY", + "FREESTREAM_NU_FACTOR", + "FREESTREAM_TURB2LAMVISCRATIO", + "FREESTREAM_TURBULENCEINTENSITY", + "INITIAL_BCTHRUST", + "LOWER_LIMIT_K_FACTOR", + "LOWER_LIMIT_OMEGA_FACTOR", + "MARKER_ACTDISK", + "NUM_NEAREST_NEIGHBORS", + "PRINT_INLET_INTERPOLATED_DATA", + "SPECIFIED_INLET_PROFILE", + "TOPOL_OPTIM_FILTER_KERNEL", + "TOPOL_OPTIM_FILTER_RADIUS", + "TOPOL_OPTIM_KERNEL_PARAM", + "TOPOL_OPTIM_OUTFILE", + "TOPOL_OPTIM_PROJECTION_PARAM", + "TOPOL_OPTIM_PROJECTION_TYPE", + "TOPOL_OPTIM_SEARCH_LIMIT", + "TOPOL_OPTIM_SIMP_EXPONENT", + "TOPOL_OPTIM_SIMP_MINSTIFF" + ], + "links": [], + "enum_values": [] +} diff --git a/Docs/website/tools/check_site.py b/Docs/website/tools/check_site.py new file mode 100644 index 000000000000..b5d69ceeb65a --- /dev/null +++ b/Docs/website/tools/check_site.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +"""Check generated SU2 documentation without requesting external websites.""" + +import argparse +from html.parser import HTMLParser +import json +from pathlib import Path +import re +from urllib.parse import unquote, urljoin, urlsplit + +import build_site + +COLLECTIONS = ("docs_v7", "tutorials", "vandv") + + +class HTML(HTMLParser): + def __init__(self, text): + super().__init__() + self.ids, self.links = set(), [] + self.feed(text) + + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) + self.ids.update(attrs[k] for k in ("id", "name") if k in attrs) + self.links.extend(attrs[k].strip() for k in ("href", "src") if attrs.get(k)) + + +def local_target(root, page, href, baseurl): + base = "/" + baseurl.strip("/") if baseurl.strip("/") else "" + link = urlsplit(urljoin("https://docs.invalid" + base + "/" + page, href)) + if link.scheme not in ("http", "https") or link.netloc != "docs.invalid": + return None + path = unquote(link.path) + if base and path != base and not path.startswith(base + "/"): + return root / "__outside_baseurl__", path, "" + path = path[len(base) :].lstrip("/") + target = (root / path).resolve() + if root != target and root not in target.parents: + return root / "__outside_site__", path, "" + if target.is_dir(): + target /= "index.html" + return target, path, unquote(link.fragment) + + +def check_links(root, baseurl): + root = Path(root).resolve() + cache, problems, checked = {}, set(), 0 + + def parse(path): + if path not in cache: + cache[path] = HTML(path.read_text(encoding="utf-8", errors="replace")) + return cache[path] + + for collection in COLLECTIONS: + for page in sorted((root / collection).rglob("*.html")): + relative = page.relative_to(root).as_posix() + for href in parse(page).links: + result = local_target(root, relative, href, baseurl) + if result is None: + continue + target, path, fragment = result + checked += 1 + reason = "" + if not target.is_file(): + reason = "missing file" + elif ( + fragment + and target.suffix == ".html" + and fragment not in parse(target).ids + ): + reason = "missing anchor" + if reason: + problems.add( + "{}: {}{} ({})".format( + relative, path, "#" + fragment if fragment else "", reason + ) + ) + return problems, checked + + +def check_enum_values(source, options, maps): + choices = { + o["name"]: set(build_site.describe_type(o, maps)[1]) + for o in options + if o["kind"] in ("Enum", "EnumList") + } + known = {o["name"] for o in options} + problems = set() + for collection in COLLECTIONS: + for page in (source / ("_" + collection)).rglob("*.md"): + text = page.read_text(encoding="utf-8") + for block in re.finditer(r"^```([^\n]*)\n(.*?)^```", text, re.M | re.S): + if block[1].strip().lower() in ( + "cpp", + "c++", + "c", + "hpp", + "python", + "py", + "bash", + "sh", + "shell", + ): + continue + settings = re.findall( + r"^\s*([A-Z][A-Z0-9_]+)\s*=\s*([^%\n]*)", block[2], re.M + ) + if not settings or 2 * sum(name in known for name, _ in settings) < len( + settings + ): + continue + for name, value in settings: + value = value.strip() + if not choices.get(name) or not re.fullmatch( + r"[A-Za-z0-9_, ()-]+", value + ): + continue + keywords = re.findall(r"[A-Za-z0-9_-]+", value) + for keyword in set(keywords) - choices[name]: + if keyword.startswith("__") and keyword.endswith("__"): + continue + problems.add( + "{}: {}= {} (unknown keyword {})".format( + page.relative_to(source).as_posix(), + name, + value, + keyword, + ) + ) + return problems + + +def check_source(su2, source, baseline): + options = build_site.read_options(su2) + known = {o["name"] for o in options} + problems = [ + "{}:{}: unknown option {}".format(p, n, name) + for p, n, name, _ in build_site.check_docs( + str(source), known, build_site.read_code_names(su2) + ) + ] + template = {name for _, name in build_site.check_template(su2, known)} + problems += [ + "config_template.cfg: unknown option " + name + for name in sorted(template - set(baseline["template_options"])) + ] + missing = { + o["name"] + for o in options + if not build_site.clean_comment(o["comment"] or "")[0] + } + problems += [ + "New option without a source description: " + name + for name in sorted(missing - set(baseline["missing_descriptions"])) + ] + problems += sorted( + check_enum_values(source, options, build_site.read_maps(su2)) + - set(baseline["enum_values"]) + ) + return problems + + +def check_search(root, source, su2, baseurl): + data = json.loads((root / "search.json").read_text()) + problems, urls = [], set() + for entry in data: + if not all( + isinstance(entry.get(k), str) and entry[k] + for k in ("title", "url", "text", "section") + ): + problems.append( + "Search entry has missing or empty fields: " + str(entry.get("url")) + ) + continue + url = entry["url"] + if url in urls: + problems.append("Duplicate search URL: " + url) + urls.add(url) + target = local_target(root, "index.html", url, baseurl) + if target is None or not target[0].is_file(): + problems.append("Search URL has no local page: " + url) + base = "/" + baseurl.strip("/") if baseurl.strip("/") else "" + for collection in COLLECTIONS: + for page in (source / ("_" + collection)).rglob("*.md"): + text = page.read_text(encoding="utf-8") + front = re.match(r"\A---\s*\n(.*?)\n---", text, re.S) + if not front or not re.search(r"^title:", front[1], re.M): + continue + permalink = re.search(r"^permalink:\s*(\S+)", front[1], re.M) + if permalink and base + "/" + permalink[1].strip("\"'/") + "/" not in urls: + problems.append( + "Page missing from search: " + str(page.relative_to(source)) + ) + indexed = " ".join(entry.get("text", "") for entry in data).lower() + fields = { + f["name"] + for fs in build_site.read_outputs(su2).values() + for f in fs + if f["literal"] + } + problems += [ + "Output field missing from search: " + name + for name in sorted(fields) + if name.lower() not in indexed + ] + return problems, len(data) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--su2", required=True, type=Path) + parser.add_argument( + "--source", required=True, type=Path, help="staged website after generation" + ) + parser.add_argument( + "--site", required=True, type=Path, help="assembled HTML website" + ) + parser.add_argument("--baseurl", default="/SU2") + args = parser.parse_args() + baseline = json.loads(Path(__file__).with_name("check-baseline.json").read_text()) + problems = check_source(args.su2, args.source, baseline) + report_path = args.site / "docs-check.json" + # Pages can link to this report before its final results have been written. + report_path.touch() + links, count = check_links(args.site, args.baseurl) + problems += sorted(links - set(baseline["links"])) + search, entries = check_search( + args.site.resolve(), args.source, args.su2, args.baseurl + ) + problems += search + report = { + "new_problems": problems, + "references_checked": count, + "search_entries": entries, + "existing_link_problems": sorted(links & set(baseline["links"])), + } + report_path.write_text(json.dumps(report, indent=2) + "\n") + for problem in problems: + print(problem) + print( + "Checked {} local references and {} search entries; {} new problems, {} existing link problems.".format( + count, entries, len(problems), len(report["existing_link_problems"]) + ) + ) + return bool(problems) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Docs/website/tools/rebase_links.py b/Docs/website/tools/rebase_links.py new file mode 100644 index 000000000000..467c0239c8c5 --- /dev/null +++ b/Docs/website/tools/rebase_links.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +## \file rebase_links.py +# \brief Make the absolute links of a built SU2 website work under a sub-path +# (e.g. https://.github.io/SU2/). Links to the download folders that +# the test site does not carry go to the real website. +# \version 8.5.0 "Harrier" +# +# SU2 Project Website: https://su2code.github.io +# +# The SU2 Project is maintained by the SU2 Foundation +# (http://su2foundation.org) +# +# Copyright 2012-2026, SU2 Contributors (cf. AUTHORS.md) +# +# SU2 is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# SU2 is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with SU2. If not, see . + +import os +import re +import sys + +REAL_SITE = "https://su2code.github.io" +REMOTE_FOLDERS = ("documents/", "download/", "emails/") + + +def main(): + if len(sys.argv) != 3: + print("usage: rebase_links.py ") + return 1 + root = sys.argv[1] + base = "/" + sys.argv[2].strip("/") if sys.argv[2].strip("/") else "" + link = re.compile(r'((?:href|src|action)\s*=\s*["\'])/(?!/)') + + def fix(m, text): + rest = text[m.end() :] + if base and rest.startswith(base.lstrip("/") + "/"): + return m.group(0) + if rest.startswith(REMOTE_FOLDERS): + return m.group(1) + REAL_SITE + "/" + return m.group(1) + base + "/" + + changed = 0 + for folder, _, files in os.walk(root): + for name in files: + if not name.endswith((".html", ".xml")): + continue + path = os.path.join(folder, name) + with open(path, encoding="utf-8", errors="replace") as f: + text = f.read() + new = link.sub(lambda m: fix(m, text), text) + if new != text: + with open(path, "w", encoding="utf-8") as f: + f.write(new) + changed += 1 + print("Rebased links in {} files".format(changed)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Docs/website/tools/test_build_site.py b/Docs/website/tools/test_build_site.py new file mode 100644 index 000000000000..9e4162780433 --- /dev/null +++ b/Docs/website/tools/test_build_site.py @@ -0,0 +1,174 @@ +"""Run with python3 -m unittest discover -s Docs/website/tools.""" + +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +import build_site + + +class ReferenceExtractionTests(unittest.TestCase): + def test_comments_are_not_enum_values(self): + with tempfile.TemporaryDirectory() as folder: + include = Path(folder) / "Common/include" + include.mkdir(parents=True) + (include / "options.hpp").write_text( + """static const MapType Example_Map = { + MakePair("ACTIVE", 1) + // MakePair("DISABLED_LINE", 2) + /* MakePair("DISABLED_BLOCK", 3) */ + MakePair("https://example.test/a/*b*/", 4) +};""" + ) + self.assertEqual( + build_site.read_maps(folder)["Example_Map"], + [("ACTIVE", "1"), ("https://example.test/a/*b*/", "4")], + ) + + def test_current_disabled_model_values(self): + su2 = Path(__file__).resolve().parents[3] + maps = build_site.read_maps(su2) + sst = dict(maps["SST_Options_Map"]) + self.assertIn("V1994m", sst) + self.assertIn("V2003m", sst) + self.assertNotIn("V1994", sst) + self.assertNotIn("V2003", sst) + self.assertNotIn("GENERALIZED_ALPHA", dict(maps["Time_Int_Map_FEA"])) + + def test_array_storage_is_not_a_default(self): + su2 = Path(__file__).resolve().parents[3] + arrays = [ + o + for o in build_site.read_options(su2) + if o["kind"] in ("DoubleArray", "UShortArray", "ULongArray") + ] + self.assertTrue(arrays) + for option in arrays: + with self.subTest(option=option["name"]): + self.assertEqual(build_site.describe_type(option, {})[2], "") + + def test_parser_renames_accept_single_and_or_conditions(self): + with tempfile.TemporaryDirectory() as folder: + source = Path(folder) / "Common/src/CConfig.cpp" + source.parent.mkdir(parents=True) + source.write_text( + r""" + if (!option_name.compare("OLD")) + newString.append("Use NEW instead.\n\n"); + else if ( !option_name.compare("FIRST") || + !option_name.compare("SECOND") ) + newString.append("Both names were removed.\n"); + if (!option_name.compare("BAD_FIRST") !option_name.compare("BAD_SECOND")) + newString.append("No OR operator."); + """ + ) + self.assertEqual( + build_site.read_parser_renames(folder), + { + "OLD": "Use NEW instead.", + "FIRST": "Both names were removed.", + "SECOND": "Both names were removed.", + }, + ) + + def test_parser_renames_reject_long_malformed_conditions_promptly(self): + with tempfile.TemporaryDirectory() as folder: + source = Path(folder) / "Common/src/CConfig.cpp" + source.parent.mkdir(parents=True) + source.write_text("if(" + ' !option_name.compare("OLD")' * 48) + result = subprocess.run( + [ + sys.executable, + "-c", + "import build_site, sys; print(build_site.read_parser_renames(sys.argv[1]))", + folder, + ], + cwd=Path(__file__).resolve().parent, + capture_output=True, + text=True, + timeout=5, + check=True, + ) + self.assertEqual(result.stdout.strip(), "{}") + + def test_cpp_arguments_keep_quoted_text_grouping_and_operators(self): + text = r"""config->GetField(i), "a,b)\"quoted\"", nested(1, 2), (a < b), {1, 2}, ',' """ + self.assertEqual( + build_site.split_args(text), + [ + "config->GetField(i)", + r'''"a,b)\"quoted\""''', + "nested(1, 2)", + "(a < b)", + "{1, 2}", + "','", + ], + ) + + def test_output_registration_ignores_comments_and_strings(self): + with tempfile.TemporaryDirectory() as folder: + source = Path(folder) / "SU2_CFD/src/output/CExampleOutput.cpp" + source.parent.mkdir(parents=True) + source.write_text( + r""" +// The "AddHistoryOutput(" must stay on one line. +/* AddVolumeOutput("DISABLED", "Disabled", "GROUP", "Not active"); */ +const char* help = "Use AddVolumeOutput( here"; +AddVolumeOutput("ACTIVE", "Active", "GROUP", "Real output"); +""" + ) + fields = build_site.read_outputs(folder)["CExampleOutput"] + self.assertEqual([f["name"] for f in fields], ["ACTIVE"]) + self.assertEqual(fields[0]["line"], 5) + + def test_output_fields_keep_descriptions_and_dynamic_names(self): + with tempfile.TemporaryDirectory() as folder: + source = Path(folder) / "SU2_CFD/src/output/CExampleOutput.cpp" + source.parent.mkdir(parents=True) + source.write_text( + r""" +AddVolumeOutput("EXTRAPOLATION", "Extrapolation", "PRIMITIVE", "Density, energy outside data range)"); +AddHistoryOutput("SPECIES_" + config->GetSpecies_Label(i), "Species", Format::FIXED, "SPECIES_COEFF", "Species mean"); +""" + ) + fields = build_site.read_outputs(folder)["CExampleOutput"] + self.assertEqual(fields[0]["desc"], "Density, energy outside data range)") + self.assertEqual( + fields[1]["name"], '"SPECIES_" + config->GetSpecies_Label(i)' + ) + self.assertEqual(fields[1]["group"], "SPECIES_COEFF") + self.assertEqual(fields[1]["desc"], "Species mean") + + def test_error_messages_keep_commas_parentheses_and_ignore_comments(self): + with tempfile.TemporaryDirectory() as folder: + source = Path(folder) / "Common/example.cpp" + source.parent.mkdir(parents=True) + source.write_text( + r""" +// SU2_MPI::Error("Comment only", CURRENT_FUNCTION); +SU2_MPI::Error("Structure has changed, cannot refresh.)", CURRENT_FUNCTION); +""" + ) + self.assertEqual( + build_site.read_error_messages(folder), + [("Common/example.cpp", 3, "Structure has changed, cannot refresh.)")], + ) + + def test_string_defaults_are_values_or_explicitly_unresolved(self): + cases = [ + ('string("nitrogen")', "nitrogen"), + ('std::string("history")', "history"), + ('"mesh"', "mesh"), + ('string("")', '""'), + ("config->GetName()", "not extracted (C++ expression: config->GetName())"), + ] + for expression, expected in cases: + with self.subTest(expression=expression): + option = {"kind": "String", "args": ['"NAME"', "name", expression]} + self.assertEqual(build_site.describe_type(option, {})[2], expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/Docs/website/tools/test_docs_checks.py b/Docs/website/tools/test_docs_checks.py new file mode 100644 index 000000000000..52764057b44f --- /dev/null +++ b/Docs/website/tools/test_docs_checks.py @@ -0,0 +1,178 @@ +"""Regression checks for build isolation, links and searchable output fields.""" + +import json +from pathlib import Path +import tempfile +import sys +import unittest +from unittest.mock import patch + +import build_docs +import check_site +import rebase_links + + +class DocsChecksTests(unittest.TestCase): + def test_links_under_baseurl_with_escaped_anchor_and_relative_asset(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + page = root / "docs_v7/example/index.html" + page.parent.mkdir(parents=True) + (root / "asset.png").touch() + page.write_text( + '

Title

ok' + 'broken' + 'brokenexternal' + ) + problems, checked = check_site.check_links(root, "/SU2") + self.assertEqual(checked, 4) + self.assertEqual(len(problems), 2) + self.assertTrue(any("missing anchor" in p for p in problems)) + self.assertTrue(any("missing file" in p for p in problems)) + + def test_search_detects_missing_page_and_output_field(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source = root / "source" + (source / "_docs_v7").mkdir(parents=True) + (source / "_docs_v7/example.md").write_text( + "---\ntitle: Example\npermalink: /docs_v7/example/\n---\nContent" + ) + (root / "search.json").write_text(json.dumps([])) + with patch.object( + check_site.build_site, + "read_outputs", + return_value={"Flow": [{"name": "RMS_TEST", "literal": True}]}, + ): + problems, count = check_site.check_search(root, source, root, "/SU2") + self.assertEqual(count, 0) + self.assertEqual(len(problems), 2) + + def test_source_checks_reject_new_unknown_names_and_undocumented_options(self): + with patch.object( + check_site.build_site, + "read_options", + return_value=[{"name": "NEW_OPTION", "comment": None, "kind": "Double"}], + ), patch.object( + check_site.build_site, "read_maps", return_value={} + ), patch.object( + check_site.build_site, "read_code_names", return_value=set() + ), patch.object( + check_site.build_site, "check_docs", return_value=[("page", 1, "TYPO", "")] + ), patch.object( + check_site.build_site, + "check_template", + return_value=[(1, "OLD"), (2, "NEW")], + ): + problems = check_site.check_source( + Path("."), + Path("."), + { + "template_options": ["OLD"], + "missing_descriptions": [], + "enum_values": [], + }, + ) + self.assertEqual(len(problems), 3) + + def test_config_enum_values_ignore_source_code_and_check_disabled_variants(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "_docs_v7").mkdir() + (root / "_docs_v7/example.md").write_text( + "```\nSST_OPTIONS= V2003m\n```\n```\nSST_OPTIONS= V2003\n```\n" + "```cpp\nSST_OPTIONS= SOMETHING_ELSE\n```\n" + ) + options = [ + { + "name": "SST_OPTIONS", + "kind": "EnumList", + "args": ["", "", "", "SST_Map"], + } + ] + problems = check_site.check_enum_values( + root, options, {"SST_Map": [("V2003m", "value")]} + ) + self.assertEqual(len(problems), 1) + self.assertIn("unknown keyword V2003", next(iter(problems))) + + def test_config_enum_checks_hyphenated_keywords(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "_docs_v7").mkdir() + (root / "_docs_v7/example.md").write_text( + "```\nSST_OPTIONS= V2003m, KATO-LAUNDER\n```\n" + "```\nSST_OPTIONS= KATO-LAUNDR\n```\n" + ) + options = [ + { + "name": "SST_OPTIONS", + "kind": "EnumList", + "args": ["", "", "", "SST_Map"], + } + ] + maps = {"SST_Map": [("V2003m", "value"), ("KATO-LAUNDER", "value")]} + problems = check_site.check_enum_values(root, options, maps) + self.assertEqual(len(problems), 1) + self.assertIn("unknown keyword KATO-LAUNDR", next(iter(problems))) + + def test_rebase_links_handles_root_and_subpath_without_double_prefix(self): + with tempfile.TemporaryDirectory() as directory: + page = Path(directory) / "index.html" + original = 'docspdf' + for base in ("", "/SU2"): + page.write_text(original) + with patch.object(sys, "argv", ["rebase_links.py", directory, base]): + self.assertEqual(rebase_links.main(), 0) + expected = page.read_text() + self.assertIn('href="' + base + '/docs_v7/home/"', expected) + self.assertIn( + 'href="https://su2code.github.io/documents/example.pdf"', + expected, + ) + rebase_links.main() + self.assertEqual(page.read_text(), expected) + + def test_link_to_check_report_exists_during_its_own_validation(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + page = root / "docs_v7/example/index.html" + page.parent.mkdir(parents=True) + page.write_text('Check results') + with patch.object( + sys, + "argv", + [ + "check_site.py", + "--su2", + directory, + "--source", + directory, + "--site", + directory, + ], + ), patch.object(check_site, "check_source", return_value=[]), patch.object( + check_site, "check_search", return_value=([], 0) + ): + self.assertEqual(check_site.main(), 0) + report = json.loads((root / "docs-check.json").read_text()) + self.assertEqual(report["new_problems"], []) + self.assertEqual(report["references_checked"], 1) + + def test_build_refuses_sources_and_unmarked_nonempty_directory(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + source, work = root / "source", root / "work" + source.mkdir() + work.mkdir() + (work / "keep.txt").write_text("keep") + for destination in (source, source / "build", root, work): + with self.subTest(destination=destination), self.assertRaises( + ValueError + ): + build_docs.prepare_directory(destination, (source,)) + self.assertEqual((work / "keep.txt").read_text(), "keep") + + +if __name__ == "__main__": + unittest.main() diff --git a/SU2_CFD/flamelet_python_bc.md b/SU2_CFD/flamelet_python_bc.md index 3aa393cee60b..1dd28cd2384a 100644 --- a/SU2_CFD/flamelet_python_bc.md +++ b/SU2_CFD/flamelet_python_bc.md @@ -2,7 +2,7 @@ This document explains how to use the Python wrapper (`pysu2`) to set custom per-vertex boundary conditions for the flamelet (FGM) scalar transport -solver, and how the setup differs between the two `FLAMELET_ENTHALPY_BC` modes. +solver, and how the setup differs between the two `FLAME_ENTHALPY_BC` modes. --- @@ -20,15 +20,15 @@ The flamelet solver transports `nVar` scalar variables. Their order is: Two modes control how the enthalpy boundary condition is derived at thermal walls (`MARKER_ISOTHERMAL`, `MARKER_HEATFLUX`): -- **`FLOW_MARKERS`** — enthalpy is derived from the flow thermal field (wall +- **`FLOW_MARKERS`** (default) — enthalpy is derived from the flow thermal field (wall temperature via `MARKER_ISOTHERMAL`, or wall heat flux via `MARKER_HEATFLUX`). -- **`SPECIES_MARKERS`** (default) — enthalpy and all other scalars are taken +- **`SPECIES_MARKERS`** — enthalpy and all other scalars are taken directly from `MARKER_WALL_SPECIES`, or from the Python wrapper when `MARKER_PYTHON_CUSTOM` is active. --- -## Mode 1: `FLAMELET_ENTHALPY_BC = FLOW_MARKERS` +## Mode 1: `FLAME_ENTHALPY_BC = FLOW_MARKERS` (default) ### How it works @@ -57,7 +57,7 @@ driver.SetMarkerCustomNormalHeatFlux(iMarker, iVertex, q_wall) ### config.cfg ```cfg -FLAMELET_ENTHALPY_BC = FLOW_MARKERS +FLAME_ENTHALPY_BC = FLOW_MARKERS % Flow solver wall BC — sets KindBC and provides the fallback heat flux value % used when MARKER_PYTHON_CUSTOM is not active. @@ -103,7 +103,7 @@ driver.Finalize() --- -## Mode 2: `FLAMELET_ENTHALPY_BC = SPECIES_MARKERS` (default) +## Mode 2: `FLAME_ENTHALPY_BC = SPECIES_MARKERS` ### How it works @@ -129,7 +129,7 @@ Adjust the number of `BC_TYPE, value` pairs to match the actual number of scalar variables in your setup. ```cfg -FLAMELET_ENTHALPY_BC = SPECIES_MARKERS +FLAME_ENTHALPY_BC = SPECIES_MARKERS % Flow solver wall BC — still required to set KindBC. % The choice between MARKER_ISOTHERMAL and MARKER_HEATFLUX only affects @@ -222,4 +222,4 @@ driver.Finalize() should receive `0.0` unless you intentionally want a non-zero flux there. - The `CHT` (conjugate heat transfer) path (`BC_ConjugateHeat_Interface`) always uses `BC_Isothermal_Wall_Generic` and is unaffected by - `FLAMELET_ENTHALPY_BC`. Python custom BCs are not applicable to CHT markers. + `FLAME_ENTHALPY_BC`. Python custom BCs are not applicable to CHT markers. diff --git a/config_template.cfg b/config_template.cfg index 0aa29398b90a..bb664a0694e6 100644 --- a/config_template.cfg +++ b/config_template.cfg @@ -424,8 +424,8 @@ ITER_DCL_DALPHA= 500 % Evaluate the dOF_dCL or dOF_dCMy during run time EVAL_DOF_DCX= NO % -% Damping factor for thrust BC (actuator disk). -NETTHRUST_DBCTHRUST= 1.0 +% Estimate of dNetThrust/dBCThrust used in the actuator-disk thrust update. +DNETTHRUST_DBCTHRUST= 1.0 % % parameter for the definition of a complex objective function DCD_DCL_VALUE= 0.0 @@ -677,8 +677,8 @@ MOVE_MOTION_ORIGIN = 0 % % See doi: 10.2514/1.J055172 % -% Evaluate buffet sensor on Navier-Stokes markers (NO, YES) -BUFFET_MONITORING= NO +% The compressible viscous solver evaluates the sensor during force evaluation. +% Request BUFFET in HISTORY_OUTPUT/SCREEN_OUTPUT to monitor its integral. % % Sharpness coefficient for the buffet sensor Heaviside function BUFFET_K= 10.0 @@ -878,9 +878,6 @@ HB_PRECONDITION= NO % Omega_HB = 2*PI*frequency - frequencies for Harmonic Balance method OMEGA_HB= (0,1.0,-1.0) % -% Determines if the single-zone driver is used. (deprecated) -SINGLEZONE_DRIVER= NO -% % Determines if the special output is written out SPECIAL_OUTPUT= NO @@ -1060,7 +1057,7 @@ CONTROLLING_VARIABLE_SOURCE_NAMES= (ProdRateTot_PV, NULL) % Method used to ignite the solution: % FLAME_FRONT : the flame is initialized using a plane, defined by a point and a normal. On one side, the solution is initialized % using 'burnt' conditions and on the other side 'unburnt' conditions. The normal points in the direction of the 'burnt' -% condition. The 'burnt' condition is defined as the value of the progress variable for which the temperature exceeds the IGNITION_TEMPERATURE. +% condition. The 'burnt' condition is defined as the value of the progress variable for which the temperature exceeds the FLAME_INIT_IGNITION. % SPARK : the solution is ignited through application of a set of source terms within a specified region for a set number % of solver iterations. This artificial spark can be applied after a certain number of iterations has passed, allowing for % the flow to evolve before igniting the mixture. @@ -1118,7 +1115,7 @@ THICKENED_FLAME_CORRECTION= YES % Flame ignition temperature % Flame thickness calculations are performed when the maximum temperature in the flow solution exceeds this value. -IGNITION_TEMPERATURE= 1100.0 +FLAME_INIT_IGNITION= 1100.0 % % --------------------- INVERSE DESIGN SIMULATION -----------------------------% @@ -1865,11 +1862,6 @@ MG_STARTUP_STAGNATION= 0.99 % 0 disables the criterion, as MG_STARTUP_STAGNATION= 0 does (default 5). MG_STARTUP_STAGNATION_ITER= 5 -% -------------------------- MESH SMOOTHING -----------------------------% -% -% Before each computation, implicitly smooth the nodal coordinates -SMOOTH_GEOMETRY= 0 - % -------------------- FLOW NUMERICAL METHOD DEFINITION -----------------------% % % Convective numerical method (JST, JST_KE, JST_MAT, LAX-FRIEDRICH, ROE, AUSM, @@ -2157,8 +2149,9 @@ USE_ACCURATE_TURB_JACOBIANS= NO % % --------------------- HEAT NUMERICAL METHOD DEFINITION ----------------------% % -% Value of the thermal diffusivity -THERMAL_DIFFUSIVITY= 1.0 +% For a standalone HEAT_EQUATION solid, thermal diffusivity is computed as +% THERMAL_CONDUCTIVITY_CONSTANT / (MATERIAL_DENSITY * SPECIFIC_HEAT_CP). +% Set those material properties in the solid configuration. % % Convective numerical method for the heat equation (only SCALAR_UPWIND) CONV_NUM_METHOD_HEAT= SCALAR_UPWIND @@ -2336,7 +2329,7 @@ DV_UNORDERED_SENS_FILENAME= unordered_sensitivity.dat HOLD_GRID_FIXED= NO % % Coordinates of the box where the grid will be deformed (Xmin, Ymin, Zmin, Xmax, Ymax, Zmax) -HOLD_GRID_FIXED_COORDS= (-1e15, -1e15, -1e15, 1e15, 1e15, 1e15) +HOLD_GRID_FIXED_COORD= (-1e15, -1e15, -1e15, 1e15, 1e15, 1e15) % % Design variable for FEA problems % options: NONE, YOUNG_MODULUS, POISSON_RATIO, DENSITY, DEAD_WEIGHT, ELECTRIC_FIELD @@ -2406,7 +2399,7 @@ DEFORM_LIMIT = 1E6 DEFORM_ELASTICITY_MODULUS= 2e11 % % Poisson ratio for constant stiffness FEA method of grid deformation -DEFORM_POISSON_RATIO= 0.3 +DEFORM_POISSONS_RATIO= 0.3 % % Size of the layer of highest stiffness for wall distance-based mesh stiffness DEFORM_STIFF_LAYER_SIZE= 0.0