From 85884ab0972db8788997d807c204e694474ca6cd Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 21 Oct 2026 10:28:08 +0200 Subject: [PATCH 01/25] Build a test SU2 website with a generated option reference and Doxygen Add Docs/website: a script that generates the configuration option reference from CConfig.cpp, checks the options named in the docs and copies the pages kept with the code into the website. A workflow builds the website with Jekyll and Doxygen and publishes it on GitHub Pages. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NSfEWrhpF27TnrkJH74sv8 --- .github/workflows/docs-website.yml | 87 +++++++ Docs/website/README.md | 31 +++ Docs/website/pages/Code-Reference.md | 23 ++ Docs/website/tools/build_site.py | 370 +++++++++++++++++++++++++++ Docs/website/tools/rebase_links.py | 70 +++++ 5 files changed, 581 insertions(+) create mode 100644 .github/workflows/docs-website.yml create mode 100644 Docs/website/README.md create mode 100644 Docs/website/pages/Code-Reference.md create mode 100644 Docs/website/tools/build_site.py create mode 100644 Docs/website/tools/rebase_links.py diff --git a/.github/workflows/docs-website.yml b/.github/workflows/docs-website.yml new file mode 100644 index 000000000000..7cb7f89800e6 --- /dev/null +++ b/.github/workflows/docs-website.yml @@ -0,0 +1,87 @@ +name: Docs Website + +# Builds the SU2 website with the pages of this repository, the generated +# option reference and the Doxygen code reference, then publishes it on the +# GitHub Pages of this repository. + +on: + push: + branches: + - 'docs_site' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: docs-website + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout SU2 + uses: actions/checkout@v4 + with: + path: su2 + fetch-depth: 1 + - name: Checkout website + uses: actions/checkout@v4 + with: + repository: su2code/su2code.github.io + path: site + fetch-depth: 1 + - name: Configure Pages + id: pages + uses: actions/configure-pages@v5 + - name: Prepare website + run: python3 su2/Docs/website/tools/build_site.py --su2 su2 --site site --light + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.3' + - name: Build website + working-directory: site + run: | + bundle install --jobs 4 + bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" -d ../_site + - name: Build code reference + working-directory: su2 + run: | + sudo apt-get update && sudo apt-get install -y doxygen graphviz + ( cat doxyfile + echo "OUTPUT_DIRECTORY = ../doxygen" + echo "PROJECT_NUMBER = $(git rev-parse --short=10 HEAD)" + echo "SOURCE_BROWSER = NO" + echo "CALL_GRAPH = NO" + echo "CALLER_GRAPH = NO" + echo "COLLABORATION_GRAPH = NO" + echo "INCLUDE_GRAPH = NO" + echo "INCLUDED_BY_GRAPH = NO" + echo "DIRECTORY_GRAPH = NO" + echo "DOT_IMAGE_FORMAT = svg" + echo "WARN_LOGFILE = ../doxygen_warnings.txt" + ) | doxygen - + - name: Assemble + run: | + python3 su2/Docs/website/tools/rebase_links.py _site "${{ steps.pages.outputs.base_path }}" + cp -r doxygen/html _site/doxygen + du -sh _site + - name: Upload + uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy + 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..d77d5db645b3 --- /dev/null +++ b/Docs/website/README.md @@ -0,0 +1,31 @@ +# SU2 website: pages kept with the code + +This folder holds the parts of the SU2 website (https://su2code.github.io) that must change together with the code. +The workflow `.github/workflows/docs-website.yml` builds the website and publishes it on the GitHub Pages of this repository. + +## What the build does + +1. Gets the current website from `su2code/su2code.github.io`. +2. Copies every page in `pages/` into the user guide (`_docs_v7/`). A page with the same name as a website page replaces it. +3. Makes the **Option Reference** from `Common/src/CConfig.cpp`. Each option gets its description, type, allowed values, default value and a link to the code. +4. Makes the **Docs Check** page: options named in the docs, tutorials and V&V pages that do not exist in the code. +5. Builds the website with Jekyll and the **Code Reference** with Doxygen. + +## How to change the docs + +- **New option:** write the comment above the `add...Option` call in `CConfig.cpp`. Use the usual form: + `/*!\brief NAME \n DESCRIPTION: what it does \n DEFAULT: value \ingroup Config*/`. + The Option Reference updates by itself. Also add the option to `config_template.cfg`. +- **New or changed page:** add or edit a Markdown file in `pages/`. It needs a front matter with `title` and + `permalink: /docs_v7//`. New pages appear at the end of "Developer Docs" in the menu. +- **Images:** put them in `pages/img/`. Use them as `../../docs_files/website/.png`. + +## Run it on your computer + +``` +git clone --depth 1 https://github.com/su2code/su2code.github.io.git site +python3 Docs/website/tools/build_site.py --su2 . --site site +cd site && bundle install && bundle exec jekyll serve +``` + +Open http://localhost:4000. Run `doxygen doxyfile` in the SU2 folder for the Code Reference (`Docs/html/index.html`). diff --git a/Docs/website/pages/Code-Reference.md b/Docs/website/pages/Code-Reference.md new file mode 100644 index 000000000000..966160f8d1d1 --- /dev/null +++ b/Docs/website/pages/Code-Reference.md @@ -0,0 +1,23 @@ +--- +title: Code Reference (Doxygen) +permalink: /docs_v7/Code-Reference/ +--- + +The code reference is made by [Doxygen](https://www.doxygen.nl) from the comments in the SU2 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 in the SU2 folder. The result is in `Docs/html/index.html`. + +``` +doxygen doxyfile +``` diff --git a/Docs/website/tools/build_site.py b/Docs/website/tools/build_site.py new file mode 100644 index 000000000000..8761f6177d39 --- /dev/null +++ b/Docs/website/tools/build_site.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 + +## \file build_site.py +# \brief Prepare a checkout of the SU2 website (su2code.github.io) for a Jekyll build: +# copy the pages kept in this repository, generate the configuration option +# reference from Common/src/CConfig.cpp and check the options named in the docs. +# \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 argparse +import datetime +import glob +import os +import re +import shutil +import subprocess +import sys + +DOCS = "_docs_v7" +URL = "docs_v7" +NAV = os.path.join("_data", "docs_v7.yml") +REF_PREFIX = "Option-Reference" + +# Folders of the website that are only downloads; the test site links to the real site instead. +HEAVY_FOLDERS = ["documents", "download", "emails"] + +# Upper case words followed by "=" in the docs that are not configuration options +# (keywords of the SU2 mesh format, etc.). +NOT_OPTIONS = {"NDIME", "NELEM", "NPOIN", "NMARK", "NZONE", "IZONE", "MARKER_TAG", "MARKER_ELEMS", "FFD_NBOX", + "FFD_NLEVEL", "FFD_TAG", "FFD_LEVEL", "FFD_DEGREE_I", "FFD_DEGREE_J", "FFD_DEGREE_K", + "FFD_PARENTS", "FFD_CHILDREN", "FFD_CORNER_POINTS", "FFD_CONTROL_POINTS", "FFD_SURFACE_POINTS", + "NMARK_FFD", "NPERIODIC", "PERIODIC_INDEX", "NELEM_BOUND", "VALUE", "OPTION"} + +TYPE_NAMES = { + "Double": "real number", "Bool": "YES / NO", "String": "string", "UnsignedLong": "integer", + "UnsignedShort": "integer", "Long": "integer", "DoubleList": "list of real numbers", + "DoubleArray": "fixed-size list of real numbers", "StringList": "list of strings", + "UShortList": "list of integers", "ShortList": "list of integers", "ULongList": "list of integers", + "ULongArray": "fixed-size list of integers", "UShortArray": "fixed-size list of integers", + "StringDoubleList": "list of (string, real number) pairs", "Python": "used by the Python scripts only", +} + + +def slugify(text): + return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") + + +def split_args(text): + """Split the arguments of a C++ call at the commas that are not inside brackets.""" + args, depth, start = [], 0, 0 + for i, c in enumerate(text): + if c in "([{<": + depth += 1 + elif c in ")]}>": + depth -= 1 + elif c == "," and depth == 0: + args.append(text[start:i].strip()) + start = i + 1 + args.append(text[start:].strip()) + return args + + +def call_body(text, open_paren): + """Return the text between the parenthesis at open_paren and its matching parenthesis.""" + depth = 0 + for i in range(open_paren, len(text)): + if text[i] == "(": + depth += 1 + elif text[i] == ")": + depth -= 1 + if depth == 0: + return text[open_paren + 1:i], i + return text[open_paren + 1:], len(text) + + +def read_maps(su2): + """Map name -> list of (key, C++ value) for every MapType in Common/include.""" + maps = {} + pattern = re.compile(r"MapType\s*<\s*std::string\s*,[^>]*>\s+(\w+)\s*=\s*\{(.*?)\};", re.S) + for path in glob.glob(os.path.join(su2, "Common", "include", "**", "*.*pp"), recursive=True): + with open(path, encoding="utf-8", errors="replace") as f: + text = f.read() + for m in pattern.finditer(text): + pairs = re.findall(r'MakePair\(\s*"([^"]+)"\s*,\s*([^)]*?)\s*\)', m.group(2)) + maps[m.group(1)] = pairs + return maps + + +def clean_comment(raw): + """Turn a /*! ... */ comment of CConfig.cpp into (description, allowed values text, default text).""" + text = raw.replace("\\ingroup Config", "") + text = re.sub(r"\\brief\s+[A-Z0-9_]+\s*", "", text) + text = text.replace("\\brief", "") + text = re.sub(r"\\link\s+(\w+)\s*\\endlink", r"\1", text) + text = re.sub(r"\s*\*\s*", " ", text) if "\n" in text else text + parts = {"DESCRIPTION": "", "OPTIONS": "", "DEFAULT": ""} + key = "DESCRIPTION" + for chunk in re.split(r"\\n", text): + chunk = chunk.strip() + m = re.match(r"(DESCRIPTION|Options|OPTIONS|DEFAULT|Default)\s*:\s*(.*)", chunk, re.S) + if m: + key = m.group(1).upper() + chunk = m.group(2) + if chunk: + parts[key] = (parts[key] + " " + chunk).strip() + return parts["DESCRIPTION"], parts["OPTIONS"], parts["DEFAULT"] + + +def read_options(su2): + """Read every option registered in CConfig::SetConfig_Options, in order, with its category.""" + with open(os.path.join(su2, "Common", "src", "CConfig.cpp"), encoding="utf-8") as f: + text = f.read() + + token = re.compile(r"/\*!(.*?)\*/|\badd(\w*)Option\s*\(", re.S) + options, category, comment = [], "General", None + pos = 0 + while True: + m = token.search(text, pos) + if not m: + break + if m.group(1) is not None: + body = m.group(1) + cat = re.search(r"\\par\s+CONFIG_CATEGORY\s*:\s*(.*?)\s*\\ingroup", body, re.S) + if cat: + category, comment = " ".join(cat.group(1).split()), None + else: + comment = body + pos = m.end() + continue + args_text, end = call_body(text, m.end() - 1) + pos = end + 1 + args = split_args(args_text) + name = re.fullmatch(r'"([A-Z0-9_]+)"', args[0]) + if not name: + continue + line = text.count("\n", 0, m.start()) + 1 + options.append({"name": name.group(1), "kind": m.group(2), "args": args, "category": category, + "comment": comment, "line": line}) + comment = None + return options + + +def read_template(su2): + """Option name -> (comment lines above it, example line) from config_template.cfg.""" + examples = {} + with open(os.path.join(su2, "config_template.cfg"), encoding="utf-8") as f: + lines = f.read().splitlines() + notes = [] + for line in lines: + m = re.match(r"^\s*%?\s*([A-Z][A-Z0-9_]+)\s*=(.*)$", line) + if m and not line.lstrip().startswith("%%"): + examples.setdefault(m.group(1), (list(notes), line.strip())) + notes = [] + elif line.lstrip().startswith("%") and not line.lstrip().startswith("% ---") and line.strip("% -"): + notes.append(line.strip().lstrip("%").strip()) + else: + notes = [] + return examples + + +def describe_type(opt, maps): + """Return (type text, list of allowed values, default text).""" + kind, args = opt["kind"], opt["args"] + values, default = [], "" + if kind in ("Enum", "EnumList"): + map_name = args[2] if kind == "Enum" else args[3] + values = [k for k, _ in maps.get(map_name, [])] + if kind == "Enum" and len(args) > 3: + default = next((k for k, v in maps.get(map_name, []) if v == args[3]), args[3]) + return ("one of the values below" if kind == "Enum" else "list of the values below"), values, default + if kind == "Bool" and len(args) > 2: + default = {"true": "YES", "false": "NO"}.get(args[2], args[2]) + elif kind in ("Double", "String", "UnsignedLong", "UnsignedShort", "Long") and len(args) > 2: + default = args[2].strip('"') + elif kind in ("DoubleArray", "UShortArray", "ULongArray") and len(args) > 3: + default = args[3] + return TYPE_NAMES.get(kind, "special format, see config_template.cfg"), values, default + + +def write_reference(su2, site, options, maps, commit): + examples = read_template(su2) + stamp = "Generated from SU2 commit `{}` on {}.".format(commit, datetime.date.today().isoformat()) + source = "https://github.com/su2code/SU2/blob/{}/Common/src/CConfig.cpp".format(commit) + + categories = {} + for opt in options: + categories.setdefault(opt["category"], []).append(opt) + + pages = [] + for cat, opts in categories.items(): + slug = "{}-{}".format(REF_PREFIX, slugify(cat)) + for opt in opts: + opt["page"] = slug + out = ["---", 'title: "{}"'.format(cat.replace('"', "'")), "permalink: /{}/{}/".format(URL, slug), "---", + "", "{% raw %}", "", "> {} Do not edit this page. Edit the comment above the option in " + "[`Common/src/CConfig.cpp`]({}) instead.".format(stamp, source), ""] + for opt in opts: + desc, opt_text, def_text = clean_comment(opt["comment"]) if opt["comment"] else ("", "", "") + type_text, values, default = describe_type(opt, maps) + out.append("### {} {{#{}}}".format(opt["name"], opt["name"])) + out.append("") + out.append(desc if desc else "*No description in the code yet.*") + out.append("") + out.append("- **Type:** {}".format(type_text)) + if values: + out.append("- **Values:** " + ", ".join("`{}`".format(v) for v in values)) + elif opt_text and not opt_text.startswith(tuple(maps)): + out.append("- **Values:** {}".format(opt_text)) + if default or def_text: + out.append("- **Default:** `{}`".format(default if default else def_text)) + if opt["name"] in examples: + notes, line = examples[opt["name"]] + out.append("- **In config_template.cfg:** `{}`".format(line)) + else: + out.append("- **Not in config_template.cfg.**") + out.append("- **Code:** [`CConfig.cpp` line {}]({}#L{})".format(opt["line"], source, opt["line"])) + out.append("") + out += ["{% endraw %}", ""] + with open(os.path.join(site, DOCS, slug + ".md"), "w", encoding="utf-8") as f: + f.write("\n".join(out)) + pages.append((slug, cat, len(opts))) + + missing_desc = sum(1 for o in options if not o["comment"]) + missing_tmpl = sum(1 for o in options if o["name"] not in examples) + out = ["---", "title: Option Reference", "permalink: /{}/{}/".format(URL, REF_PREFIX), "---", "", + "{% raw %}", "", + "This reference lists every option of the SU2 configuration file. It is made from the source code, " + "so it always matches the code. {}".format(stamp), "", + "- Options: {}".format(len(options)), + "- Options without a description in the code: {}".format(missing_desc), + "- Options not in `config_template.cfg`: {}".format(missing_tmpl), "", + "## Groups", ""] + out += ["- [{}](../{}/) ({} options)".format(cat, slug, n) for slug, cat, n in pages] + out += ["", "## All options A-Z", ""] + for opt in sorted(options, key=lambda o: o["name"]): + out.append("[`{0}`](../{1}/#{0}) ".format(opt["name"], opt["page"])) + out += ["", "{% endraw %}", ""] + with open(os.path.join(site, DOCS, REF_PREFIX + ".md"), "w", encoding="utf-8") as f: + f.write("\n".join(out)) + return [REF_PREFIX] + [p[0] for p in pages] + + +def check_docs(site, known): + """Find options in the docs, tutorials and V&V pages that the code does not know. + Only code blocks that look like an SU2 configuration file (at least half known options) + and inline code such as `OPTION= VALUE` are checked.""" + found = [] + setting = re.compile(r"^\s*%?\s*([A-Z][A-Z0-9_]{2,})\s*=") + files = sorted(glob.glob(os.path.join(site, DOCS, "*.md")) + + glob.glob(os.path.join(site, "_tutorials", "**", "*.md"), recursive=True) + + glob.glob(os.path.join(site, "_vandv", "*.md"))) + for path in files: + if os.path.basename(path).startswith(REF_PREFIX): + continue + with open(path, encoding="utf-8", errors="replace") as f: + lines = f.read().splitlines() + hits, block = set(), None + for num, line in enumerate(lines, 1): + if line.lstrip().startswith("```"): + if block is None: + block = [] + else: + names = [(n, m.group(1)) for n, m in block if m] + if names and 2 * sum(name in known for _, name in names) >= len(names): + hits.update((n, name) for n, name in names) + block = None + continue + if block is not None: + block.append((num, setting.match(line))) + else: + hits.update((num, name) for name in re.findall(r"`\s*([A-Z][A-Z0-9_]{2,})\s*=", line)) + for num, name in sorted(hits): + if name not in known and name not in NOT_OPTIONS: + found.append((os.path.relpath(path, site), num, name, lines[num - 1].strip())) + return found + + +def write_check_page(site, found, commit): + out = ["---", "title: Docs Check", "permalink: /{}/Docs-Check/".format(URL), "---", "", "{% raw %}", "", + "These pages name options that do not exist in SU2 commit `{}`. " + "The option was renamed or removed, or it has a typo.".format(commit), "", + "Found: {}".format(len(found)), ""] + last = None + for path, num, name, line in found: + if path != last: + out += ["", "### `{}`".format(path), ""] + last = path + out.append("- line {}: `{}`".format(num, name)) + out += ["", "{% endraw %}", ""] + with open(os.path.join(site, DOCS, "Docs-Check.md"), "w", encoding="utf-8") as f: + f.write("\n".join(out)) + + +def patch_nav(site, ref_pages, extra_dev_pages): + path = os.path.join(site, NAV) + with open(path, encoding="utf-8") as f: + nav = f.read() + section = "- title: Option Reference\n docs_v7:\n" + "".join(" - {}\n".format(p) for p in ref_pages) + "\n" + nav = nav.replace("- title: Developer Docs", section + "- title: Developer Docs", 1) + dev = re.search(r"- title: Developer Docs\n docs_v7:\n((?: - .*\n)+)", nav) + if dev: + listed = dev.group(1) + added = "".join(" - {}\n".format(p) for p in extra_dev_pages if " - {}\n".format(p) not in listed) + nav = nav[:dev.end(1)] + added + nav[dev.end(1):] + with open(path, "w", encoding="utf-8") as f: + f.write(nav) + + +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)") + 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) + + maps = read_maps(args.su2) + options = read_options(args.su2) + ref_pages = write_reference(args.su2, args.site, options, maps, commit) + found = check_docs(args.site, {o["name"] for o in options}) + write_check_page(args.site, found, commit) + patch_nav(args.site, ref_pages, dev_pages + ["Docs-Check"]) + + 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: {}".format( + len(options), len(ref_pages) - 1, len(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/rebase_links.py b/Docs/website/tools/rebase_links.py new file mode 100644 index 000000000000..6056e6fd662f --- /dev/null +++ b/Docs/website/tools/rebase_links.py @@ -0,0 +1,70 @@ +#!/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, base = sys.argv[1], "/" + sys.argv[2].strip("/") + link = re.compile(r'((?:href|src|action)\s*=\s*["\'])/(?!/)') + + def fix(m, text): + rest = text[m.end():] + if 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()) From 7e5c0f1f994343f1c0d866deb842a4304b68de62 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 21 Oct 2026 11:15:41 +0200 Subject: [PATCH 02/25] Add developer pages to the test website and build it from the website fork New pages: Code Structure, Running Regression Tests, Adding a Config Option and Adding Output Fields, written against develop 11c177b904. The workflow builds the website from rois1995/su2code.github.io (branch docs_site) by default and keeps Docs/website out of Doxygen. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NSfEWrhpF27TnrkJH74sv8 --- .github/workflows/docs-website.yml | 15 +- Docs/website/README.md | 5 +- Docs/website/pages/Adding-Output-Fields.md | 253 ++++++++++++++ Docs/website/pages/Adding-a-Config-Option.md | 323 ++++++++++++++++++ Docs/website/pages/Code-Structure.md | 283 +++++++++++++++ .../website/pages/Running-Regression-Tests.md | 283 +++++++++++++++ Docs/website/tools/build_site.py | 4 +- 7 files changed, 1163 insertions(+), 3 deletions(-) create mode 100644 Docs/website/pages/Adding-Output-Fields.md create mode 100644 Docs/website/pages/Adding-a-Config-Option.md create mode 100644 Docs/website/pages/Code-Structure.md create mode 100644 Docs/website/pages/Running-Regression-Tests.md diff --git a/.github/workflows/docs-website.yml b/.github/workflows/docs-website.yml index 7cb7f89800e6..5eecd835f999 100644 --- a/.github/workflows/docs-website.yml +++ b/.github/workflows/docs-website.yml @@ -9,6 +9,17 @@ on: branches: - 'docs_site' workflow_dispatch: + inputs: + site_repo: + description: 'Website repository' + default: 'rois1995/su2code.github.io' + site_ref: + description: 'Website branch' + default: 'docs_site' + +env: + SITE_REPO: ${{ inputs.site_repo || 'rois1995/su2code.github.io' }} + SITE_REF: ${{ inputs.site_ref || 'docs_site' }} permissions: contents: read @@ -31,7 +42,8 @@ jobs: - name: Checkout website uses: actions/checkout@v4 with: - repository: su2code/su2code.github.io + repository: ${{ env.SITE_REPO }} + ref: ${{ env.SITE_REF }} path: site fetch-depth: 1 - name: Configure Pages @@ -64,6 +76,7 @@ jobs: echo "DIRECTORY_GRAPH = NO" echo "DOT_IMAGE_FORMAT = svg" echo "WARN_LOGFILE = ../doxygen_warnings.txt" + echo "EXCLUDE = ./Docs/website" ) | doxygen - - name: Assemble run: | diff --git a/Docs/website/README.md b/Docs/website/README.md index d77d5db645b3..40ff3a1c3f37 100644 --- a/Docs/website/README.md +++ b/Docs/website/README.md @@ -5,7 +5,7 @@ The workflow `.github/workflows/docs-website.yml` builds the website and publish ## What the build does -1. Gets the current website from `su2code/su2code.github.io`. +1. Gets the website (by default `rois1995/su2code.github.io`, branch `docs_site`; other ones can be chosen when the workflow is started by hand). 2. Copies every page in `pages/` into the user guide (`_docs_v7/`). A page with the same name as a website page replaces it. 3. Makes the **Option Reference** from `Common/src/CConfig.cpp`. Each option gets its description, type, allowed values, default value and a link to the code. 4. Makes the **Docs Check** page: options named in the docs, tutorials and V&V pages that do not exist in the code. @@ -13,6 +13,9 @@ The workflow `.github/workflows/docs-website.yml` builds the website and publish ## How to change the docs +Pages that explain the code (developer pages) live here, so they change in the same PR as the code. +User guide pages and tutorials live in the website repository. + - **New option:** write the comment above the `add...Option` call in `CConfig.cpp`. Use the usual form: `/*!\brief NAME \n DESCRIPTION: what it does \n DEFAULT: value \ingroup Config*/`. The Option Reference updates by itself. Also add the option to `config_template.cfg`. diff --git a/Docs/website/pages/Adding-Output-Fields.md b/Docs/website/pages/Adding-Output-Fields.md new file mode 100644 index 000000000000..78d9f3ddf59f --- /dev/null +++ b/Docs/website/pages/Adding-Output-Fields.md @@ -0,0 +1,253 @@ +--- +title: Adding Output Fields +permalink: /docs_v7/Adding-Output-Fields/ +--- + +This page shows how to add a new field to the screen and history output, or to the volume (restart and visualization) output of SU2. It 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 a **name** (used in the configuration file and in the code, for example `DRAG`) and 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 and the `MARKER_ANALYZE` fields, and it 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 you do not need to change it for a new zone field. +- `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: + +| 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)` | for each point owned by the rank, when files are written (for time-domain problems at every call of `SetResultFiles`, to update 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 | + +The DG output uses `LoadVolumeDataFEM` instead of `LoadVolumeData`. Multizone output uses `SetMultizoneHistoryOutputFields` and `LoadMultizoneHistoryData`. Other hooks are `SetAdditionalScreenOutput` and `WriteAdditionalFiles`. + +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), and `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, and `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`) and 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. Use the same condition in both functions. `SetHistoryOutputValue` stops SU2 with the error "Cannot find output field with name ..." if the field was not added. + +### Example: the buffet sensor + +The buffet sensor is computed by `CNSSolver::Buffet_Monitoring` and stored 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, and 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 marker index in the list. 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); +``` + +Per-surface fields can be selected 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, and history fields with an empty description are left out of that list. +- History fields can be read from Python 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` (for example for the objectives of unsteady optimizations, in `SU2_PY/SU2/io/config.py`, and in `direct_differentiation.py`). This file is made by `SU2_PY/updateHistoryMap.py`, which reads the lines that start with `AddHistoryOutput(` in the output source files listed in the script. If the new field should be usable there, run that script (it needs `SU2_HOME`) and commit the new `historyMap.py`. Write the `AddHistoryOutput(...)` call on one line, at the start of the line, or the script does not find it. + +## 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` and 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. Use the same condition in both functions. + +**Important:** `SetVolumeOutputValue` caches the position of each field. The cache is built at the first point and reused for all other points, in the order of the calls. So `LoadVolumeData` must make the same calls, in the same order, for every point. Conditions may depend on the configuration, but not on `iPoint`. The same holds for `LoadSurfaceData` (either no calls for a vertex, 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 + +Values that only exist on walls (they are stored per marker and vertex) are set in `LoadSurfaceData`. It is called only for the markers where `config->GetSolid_Wall(iMarker)` is true. The field is still declared 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. Add a field to `SOLUTION` only if it is a new solution variable, and change the restart reading (`LoadRestart` of the solver) together with it. 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 + +Turbulence, transition and species fields are shared by the compressible, incompressible and NEMO flow outputs. They are in `CFlowOutput` (`SU2_CFD/src/output/CFlowOutput.cpp`), in functions that 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. 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 fields or whole groups in the configuration file. Names that do not exist for the current setup are ignored, with 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` takes field names or groups. +- `HISTORY_OUTPUT` takes groups or field names. +- `VOLUME_OUTPUT` takes field names or groups. +- `CONV_FIELD` takes history field names (not per-surface fields). +- `CUSTOM_OUTPUTS` lets users define new history values with 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` and 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. If the field is a history field, check the extra fields that its type creates, for example `REL_` or `TAVG_`, and that it works in `CONV_FIELD` if it is meant to. +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 and 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. If you added the field to a group that existing tests print on screen (for example `RMS_RES` or `AERO_COEFF`), the screen columns of those tests change. Run them and update their `test_vals`. +7. Unit tests for output classes are in `UnitTests/SU2_CFD/output/`. +8. Document the field for users in [History and Solution Output](../Custom-Output/) if it is of general interest. + +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..20b18bd9be45 --- /dev/null +++ b/Docs/website/pages/Adding-a-Config-Option.md @@ -0,0 +1,323 @@ +--- +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. An 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 | + +The [Option Reference](../Option-Reference/) of this website is generated from `Common/src/CConfig.cpp`, +`Common/include/option_structure.hpp` and `config_template.cfg` by `Docs/website/tools/build_site.py`. 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 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`, in the group where the +option belongs. The first argument is the name used in the configuration file, in capital letters. The second is +the member. Most calls take a default value as the last argument: + +```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`, which 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 and 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. Set a default in `SetPostprocessing` if you need one (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. + +Options with a special format (boundary conditions, design variables, FFD boxes, ...) have their own functions, +such as `addInletOption`, `addRiemannOption`, `addPeriodicOption` and `addDVParamOption`. Their parsers are the +`COption...` classes in `Common/include/option_structure.inl`. Look at an existing option of the same kind before +you write a new parser. + +### Multizone problems + +In a multizone problem each zone has its own configuration file. An option that is not set in the file of a zone +takes the value of the main configuration file (`CConfig::SetDefaultFromConfig`), except `SCREEN_OUTPUT` and +`HISTORY_OUTPUT`. 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 as follows. Follow these rules, or the Option Reference will be incomplete: + +- The comment must start with `/*!`. A comment that starts with `/*` (such as `/* DESCRIPTION: ... */`) is not read, + and the option gets "No description in the code yet". +- The comment must be the last `/*!` comment before the call. +- The parts are separated by `\n`. 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 also adds, without any comment: + +- the type, from the `add...Option` function; +- for enum options, all the keywords of the map; +- the default, from the last argument of the call (for an enum, the keyword of the map that has this value); +- the line of the option in `config_template.cfg`, or "Not in config_template.cfg"; +- a link to the line of the call in `CConfig.cpp`. + +### Categories + +The options are grouped into pages by the category comments in `SetConfig_Options`, 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 + (`Common/src/CConfig.cpp` and `SU2_DEF/src/drivers/CDiscAdjDeformationDriver.cpp`). Everywhere else + `MapType` is the empty class `CEmptyMap`, to make compiling faster. Use the enum in the rest of the code, not the map. +- `build_site.py` finds the keywords with the pattern `static const MapType Name = { MakePair(...) ... };` + in `Common/include`. Keep this form. Write the default of `addEnumOption` exactly as in the map + (`VOLUME_OUTPUT_PRECISION::SINGLE`), so the website can show it as a keyword. + +## 5. Check the value and set derived values + +The constructors of `CConfig` call, among other steps and in this order: `SetConfig_Parsing` (reads the values), +`SetDefault` (sets the default of the options that were not in the file), `SetPostprocessing` (checks and derived +values) and `SetMarkers` (boundary markers). Most constructors then call `SetOutput`, which prints the summary of +the settings at start-up. + +Put the checks of the new option in `CConfig::SetPostprocessing`. 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 when the default depends on other +options, or to change a value silently only when the user did not ask for it: + +```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 with what +it does and the allowed values, then 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, e.g. 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"); +``` + +These are the only mechanisms. There is no automatic translation of old names: 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 + +The 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/Code-Structure.md b/Docs/website/pages/Code-Structure.md new file mode 100644 index 000000000000..29254ed3d0bc --- /dev/null +++ b/Docs/website/pages/Code-Structure.md @@ -0,0 +1,283 @@ +--- +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`, and 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 types, grid movement, interface interpolation. Headers 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` (sections, thickness, volume, used as constraints in shape design). | +| `SU2_SOL` | The program `SU2_SOL`, which writes solution files from restart files (uses `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`. Built 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`. The shared runner is `TestCase.py`. | +| `externals` | Third-party code: CGNS, METIS, ParMETIS, TecIO, CLI11, Catch2, and 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 in `SU2_CFD/src/`. The folders `gradients` and `numerics_simd` only exist under `include`. + +| Folder | Content | +|---|---| +| `drivers` | `CDriver` and its children. They create all objects and run the time loop. | +| `iteration` | `CIteration` and its children. One iteration of one physics (fluid, heat, structure, adjoint). | +| `integration` | `CIntegration` and its children. Multigrid, single grid, Newton-Krylov, structural and DG time integration. | +| `solvers` | `CSolver` and its children. Residuals, boundary conditions, linear system of one set of equations. Also `CSolverFactory`. | +| `variables` | `CVariable` and its children. The data stored at the mesh points. | +| `numerics` | `CNumerics` and its children. Fluxes and source terms for one edge or one point, in the 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 driver functions used by the Python wrapper. + +## How SU2_CFD runs + +``` +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). +- In `CFluidIteration::Iterate`, the flow is solved first. Then `CommonAuxiliarySolvers` runs `SingleGrid_Iteration` for the transition, turbulence and species solvers. The coupling is segregated. +- `CIntegration::Space_Integration` and `CIntegration::Time_Integration` are in `SU2_CFD/src/integration/CIntegration.cpp`. They show exactly which solver functions are called, and in which order. +- The discrete adjoint drivers (`CDiscAdjSinglezoneDriver`, `CDiscAdjMultizoneDriver`) record one primal iteration on the AD tape, 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. They are passed 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 used by the Python wrapper. +- [`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 zones with block Gauss-Seidel or Jacobi iterations and transfers data 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`). The children are created by `CIterationFactory`: + +- [`CFluidIteration`](../../doxygen/classCFluidIteration.html), with 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, 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 base `CIntegration` is used for the discrete adjoint solvers. + +### Solvers + +[`CSolver`](../../doxygen/classCSolver.html) solves one set of equations on one grid level. It computes residuals, applies boundary conditions (`BC_...` functions), and owns the linear system (`Jacobian`, `LinSysRes`, `LinSysSol`, `System`). `CSolverFactory` (`SU2_CFD/src/solvers/CSolverFactory.cpp`) decides which solvers are created for each `SOLVER` option. + +Main families: + +- Finite volume flow: [`CFVMFlowSolverBase`](../../doxygen/classCFVMFlowSolverBase.html) (template with the shared code) and its children [`CEulerSolver`](../../doxygen/classCEulerSolver.html) / [`CNSSolver`](../../doxygen/classCNSSolver.html) (compressible), [`CIncEulerSolver`](../../doxygen/classCIncEulerSolver.html) / [`CIncNSSolver`](../../doxygen/classCIncNSSolver.html) (incompressible), [`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 [`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`, ... Matrices are row-major (`su2activematrix`), so the values of one point are next to each other in memory. Access is by point index, for example `nodes->GetSolution(iPoint, iVar)`. + +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 loops over the edges or points, sets the inputs of the numerics object, calls `ComputeResidual`, and adds the result to `LinSysRes` and `Jacobian`. + +The numerics objects are created in `CDriver::InitializeNumerics` (`SU2_CFD/src/drivers/CDriver.cpp`), with the helpers `InstantiateTurbulentNumerics`, `InstantiateTransitionNumerics` and `InstantiateSpeciesNumerics`. + +### Geometry + +[`CGeometry`](../../doxygen/classCGeometry.html) holds the mesh and the dual grid used by the edge-based finite volume method: + +- `nodes` ([`CPoint`](../../doxygen/classCPoint.html)): coordinates, volumes, 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. + +Children: + +- [`CPhysicalGeometry`](../../doxygen/classCPhysicalGeometry.html): the mesh read from file (SU2 or CGNS format, readers in `Common/include/geometry/meshreader`), partitioned with ParMETIS. +- [`CMultiGridGeometry`](../../doxygen/classCMultiGridGeometry.html): coarse levels made by agglomeration of control volumes. +- [`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 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, plus a `driver_config` read from the main configuration file (it holds the settings of the driver, for example for multizone problems). + +- Options are declared in `CConfig::SetConfig_Options` with `addDoubleOption`, `addBoolOption`, `addEnumOption`, `addStringListOption`, ... +- 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`. +- Derived settings are computed in `CConfig::SetPostprocessing` and `CConfig::SetMarkers`. + +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`, for example [`CFlowCompOutput`](../../doxygen/classCFlowCompOutput.html), [`CFlowIncOutput`](../../doxygen/classCFlowIncOutput.html), [`CHeatOutput`](../../doxygen/classCHeatOutput.html), [`CElasticityOutput`](../../doxygen/classCElasticityOutput.html). [`CMultizoneOutput`](../../doxygen/classCMultizoneOutput.html) combines the zones of a multizone run. 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 (one block per pair of neighbor points). Used for the Jacobian of implicit solvers. +- [`CSysVector`](../../doxygen/classCSysVector.html): block vector, for residual and 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: `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 one that was chosen. +- [`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 common type aliases are defined in the same file: + - `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, used by the flamelet fluid model (`CFluidFlamelet`) and the data-driven fluid model (`CDataDrivenFluid`). +- `CVertexMap`, `CFastFindAndEraseQueue`, `CPyWrapperMatrixView`. + +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`, and 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, so that MPI calls work with `su2double`. The mesh is partitioned at start-up. Each rank also stores halo points. Halo values are updated with `InitiateComms` / `CompleteComms` of `CGeometry` and `CSolver`, with an `MPI_QUANTITIES` value that 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` + +The integrations open one parallel region for a whole iteration, when the solver supports it (`CSolver::GetHasHybridParallel()`). Edge loops write to both points of an edge. To avoid race conditions, edges are grouped by color (`CGeometry::GetEdgeColoring`, option `EDGE_COLORING_GROUP_SIZE`). If the coloring is poor, the flow and scalar solvers store the edge fluxes first and sum them per point afterwards (`ReducerStrategy`, `EdgeFluxes`). The number of threads per rank is set with `SU2_CFD -t `. + +**Vectorization.** `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, and `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 these schemes for all centered schemes, and 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. It is `passivedouble` (normally `double`) in a normal build, a CoDiPack reverse type with `-Denable-autodiff=true` (builds `SU2_CFD_AD`, `SU2_DOT_AD`), and a forward type with `-Denable-directdiff=true` (builds `SU2_CFD_DIRECTDIFF`). +- `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`, and preaccumulation (`StartPreacc`, `SetPreaccIn`, `SetPreaccOut`, `EndPreacc`). The functions do nothing in non-AD builds, so the code can call them always. +- 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... + +| Task | Where | +|---|---| +| **New boundary condition** | 1. Add the kind to `enum BC_TYPE` in `Common/include/option_structure.hpp`. 2. Add the marker option in `CConfig::SetConfig_Options` and set `Marker_CfgFile_KindBC` in `CConfig::SetMarkers` (`Common/src/CConfig.cpp`). 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, compressible ideal gas only: a vectorized version in `SU2_CFD/include/numerics_simd/flow/convection/upwind.hpp`, created in `createUpwindIdealNumerics` (`CNumericsSIMD.cpp`) and selected in `CEulerSolver::Upwind_Residual`. | +| **New turbulence model source term** | Source terms are in `SU2_CFD/include/numerics/turbulent/turb_sources.hpp` (`CSourceBase_TurbSA` and `SAFactory` for SA, `CSourcePieceWise_TurbSST` for SST). They are created in `CDriver::InstantiateTurbulentNumerics` and used in `CTurbSASolver::Source_Residual` / `CTurbSSTSolver::Source_Residual`. Model variants are chosen with `SA_OPTIONS` / `SST_OPTIONS` (`enum class SA_OPTIONS`, `SST_OPTIONS` in `option_structure.hpp`). | +| **New output field** | 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** | `CConfig::SetConfig_Options`, a member and a getter in `CConfig.hpp`, 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: a value in `enum class MAIN_SOLVER` and `Solver_Map` (`option_structure.hpp`); the solver in `CSolverFactory::CreateSolverContainer` and `CreateSubSolver` (with its `INTEGRATION_TYPE`); its numerics in `CDriver::InitializeNumerics`; an iteration in `CIterationFactory::CreateIteration`; an output class in `COutputFactory::CreateOutput`. | + +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/)), and follow the [Style Guide](../Style-Guide/). + +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..24ebcd0794a1 --- /dev/null +++ b/Docs/website/pages/Running-Regression-Tests.md @@ -0,0 +1,283 @@ +--- +title: Running Regression Tests +permalink: /docs_v7/Running-Regression-Tests/ +--- + +The regression tests run SU2 on many small cases and compare a few numbers of the output with stored values. +They catch changes in the results that nobody intended. Every pull request runs them, and a pull request is +merged only when they pass. + +This page explains where the tests are, how the CI runs them, how to run them on your computer, how to add a +test and 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. The 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, used by `tutorials.py`. + +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`. + It changes `ITER`, or `TIME_ITER` if `unsteady` is set, or `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`, and 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`, if the row is not found, if the run takes longer + than `timeout` seconds or if 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` | + +The defaults of `command`, `timeout` and `tol` in `TestCase.py` 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`, and `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. `reference_file_aarch64` is used on ARM64. +- `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 main loop. Tests added there must set `command`, +`timeout` and `tol` themselves, because the defaults loop has already run. + +## 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`) and `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`. +The jobs **Build SU2 (tsan)** and **Build SU2 (asan)** make the builds for the thread and address sanitizers. + +### Test jobs + +Each test job takes the `Base`, `Reverse` and `Forward` binaries of one build type (those that exist) and runs one +script: + +| 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` | | + +The Tape Tests and Thread Sanitizer Tests jobs do not run on the ARM64 runner. +The extra arguments are read by `parse_args()` in `TestCase.py`. 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, `test/runTests.sh` in the +repository `su2code/Docker-Builds`, is called 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("--mark-changes", action="store_true", + help="debug: show in red the pages that differ from the official website") args = parser.parse_args() try: @@ -750,6 +815,11 @@ def main(): write_check_page(args.site, found, commit, options, extra) patch_nav(args.site, ref_pages, dev_pages + ["Docs-Check"], user_pages + [output_page]) + if args.mark_changes: + generated = ref_pages + dev_pages + user_pages + [output_page, "Docs-Check"] + urls = ["/{}/{}/".format(URL, p) for p in generated] + changed_site_pages(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) From 5c2e0e4ec91e367f79f5cbd3813e7946d43ee079 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 21 Oct 2026 14:04:34 +0200 Subject: [PATCH 07/25] Also show in red the menu sections that contain changed pages Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NSfEWrhpF27TnrkJH74sv8 --- Docs/website/tools/build_site.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Docs/website/tools/build_site.py b/Docs/website/tools/build_site.py index 91a3f7e6404b..c57b4dcea3b9 100644 --- a/Docs/website/tools/build_site.py +++ b/Docs/website/tools/build_site.py @@ -722,11 +722,20 @@ def patch_nav(site, ref_pages, extra_dev_pages, user_pages): function changed(path) { return pages.some(function (p) { return path.replace(/index\\.html$/, "").endsWith(p); }); } + function red(el) { + el.style.color = "#c00"; + el.querySelectorAll("a").forEach(function (a) { a.style.color = "#c00"; }); + } + // Menu items of changed pages. document.querySelectorAll("a.list-group-item").forEach(function (a) { - if (changed(new URL(a.href, location.href).pathname)) { a.style.color = "#c00"; a.style.fontWeight = "bold"; } + if (changed(new URL(a.href, location.href).pathname)) { red(a); a.style.fontWeight = "bold"; a.dataset.changed = "1"; } }); - document.querySelectorAll(".panel-title").forEach(function (t) { - if (sections.indexOf(t.textContent.trim()) >= 0) { t.style.color = "#c00"; } + // Menu sections that are new or contain a changed page. + document.querySelectorAll(".panel").forEach(function (panel) { + var title = panel.querySelector(".panel-title"); + if (title && (sections.indexOf(title.textContent.trim()) >= 0 || panel.querySelector("a[data-changed]"))) { + red(title); + } }); if (changed(location.pathname)) { document.querySelectorAll("h1").forEach(function (h) { h.style.color = "#c00"; }); From 65738ad57473fc99a5a1ff6f47407b7a6e829b8a Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 21 Oct 2026 15:10:01 +0200 Subject: [PATCH 08/25] Check the options written in config_template.cfg on the Docs Check page Users copy config_template.cfg, so a name the code does not register stops their run. The check found eight such names. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NSfEWrhpF27TnrkJH74sv8 --- Docs/website/tools/build_site.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/Docs/website/tools/build_site.py b/Docs/website/tools/build_site.py index c57b4dcea3b9..e24b343ae73b 100644 --- a/Docs/website/tools/build_site.py +++ b/Docs/website/tools/build_site.py @@ -56,7 +56,9 @@ "FFD_PARENTS", "FFD_CHILDREN", "FFD_CORNER_POINTS", "FFD_CONTROL_POINTS", "FFD_SURFACE_POINTS", "NMARK_FFD", "NPERIODIC", "PERIODIC_INDEX", "NELEM_BOUND", "VALUE", "OPTION", # keys of the Python FSI configuration file (Unsteady_FSI_Python tutorial) - "IMPOSED_MODES", "IMPOSED_PARAMETERS"} + "IMPOSED_MODES", "IMPOSED_PARAMETERS", + # formulas in the comment of RAMP_MUSCL in config_template.cfg + "ITERATION", "SMOOTH_FUNCTION"} TYPE_NAMES = { "Double": "real number", "Bool": "YES / NO", "String": "string", "UnsignedLong": "integer", @@ -615,6 +617,17 @@ def quality_sections(stale, coverage, doxy): return out +def check_template(su2, known): + """Options written in config_template.cfg (active or commented out) that the code does not know.""" + found = [] + with open(os.path.join(su2, "config_template.cfg"), encoding="utf-8", errors="replace") as f: + for num, line in enumerate(f, 1): + m = re.match(r"^\s*%?\s*([A-Z][A-Z0-9_]{2,})\s*=", line) + if m and m.group(1) not in known and m.group(1) not in NOT_OPTIONS: + found.append((num, m.group(1))) + return found + + def write_check_page(site, found, commit, options, extra=()): out = ["---", "title: Docs Check", "permalink: /{}/Docs-Check/".format(URL), "---", "", "{% raw %}", "", "This page is made at each build. It lists what needs work in the documentation of SU2 commit `{}`." @@ -819,8 +832,13 @@ def main(): found = check_docs(args.site, {o["name"] for o in options}, read_code_names(args.su2)) output_page = write_output_page(args.site, read_outputs(args.su2), commit, "https://github.com/{}/blob/{}/".format(repo, commit)) - extra = quality_sections(find_stale_pages(args.su2, pages_dir), doc_coverage(args.su2), - read_doxygen_log(args.doxygen_log)) + 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 \"unknown option\" 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, dev_pages + ["Docs-Check"], user_pages + [output_page]) @@ -833,8 +851,8 @@ def main(): 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: {}".format( - len(options), len(ref_pages) - 1, len(found))) + 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 From 9a78733ca96632d1e77e31022c821ff83d2929f0 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 21 Oct 2026 16:12:15 +0200 Subject: [PATCH 09/25] Generate the error messages and the renamed options pages Error Messages lists every SU2_MPI::Error message with the options it names and a link to the code. Renamed and Removed Options lists the options of the releases since v7.0.0 that the code no longer has, with the message SU2 prints for them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NSfEWrhpF27TnrkJH74sv8 --- Docs/website/tools/build_site.py | 120 +++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 5 deletions(-) diff --git a/Docs/website/tools/build_site.py b/Docs/website/tools/build_site.py index e24b343ae73b..9040a26353ff 100644 --- a/Docs/website/tools/build_site.py +++ b/Docs/website/tools/build_site.py @@ -628,6 +628,111 @@ def check_template(su2, known): return found +def read_parser_renames(su2): + """Old option name -> message of CConfig::SetConfig_Parsing for options that were renamed or removed.""" + with open(os.path.join(su2, "Common", "src", "CConfig.cpp"), encoding="utf-8") as f: + text = f.read() + found = {} + pattern = re.compile(r'if\s*\(((?:\s*(?:\|\|)?\s*!option_name\.compare\("\w+"\))+)\s*\)\s*' + r'newString\.append\("((?:[^"\\]|\\.)*)"\)') + for m in pattern.finditer(text): + message = m.group(2).replace("\\n", " ").strip() + for name in re.findall(r'compare\("(\w+)"\)', m.group(1)): + found[name] = message + return found + + +def option_history(su2): + """Option name -> last release tag that had it, for options that are no longer in the code.""" + tags = subprocess.run(["git", "-C", su2, "tag", "-l", "v7.*", "v8.*"], capture_output=True, text=True).stdout.split() + tags.sort(key=lambda t: [int(x) if x.isdigit() else x for x in re.split(r"[.v-]", t) if x]) + last_seen = {} + for tag in tags: + for path in ("Common/src/CConfig.cpp", "Common/src/config_structure.cpp"): + show = subprocess.run(["git", "-C", su2, "show", "{}:{}".format(tag, path)], capture_output=True, text=True) + if show.returncode == 0: + for name in re.findall(r'add\w*Option\s*\(\s*"([A-Z0-9_]+)"', show.stdout): + last_seen[name] = tag + break + return last_seen, tags + + +def write_renamed_page(su2, site, known, commit): + name = "Renamed-Options" + renames = read_parser_renames(su2) + last_seen, tags = option_history(su2) + removed = sorted(n for n in set(last_seen) | set(renames) if n not in known) + out = ["---", "title: Renamed and Removed Options", "permalink: /{}/{}/".format(URL, name), "---", "", + "{% raw %}", "", + "This page lists the options that SU2 had in a release since v7.0.0 but no longer has in commit `{}`. " + "It is made from the release history and from the messages SU2 prints when it reads an old option. " + "If your config file stops with \"invalid option name\", look for the option here.".format(commit), ""] + if not tags: + out.append("*The release history was not available for this build.*") + out += ["", "| Option | Last release with it | What SU2 says |", "|---|---|---|"] + for opt in removed: + out.append("| `{}` | {} | {} |".format(opt, last_seen.get(opt, "-"), cell(renames.get(opt, "")))) + out += ["", "{% endraw %}", ""] + with open(os.path.join(site, DOCS, name + ".md"), "w", encoding="utf-8") as f: + f.write("\n".join(out)) + return name + + +def read_error_messages(su2): + """List of (file, line, message) for every SU2_MPI::Error call; message pieces built at run time become '...'.""" + token = re.compile(r"\bSU2_MPI::Error\s*\(") + found = [] + for folder in ("Common", "SU2_CFD", "SU2_DEF", "SU2_DOT", "SU2_GEO", "SU2_SOL"): + for path in sorted(glob.glob(os.path.join(su2, folder, "**", "*.*"), recursive=True)): + if not path.endswith((".cpp", ".hpp", ".inl")): + continue + with open(path, encoding="utf-8", errors="replace") as f: + text = f.read() + for m in token.finditer(text): + body, _ = call_body(text, m.end() - 1) + first = split_args(body)[0] + pieces = re.findall(r'"((?:[^"\\]|\\.)*)"', first) + if not pieces: + continue + message = re.sub(r"\\n|\\t", " ", " ... ".join(p for p in pieces if p.strip())) + message = re.sub(r"\s+", " ", message.replace('\\"', '"')).strip() + found.append((os.path.relpath(path, su2), text.count("\n", 0, m.start()) + 1, message)) + return found + + +def write_errors_page(site, errors, options, commit, blob): + name = "Error-Messages" + pages = {o["name"]: o["page"] for o in options} + groups = {} + for path, line, message in errors: + if path == "Common/src/CConfig.cpp": + group = "Configuration checks" + else: + group = "/".join(path.split("/")[:-1]) + groups.setdefault(group, []).append((path, line, message)) + out = ["---", "title: Error Messages", "permalink: /{}/{}/".format(URL, name), "---", "", "{% raw %}", "", + "This page lists every error message that can stop SU2 (`SU2_MPI::Error` in the code), " + "{} in commit `{}`. Use the search of your browser (Ctrl+F) with a few words of the message you see. " + "Parts of a message that SU2 fills in at run time are shown as \"...\".".format(len(errors), commit), "", + "Each message links to the code that prints it, and to the options it names.", "", + "See also: [Renamed and Removed Options](../Renamed-Options/).", ""] + order = ["Configuration checks"] + sorted(g for g in groups if g != "Configuration checks") + for group in order: + if group not in groups: + continue + out += ["## {} ({})".format(group, len(groups[group])), "", "| Message | Options | Code |", "|---|---|---|"] + for path, line, message in groups[group]: + named = sorted({w for w in re.findall(r"\b[A-Z][A-Z0-9_]{2,}\b", message) if w in pages}) + links = ", ".join("[`{0}`](../{1}/#{0})".format(n, pages[n]) for n in named) + out.append("| {} | {} | [{}:{}]({}{}#L{}) |".format(cell(message), links, os.path.basename(path), line, + blob, path, line)) + out.append("") + out += ["{% endraw %}", ""] + with open(os.path.join(site, DOCS, name + ".md"), "w", encoding="utf-8") as f: + f.write("\n".join(out)) + return name + + def write_check_page(site, found, commit, options, extra=()): out = ["---", "title: Docs Check", "permalink: /{}/Docs-Check/".format(URL), "---", "", "{% raw %}", "", "This page is made at each build. It lists what needs work in the documentation of SU2 commit `{}`." @@ -741,7 +846,9 @@ def patch_nav(site, ref_pages, extra_dev_pages, user_pages): } // Menu items of changed pages. document.querySelectorAll("a.list-group-item").forEach(function (a) { - if (changed(new URL(a.href, location.href).pathname)) { red(a); a.style.fontWeight = "bold"; a.dataset.changed = "1"; } + if (changed(new URL(a.href, location.href).pathname)) { + red(a); a.style.fontWeight = "bold"; a.dataset.changed = "1"; + } }); // Menu sections that are new or contain a changed page. document.querySelectorAll(".panel").forEach(function (panel) { @@ -830,8 +937,10 @@ def main(): 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)) - output_page = write_output_page(args.site, read_outputs(args.su2), commit, - "https://github.com/{}/blob/{}/".format(repo, commit)) + 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 \"unknown option\" error.", "", @@ -840,10 +949,11 @@ def main(): 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, dev_pages + ["Docs-Check"], user_pages + [output_page]) + 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, "Docs-Check"] + 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.site) mark_changes(args.site, urls, ["Option Reference", "How to Add..."]) From 499d7dc32e044dfe21aa384f49440d7a3a79ffbd Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 21 Oct 2026 16:12:28 +0200 Subject: [PATCH 10/25] Fetch the release tags from su2code/SU2 for the renamed options page Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NSfEWrhpF27TnrkJH74sv8 --- .github/workflows/docs-website.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docs-website.yml b/.github/workflows/docs-website.yml index 6b4701eecb47..ad34af76c863 100644 --- a/.github/workflows/docs-website.yml +++ b/.github/workflows/docs-website.yml @@ -41,6 +41,9 @@ jobs: # The history (without file contents) finds the developer pages that may be out of date. fetch-depth: 0 filter: blob:none + - name: Get the release tags + # The fork may miss some tags; the renamed options page needs all of them. + 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: From af228bf75cc974c18f65fbb97d45a8a9b7e9a58d Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 21 Oct 2026 19:33:18 +0200 Subject: [PATCH 11/25] List only options that a release had on the renamed options page Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NSfEWrhpF27TnrkJH74sv8 --- Docs/website/tools/build_site.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Docs/website/tools/build_site.py b/Docs/website/tools/build_site.py index 9040a26353ff..c8dd626aad96 100644 --- a/Docs/website/tools/build_site.py +++ b/Docs/website/tools/build_site.py @@ -661,7 +661,8 @@ def write_renamed_page(su2, site, known, commit): name = "Renamed-Options" renames = read_parser_renames(su2) last_seen, tags = option_history(su2) - removed = sorted(n for n in set(last_seen) | set(renames) if n not in known) + # Parser messages for names that no release ever had are left out (they would only confuse readers). + removed = sorted(n for n in last_seen if n not in known) if tags else sorted(n for n in renames if n not in known) out = ["---", "title: Renamed and Removed Options", "permalink: /{}/{}/".format(URL, name), "---", "", "{% raw %}", "", "This page lists the options that SU2 had in a release since v7.0.0 but no longer has in commit `{}`. " From bec864157bb60b8eba435bacf9da36e1bda6d927 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Wed, 21 Oct 2026 19:34:47 +0200 Subject: [PATCH 12/25] Add the AD guide, a glossary and a debugging page Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NSfEWrhpF27TnrkJH74sv8 --- .../pages/Automatic-Differentiation.md | 740 ++++++++++++++++++ Docs/website/pages/Debugging.md | 407 ++++++++++ Docs/website/pages/Glossary.md | 274 +++++++ 3 files changed, 1421 insertions(+) create mode 100644 Docs/website/pages/Automatic-Differentiation.md create mode 100644 Docs/website/pages/Debugging.md create mode 100644 Docs/website/pages/Glossary.md diff --git a/Docs/website/pages/Automatic-Differentiation.md b/Docs/website/pages/Automatic-Differentiation.md new file mode 100644 index 000000000000..0d528b5fe221 --- /dev/null +++ b/Docs/website/pages/Automatic-Differentiation.md @@ -0,0 +1,740 @@ +--- +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, which CoDiPack later +replays backwards to get the adjoint (the derivative of the output). + +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. + +### 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. `CScalarSolver::EdgeFluxResidual` therefore pauses it when the + fluxes are summed with the reducer strategy: + + ```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 and reverse mode should agree to many digits, because both are exact derivatives of the same discrete + code. A difference between them points to an AD error, not to convergence or step size. + +### 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/Debugging.md b/Docs/website/pages/Debugging.md new file mode 100644 index 000000000000..075182188d49 --- /dev/null +++ b/Docs/website/pages/Debugging.md @@ -0,0 +1,407 @@ +--- +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. +It names the build options, config options and tools that exist for this. + +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. The CI builds with `--warnlevel=3` and `--werror` (warnings + become errors) for most configurations, so check your change with these flags before you push. +- `-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. + +When only rank 0 prints an error, the bug may be on another rank. `SU2_MPI::Error` prints the message from the +lowest rank that called it. + +### 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 themselves are not in these flags. They are set by the build containers +`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 solver (`CFEM_DG_EulerSolver`) has its own checks. + +The NEMO 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, write the files only from a few iterations before the failure: restart from a solution saved +before that **(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/Glossary.md b/Docs/website/pages/Glossary.md new file mode 100644 index 000000000000..ecd610c79583 --- /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. Each entry is short. Where it helps, it 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 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.** +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. +`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 + +These entries are short. 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`. From 6ca0da5e277a55db4359fffbb19b45c529b614c8 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Thu, 22 Oct 2026 10:23:02 +0200 Subject: [PATCH 13/25] Docs style pass: cut repeated and empty sentences, explain unclear terms Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NSfEWrhpF27TnrkJH74sv8 --- Docs/website/README.md | 18 ++++++++----- Docs/website/pages/Adding-Output-Fields.md | 18 ++++++------- .../pages/Adding-a-Boundary-Condition.md | 19 +++++--------- Docs/website/pages/Adding-a-Config-Option.md | 22 +++++++++------- Docs/website/pages/Adding-a-Fluid-Model.md | 20 +++++++------- .../pages/Adding-a-Numerical-Scheme.md | 17 +++++------- Docs/website/pages/Adding-a-Solver.md | 16 +++++------- .../pages/Adding-a-Transition-Model.md | 14 +++++----- .../pages/Adding-a-Turbulence-Model.md | 26 ++++++++----------- .../pages/Automatic-Differentiation.md | 12 +++++---- Docs/website/pages/Code-Structure.md | 14 +++++----- Docs/website/pages/Debugging.md | 20 +++++++------- Docs/website/pages/Glossary.md | 10 +++---- .../website/pages/Running-Regression-Tests.md | 17 ++++++------ Docs/website/tools/build_site.py | 18 +++++++------ 15 files changed, 124 insertions(+), 137 deletions(-) diff --git a/Docs/website/README.md b/Docs/website/README.md index 4f8584726393..3ed3f0223a7f 100644 --- a/Docs/website/README.md +++ b/Docs/website/README.md @@ -14,13 +14,17 @@ The workflow `.github/workflows/docs-website.yml` builds the website and publish - the website pages and tutorials that mention it; - up to three test case files that use it, with their values; - a link to the code. -5. It makes the **Docs Check** page. This page lists: - - options that the docs, tutorials and V&V pages name, but that do not exist in the code; - - options with no description, a very short one, or no example. -6. It makes the **Output Fields Reference**: all screen, history and volume fields of each output class, from the +5. It makes the **Output Fields Reference**: all screen, history and volume fields of each output class, from the `AddHistoryOutput` and `AddVolumeOutput` calls. -7. The **Docs Check** page also lists the developer pages that may be out of date (the code files they name changed - since the commit in their "Last checked against" line), the Doxygen coverage of the classes, and the Doxygen warnings. +6. It makes the **Error Messages** page, from the `SU2_MPI::Error` calls, and the **Renamed and Removed Options** + page, from the option names of past releases (git tags). +7. It makes the **Docs Check** page. This page lists: + - options that the docs, tutorials and V&V (verification and validation) pages name, but that do not exist in + the code; + - options with no description, a very short one, or no example; + - developer pages that may be out of date: the code files they name changed since the commit in their + "Last checked against" line; + - the Doxygen coverage of the classes, and the Doxygen warnings. 8. It builds the website with Jekyll, and the **Code Reference** with Doxygen (Doxygen runs first, for its warnings). **Debug option.** The test site runs `build_site.py` with `--mark-changes`. It shows in red the menu items and @@ -29,7 +33,7 @@ workflow for a normal build. ## How to change the docs -Pages that explain the code (developer pages) live here, so they change in the same PR as the code. +Pages that explain the code (developer pages) live here, so they change in the same pull request as the code. User guide pages and tutorials live in the website repository. - **New option:** write the comment above the `add...Option` call in `CConfig.cpp`, in the usual form: diff --git a/Docs/website/pages/Adding-Output-Fields.md b/Docs/website/pages/Adding-Output-Fields.md index 6ef5d79685d6..9a350dee681d 100644 --- a/Docs/website/pages/Adding-Output-Fields.md +++ b/Docs/website/pages/Adding-Output-Fields.md @@ -59,7 +59,7 @@ Shared parents and other output classes: 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 you do not need to change it for a new zone field. +- [`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 @@ -78,7 +78,7 @@ Other hooks: - The DG output uses `LoadVolumeDataFEM` instead of `LoadVolumeData`. - Multizone output uses `SetMultizoneHistoryOutputFields` and `LoadMultizoneHistoryData`. -- `SetAdditionalScreenOutput` and `WriteAdditionalFiles`. +- `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`): @@ -116,7 +116,7 @@ These steps are in `COutput::PostprocessHistoryFields`, `COutput::PostprocessHis 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. Use the same condition in both functions. If the field was not added, `SetHistoryOutputValue` stops SU2 with the error "Cannot find output field with 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 @@ -183,7 +183,7 @@ Users can select per-surface fields in `SCREEN_OUTPUT` and `HISTORY_OUTPUT`, but 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. Use the same condition in both functions. +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. @@ -247,7 +247,7 @@ For the adjoint outputs, the same pattern is in `CAdjFlowOutput` (`AddHistoryOut ## How users select the new field -Users request 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. +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 @@ -256,10 +256,8 @@ VOLUME_OUTPUT= COORDINATES, SOLUTION, PRIMITIVE, ROE_DISSIPATION CONV_FIELD= RMS_DENSITY ``` -- `SCREEN_OUTPUT` takes field names or groups. -- `HISTORY_OUTPUT` takes groups or field names. -- `VOLUME_OUTPUT` takes field names or groups. -- `CONV_FIELD` takes history field names (not per-surface fields). +- `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. @@ -276,7 +274,7 @@ See [History and Solution Output](../Custom-Output/) for the user documentation, 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. Unit tests for output classes are in `UnitTests/SU2_CFD/output/`. +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 index 2ecd3f4334a1..2753d006e551 100644 --- a/Docs/website/pages/Adding-a-Boundary-Condition.md +++ b/Docs/website/pages/Adding-a-Boundary-Condition.md @@ -3,12 +3,7 @@ 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`. It covers: - -- the marker option; -- how the marker reaches the solvers; -- how the BC is called in each iteration; -- how to write the BC for the flow solver, and for the turbulence, species and heat solvers. +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. @@ -125,13 +120,13 @@ Add the `MARKER_...` option that users write in the configuration file. The rest } ``` - The solvers call them with `config->GetMarker_All_TagBound(val_marker)`. The search is a loop over strings. So call the getter once per marker, outside the vertex loop. + 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 -You need no new code in this step for a new kind. This section explains how the marker data reaches the solvers, and which functions the solvers use to read it. +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: @@ -424,7 +419,7 @@ The pressure-based incompressible solver also needs `nodes->SetStrongBC(iPoint)` 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 edge kernel computes their boundary fluxes. The edge kernel is the class that computes the flux of one edge (see [Adding a Solver](../Adding-a-Solver/#6-numerics)). +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: @@ -489,16 +484,16 @@ void CTurbSSTSolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, - `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 marks the vertex as active (`ghostSkip`). +- `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 skipped vertices. It adds the flux to the row of the boundary point, and adds the diagonal Jacobian block. +- `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 -Find and update every other place that tests the BC kind. The kind is tested in many places besides the dispatch. Before you finish, search the code for an existing kind that behaves like yours, for example `grep -rn "SUPERSONIC_INLET" Common SU2_CFD`. The main places: +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 | |---|---| diff --git a/Docs/website/pages/Adding-a-Config-Option.md b/Docs/website/pages/Adding-a-Config-Option.md index db9be5e9fb56..771c7fb3101b 100644 --- a/Docs/website/pages/Adding-a-Config-Option.md +++ b/Docs/website/pages/Adding-a-Config-Option.md @@ -20,7 +20,8 @@ The class `CConfig` reads the configuration file. A new option touches these fil 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`). Follow the layout of the code around your change. +`.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 @@ -101,16 +102,16 @@ Notes on the kinds: - **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 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. +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 the exceptions. Options that are not set anywhere take their default value. +`HISTORY_OUTPUT` are never taken from the main file. Options that are not set anywhere take their default value. ## 3. Write the comment @@ -135,12 +136,13 @@ incomplete. - The comment can start with `/*!` or `/*`. It must contain `DESCRIPTION:` or `\brief`. Other comments, such as `/*--- ... ---*/`, are ignored. -- The comment must be the last such comment before the call. +- 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 also adds these, without any comment: +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; @@ -239,7 +241,7 @@ When a check fails, stop with a clear message that names the options and the all `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 silently, only when the user did not ask for it. +- 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)) { @@ -312,7 +314,7 @@ value is not valid. For example, for `KIND_TURB_MODEL`: errorString.append("Option KIND_TURB_MODEL=SST_SUST is deprecated. Use KIND_TURB_MODEL=SST, SST_OPTIONS=SUSTAINING instead.\n"); ``` -These are the only mechanisms. SU2 does not translate old names to new ones: the run stops with the message. +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: diff --git a/Docs/website/pages/Adding-a-Fluid-Model.md b/Docs/website/pages/Adding-a-Fluid-Model.md index 67c7b9c9ac7b..75951a795ed6 100644 --- a/Docs/website/pages/Adding-a-Fluid-Model.md +++ b/Docs/website/pages/Adding-a-Fluid-Model.md @@ -48,8 +48,6 @@ For the overall layout of the code, see [Code Structure](../Code-Structure/). Fo ## The FLUID_MODEL option -This section shows how to register the keyword of your model and its parameters. - The option is registered in `CConfig::SetConfig_Options`: ```cpp @@ -134,7 +132,7 @@ A call that is not overridden does nothing and gives no error. So check the [tab ## What the solvers call -This section lists which functions each solver calls, and so which ones your model must implement. +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: @@ -194,7 +192,7 @@ Some boundary conditions do not use the fluid model. For example, `CEulerSolver: - `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. It is set in the constructor of the model, and `SetPressure` can change it. +- 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 @@ -263,7 +261,7 @@ Follow these steps to add a model for `CEulerSolver` and `CNSSolver`. 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. It makes the MUSCL states consistent with `ComputeConsistentExtrapolation`, which calls `SetTDState_Prho`. +- `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 @@ -285,7 +283,7 @@ Follow these steps to add a model for `CIncEulerSolver` and `CIncNSSolver`. ## Non-dimensionalization and free-stream values -This section shows how the solver sets the free-stream state and the reference values, and which scales your model must follow. `SetNondimensionalization` creates each fluid model twice: +`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. @@ -294,8 +292,8 @@ This section shows how the solver sets the free-stream state and the reference v 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`, it gets `SetTDState_PT(Pressure_FreeStream, Temperature_FreeStream)` and gives the density. - - With `DENSITY_FS`, it gets `SetTDState_Prho` and gives the temperature. + - 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`, ... @@ -326,11 +324,11 @@ Some models cannot be scaled, for example tables or external libraries in physic - `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`. Most of them call `SetTDState_T(Temperature_FreeStreamND)` right away. +- 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 -This section shows how to add a model for viscosity, thermal conductivity or mass diffusivity. These are separate objects owned by the fluid model: +Viscosity, thermal conductivity and mass diffusivity come from separate objects owned by the fluid model: ```cpp unique_ptr LaminarViscosity; /*!< \brief Laminar Viscosity Model */ @@ -350,7 +348,7 @@ Existing children: - conductivity: `CConstantConductivity`, `CConstantConductivityRANS`, `CConstantPrandtl`, `CConstantPrandtlRANS`, `CPolynomialConductivity`, `CPolynomialConductivityRANS`, `CCoolPropConductivity`; - diffusivity: `CConstantDiffusivity`, `CConstantSchmidt`, `CConstantLewisDiffusivity`. -The `FLAMELET` values return `nullptr`, because `CFluidFlamelet` overrides the getters and reads the table. +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`: diff --git a/Docs/website/pages/Adding-a-Numerical-Scheme.md b/Docs/website/pages/Adding-a-Numerical-Scheme.md index 1ea933b87aa8..37b498518806 100644 --- a/Docs/website/pages/Adding-a-Numerical-Scheme.md +++ b/Docs/website/pages/Adding-a-Numerical-Scheme.md @@ -43,8 +43,6 @@ For the overall layout of the code, see [Code Structure](../Code-Structure/). Tu ## How the configuration picks the scheme -This section shows where to register the keyword of your scheme, and how the solver finds it. - The option `CONV_NUM_METHOD_FLOW` is registered in `CConfig::SetConfig_Options` (`Common/src/CConfig.cpp`) with `addConvectOption`: ```cpp @@ -87,7 +85,7 @@ At run time, the solver picks the residual function as follows: ## 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. This section tells you which one your scheme needs. +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/`. @@ -300,7 +298,7 @@ Nothing else is needed. The solver gets the object from `numerics_container[CONV ## MUSCL reconstruction and limiters -Your scheme does not reconstruct. The solver reconstructs the left and right states and passes them through `SetPrimitive` (and `SetSecondary`). This section shows where that happens. +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`). @@ -313,7 +311,7 @@ The incompressible solver (`CIncEulerSolver::Upwind_Residual`) and the NEMO solv ## Add a vectorized scheme -This section shows how to add the vectorized (SIMD) version of a scheme. The framework is in `SU2_CFD/include/numerics_simd/`: +The framework for vectorized (SIMD) schemes is in `SU2_CFD/include/numerics_simd/`: | File | Content | |---|---| @@ -371,7 +369,7 @@ public: 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 values. Use `fmax`, `fmin`, `abs`, `sign` and masks instead, as `CMSWScheme` does. +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 @@ -424,7 +422,7 @@ With `TIME_DISCRE_FLOW= EULER_IMPLICIT`, the solver needs the Jacobians of the f - 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 AD does not track. So AD does not differentiate the Jacobians. +- 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. @@ -473,8 +471,6 @@ The other finite volume solvers have their own scheme classes. ## Viscous fluxes and source terms -This section shows where to change the viscous flux and how to add a source term. - **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`. @@ -494,7 +490,7 @@ This section shows where to change the viscous flux and how to add a source term ## Discrete adjoint -A new scheme does not need adjoint code, but you must write it so that automatic differentiation (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. +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: @@ -506,7 +502,6 @@ Rules for the scheme: 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. -- The Jacobians are not differentiated, because the matrix is passive. - 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 diff --git a/Docs/website/pages/Adding-a-Solver.md b/Docs/website/pages/Adding-a-Solver.md index 9b27a8802d9a..f5da0350c44b 100644 --- a/Docs/website/pages/Adding-a-Solver.md +++ b/Docs/website/pages/Adding-a-Solver.md @@ -55,7 +55,7 @@ First find which of three cases you are in. Each case needs a different subset o | 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. No new keyword: a model option instead. | +| 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: @@ -173,7 +173,7 @@ unsigned short CConfig::GetContainerPosition(unsigned short val_eqsystem) { An unknown value returns 0, the flow solver, without an error. Add your case. -Then set the schemes of your system in `CConfig::SetGlobalParam`. `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 for your solver and system: +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: @@ -275,7 +275,7 @@ Store extra per-point quantities as members of the variable class (`su2activevec ## 5. The CSolver class -The solver class holds the residual loops, the solution update and the restart. This section covers its constructor, the virtual functions that the integration calls, and the implicit update. +The solver class holds the residual loops, the solution update and the restart. ### Constructor @@ -311,11 +311,11 @@ Follow the order of `CHeatSolver::CHeatSolver` (`SU2_CFD/src/solvers/CHeatSolver ### Virtual functions -All 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`): +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`) | helper only | +| `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 | @@ -520,7 +520,7 @@ Your solver may add fields to the restart file of another solver, for example a ## 10. MPI communication -Update the halo points after you change per-point data. 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: +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); @@ -537,8 +537,6 @@ Periodic boundaries use `InitiatePeriodicComms` / `CompletePeriodicComms`, with ## 11. OpenMP -Make every solver function thread safe before you turn on 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: @@ -691,8 +689,6 @@ The other parts follow the same rule: - **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. -All of this assumes a conservative flux. - ### 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. diff --git a/Docs/website/pages/Adding-a-Transition-Model.md b/Docs/website/pages/Adding-a-Transition-Model.md index f7c5e06253b0..ef3dbbeb0236 100644 --- a/Docs/website/pages/Adding-a-Transition-Model.md +++ b/Docs/website/pages/Adding-a-Transition-Model.md @@ -3,7 +3,7 @@ title: Adding a Transition Model permalink: /docs_v7/Adding-a-Transition-Model/ --- -This page is for developers. It shows how the transition model of SU2 is built and how it is coupled with the turbulence models. It then explains what to change to add a correlation, a variant or a new transition model. The code examples are copied from the SU2 source. +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: @@ -51,11 +51,9 @@ Read [Adding a Turbulence Model](../Adding-a-Turbulence-Model/) first. The trans ## Overview -The table lists the classes and settings of the LM model. - | Item | LM in SU2 | |---|---| -| Solver | [`CTransLMSolver`](../../doxygen/classCTransLMSolver.html), position `TRANS_SOL`, run with `RUNTIME_TRANS_SYS` | +| 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) | @@ -88,7 +86,7 @@ enum class LM_OPTIONS { `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. The parsed options struct holds the result of reading the list: +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 { @@ -175,9 +173,9 @@ void CTransLMVariable::SetIntermittencyEff(unsigned long iPoint, su2double val_I nodes->SetIntermittencyEff(iPoint, Intermittency_Sep); ``` -The other functions are `Preprocessing` (`CommonPreprocessing`), `Upwind_Residual`, `Source_Residual`, the boundary conditions and `LoadRestart`. +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`: +`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 : @@ -281,7 +279,7 @@ class CScalarFlux_TransLM ## Coupling with the turbulence model -The transition and turbulence models exchange data in both directions. This section lists what each one reads from the other, and where. +The transition and turbulence models exchange data in both directions. **From the turbulence model to the transition model:** diff --git a/Docs/website/pages/Adding-a-Turbulence-Model.md b/Docs/website/pages/Adding-a-Turbulence-Model.md index eb79c21423db..077a43a84e71 100644 --- a/Docs/website/pages/Adding-a-Turbulence-Model.md +++ b/Docs/website/pages/Adding-a-Turbulence-Model.md @@ -3,7 +3,7 @@ title: Adding a Turbulence Model permalink: /docs_v7/Adding-a-Turbulence-Model/ --- -This page is for developers. It shows how the RANS 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. +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/). @@ -43,8 +43,6 @@ Read [Code Structure](../Code-Structure/) first. The parts that are not specific ## How the turbulence equations are solved -This section shows when the turbulence model runs in an iteration, and what its solver does in each step. - 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 @@ -69,7 +67,7 @@ The turbulence model has its own solver object. It sits at position `TURB_SOL` o `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`. +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. @@ -159,7 +157,7 @@ Then it sets `version` (`NONE`, `NEG` or `EDW`) and the booleans. Some combinati - `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`. +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: @@ -421,7 +419,7 @@ Derive from `CTurbSolver`. Its constructor is: The constructor of the new solver does what `CTurbSSTSolver::CTurbSSTSolver` does: 1. Set `nVar`, `nPrimVar`, `nVarGrad`, `nPoint`, `nPointDomain`. -2. On `MESH_0`, or with a full multigrid cycle, allocate the residual vectors and the linear system: `Jacobian.Initialize(...)`, `LinSysSol.Initialize(...)`, `LinSysRes.Initialize(...)`. Also allocate `EdgeFluxes` and `EdgeFluxesDiff` when `ReducerStrategy` is set. +2. On `MESH_0`, or with a full multigrid cycle, allocate the residual vectors and the linear system: `Jacobian.Initialize(...)`, `LinSysSol.Initialize(...)`, `LinSysRes.Initialize(...)`. Also allocate `EdgeFluxes` and `EdgeFluxesDiff` when `ReducerStrategy` is set. With this strategy, used when edge coloring is not efficient, the fluxes are stored per edge and summed per point afterwards. 3. Set the model constants, `Solution_Inf`, `lowerlimit` and `upperlimit`. 4. Create `nodes` and call `SetBaseClassPointerToNodes()`. Create `ghostNodes`, sized to the largest marker. The boundary conditions use `ghostNodes` as the outside state. 5. Exchange the halo values with `InitiateComms` / `CompleteComms` and `MPI_QUANTITIES::SOLUTION_EDDY`. Halo points are local copies of points owned by other MPI ranks. @@ -474,8 +472,8 @@ 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(...)`; -- `DiffusionReadsDensity`. +- `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: @@ -493,7 +491,7 @@ Its `coefficients` returns a 2x2 matrix per side. The matrix holds the blended d - the flow primitives (`GetPrimitive`); - the turbulence containers (`GetSolution`, `GetF1blending`). -So any variable container that a kernel reads must exist for the whole field, like `CTurbVariable::GetF1blending()`. `CTransLMSolver` has the smallest kernel, `CScalarFlux_TransLM`. See [Adding a Transition Model](../Adding-a-Transition-Model/). +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 @@ -592,11 +590,9 @@ In the other direction, the turbulence solver reads the flow through `solver_con ## Walls, wall functions and wall distance -This section covers the wall distance, the values imposed at walls, and wall functions. - **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.** The wall conditions are strong (Dirichlet) conditions in `BC_HeatFlux_Wall`. `BC_Isothermal_Wall` calls it. The pattern has three steps: +**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. @@ -692,12 +688,12 @@ A new model needs no adjoint equations. The discrete adjoint differentiates the } ``` -- Loops over points use `AD::StartNoSharedReading()` / `AD::EndNoSharedReading()`. +- 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 -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`: +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: @@ -735,7 +731,7 @@ These regression tests cover the turbulence models. The scripts are in `TestCase - `dsma661_sa` and `dsma661_sst`; - the 30P30N airfoil. -They are short runs of the configuration files. 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. +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. diff --git a/Docs/website/pages/Automatic-Differentiation.md b/Docs/website/pages/Automatic-Differentiation.md index 0d528b5fe221..ae38b53be161 100644 --- a/Docs/website/pages/Automatic-Differentiation.md +++ b/Docs/website/pages/Automatic-Differentiation.md @@ -29,8 +29,8 @@ with each value, and gives the derivative of all outputs with respect to one inp 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, which CoDiPack later -replays backwards to get the adjoint (the derivative of the output). +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`: @@ -503,7 +503,8 @@ CPassiveMPI::Sendrecv(dirOut.data() + hp.offR * nDim, hp.nVertexR * nDim, MPI_DO SU2_MPI::GetComm(), MPI_STATUS_IGNORE); ``` -Halo updates of solver data (`InitiateComms` / `CompleteComms`) are recorded like any other operation. +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 @@ -519,8 +520,9 @@ the parallel structure for the reverse sweep. Rules: - `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. `CScalarSolver::EdgeFluxResidual` therefore pauses it when the - fluxes are summed with the reducer strategy: + 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; diff --git a/Docs/website/pages/Code-Structure.md b/Docs/website/pages/Code-Structure.md index 22794786ce39..31c70625dfab 100644 --- a/Docs/website/pages/Code-Structure.md +++ b/Docs/website/pages/Code-Structure.md @@ -29,7 +29,7 @@ For the full class documentation, see the [Code Reference](../Code-Reference/) ( | 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 types, grid movement, interface interpolation. Headers are in `Common/include`, sources in `Common/src`. | +| `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`. | @@ -54,7 +54,7 @@ Headers are in `SU2_CFD/include/` and sources are in `SU2_CFD/src/`. - `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 these schemes 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`. +- `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 @@ -321,8 +323,6 @@ See [Advanced AD Techniques](../Advanced-AD-Techniques/) for more. ## Where do I change... -This section lists the files and functions to change for common tasks. - ### New boundary condition 1. Add the kind to `enum BC_TYPE` in `Common/include/option_structure.hpp`. @@ -340,7 +340,7 @@ This section lists the files and functions to change for common tasks. ### New turbulence model source term -- The source terms are in `SU2_CFD/include/numerics/turbulent/turb_sources.hpp`: `CSourceBase_TurbSA` and `SAFactory` for SA, `CSourcePieceWise_TurbSST` for SST. +- 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`). diff --git a/Docs/website/pages/Debugging.md b/Docs/website/pages/Debugging.md index 075182188d49..1ca0a349dd09 100644 --- a/Docs/website/pages/Debugging.md +++ b/Docs/website/pages/Debugging.md @@ -4,7 +4,6 @@ permalink: /docs_v7/Debugging/ --- This page shows practical ways to find bugs in SU2: crashes, wrong results, divergence, data races and slow code. -It names the build options, config options and tools that exist for this. 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/). @@ -53,8 +52,9 @@ Notes: - `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. The CI builds with `--warnlevel=3` and `--werror` (warnings - become errors) for most configurations, so check your change with these flags before you push. +- `--warnlevel=3` turns on most compiler warnings. The CI (continuous integration: the automatic tests on GitHub) + builds with `--warnlevel=3` and `--werror` (warnings become errors) for most configurations, so check your change + with these flags before you push. - `-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 @@ -171,8 +171,8 @@ Some bugs only show in parallel, for example at the halo points. Three ways to u - 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. -When only rank 0 prints an error, the bug may be on another rank. `SU2_MPI::Error` prints the message from the -lowest rank that called it. +Only one rank prints the error box: the lowest rank that called `SU2_MPI::Error`. Other ranks may have failed +too. ### The Python wrapper @@ -198,7 +198,7 @@ The workflow `.github/workflows/regression.yml` has these jobs: | 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 themselves are not in these flags. They are set by the build containers +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. @@ -289,9 +289,9 @@ SU2 checks for NaN in a few places and stops with `SU2 has diverged (NaN detecte 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 solver (`CFEM_DG_EulerSolver`) has its own checks. +- The DG (discontinuous Galerkin) solver (`CFEM_DG_EulerSolver`) has its own checks. -The NEMO solvers also check each flux and source term with `CNumerics::CheckResidualNaNs`. +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. @@ -319,8 +319,8 @@ The last file written before the error shows the state just before the NaN. Usef 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, write the files only from a few iterations before the failure: restart from a solution saved -before that **(advice)**. +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 diff --git a/Docs/website/pages/Glossary.md b/Docs/website/pages/Glossary.md index ecd610c79583..a39d4dc4cee9 100644 --- a/Docs/website/pages/Glossary.md +++ b/Docs/website/pages/Glossary.md @@ -4,7 +4,7 @@ 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. Each entry is short. Where it helps, it names the class or option, and links to more. +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). @@ -80,7 +80,7 @@ See [Multizone](../Multizone/). ## Solvers and numerics **CFL adaptation.** -The CFL number sets the size of the local pseudo time step. With `CFL_ADAPT= YES`, SU2 raises the CFL number when +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. @@ -115,7 +115,7 @@ extrema and oscillations. Set it with `SLOPE_LIMITER_FLOW`, `SLOPE_LIMITER_TURB` `LIMITER` writes the values. See [Slope Limiters](../Slope-Limiters-and-Shock-Resolution/). **MUSCL.** -A way to reach second order accuracy with an upwind scheme. Before the flux is computed, the states at the two +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/). @@ -157,7 +157,7 @@ time loop. [`CSinglezoneDriver`](../../doxygen/classCSinglezoneDriver.html) and [`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 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).** @@ -231,7 +231,7 @@ build option `cpu-arch` sets which instructions the compiler may use. ## Algorithmic differentiation -These entries are short. See [Automatic Differentiation](../Automatic-Differentiation/) for the full guide. +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 diff --git a/Docs/website/pages/Running-Regression-Tests.md b/Docs/website/pages/Running-Regression-Tests.md index 1d7204e72fff..abee93353908 100644 --- a/Docs/website/pages/Running-Regression-Tests.md +++ b/Docs/website/pages/Running-Regression-Tests.md @@ -10,7 +10,7 @@ pull request is merged only when they pass. This page explains: - where the tests are; -- how the CI runs them; +- 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. @@ -124,8 +124,8 @@ Some scripts overwrite values that you set, for all tests in `test_list`: - `run_geo()`: for `SU2_GEO`. - `run_def()`: for `SU2_DEF`. -The scripts call these methods one test at a time, after the main loop. The defaults loop has already run by then. -So tests added there must set `command`, `timeout` and `tol` themselves. +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 @@ -142,15 +142,15 @@ The job **Build SU2** compiles SU2 in a container (`ghcr.io/su2code/su2/build-su - `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`. +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 `Base`, `Reverse` and `Forward` binaries of one build type (those that -exist): +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 | |---|---|---|---| @@ -306,11 +306,12 @@ Use the values computed by the CI, because your computer can give slightly diffe 3. Put the mesh and other large files in the same folder of the TestCases repository. Open a pull request there against `develop` (see [Where the meshes come from](#where-the-meshes-come-from)). 4. In the configuration file, set `SCREEN_OUTPUT` to the values you want to check. Make sure that the row of - `test_iter` is printed (see `SCREEN_WRT_FREQ_INNER`). + `test_iter` is printed: `SCREEN_WRT_FREQ_INNER` sets how often a row is printed. 5. Add a `TestCase` entry to the script that matches the build (serial, parallel, AD, hybrid), before its block `RUN TESTS`. Add it to `test_list` with `test_list.append(...)`. Set at least `cfg_dir`, `cfg_file`, `test_iter` and `test_vals`. 6. To get the first values, set `test_vals` to a list of zeros, with one entry per column you check. Run the test, - and copy `sim_vals (computed)` from the output. Then check the values with the CI, as explained above. + and copy `sim_vals (computed)` from the output. Then replace them with the values computed by the CI (see + [Update the stored values](#update-the-stored-values)). Last checked against SU2 develop `11c177b904`. diff --git a/Docs/website/tools/build_site.py b/Docs/website/tools/build_site.py index c8dd626aad96..e2213db25d6d 100644 --- a/Docs/website/tools/build_site.py +++ b/Docs/website/tools/build_site.py @@ -414,7 +414,8 @@ def check_docs(site, known, code_names=frozenset()): # Output classes, in the order of the page, with the solvers that use them. OUTPUT_CLASSES = [ ("COutput", "All solvers"), ("CFVMOutput", "All finite volume solvers"), - ("CFlowOutput", "All flow solvers (compressible, incompressible, NEMO), incl. turbulence and species"), + ("CFlowOutput", + "All flow solvers (compressible, incompressible, NEMO), also for their turbulence and species fields"), ("CFlowCompOutput", "Compressible flow"), ("CFlowIncOutput", "Incompressible flow"), ("CNEMOCompOutput", "Non-equilibrium flow (NEMO)"), ("CFlowCompFEMOutput", "Finite element flow (DG-FEM)"), ("CTurboOutput", "Turbomachinery"), ("CHeatOutput", "Heat equation"), ("CElasticityOutput", "Structural analysis"), @@ -593,9 +594,9 @@ def quality_sections(stale, coverage, doxy): documented = sum(v[1] for v in coverage.values()) grouped = sum(v[2] for v in coverage.values()) out += ["", "## Code documentation coverage", "", - "Classes declared in the headers of `Common/include` and `SU2_CFD/include` (counted with a simple " - "text search, so the numbers are approximate). `Docs/docmain.hpp` asks that every class has a Doxygen " - "comment with `\\brief` and `\\ingroup`.", "", + "This section counts the classes declared in the headers of `Common/include` and `SU2_CFD/include`. " + "The count uses a simple text search, so the numbers are approximate. `Docs/docmain.hpp` asks that " + "every class has a Doxygen comment with `\\brief` and `\\ingroup`.", "", "- Classes: {}".format(total), "- With a Doxygen comment: {} ({:.0f}%)".format(documented, 100.0 * documented / max(total, 1)), "- In a Doxygen group (`\\ingroup`): {} ({:.0f}%)".format(grouped, 100.0 * grouped / max(total, 1)), "", @@ -712,8 +713,8 @@ def write_errors_page(site, errors, options, commit, blob): group = "/".join(path.split("/")[:-1]) groups.setdefault(group, []).append((path, line, message)) out = ["---", "title: Error Messages", "permalink: /{}/{}/".format(URL, name), "---", "", "{% raw %}", "", - "This page lists every error message that can stop SU2 (`SU2_MPI::Error` in the code), " - "{} in commit `{}`. Use the search of your browser (Ctrl+F) with a few words of the message you see. " + "This page lists the {} error messages that can stop SU2 in commit `{}` (the `SU2_MPI::Error` calls " + "in the code). Use the search of your browser (Ctrl+F) with a few words of the message you see. " "Parts of a message that SU2 fills in at run time are shown as \"...\".".format(len(errors), commit), "", "Each message links to the code that prints it, and to the options it names.", "", "See also: [Renamed and Removed Options](../Renamed-Options/).", ""] @@ -739,7 +740,8 @@ def write_check_page(site, found, commit, options, extra=()): "This page is made at each build. It lists what needs work in the documentation of SU2 commit `{}`." .format(commit), "", "## Options named in the docs that do not exist", "", - "The option was renamed or removed, or it has a typo. Fix the page in the website repository.", "", + "These names are not options of the code: the option was renamed or removed, or the name has a typo. " + "Fix the page in the website repository.", "", "Found: {}".format(len(found)), ""] last = None for path, num, name, line in found: @@ -944,7 +946,7 @@ def main(): 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 \"unknown option\" error.", "", + "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), From 658e3f1a150300f6d038ea86e9c52a8f726ed7d1 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Thu, 22 Oct 2026 14:40:58 +0200 Subject: [PATCH 14/25] Correct documentation references and explain solver limitations --- Docs/website/README.md | 3 +- Docs/website/pages/Adding-a-Config-Option.md | 4 +- .../pages/Adding-a-Turbulence-Model.md | 2 +- .../pages/Automatic-Differentiation.md | 7 ++- Docs/website/pages/Debugging.md | 7 +-- Docs/website/tools/build_site.py | 13 ++++-- Docs/website/tools/test_build_site.py | 45 +++++++++++++++++++ SU2_CFD/flamelet_python_bc.md | 16 +++---- 8 files changed, 77 insertions(+), 20 deletions(-) create mode 100644 Docs/website/tools/test_build_site.py diff --git a/Docs/website/README.md b/Docs/website/README.md index 3ed3f0223a7f..14394b71bd5e 100644 --- a/Docs/website/README.md +++ b/Docs/website/README.md @@ -9,7 +9,8 @@ The workflow `.github/workflows/docs-website.yml` builds the website and publish 2. It copies every page in `pages/` into the user guide (`_docs_v7/`). A page with the same name as a website page replaces it. 3. It copies the Markdown pages kept in the code folders (for example `Docs/Aeroacoustics.md`) into the user guide. 4. It makes the **Option Reference** from `Common/src/CConfig.cpp`. Each option shows: - - its description, type, allowed values and default value; + - its description, type, allowed values and default value when it can be extracted (array defaults need a + `DEFAULT:` comment; template examples may differ from defaults); - the comment of `config_template.cfg`, when it adds something; - the website pages and tutorials that mention it; - up to three test case files that use it, with their values; diff --git a/Docs/website/pages/Adding-a-Config-Option.md b/Docs/website/pages/Adding-a-Config-Option.md index 771c7fb3101b..e215d47d82af 100644 --- a/Docs/website/pages/Adding-a-Config-Option.md +++ b/Docs/website/pages/Adding-a-Config-Option.md @@ -146,7 +146,9 @@ 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, from the last argument of the call (for an enum, the keyword of the map that has this value); +- 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); diff --git a/Docs/website/pages/Adding-a-Turbulence-Model.md b/Docs/website/pages/Adding-a-Turbulence-Model.md index 077a43a84e71..608415418cfe 100644 --- a/Docs/website/pages/Adding-a-Turbulence-Model.md +++ b/Docs/website/pages/Adding-a-Turbulence-Model.md @@ -373,7 +373,7 @@ inline TURB_FAMILY TurbModelFamily(TURB_MODEL model) { 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 add the turbulent kinetic energy to the energy and to the stresses only for `TURB_MODEL::SST`. This happens in `CNSSolver::SetPrimitive_Variables`, `CIncNSSolver`, `CFVMFlowSolverBase::Viscous_Residual_impl`, and many boundary conditions in `CEulerSolver.cpp` and `CIncEulerSolver.cpp`. A new model that transports k must be added there. +- `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`. diff --git a/Docs/website/pages/Automatic-Differentiation.md b/Docs/website/pages/Automatic-Differentiation.md index ae38b53be161..f8925f0fab62 100644 --- a/Docs/website/pages/Automatic-Differentiation.md +++ b/Docs/website/pages/Automatic-Differentiation.md @@ -671,8 +671,11 @@ 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 and reverse mode should agree to many digits, because both are exact derivatives of the same discrete - code. A difference between them points to an AD error, not to convergence or step size. +- 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 diff --git a/Docs/website/pages/Debugging.md b/Docs/website/pages/Debugging.md index 1ca0a349dd09..e489e4d7ef26 100644 --- a/Docs/website/pages/Debugging.md +++ b/Docs/website/pages/Debugging.md @@ -52,9 +52,10 @@ Notes: - `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. The CI (continuous integration: the automatic tests on GitHub) - builds with `--warnlevel=3` and `--werror` (warnings become errors) for most configurations, so check your change - with these flags before you push. +- `--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 diff --git a/Docs/website/tools/build_site.py b/Docs/website/tools/build_site.py index e2213db25d6d..26254559f323 100644 --- a/Docs/website/tools/build_site.py +++ b/Docs/website/tools/build_site.py @@ -109,6 +109,9 @@ def read_maps(su2): for path in glob.glob(os.path.join(su2, "Common", "include", "**", "*.*pp"), recursive=True): with open(path, encoding="utf-8", errors="replace") as f: text = f.read() + # Ignore disabled entries, but keep comment markers inside quoted strings. + text = re.sub(r'"(?:\\.|[^"\\])*"|//[^\n]*|/\*.*?\*/', + lambda m: m[0] if m[0].startswith('"') else " ", text, flags=re.S) for m in pattern.finditer(text): pairs = re.findall(r'MakePair\(\s*"([^"]+)"\s*,\s*([^)]*?)\s*\)', m.group(2)) maps[m.group(1)] = pairs @@ -254,8 +257,6 @@ def describe_type(opt, maps): default = {"true": "YES", "false": "NO"}.get(args[2], args[2]) elif kind in ("Double", "String", "UnsignedLong", "UnsignedShort", "Long") and len(args) > 2: default = args[2].strip('"') - elif kind in ("DoubleArray", "UShortArray", "ULongArray") and len(args) > 3: - default = args[3] return TYPE_NAMES.get(kind, "special format, see config_template.cfg"), values, default @@ -317,6 +318,8 @@ def write_reference(su2, site, options, maps, commit, repo, skip): out.append("- **Values:** {}".format(opt_text)) if default or def_text: out.append("- **Default:** `{}`".format(default if default else def_text)) + elif opt["kind"] in ("DoubleArray", "UShortArray", "ULongArray"): + out.append("- **Default:** *Not extracted; see the code.*") if tmpl_line: out.append("- **In config_template.cfg:** `{}`".format(tmpl_line)) else: @@ -342,8 +345,10 @@ def write_reference(su2, site, options, maps, commit, repo, skip): missing_tmpl = sum(1 for o in options if o["name"] not in examples) out = ["---", "title: Option Reference", "permalink: /{}/{}/".format(URL, REF_PREFIX), "---", "", "{% raw %}", "", - "This reference lists every option of the SU2 configuration file. It is made from the source code, " - "so it always matches the code. {}".format(stamp), "", + "This reference lists the options registered in the SU2 source code. {}".format(stamp), "", + "Defaults initialized outside the registration calls may not be extracted. " + "The template lines are examples and may differ from the defaults. " + "Allowed keywords can have additional restrictions for a particular solver or option combination.", "", "Each option shows the description in the code, the text of `config_template.cfg` when it adds " "something, the pages of this website that mention it, and real test case files that use it.", "", "- Options: {}".format(len(options)), diff --git a/Docs/website/tools/test_build_site.py b/Docs/website/tools/test_build_site.py new file mode 100644 index 000000000000..aaa4aacd6239 --- /dev/null +++ b/Docs/website/tools/test_build_site.py @@ -0,0 +1,45 @@ +"""Run with python3 -m unittest discover -s Docs/website/tools.""" + +from pathlib import Path +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], "") + + +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. From 293e1da159a78b67cbd875e566e02f4a0f8b76a5 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Thu, 22 Oct 2026 15:37:08 +0200 Subject: [PATCH 15/25] Build and check documentation consistently in local and CI previews --- .github/workflows/docs-website.yml | 105 +++++------- Docs/website/README.md | 135 +++++++++------ Docs/website/tools/build_docs.py | 106 ++++++++++++ Docs/website/tools/build_site.py | 7 +- Docs/website/tools/check-baseline.json | 224 +++++++++++++++++++++++++ Docs/website/tools/check_site.py | 172 +++++++++++++++++++ Docs/website/tools/rebase_links.py | 5 +- Docs/website/tools/test_docs_checks.py | 96 +++++++++++ 8 files changed, 727 insertions(+), 123 deletions(-) create mode 100644 Docs/website/tools/build_docs.py create mode 100644 Docs/website/tools/check-baseline.json create mode 100644 Docs/website/tools/check_site.py create mode 100644 Docs/website/tools/test_docs_checks.py diff --git a/.github/workflows/docs-website.yml b/.github/workflows/docs-website.yml index ad34af76c863..37f2de792fab 100644 --- a/.github/workflows/docs-website.yml +++ b/.github/workflows/docs-website.yml @@ -1,107 +1,84 @@ name: Docs Website -# Builds the SU2 website with the pages of this repository, the generated -# option reference and the Doxygen code reference, then publishes it on the -# GitHub Pages of this repository. - on: push: - branches: - - 'docs_site' + branches: ['docs_site'] + pull_request: + branches: ['docs_site'] workflow_dispatch: inputs: site_repo: - description: 'Website repository' - default: 'rois1995/su2code.github.io' + description: Website repository + default: rois1995/su2code.github.io site_ref: - description: 'Website branch' - default: 'docs_site' - -env: - SITE_REPO: ${{ inputs.site_repo || 'rois1995/su2code.github.io' }} - SITE_REF: ${{ inputs.site_ref || 'docs_site' }} + description: Website commit SHA or branch (use a SHA to reproduce a build) + default: docs_site permissions: contents: read - pages: write - id-token: write concurrency: - group: docs-website + group: docs-website-${{ github.event_name }}-${{ github.ref }} cancel-in-progress: true jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout SU2 uses: actions/checkout@v4 with: path: su2 - # The history (without file contents) finds the developer pages that may be out of date. fetch-depth: 0 filter: blob:none - - name: Get the release tags - # The fork may miss some tags; the renamed options page needs all of them. + 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: ${{ env.SITE_REPO }} - ref: ${{ env.SITE_REF }} + repository: ${{ inputs.site_repo || 'rois1995/su2code.github.io' }} + ref: ${{ inputs.site_ref || 'docs_site' }} path: site - # Some history to compare with the official website (--mark-changes). - fetch-depth: 50 + fetch-depth: 0 filter: blob:none - - name: Configure Pages - id: pages - uses: actions/configure-pages@v5 - - name: Build code reference - working-directory: su2 - run: | - sudo apt-get update && sudo apt-get install -y doxygen graphviz - ( cat doxyfile - echo "OUTPUT_DIRECTORY = ../doxygen" - echo "PROJECT_NUMBER = $(git rev-parse --short=10 HEAD)" - echo "SOURCE_BROWSER = NO" - echo "CALL_GRAPH = NO" - echo "CALLER_GRAPH = NO" - echo "COLLABORATION_GRAPH = NO" - echo "INCLUDE_GRAPH = NO" - echo "INCLUDED_BY_GRAPH = NO" - echo "DIRECTORY_GRAPH = NO" - echo "DOT_IMAGE_FORMAT = svg" - echo "WARN_LOGFILE = ../doxygen_warnings.txt" - echo "EXCLUDE = ./Docs/website" - ) | doxygen - - - name: Prepare website - run: python3 su2/Docs/website/tools/build_site.py --su2 su2 --site site --light --doxygen-log doxygen_warnings.txt --mark-changes - - name: Setup Ruby + persist-credentials: false + - name: Setup Ruby and locked gems uses: ruby/setup-ruby@v1 with: - ruby-version: '3.3' - - name: Build website - working-directory: site - run: | - bundle install --jobs 4 - bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" -d ../_site - - name: Assemble - run: | - python3 su2/Docs/website/tools/rebase_links.py _site "${{ steps.pages.outputs.base_path }}" - cp -r doxygen/html _site/doxygen - du -sh _site - - name: Upload + 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.event_name != 'pull_request' uses: actions/upload-pages-artifact@v3 with: - path: _site + path: ${{ runner.temp }}/docs-site/site deploy: + if: github.event_name != 'pull_request' needs: build - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + permissions: + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - - name: Deploy + - name: Deploy checked website id: deployment uses: actions/deploy-pages@v4 diff --git a/Docs/website/README.md b/Docs/website/README.md index 14394b71bd5e..8db6b7511575 100644 --- a/Docs/website/README.md +++ b/Docs/website/README.md @@ -1,59 +1,84 @@ -# SU2 website: pages kept with the code - -This folder holds the parts of the SU2 website (https://su2code.github.io) that must change together with the code. -The workflow `.github/workflows/docs-website.yml` builds the website and publishes it on the GitHub Pages of this repository. - -## What the build does - -1. It gets the website. By default this is `rois1995/su2code.github.io`, branch `docs_site`. When you start the workflow by hand, you can choose another repository and branch. -2. It copies every page in `pages/` into the user guide (`_docs_v7/`). A page with the same name as a website page replaces it. -3. It copies the Markdown pages kept in the code folders (for example `Docs/Aeroacoustics.md`) into the user guide. -4. It makes the **Option Reference** from `Common/src/CConfig.cpp`. Each option shows: - - its description, type, allowed values and default value when it can be extracted (array defaults need a - `DEFAULT:` comment; template examples may differ from defaults); - - the comment of `config_template.cfg`, when it adds something; - - the website pages and tutorials that mention it; - - up to three test case files that use it, with their values; - - a link to the code. -5. It makes the **Output Fields Reference**: all screen, history and volume fields of each output class, from the - `AddHistoryOutput` and `AddVolumeOutput` calls. -6. It makes the **Error Messages** page, from the `SU2_MPI::Error` calls, and the **Renamed and Removed Options** - page, from the option names of past releases (git tags). -7. It makes the **Docs Check** page. This page lists: - - options that the docs, tutorials and V&V (verification and validation) pages name, but that do not exist in - the code; - - options with no description, a very short one, or no example; - - developer pages that may be out of date: the code files they name changed since the commit in their - "Last checked against" line; - - the Doxygen coverage of the classes, and the Doxygen warnings. -8. It builds the website with Jekyll, and the **Code Reference** with Doxygen (Doxygen runs first, for its warnings). - -**Debug option.** The test site runs `build_site.py` with `--mark-changes`. It shows in red the menu items and -titles of the pages that differ from the official website, and the new menu sections. Remove the option from the -workflow for a normal build. - -## How to change the docs - -Pages that explain the code (developer pages) live here, so they change in the same pull request as the code. -User guide pages and tutorials live in the website repository. - -- **New option:** write the comment above the `add...Option` call in `CConfig.cpp`, in the usual form: - `/*!\brief NAME \n DESCRIPTION: what it does \n DEFAULT: value \ingroup Config*/`. - The Option Reference updates by itself. Also add the option to `config_template.cfg`. -- **New or changed page:** add or edit a Markdown file in `pages/`. The file must start with a front matter: a block - between two `---` lines that holds the page settings. It needs `title` and `permalink: /docs_v7//`. - Pages named `Adding-...` appear under "How to Add..." in the menu. Other new pages appear at the end of - "Developer Docs". -- **After you check a developer page against the code:** update the commit in its last line, - "Last checked against SU2 develop ``". The Docs Check page then stops listing it. -- **Images:** put them in `pages/img/`. Link to them as `../../docs_files/website/.png`. - -## Run it on your computer +# 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 + +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. 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" ``` -git clone --depth 1 https://github.com/su2code/su2code.github.io.git site -python3 Docs/website/tools/build_site.py --su2 . --site site -cd site && bundle install && bundle exec jekyll 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 ``` -Then open http://localhost:4000. For the Code Reference, run `doxygen doxyfile` in the SU2 folder and open `Docs/html/index.html`. +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` records pre-existing broken links, eight template names unknown to the C++ registration, and 24 options missing source descriptions, and historical enum-keyword findings. 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 SU2 `docs_site` pushes and pull requests. Pull requests build an artifact without deploying. Successful pushes and manual runs publish the checked artifact to this fork's Pages site. Checkout references are resolved once; the manifest records the exact revisions used. + +The website repository's `docs-check.yml` checks its own pushes/pull requests against SU2 `docs_site` 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. + +Both workflows target the `rois1995` forks. Do not push or publish these development changes to the official repositories. diff --git a/Docs/website/tools/build_docs.py b/Docs/website/tools/build_docs.py new file mode 100644 index 000000000000..b33946e25ce5 --- /dev/null +++ b/Docs/website/tools/build_docs.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Build and check the combined SU2 website in an isolated output directory.""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys + +TOOLS = Path(__file__).resolve().parent + + +def output(command, **kwargs): + return subprocess.check_output(command, text=True, **kwargs).strip() + + +def revision(path): + return {"commit": output(["git", "-C", str(path), "rev-parse", "HEAD"]), + "dirty": bool(output(["git", "-C", str(path), "status", "--porcelain"]))} + + +def prepare_directory(destination, sources): + destination = destination.resolve() + for source in sources: + source = source.resolve() + if destination == source or destination in source.parents or source in destination.parents: + raise ValueError("Build directory must be separate from both source trees") + marker = destination / ".su2-docs-build" + if destination.exists() and any(destination.iterdir()) and not marker.is_file(): + raise ValueError("Refusing to reuse a nonempty directory without .su2-docs-build") + destination.mkdir(parents=True, exist_ok=True) + marker.touch() + for name in ("source", "site", "doxygen"): + path = destination / name + if path.is_symlink(): + raise ValueError("Build output must not be a symlink: " + str(path)) + if path.exists(): + shutil.rmtree(path) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--su2", type=Path, default=TOOLS.parents[2]) + parser.add_argument("--site", type=Path, required=True, help="website source checkout (not modified)") + parser.add_argument("--build-dir", type=Path, required=True) + parser.add_argument("--baseurl", default="/SU2") + parser.add_argument("--mark-changes", action="store_true", help="keep red preview highlighting") + args = parser.parse_args() + su2, website, work = args.su2.resolve(), args.site.resolve(), args.build_dir.resolve() + if not (su2 / "Common/src/CConfig.cpp").is_file() or not (website / "_config.yml").is_file(): + parser.error("--su2 and --site must point to their respective source checkouts") + expected = (website / ".ruby-version").read_text().strip() + actual = output(["ruby", "-e", "print RUBY_VERSION"]) + if actual != expected: + parser.error("Use Ruby {} from the website .ruby-version (found {})".format(expected, actual)) + for executable in ("bundle", "doxygen", "dot"): + if not shutil.which(executable): + parser.error("Required tool not found: " + executable) + sources = {"su2": revision(su2), "website": revision(website)} + epoch = max(int(output(["git", "-C", str(p), "show", "-s", "--format=%ct", "HEAD"])) for p in (su2, website)) + env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1", SOURCE_DATE_EPOCH=str(epoch), BUNDLE_FROZEN="true") + subprocess.run([sys.executable, "-m", "unittest", "discover", "-s", str(TOOLS)], env=env, check=True) + prepare_directory(work, (su2, website)) + source, site = work / "source", work / "site" + shutil.copytree(website, source, ignore=shutil.ignore_patterns( + ".git", ".github", "_site", ".jekyll-cache", ".jekyll-metadata", ".bundle", "vendor", + "documents", "download", "emails", "__pycache__")) + env["BUNDLE_GEMFILE"] = str(source / "Gemfile") + subprocess.run(["bundle", "check"], cwd=source, env=env, check=True) + flags = {"OUTPUT_DIRECTORY": str(work / "doxygen"), "PROJECT_NUMBER": sources["su2"]["commit"][:10], + "SOURCE_BROWSER": "NO", "CALL_GRAPH": "NO", "CALLER_GRAPH": "NO", "COLLABORATION_GRAPH": "NO", + "INCLUDE_GRAPH": "NO", "INCLUDED_BY_GRAPH": "NO", "DIRECTORY_GRAPH": "NO", "DOT_IMAGE_FORMAT": "svg", + "WARN_LOGFILE": str(work / "doxygen_warnings.txt"), "EXCLUDE": "./Docs/website"} + config = (su2 / "doxyfile").read_text() + "\n" + "\n".join(k + ' = "' + v + '"' for k, v in flags.items()) + print("Building Doxygen; log: " + str(work / "doxygen.log"), flush=True) + with (work / "doxygen.log").open("w") as log: + subprocess.run(["doxygen", "-"], cwd=su2, input=config + "\n", text=True, stdout=log, + stderr=subprocess.STDOUT, env=env, check=True) + command = [sys.executable, str(TOOLS / "build_site.py"), "--su2", str(su2), "--site", str(source), + "--light", "--doxygen-log", str(work / "doxygen_warnings.txt")] + if args.mark_changes: + command += ["--mark-changes", "--compare-site", str(website)] + subprocess.run(command, env=env, check=True) + subprocess.run(["bundle", "exec", "jekyll", "build", "--baseurl", args.baseurl, + "--destination", str(site)], cwd=source, env=env, check=True) + subprocess.run([sys.executable, str(TOOLS / "rebase_links.py"), str(site), args.baseurl], env=env, check=True) + shutil.copytree(work / "doxygen/html", site / "doxygen") + sources.update({"baseurl": args.baseurl, "mark_changes": args.mark_changes, + "source_date_epoch": epoch, "light": True, + "gemfile_lock_sha256": hashlib.sha256((source / "Gemfile.lock").read_bytes()).hexdigest(), + "versions": {"python": sys.version.split()[0], "ruby": actual, + "bundler": output(["bundle", "--version"], cwd=source, env=env), + "jekyll": output(["bundle", "exec", "jekyll", "--version"], cwd=source, env=env), + "doxygen": output(["doxygen", "--version"]), + "graphviz": subprocess.run(["dot", "-V"], capture_output=True, text=True, check=True).stderr.strip()}}) + (site / "build-info.json").write_text(json.dumps(sources, indent=2) + "\n") + subprocess.run([sys.executable, str(TOOLS / "check_site.py"), "--su2", str(su2), "--source", str(source), + "--site", str(site), "--baseurl", args.baseurl], env=env, check=True) + print("Checked website: " + str(site)) + + +if __name__ == "__main__": + main() diff --git a/Docs/website/tools/build_site.py b/Docs/website/tools/build_site.py index 26254559f323..1b0a03b38655 100644 --- a/Docs/website/tools/build_site.py +++ b/Docs/website/tools/build_site.py @@ -273,7 +273,9 @@ def write_reference(su2, site, options, maps, commit, repo, skip): examples = read_template(su2) usage = read_testcases(su2) mentions = read_page_mentions(site, {o["name"] for o in options}, skip) - stamp = "Generated from SU2 commit `{}` on {}.".format(commit, datetime.date.today().isoformat()) + epoch = os.environ.get("SOURCE_DATE_EPOCH") + date = datetime.datetime.fromtimestamp(int(epoch), datetime.timezone.utc).date() if epoch else datetime.date.today() + stamp = "Generated from SU2 commit `{}` on {}.".format(commit, date.isoformat()) blob = "https://github.com/{}/blob/{}/".format(repo, commit) source = blob + "Common/src/CConfig.cpp" @@ -917,6 +919,7 @@ def main(): 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() @@ -962,7 +965,7 @@ def main(): 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.site) + 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: diff --git a/Docs/website/tools/check-baseline.json b/Docs/website/tools/check-baseline.json new file mode 100644 index 000000000000..e2de71750ab1 --- /dev/null +++ b/Docs/website/tools/check-baseline.json @@ -0,0 +1,224 @@ +{ + "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.", + "template_options": [ + "BUFFET_MONITORING", + "DEFORM_POISSON_RATIO", + "HOLD_GRID_FIXED_COORDS", + "IGNITION_TEMPERATURE", + "NETTHRUST_DBCTHRUST", + "SINGLEZONE_DRIVER", + "SMOOTH_GEOMETRY", + "THERMAL_DIFFUSIVITY" + ], + "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": [ + "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/2D_MidSpan_RelMach.png (missing file)", + "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/FullAnnulus.png (missing file)", + "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/FullAnnulus_MachContour.png (missing file)", + "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/Full_View_MachContour.png (missing file)", + "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/SingleMesh_Adiabatic_Giles.png (missing file)", + "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/SingleMesh_Adiabatic_Giles_MixingPlane.png (missing file)", + "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/SingleMesh_Adiabatic_NoSlip.png (missing file)", + "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/SingleMesh_Adiabatic_Periodics.png (missing file)", + "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/SingleMesh_shroud.png (missing file)", + "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/SinglePassage.png (missing file)", + "tutorials/Aachen_Turbine/index.html: tutorials/Actuator_Disk/ (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/ContourCp.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/DeltaCp.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/Domain.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/GridDisk.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/GridSpinner.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/GridXY.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/MachNumber.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/MomentumX.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/PressureCoeff.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/SwirlAndCp.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/Swirl_Vectors.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/Vt.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/Vt_x.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/dcp_input.png (missing file)", + "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/dct_input.png (missing file)", + "tutorials/Adjoint_FSI_Python/index.html: tutorials/multiphysics/images/fsipyadj1.png (missing file)", + "tutorials/Basic_RHT/index.html: tutorials/multiphysics/images/rht1.png (missing file)", + "tutorials/Basic_RHT/index.html: tutorials/multiphysics/images/rht2.png (missing file)", + "tutorials/Basic_RHT/index.html: tutorials/multiphysics/images/rht3.png (missing file)", + "tutorials/Basic_RHT/index.html: tutorials/multiphysics/images/rht4.png (missing file)", + "tutorials/Basic_RHT/index.html: tutorials/multiphysics/images/rht5.png (missing file)", + "tutorials/Coupled_RHT_Adjoint/index.html: tutorials/multiphysics/images/rhtadj1.png (missing file)", + "tutorials/Coupled_RHT_Adjoint/index.html: tutorials/multiphysics/images/rhtadj2.png (missing file)", + "tutorials/Coupled_RHT_Adjoint/index.html: tutorials/multiphysics/images/rhtadj3.png (missing file)", + "tutorials/Coupled_RHT_Adjoint/index.html: tutorials/multiphysics/images/rhtadj4.png (missing file)", + "tutorials/Coupled_RHT_Adjoint/index.html: tutorials/multiphysics/images/rhtadj5.png (missing file)", + "tutorials/Coupled_RHT_Adjoint/index.html: tutorials/multiphysics/images/rhtadj6.png (missing file)", + "tutorials/Dynamics/index.html: tutorials/structural_mechanics/images/dynamic_linear.gif (missing file)", + "tutorials/Dynamics/index.html: tutorials/structural_mechanics/images/lin1.png (missing file)", + "tutorials/Dynamics/index.html: tutorials/structural_mechanics/images/lin2.png (missing file)", + "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/MLP_PD.png (missing file)", + "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/MLP_PNO.png (missing file)", + "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/MLP_SPV.png (missing file)", + "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/MLP_TD1.png (missing file)", + "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/MLP_TD2.png (missing file)", + "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/SolverProcess.png (missing file)", + "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/T_Qdot.png (missing file)", + "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/ignition_location.png (missing file)", + "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/mesh.png (missing file)", + "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/mixfrac_NO.png (missing file)", + "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/pv_enth.png (missing file)", + "tutorials/Inc_Combustion/index.html: docs_v8/Download/ (missing file)", + "tutorials/Inc_Combustion/index.html: docs_v8/Installation/ (missing file)", + "tutorials/Inc_Combustion/index.html: tutorials/Inc_Combustion/index.html#solutionprocess (missing anchor)", + "tutorials/Inc_Heated_Cylinders_Unsteady/index.html: tutorials/Inc_Heated_Cylinders/ (missing file)", + "tutorials/Inc_Inviscid_Hydrofoil/index.html: tutorials/Actuator_Disk/ (missing file)", + "tutorials/Inc_Laminar_Cavity/index.html: tutorials/Inc_Laminar_Cavity/incompressible_flow/https:/github.com/su2code/Tutorials/tree/master/incompressible_flow/Inc_Laminar_Cavity (missing file)", + "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/ZH_fields.png (missing file)", + "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/ZH_lines_Temperature.png (missing file)", + "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_S56_CH4.png (missing file)", + "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_S56_CO.png (missing file)", + "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_S56_CO2.png (missing file)", + "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_S56_Z_T.png (missing file)", + "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_S56_temperature.png (missing file)", + "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_diffusion_convergence.png (missing file)", + "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_diffusion_flame_temperature_streamlines.png (missing file)", + "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/mesh.png (missing file)", + "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/opflow01-1024x766_thomsonlab.jpg (missing file)", + "tutorials/Inc_Turbulent_Bend_Opt/index.html: /tutorials_files/design_features/Inc_Turbulent_Bend/bend_with_FFD.png (missing file)", + "tutorials/Inc_Turbulent_Bend_Opt/index.html: /tutorials_files/design_features/Inc_Turbulent_Bend/mesh_deformation.png (missing file)", + "tutorials/Inc_Turbulent_Bend_Opt/index.html: /tutorials_files/design_features/Inc_Turbulent_Bend/opt_iter0_pressure_ffdbox.png (missing file)", + "tutorials/Inc_Turbulent_Bend_Opt/index.html: /tutorials_files/design_features/Inc_Turbulent_Bend/opt_iter5_pressure_ffdbox.png (missing file)", + "tutorials/Inc_Turbulent_Flat_Plate/index.html: tutorials_files/incompressible_flow/Inc_Turbulent_Flat_Plate/mesh_flatplate_turb_545x385.su2 (missing file)", + "tutorials/Inc_Turbulent_Flat_Plate/index.html: tutorials_files/incompressible_flow/Inc_Turbulent_Flat_Plate/turb_flatplate.cfg (missing file)", + "tutorials/Inc_Urban_City/index.html: /tutorials_files/incompressible_flow/Inc_Urban_City/images/amsterdam_buildings.png (missing file)", + "tutorials/Inc_Urban_City/index.html: /tutorials_files/incompressible_flow/Inc_Urban_City/images/fire_zoom.png (missing file)", + "tutorials/Inc_Urban_City/index.html: /tutorials_files/incompressible_flow/Inc_Urban_City/images/gmsh_mesh.png (missing file)", + "tutorials/Inc_Urban_City/index.html: /tutorials_files/incompressible_flow/Inc_Urban_City/images/gmsh_mesh_zoom.png (missing file)", + "tutorials/Inc_Urban_City/index.html: /tutorials_files/incompressible_flow/Inc_Urban_City/images/wind_velocity.png (missing file)", + "tutorials/Inc_Urban_City/index.html: /tutorials_files/incompressible_flow/Inc_Urban_City/images/wind_velocity_zoom.png (missing file)", + "tutorials/Inc_Urban_City/index.html: tutorials/Inc_Urban_City/gmsh_mesh (missing file)", + "tutorials/Inc_Urban_City/index.html: tutorials/Inc_Urban_City/gmsh_mesh_zoom (missing file)", + "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/mesh.png (missing file)", + "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/particles.png (missing file)", + "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/strouhal_cylinder_karman.png (missing file)", + "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/strouhal_cylinder_karman_Re.png (missing file)", + "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/strouhal_cylinder_karman_variation.png (missing file)", + "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/unsteady_cylinder.gif (missing file)", + "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/unsteady_cylinder.mp4 (missing file)", + "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/unsteady_cylinder_arrow.gif (missing file)", + "tutorials/NICFD_nozzle_datadriven/index.html: tutorials/NICFD_nozzle_datadriven/index.html#mesh_highlight (missing anchor)", + "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/contact_resistance_effect.png (missing file)", + "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/problem_mesh.png (missing file)", + "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/problem_setup.png (missing file)", + "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/solid_domains.png (missing file)", + "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/solution_temperature.png (missing file)", + "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/solution_temperature_lines.png (missing file)", + "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/solution_temperature_lines_n.png (missing file)", + "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/solution_temperature_n.png (missing file)", + "tutorials/SS_CR_CHT/index.html: docs_v8/Download/ (missing file)", + "tutorials/SS_CR_CHT/index.html: docs_v8/Installation/ (missing file)", + "tutorials/Static_FSI_Python/index.html: tutorials/multiphysics/images/fsipy1.png (missing file)", + "tutorials/Static_FSI_Python/index.html: tutorials/multiphysics/images/fsipy2.png (missing file)", + "tutorials/Static_FSI_Python/index.html: tutorials/multiphysics/images/fsipy3.png (missing file)", + "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_c_correction.png (missing file)", + "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_source.png (missing file)", + "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_source_correction.png (missing file)", + "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_temp.png (missing file)", + "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_temp_correction.png (missing file)", + "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_temp_heatloss.png (missing file)", + "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_temp_init.png (missing file)", + "tutorials/Transitional_Flat_Plate_T3A/index.html: /tutorials_files/compressible_flow/Transitional_Flat_Plate/images/LM_flat_plate/Boundary_conditions.png (missing file)", + "tutorials/Transitional_Flat_Plate_T3A/index.html: /tutorials_files/compressible_flow/Transitional_Flat_Plate/images/LM_flat_plate/Cf_T3A-.png (missing file)", + "tutorials/Transitional_Flat_Plate_T3A/index.html: /tutorials_files/compressible_flow/Transitional_Flat_Plate/images/LM_flat_plate/Cf_T3A.png (missing file)", + "tutorials/Turbulent_RHT_CHT/index.html: tutorials/Inc_Heated_Cylinders (missing file)", + "tutorials/Turbulent_RHT_CHT/index.html: tutorials/multiphysics/images/chtrht1.png (missing file)", + "tutorials/Turbulent_RHT_CHT/index.html: tutorials/multiphysics/images/chtrht2.png (missing file)", + "tutorials/Turbulent_RHT_CHT/index.html: tutorials/multiphysics/images/chtrht3.png (missing file)", + "tutorials/Turbulent_RHT_CHT/index.html: tutorials/multiphysics/images/chtrht4.png (missing file)", + "tutorials/Turbulent_RHT_CHT/index.html: tutorials/multiphysics/images/chtrht5.png (missing file)", + "tutorials/event_content/index.html: documents/Design_and_Optimization_Using_SU2.pdf (missing file)", + "tutorials/event_content/index.html: documents/Introduction_to_the_SU2_Code_Structure.pdf (missing file)", + "tutorials/event_content/index.html: documents/LM-1021-EulerMesh.pw (missing file)", + "tutorials/event_content/index.html: documents/ONERAM6.zip (missing file)", + "tutorials/event_content/index.html: documents/OpenMDAO_SU2_Workshop_Agenda.pdf (missing file)", + "tutorials/event_content/index.html: documents/Optimization_Using_SU2.pdf (missing file)", + "tutorials/event_content/index.html: documents/Pointwise-Stanford-Webinar-Slides_20140429.pdf (missing file)", + "tutorials/event_content/index.html: documents/Running_SU2.pdf (missing file)", + "tutorials/event_content/index.html: documents/SU2_AdvancedAnalysis.pdf (missing file)", + "tutorials/event_content/index.html: documents/SU2_Advanced_Topics.zip (missing file)", + "tutorials/event_content/index.html: documents/SU2_Dev_Meeting_Agenda_20171218.pdf (missing file)", + "tutorials/event_content/index.html: documents/SU2_IntroInstallation.pdf (missing file)", + "tutorials/event_content/index.html: documents/SU2_Workshop_Agenda_v2.0.pdf (missing file)", + "tutorials/event_content/index.html: documents/SU2_as_a_High-fidelity_Analysis_Tool.pdf (missing file)", + "tutorials/event_content/index.html: documents/Task-Based_Design_Evaluation_with_SU2.pdf (missing file)", + "tutorials/event_content/index.html: documents/Welcome_and_introduction_to_SU2.pdf (missing file)", + "tutorials/event_content/index.html: documents/WorkshopFeb2017/Part2_CodeStructure.pdf (missing file)", + "tutorials/event_content/index.html: documents/WorkshopFeb2017/PythonMods.pdf (missing file)", + "tutorials/event_content/index.html: documents/WorkshopFeb2017/Tutorial1Presentation.pdf (missing file)", + "tutorials/event_content/index.html: documents/WorkshopFeb2017/Tutorial1_OneraM6Files.zip (missing file)", + "tutorials/event_content/index.html: documents/WorkshopFeb2017/WorkshopTutorial2.pdf (missing file)", + "tutorials/event_content/index.html: documents/WorkshopFeb2017/WorkshopTutorial2naca.zip (missing file)", + "tutorials/event_content/index.html: documents/WorkshopFeb2017/WorkshopTutorial2oneram6.zip (missing file)", + "tutorials/event_content/index.html: documents/WorkshopFeb2017/compute_Mpolar.py (missing file)", + "tutorials/event_content/index.html: documents/WorkshopFeb2017/dev_process.pdf (missing file)", + "tutorials/event_content/index.html: documents/WorkshopFeb2017/welcome_and_intro.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_albring.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_alonso.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_drikakis.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_economon.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_fossati.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_gori.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_mishra.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_molina.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_palacios.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_pini.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_vanderweide.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/00_su2_hackathon_economon.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/01_su2_dev_alonso_economon.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/02_su2_dev_fossati.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/03_su2_dev_beishuizen.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/04_su2_dev_burghardt.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/05_su2_dev_mukhopadhaya.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/06_su2_dev_zhou.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/07_su2_dev_vanderweide_singh.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/08_su2_dev_rubino.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/09_su2_dev_morelli.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/10_su2_dev_loseille.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/11_su2_dev_economon.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/12_su2_dev_koodlyravishankara.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/13_su2_dev_albring_sanchez.pdf (missing file)", + "tutorials/event_content/index.html: documents/dev_meeting_2018/SU2_Dev_Meeting_PresentationAgenda_20180916.pdf (missing file)", + "tutorials/event_content/index.html: documents/su2_dev_alonso.pdf (missing file)", + "tutorials/event_content/index.html: documents/su2_dev_economon.pdf (missing file)", + "tutorials/event_content/index.html: documents/su2_dev_gauger.pdf (missing file)", + "tutorials/event_content/index.html: documents/su2_dev_guardone.pdf (missing file)", + "tutorials/event_content/index.html: documents/su2_dev_pini.pdf (missing file)", + "tutorials/event_content/index.html: documents/su2_dev_sanchez.pdf (missing file)", + "tutorials/event_content/index.html: documents/su2_dev_vanderweide.pdf (missing file)" + ], + "enum_values": [ + "_tutorials/structural_mechanics/Linear_Elasticity/Linear_Elasticity.md: OUTPUT_FILES= (RESTART, PARAVIEW_BINARY) (unknown keyword PARAVIEW_BINARY)" + ] +} diff --git a/Docs/website/tools/check_site.py b/Docs/website/tools/check_site.py new file mode 100644 index 000000000000..1f6e0303d401 --- /dev/null +++ b/Docs/website/tools/check_site.py @@ -0,0 +1,172 @@ +#!/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) + 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"]))} + (args.site / "docs-check.json").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 index 6056e6fd662f..96704840b652 100644 --- a/Docs/website/tools/rebase_links.py +++ b/Docs/website/tools/rebase_links.py @@ -38,12 +38,13 @@ def main(): if len(sys.argv) != 3: print("usage: rebase_links.py ") return 1 - root, base = sys.argv[1], "/" + sys.argv[2].strip("/") + 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 rest.startswith(base.lstrip("/") + "/"): + if base and rest.startswith(base.lstrip("/") + "/"): return m.group(0) if rest.startswith(REMOTE_FOLDERS): return m.group(1) + REAL_SITE + "/" diff --git a/Docs/website/tools/test_docs_checks.py b/Docs/website/tools/test_docs_checks.py new file mode 100644 index 000000000000..744e334f5379 --- /dev/null +++ b/Docs/website/tools/test_docs_checks.py @@ -0,0 +1,96 @@ +"""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_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_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() From f9baca946230ad12de258dbe2665582c1103229b Mon Sep 17 00:00:00 2001 From: rois1995 Date: Thu, 22 Oct 2026 16:11:35 +0200 Subject: [PATCH 16/25] Track documentation improvements, evidence and remaining priorities --- Docs/website/README.md | 2 + Docs/website/pages/Documentation-Status.md | 116 +++++++++++++++++++++ Docs/website/tools/check_site.py | 5 +- Docs/website/tools/test_docs_checks.py | 15 +++ 4 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 Docs/website/pages/Documentation-Status.md diff --git a/Docs/website/README.md b/Docs/website/README.md index 8db6b7511575..bd0d9bb53a44 100644 --- a/Docs/website/README.md +++ b/Docs/website/README.md @@ -15,6 +15,8 @@ The website's **Using the Documentation** page explains this structure to reader ## Editing +Keep completed improvements, remaining gaps and their evidence in `pages/Documentation-Status.md`, linked from **Develop** and **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. diff --git a/Docs/website/pages/Documentation-Status.md b/Docs/website/pages/Documentation-Status.md new file mode 100644 index 000000000000..26806a1b872c --- /dev/null +++ b/Docs/website/pages/Documentation-Status.md @@ -0,0 +1,116 @@ +--- +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: 22 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) +- [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 | Implemented; review needed | 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; review needed | 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. + +## What is still missing + +The quantities below are a snapshot from the same milestone. The source list of accepted historical findings is [check-baseline.json](https://github.com/rois1995/SU2/blob/a14b4df35d3863fc79b48c781fc83b8cb222d912/Docs/website/tools/check-baseline.json); current link findings also appear in the generated check results. + +| Priority | Remaining work | What completion requires | +| --- | --- | --- | +| Next | Repair the 180 historical link findings, including missing assets, old anchors and paths escaping the preview base URL. | Correct destinations or restore the intended assets, pass the checks, and remove each resolved baseline entry. Do not hide a new failure by regenerating the baseline. | +| Next | Investigate eight template names unknown to the C++ option registrations and the historical enum keyword `PARAVIEW_BINARY`. | Establish which parser owns each setting, correct obsolete examples where appropriate, and check the resulting configuration against the applicable SU2 version. | +| 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 | Review the nine draft setup guides and physical model explanations. | Record the reviewer, checked version, scope and evidence. Retain draft/review labels until that review is complete; do not generalize a short regression configuration into an engineering recommendation. | +| 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.** Repair historical links and configuration examples in small groups; remove only the entries actually resolved. This gives a measurable reduction in existing debt. +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, a link from Develop 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. | + +## 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). The [Develop page]({{ '/develop.html' | relative_url }}) provides another entry point to this status record. + +Last checked against SU2 develop `a14b4df35d`. diff --git a/Docs/website/tools/check_site.py b/Docs/website/tools/check_site.py index 1f6e0303d401..1ebac09bedba 100644 --- a/Docs/website/tools/check_site.py +++ b/Docs/website/tools/check_site.py @@ -154,13 +154,16 @@ def main(): 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"]))} - (args.site / "docs-check.json").write_text(json.dumps(report, indent=2) + "\n") + 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( diff --git a/Docs/website/tools/test_docs_checks.py b/Docs/website/tools/test_docs_checks.py index 744e334f5379..635c6863714f 100644 --- a/Docs/website/tools/test_docs_checks.py +++ b/Docs/website/tools/test_docs_checks.py @@ -79,6 +79,21 @@ def test_rebase_links_handles_root_and_subpath_without_double_prefix(self): 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) From f6f948be41865d103ff556e6d03ffa9eca15cb79 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Thu, 22 Oct 2026 16:55:24 +0200 Subject: [PATCH 17/25] Keep documentation status under Developer Docs --- Docs/website/README.md | 2 +- Docs/website/pages/Documentation-Status.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Docs/website/README.md b/Docs/website/README.md index bd0d9bb53a44..d6108af90984 100644 --- a/Docs/website/README.md +++ b/Docs/website/README.md @@ -15,7 +15,7 @@ The website's **Using the Documentation** page explains this structure to reader ## Editing -Keep completed improvements, remaining gaps and their evidence in `pages/Documentation-Status.md`, linked from **Develop** and **Developer Docs**. Update its status and dated history when a batch is completed; use the generated reports for current build counts. +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/`. diff --git a/Docs/website/pages/Documentation-Status.md b/Docs/website/pages/Documentation-Status.md index 26806a1b872c..eb00c5d7e865 100644 --- a/Docs/website/pages/Documentation-Status.md +++ b/Docs/website/pages/Documentation-Status.md @@ -98,7 +98,7 @@ These are proposed next priorities. The page records completed work; it does not | 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, a link from Develop 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 | 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. | ## Keeping this page current @@ -111,6 +111,6 @@ For each completed batch: 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). The [Develop page]({{ '/develop.html' | relative_url }}) provides another entry point to this status record. +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). Last checked against SU2 develop `a14b4df35d`. From d39c1eadbe3ce636f2f8dfa4a881bc8c2180a537 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Thu, 22 Oct 2026 20:30:30 +0200 Subject: [PATCH 18/25] docs: correct template options and record source review --- Docs/website/README.md | 2 +- Docs/website/pages/Documentation-Status.md | 42 ++++- Docs/website/tools/check-baseline.json | 200 +-------------------- Docs/website/tools/check_site.py | 4 +- Docs/website/tools/test_docs_checks.py | 13 ++ config_template.cfg | 29 ++- 6 files changed, 65 insertions(+), 225 deletions(-) diff --git a/Docs/website/README.md b/Docs/website/README.md index d6108af90984..c612298fe315 100644 --- a/Docs/website/README.md +++ b/Docs/website/README.md @@ -62,7 +62,7 @@ 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` records pre-existing broken links, eight template names unknown to the C++ registration, and 24 options missing source descriptions, and historical enum-keyword findings. 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. +`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: diff --git a/Docs/website/pages/Documentation-Status.md b/Docs/website/pages/Documentation-Status.md index eb00c5d7e865..c377ff991b01 100644 --- a/Docs/website/pages/Documentation-Status.md +++ b/Docs/website/pages/Documentation-Status.md @@ -12,6 +12,7 @@ For the current build, see [Docs Check](../Docs-Check/), the [source and tool ma - [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) @@ -29,8 +30,8 @@ For the current build, see [Docs Check](../Docs-Check/), the [source and tool ma | 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 | Implemented; review needed | 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; review needed | 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. | +| 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. | @@ -64,16 +65,40 @@ These numbers describe that milestone, not a permanent total. Use the current ge 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 quantities below are a snapshot from the same milestone. The source list of accepted historical findings is [check-baseline.json](https://github.com/rois1995/SU2/blob/a14b4df35d3863fc79b48c781fc83b8cb222d912/Docs/website/tools/check-baseline.json); current link findings also appear in the generated check results. +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 | Repair the 180 historical link findings, including missing assets, old anchors and paths escaping the preview base URL. | Correct destinations or restore the intended assets, pass the checks, and remove each resolved baseline entry. Do not hide a new failure by regenerating the baseline. | -| Next | Investigate eight template names unknown to the C++ option registrations and the historical enum keyword `PARAVIEW_BINARY`. | Establish which parser owns each setting, correct obsolete examples where appropriate, and check the resulting configuration against the applicable SU2 version. | | 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 | Review the nine draft setup guides and physical model explanations. | Record the reviewer, checked version, scope and evidence. Retain draft/review labels until that review is complete; do not generalize a short regression configuration into an engineering recommendation. | +| 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. | @@ -83,7 +108,7 @@ The quantities below are a snapshot from the same milestone. The source list of ## How to go further -1. **Keep the new checks passing.** Repair historical links and configuration examples in small groups; remove only the entries actually resolved. This gives a measurable reduction in existing debt. +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. @@ -99,6 +124,7 @@ These are proposed next priorities. The page records completed work; it does not | 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. | ## Keeping this page current @@ -113,4 +139,4 @@ For each completed batch: 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). -Last checked against SU2 develop `a14b4df35d`. +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/tools/check-baseline.json b/Docs/website/tools/check-baseline.json index e2de71750ab1..17230ebf0cfd 100644 --- a/Docs/website/tools/check-baseline.json +++ b/Docs/website/tools/check-baseline.json @@ -1,15 +1,6 @@ { - "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.", - "template_options": [ - "BUFFET_MONITORING", - "DEFORM_POISSON_RATIO", - "HOLD_GRID_FIXED_COORDS", - "IGNITION_TEMPERATURE", - "NETTHRUST_DBCTHRUST", - "SINGLEZONE_DRIVER", - "SMOOTH_GEOMETRY", - "THERMAL_DIFFUSIVITY" - ], + "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", @@ -36,189 +27,6 @@ "TOPOL_OPTIM_SIMP_EXPONENT", "TOPOL_OPTIM_SIMP_MINSTIFF" ], - "links": [ - "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/2D_MidSpan_RelMach.png (missing file)", - "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/FullAnnulus.png (missing file)", - "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/FullAnnulus_MachContour.png (missing file)", - "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/Full_View_MachContour.png (missing file)", - "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/SingleMesh_Adiabatic_Giles.png (missing file)", - "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/SingleMesh_Adiabatic_Giles_MixingPlane.png (missing file)", - "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/SingleMesh_Adiabatic_NoSlip.png (missing file)", - "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/SingleMesh_Adiabatic_Periodics.png (missing file)", - "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/SingleMesh_shroud.png (missing file)", - "tutorials/Aachen_Turbine/index.html: /tutorials_files/compressible_flow/Aachen_Turbine/images/SinglePassage.png (missing file)", - "tutorials/Aachen_Turbine/index.html: tutorials/Actuator_Disk/ (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/ContourCp.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/DeltaCp.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/Domain.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/GridDisk.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/GridSpinner.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/GridXY.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/MachNumber.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/MomentumX.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/PressureCoeff.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/SwirlAndCp.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/Swirl_Vectors.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/Vt.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/Vt_x.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/dcp_input.png (missing file)", - "tutorials/ActuatorDisk_VariableLoad/index.html: /tutorials_files/compressible_flow/ActuatorDisk_VariableLoad/images/dct_input.png (missing file)", - "tutorials/Adjoint_FSI_Python/index.html: tutorials/multiphysics/images/fsipyadj1.png (missing file)", - "tutorials/Basic_RHT/index.html: tutorials/multiphysics/images/rht1.png (missing file)", - "tutorials/Basic_RHT/index.html: tutorials/multiphysics/images/rht2.png (missing file)", - "tutorials/Basic_RHT/index.html: tutorials/multiphysics/images/rht3.png (missing file)", - "tutorials/Basic_RHT/index.html: tutorials/multiphysics/images/rht4.png (missing file)", - "tutorials/Basic_RHT/index.html: tutorials/multiphysics/images/rht5.png (missing file)", - "tutorials/Coupled_RHT_Adjoint/index.html: tutorials/multiphysics/images/rhtadj1.png (missing file)", - "tutorials/Coupled_RHT_Adjoint/index.html: tutorials/multiphysics/images/rhtadj2.png (missing file)", - "tutorials/Coupled_RHT_Adjoint/index.html: tutorials/multiphysics/images/rhtadj3.png (missing file)", - "tutorials/Coupled_RHT_Adjoint/index.html: tutorials/multiphysics/images/rhtadj4.png (missing file)", - "tutorials/Coupled_RHT_Adjoint/index.html: tutorials/multiphysics/images/rhtadj5.png (missing file)", - "tutorials/Coupled_RHT_Adjoint/index.html: tutorials/multiphysics/images/rhtadj6.png (missing file)", - "tutorials/Dynamics/index.html: tutorials/structural_mechanics/images/dynamic_linear.gif (missing file)", - "tutorials/Dynamics/index.html: tutorials/structural_mechanics/images/lin1.png (missing file)", - "tutorials/Dynamics/index.html: tutorials/structural_mechanics/images/lin2.png (missing file)", - "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/MLP_PD.png (missing file)", - "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/MLP_PNO.png (missing file)", - "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/MLP_SPV.png (missing file)", - "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/MLP_TD1.png (missing file)", - "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/MLP_TD2.png (missing file)", - "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/SolverProcess.png (missing file)", - "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/T_Qdot.png (missing file)", - "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/ignition_location.png (missing file)", - "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/mesh.png (missing file)", - "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/mixfrac_NO.png (missing file)", - "tutorials/Inc_Combustion/index.html: /tutorials_files/incompressible_flow/Inc_Combustion/pv_enth.png (missing file)", - "tutorials/Inc_Combustion/index.html: docs_v8/Download/ (missing file)", - "tutorials/Inc_Combustion/index.html: docs_v8/Installation/ (missing file)", - "tutorials/Inc_Combustion/index.html: tutorials/Inc_Combustion/index.html#solutionprocess (missing anchor)", - "tutorials/Inc_Heated_Cylinders_Unsteady/index.html: tutorials/Inc_Heated_Cylinders/ (missing file)", - "tutorials/Inc_Inviscid_Hydrofoil/index.html: tutorials/Actuator_Disk/ (missing file)", - "tutorials/Inc_Laminar_Cavity/index.html: tutorials/Inc_Laminar_Cavity/incompressible_flow/https:/github.com/su2code/Tutorials/tree/master/incompressible_flow/Inc_Laminar_Cavity (missing file)", - "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/ZH_fields.png (missing file)", - "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/ZH_lines_Temperature.png (missing file)", - "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_S56_CH4.png (missing file)", - "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_S56_CO.png (missing file)", - "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_S56_CO2.png (missing file)", - "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_S56_Z_T.png (missing file)", - "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_S56_temperature.png (missing file)", - "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_diffusion_convergence.png (missing file)", - "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/counterflow_diffusion_flame_temperature_streamlines.png (missing file)", - "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/mesh.png (missing file)", - "tutorials/Inc_Laminar_Diffusion_Flame/index.html: /tutorials_files/incompressible_flow/Inc_Laminar_Diffusion_Flame/opflow01-1024x766_thomsonlab.jpg (missing file)", - "tutorials/Inc_Turbulent_Bend_Opt/index.html: /tutorials_files/design_features/Inc_Turbulent_Bend/bend_with_FFD.png (missing file)", - "tutorials/Inc_Turbulent_Bend_Opt/index.html: /tutorials_files/design_features/Inc_Turbulent_Bend/mesh_deformation.png (missing file)", - "tutorials/Inc_Turbulent_Bend_Opt/index.html: /tutorials_files/design_features/Inc_Turbulent_Bend/opt_iter0_pressure_ffdbox.png (missing file)", - "tutorials/Inc_Turbulent_Bend_Opt/index.html: /tutorials_files/design_features/Inc_Turbulent_Bend/opt_iter5_pressure_ffdbox.png (missing file)", - "tutorials/Inc_Turbulent_Flat_Plate/index.html: tutorials_files/incompressible_flow/Inc_Turbulent_Flat_Plate/mesh_flatplate_turb_545x385.su2 (missing file)", - "tutorials/Inc_Turbulent_Flat_Plate/index.html: tutorials_files/incompressible_flow/Inc_Turbulent_Flat_Plate/turb_flatplate.cfg (missing file)", - "tutorials/Inc_Urban_City/index.html: /tutorials_files/incompressible_flow/Inc_Urban_City/images/amsterdam_buildings.png (missing file)", - "tutorials/Inc_Urban_City/index.html: /tutorials_files/incompressible_flow/Inc_Urban_City/images/fire_zoom.png (missing file)", - "tutorials/Inc_Urban_City/index.html: /tutorials_files/incompressible_flow/Inc_Urban_City/images/gmsh_mesh.png (missing file)", - "tutorials/Inc_Urban_City/index.html: /tutorials_files/incompressible_flow/Inc_Urban_City/images/gmsh_mesh_zoom.png (missing file)", - "tutorials/Inc_Urban_City/index.html: /tutorials_files/incompressible_flow/Inc_Urban_City/images/wind_velocity.png (missing file)", - "tutorials/Inc_Urban_City/index.html: /tutorials_files/incompressible_flow/Inc_Urban_City/images/wind_velocity_zoom.png (missing file)", - "tutorials/Inc_Urban_City/index.html: tutorials/Inc_Urban_City/gmsh_mesh (missing file)", - "tutorials/Inc_Urban_City/index.html: tutorials/Inc_Urban_City/gmsh_mesh_zoom (missing file)", - "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/mesh.png (missing file)", - "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/particles.png (missing file)", - "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/strouhal_cylinder_karman.png (missing file)", - "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/strouhal_cylinder_karman_Re.png (missing file)", - "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/strouhal_cylinder_karman_variation.png (missing file)", - "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/unsteady_cylinder.gif (missing file)", - "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/unsteady_cylinder.mp4 (missing file)", - "tutorials/Inc_Von_Karman/index.html: /tutorials_files/incompressible_flow/Inc_Von_Karman/images/unsteady_cylinder_arrow.gif (missing file)", - "tutorials/NICFD_nozzle_datadriven/index.html: tutorials/NICFD_nozzle_datadriven/index.html#mesh_highlight (missing anchor)", - "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/contact_resistance_effect.png (missing file)", - "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/problem_mesh.png (missing file)", - "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/problem_setup.png (missing file)", - "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/solid_domains.png (missing file)", - "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/solution_temperature.png (missing file)", - "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/solution_temperature_lines.png (missing file)", - "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/solution_temperature_lines_n.png (missing file)", - "tutorials/SS_CR_CHT/index.html: /tutorials_files/multiphysics/contact_resistance_cht/solution_temperature_n.png (missing file)", - "tutorials/SS_CR_CHT/index.html: docs_v8/Download/ (missing file)", - "tutorials/SS_CR_CHT/index.html: docs_v8/Installation/ (missing file)", - "tutorials/Static_FSI_Python/index.html: tutorials/multiphysics/images/fsipy1.png (missing file)", - "tutorials/Static_FSI_Python/index.html: tutorials/multiphysics/images/fsipy2.png (missing file)", - "tutorials/Static_FSI_Python/index.html: tutorials/multiphysics/images/fsipy3.png (missing file)", - "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_c_correction.png (missing file)", - "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_source.png (missing file)", - "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_source_correction.png (missing file)", - "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_temp.png (missing file)", - "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_temp_correction.png (missing file)", - "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_temp_heatloss.png (missing file)", - "tutorials/TFC_python/index.html: /tutorials_files/multiphysics/TFC_python/images/TFC_temp_init.png (missing file)", - "tutorials/Transitional_Flat_Plate_T3A/index.html: /tutorials_files/compressible_flow/Transitional_Flat_Plate/images/LM_flat_plate/Boundary_conditions.png (missing file)", - "tutorials/Transitional_Flat_Plate_T3A/index.html: /tutorials_files/compressible_flow/Transitional_Flat_Plate/images/LM_flat_plate/Cf_T3A-.png (missing file)", - "tutorials/Transitional_Flat_Plate_T3A/index.html: /tutorials_files/compressible_flow/Transitional_Flat_Plate/images/LM_flat_plate/Cf_T3A.png (missing file)", - "tutorials/Turbulent_RHT_CHT/index.html: tutorials/Inc_Heated_Cylinders (missing file)", - "tutorials/Turbulent_RHT_CHT/index.html: tutorials/multiphysics/images/chtrht1.png (missing file)", - "tutorials/Turbulent_RHT_CHT/index.html: tutorials/multiphysics/images/chtrht2.png (missing file)", - "tutorials/Turbulent_RHT_CHT/index.html: tutorials/multiphysics/images/chtrht3.png (missing file)", - "tutorials/Turbulent_RHT_CHT/index.html: tutorials/multiphysics/images/chtrht4.png (missing file)", - "tutorials/Turbulent_RHT_CHT/index.html: tutorials/multiphysics/images/chtrht5.png (missing file)", - "tutorials/event_content/index.html: documents/Design_and_Optimization_Using_SU2.pdf (missing file)", - "tutorials/event_content/index.html: documents/Introduction_to_the_SU2_Code_Structure.pdf (missing file)", - "tutorials/event_content/index.html: documents/LM-1021-EulerMesh.pw (missing file)", - "tutorials/event_content/index.html: documents/ONERAM6.zip (missing file)", - "tutorials/event_content/index.html: documents/OpenMDAO_SU2_Workshop_Agenda.pdf (missing file)", - "tutorials/event_content/index.html: documents/Optimization_Using_SU2.pdf (missing file)", - "tutorials/event_content/index.html: documents/Pointwise-Stanford-Webinar-Slides_20140429.pdf (missing file)", - "tutorials/event_content/index.html: documents/Running_SU2.pdf (missing file)", - "tutorials/event_content/index.html: documents/SU2_AdvancedAnalysis.pdf (missing file)", - "tutorials/event_content/index.html: documents/SU2_Advanced_Topics.zip (missing file)", - "tutorials/event_content/index.html: documents/SU2_Dev_Meeting_Agenda_20171218.pdf (missing file)", - "tutorials/event_content/index.html: documents/SU2_IntroInstallation.pdf (missing file)", - "tutorials/event_content/index.html: documents/SU2_Workshop_Agenda_v2.0.pdf (missing file)", - "tutorials/event_content/index.html: documents/SU2_as_a_High-fidelity_Analysis_Tool.pdf (missing file)", - "tutorials/event_content/index.html: documents/Task-Based_Design_Evaluation_with_SU2.pdf (missing file)", - "tutorials/event_content/index.html: documents/Welcome_and_introduction_to_SU2.pdf (missing file)", - "tutorials/event_content/index.html: documents/WorkshopFeb2017/Part2_CodeStructure.pdf (missing file)", - "tutorials/event_content/index.html: documents/WorkshopFeb2017/PythonMods.pdf (missing file)", - "tutorials/event_content/index.html: documents/WorkshopFeb2017/Tutorial1Presentation.pdf (missing file)", - "tutorials/event_content/index.html: documents/WorkshopFeb2017/Tutorial1_OneraM6Files.zip (missing file)", - "tutorials/event_content/index.html: documents/WorkshopFeb2017/WorkshopTutorial2.pdf (missing file)", - "tutorials/event_content/index.html: documents/WorkshopFeb2017/WorkshopTutorial2naca.zip (missing file)", - "tutorials/event_content/index.html: documents/WorkshopFeb2017/WorkshopTutorial2oneram6.zip (missing file)", - "tutorials/event_content/index.html: documents/WorkshopFeb2017/compute_Mpolar.py (missing file)", - "tutorials/event_content/index.html: documents/WorkshopFeb2017/dev_process.pdf (missing file)", - "tutorials/event_content/index.html: documents/WorkshopFeb2017/welcome_and_intro.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_albring.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_alonso.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_drikakis.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_economon.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_fossati.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_gori.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_mishra.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_molina.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_palacios.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_pini.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2017/su2_dev_vanderweide.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/00_su2_hackathon_economon.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/01_su2_dev_alonso_economon.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/02_su2_dev_fossati.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/03_su2_dev_beishuizen.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/04_su2_dev_burghardt.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/05_su2_dev_mukhopadhaya.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/06_su2_dev_zhou.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/07_su2_dev_vanderweide_singh.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/08_su2_dev_rubino.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/09_su2_dev_morelli.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/10_su2_dev_loseille.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/11_su2_dev_economon.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/12_su2_dev_koodlyravishankara.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/13_su2_dev_albring_sanchez.pdf (missing file)", - "tutorials/event_content/index.html: documents/dev_meeting_2018/SU2_Dev_Meeting_PresentationAgenda_20180916.pdf (missing file)", - "tutorials/event_content/index.html: documents/su2_dev_alonso.pdf (missing file)", - "tutorials/event_content/index.html: documents/su2_dev_economon.pdf (missing file)", - "tutorials/event_content/index.html: documents/su2_dev_gauger.pdf (missing file)", - "tutorials/event_content/index.html: documents/su2_dev_guardone.pdf (missing file)", - "tutorials/event_content/index.html: documents/su2_dev_pini.pdf (missing file)", - "tutorials/event_content/index.html: documents/su2_dev_sanchez.pdf (missing file)", - "tutorials/event_content/index.html: documents/su2_dev_vanderweide.pdf (missing file)" - ], - "enum_values": [ - "_tutorials/structural_mechanics/Linear_Elasticity/Linear_Elasticity.md: OUTPUT_FILES= (RESTART, PARAVIEW_BINARY) (unknown keyword PARAVIEW_BINARY)" - ] + "links": [], + "enum_values": [] } diff --git a/Docs/website/tools/check_site.py b/Docs/website/tools/check_site.py index 1ebac09bedba..80f297501697 100644 --- a/Docs/website/tools/check_site.py +++ b/Docs/website/tools/check_site.py @@ -88,9 +88,9 @@ def check_enum_values(source, options, maps): continue for name, value in settings: value = value.strip() - if not choices.get(name) or not re.fullmatch(r"[A-Za-z0-9_, ()]+", value): + 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) + keywords = re.findall(r"[A-Za-z0-9_-]+", value) for keyword in set(keywords) - choices[name]: if keyword.startswith("__") and keyword.endswith("__"): continue diff --git a/Docs/website/tools/test_docs_checks.py b/Docs/website/tools/test_docs_checks.py index 635c6863714f..821522349263 100644 --- a/Docs/website/tools/test_docs_checks.py +++ b/Docs/website/tools/test_docs_checks.py @@ -65,6 +65,19 @@ def test_config_enum_values_ignore_source_code_and_check_disabled_variants(self) 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" 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 From e83bee8ca3dbe47014db34f9966f496d08fab4da Mon Sep 17 00:00:00 2001 From: rois1995 Date: Fri, 23 Oct 2026 07:29:40 +0200 Subject: [PATCH 19/25] docs: record the hybrid RANS/LES V&V case and model page update Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013YFdgQwrRqCQ2ihG446aNe --- Docs/website/pages/Documentation-Status.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Docs/website/pages/Documentation-Status.md b/Docs/website/pages/Documentation-Status.md index c377ff991b01..7a132277ff26 100644 --- a/Docs/website/pages/Documentation-Status.md +++ b/Docs/website/pages/Documentation-Status.md @@ -125,6 +125,7 @@ These are proposed next priorities. The page records completed work; it does not | 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; the flow picture is still to be added. | ## Keeping this page current From 7e1dbf5ee6b90aad3aa63cc5564b9e87903dd0e9 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Mon, 26 Oct 2026 10:27:30 +0100 Subject: [PATCH 20/25] docs: update NACA0021 review status before contribution --- Docs/website/pages/Documentation-Status.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Docs/website/pages/Documentation-Status.md b/Docs/website/pages/Documentation-Status.md index 7a132277ff26..fb17991d5292 100644 --- a/Docs/website/pages/Documentation-Status.md +++ b/Docs/website/pages/Documentation-Status.md @@ -5,7 +5,7 @@ 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: 22 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. +**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. @@ -125,7 +125,8 @@ These are proposed next priorities. The page records completed work; it does not | 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; the flow picture is still to be added. | +| 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. | ## Keeping this page current From 4bec4ab2cb840da9fafc409944743aa2a59199bb Mon Sep 17 00:00:00 2001 From: rois1995 Date: Mon, 26 Oct 2026 10:52:33 +0100 Subject: [PATCH 21/25] docs: check the prepared PR branches without upstream deployment --- .github/workflows/docs-website.yml | 12 ++++++------ Docs/website/README.md | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docs-website.yml b/.github/workflows/docs-website.yml index 37f2de792fab..e5a78bca2492 100644 --- a/.github/workflows/docs-website.yml +++ b/.github/workflows/docs-website.yml @@ -2,9 +2,9 @@ name: Docs Website on: push: - branches: ['docs_site'] + branches: ['docs_site', 'docs/developer-guides'] pull_request: - branches: ['docs_site'] + branches: ['docs_site', 'develop'] workflow_dispatch: inputs: site_repo: @@ -12,7 +12,7 @@ on: default: rois1995/su2code.github.io site_ref: description: Website commit SHA or branch (use a SHA to reproduce a build) - default: docs_site + default: docs/user-guides permissions: contents: read @@ -38,7 +38,7 @@ jobs: uses: actions/checkout@v4 with: repository: ${{ inputs.site_repo || 'rois1995/su2code.github.io' }} - ref: ${{ inputs.site_ref || 'docs_site' }} + ref: ${{ inputs.site_ref || 'docs/user-guides' }} path: site fetch-depth: 0 filter: blob:none @@ -63,13 +63,13 @@ jobs: path: ${{ runner.temp }}/docs-site/site retention-days: 7 - name: Upload Pages artifact - if: github.event_name != 'pull_request' + 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.event_name != 'pull_request' + if: github.repository == 'rois1995/SU2' && github.ref == 'refs/heads/docs_site' && github.event_name != 'pull_request' needs: build runs-on: ubuntu-24.04 permissions: diff --git a/Docs/website/README.md b/Docs/website/README.md index c612298fe315..4f272818decf 100644 --- a/Docs/website/README.md +++ b/Docs/website/README.md @@ -73,9 +73,9 @@ For a reproducible rebuild, check out the two recorded SHAs, select the recorded ## CI and publishing -`.github/workflows/docs-website.yml` checks SU2 `docs_site` pushes and pull requests. Pull requests build an artifact without deploying. Successful pushes and manual runs publish the checked artifact to this fork's Pages site. Checkout references are resolved once; the manifest records the exact revisions used. +`.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 its own pushes/pull requests against SU2 `docs_site` 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`: +The website repository's `docs-check.yml` checks pushes to `docs_site`, `docs/user-guides` and `docs/naca0021`, and pull requests targeting `master`, `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= @@ -83,4 +83,4 @@ gh workflow run docs-website.yml --repo rois1995/SU2 --ref docs_site -f site_rep 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. -Both workflows target the `rois1995` forks. Do not push or publish these development changes to the official repositories. +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. From 804c55e12c98b001969500df42447d4620801d67 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Mon, 26 Oct 2026 11:15:44 +0100 Subject: [PATCH 22/25] docs: document develop as the website PR target --- Docs/website/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/website/README.md b/Docs/website/README.md index 4f272818decf..7c82614a9dd2 100644 --- a/Docs/website/README.md +++ b/Docs/website/README.md @@ -75,7 +75,7 @@ For a reproducible rebuild, check out the two recorded SHAs, select the recorded `.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 `master`, `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`: +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= From f724d1dbbe366f32e2628509c1a029f02a8c0b82 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Mon, 26 Oct 2026 11:29:58 +0100 Subject: [PATCH 23/25] docs: format website tools with repository hooks --- Docs/website/tools/build_docs.py | 203 ++- Docs/website/tools/build_site.py | 2066 +++++++++++++++--------- Docs/website/tools/check_site.py | 142 +- Docs/website/tools/rebase_links.py | 58 +- Docs/website/tools/test_build_site.py | 19 +- Docs/website/tools/test_docs_checks.py | 104 +- 6 files changed, 1684 insertions(+), 908 deletions(-) diff --git a/Docs/website/tools/build_docs.py b/Docs/website/tools/build_docs.py index b33946e25ce5..62e54434a9a6 100644 --- a/Docs/website/tools/build_docs.py +++ b/Docs/website/tools/build_docs.py @@ -18,19 +18,27 @@ def output(command, **kwargs): def revision(path): - return {"commit": output(["git", "-C", str(path), "rev-parse", "HEAD"]), - "dirty": bool(output(["git", "-C", str(path), "status", "--porcelain"]))} + return { + "commit": output(["git", "-C", str(path), "rev-parse", "HEAD"]), + "dirty": bool(output(["git", "-C", str(path), "status", "--porcelain"])), + } def prepare_directory(destination, sources): destination = destination.resolve() for source in sources: source = source.resolve() - if destination == source or destination in source.parents or source in destination.parents: + if ( + destination == source + or destination in source.parents + or source in destination.parents + ): raise ValueError("Build directory must be separate from both source trees") marker = destination / ".su2-docs-build" if destination.exists() and any(destination.iterdir()) and not marker.is_file(): - raise ValueError("Refusing to reuse a nonempty directory without .su2-docs-build") + raise ValueError( + "Refusing to reuse a nonempty directory without .su2-docs-build" + ) destination.mkdir(parents=True, exist_ok=True) marker.touch() for name in ("source", "site", "doxygen"): @@ -44,61 +52,182 @@ def prepare_directory(destination, sources): def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--su2", type=Path, default=TOOLS.parents[2]) - parser.add_argument("--site", type=Path, required=True, help="website source checkout (not modified)") + parser.add_argument( + "--site", + type=Path, + required=True, + help="website source checkout (not modified)", + ) parser.add_argument("--build-dir", type=Path, required=True) parser.add_argument("--baseurl", default="/SU2") - parser.add_argument("--mark-changes", action="store_true", help="keep red preview highlighting") + parser.add_argument( + "--mark-changes", action="store_true", help="keep red preview highlighting" + ) args = parser.parse_args() - su2, website, work = args.su2.resolve(), args.site.resolve(), args.build_dir.resolve() - if not (su2 / "Common/src/CConfig.cpp").is_file() or not (website / "_config.yml").is_file(): + su2, website, work = ( + args.su2.resolve(), + args.site.resolve(), + args.build_dir.resolve(), + ) + if ( + not (su2 / "Common/src/CConfig.cpp").is_file() + or not (website / "_config.yml").is_file() + ): parser.error("--su2 and --site must point to their respective source checkouts") expected = (website / ".ruby-version").read_text().strip() actual = output(["ruby", "-e", "print RUBY_VERSION"]) if actual != expected: - parser.error("Use Ruby {} from the website .ruby-version (found {})".format(expected, actual)) + parser.error( + "Use Ruby {} from the website .ruby-version (found {})".format( + expected, actual + ) + ) for executable in ("bundle", "doxygen", "dot"): if not shutil.which(executable): parser.error("Required tool not found: " + executable) sources = {"su2": revision(su2), "website": revision(website)} - epoch = max(int(output(["git", "-C", str(p), "show", "-s", "--format=%ct", "HEAD"])) for p in (su2, website)) - env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1", SOURCE_DATE_EPOCH=str(epoch), BUNDLE_FROZEN="true") - subprocess.run([sys.executable, "-m", "unittest", "discover", "-s", str(TOOLS)], env=env, check=True) + epoch = max( + int(output(["git", "-C", str(p), "show", "-s", "--format=%ct", "HEAD"])) + for p in (su2, website) + ) + env = dict( + os.environ, + PYTHONDONTWRITEBYTECODE="1", + SOURCE_DATE_EPOCH=str(epoch), + BUNDLE_FROZEN="true", + ) + subprocess.run( + [sys.executable, "-m", "unittest", "discover", "-s", str(TOOLS)], + env=env, + check=True, + ) prepare_directory(work, (su2, website)) source, site = work / "source", work / "site" - shutil.copytree(website, source, ignore=shutil.ignore_patterns( - ".git", ".github", "_site", ".jekyll-cache", ".jekyll-metadata", ".bundle", "vendor", - "documents", "download", "emails", "__pycache__")) + shutil.copytree( + website, + source, + ignore=shutil.ignore_patterns( + ".git", + ".github", + "_site", + ".jekyll-cache", + ".jekyll-metadata", + ".bundle", + "vendor", + "documents", + "download", + "emails", + "__pycache__", + ), + ) env["BUNDLE_GEMFILE"] = str(source / "Gemfile") subprocess.run(["bundle", "check"], cwd=source, env=env, check=True) - flags = {"OUTPUT_DIRECTORY": str(work / "doxygen"), "PROJECT_NUMBER": sources["su2"]["commit"][:10], - "SOURCE_BROWSER": "NO", "CALL_GRAPH": "NO", "CALLER_GRAPH": "NO", "COLLABORATION_GRAPH": "NO", - "INCLUDE_GRAPH": "NO", "INCLUDED_BY_GRAPH": "NO", "DIRECTORY_GRAPH": "NO", "DOT_IMAGE_FORMAT": "svg", - "WARN_LOGFILE": str(work / "doxygen_warnings.txt"), "EXCLUDE": "./Docs/website"} - config = (su2 / "doxyfile").read_text() + "\n" + "\n".join(k + ' = "' + v + '"' for k, v in flags.items()) + flags = { + "OUTPUT_DIRECTORY": str(work / "doxygen"), + "PROJECT_NUMBER": sources["su2"]["commit"][:10], + "SOURCE_BROWSER": "NO", + "CALL_GRAPH": "NO", + "CALLER_GRAPH": "NO", + "COLLABORATION_GRAPH": "NO", + "INCLUDE_GRAPH": "NO", + "INCLUDED_BY_GRAPH": "NO", + "DIRECTORY_GRAPH": "NO", + "DOT_IMAGE_FORMAT": "svg", + "WARN_LOGFILE": str(work / "doxygen_warnings.txt"), + "EXCLUDE": "./Docs/website", + } + config = ( + (su2 / "doxyfile").read_text() + + "\n" + + "\n".join(k + ' = "' + v + '"' for k, v in flags.items()) + ) print("Building Doxygen; log: " + str(work / "doxygen.log"), flush=True) with (work / "doxygen.log").open("w") as log: - subprocess.run(["doxygen", "-"], cwd=su2, input=config + "\n", text=True, stdout=log, - stderr=subprocess.STDOUT, env=env, check=True) - command = [sys.executable, str(TOOLS / "build_site.py"), "--su2", str(su2), "--site", str(source), - "--light", "--doxygen-log", str(work / "doxygen_warnings.txt")] + subprocess.run( + ["doxygen", "-"], + cwd=su2, + input=config + "\n", + text=True, + stdout=log, + stderr=subprocess.STDOUT, + env=env, + check=True, + ) + command = [ + sys.executable, + str(TOOLS / "build_site.py"), + "--su2", + str(su2), + "--site", + str(source), + "--light", + "--doxygen-log", + str(work / "doxygen_warnings.txt"), + ] if args.mark_changes: command += ["--mark-changes", "--compare-site", str(website)] subprocess.run(command, env=env, check=True) - subprocess.run(["bundle", "exec", "jekyll", "build", "--baseurl", args.baseurl, - "--destination", str(site)], cwd=source, env=env, check=True) - subprocess.run([sys.executable, str(TOOLS / "rebase_links.py"), str(site), args.baseurl], env=env, check=True) + subprocess.run( + [ + "bundle", + "exec", + "jekyll", + "build", + "--baseurl", + args.baseurl, + "--destination", + str(site), + ], + cwd=source, + env=env, + check=True, + ) + subprocess.run( + [sys.executable, str(TOOLS / "rebase_links.py"), str(site), args.baseurl], + env=env, + check=True, + ) shutil.copytree(work / "doxygen/html", site / "doxygen") - sources.update({"baseurl": args.baseurl, "mark_changes": args.mark_changes, - "source_date_epoch": epoch, "light": True, - "gemfile_lock_sha256": hashlib.sha256((source / "Gemfile.lock").read_bytes()).hexdigest(), - "versions": {"python": sys.version.split()[0], "ruby": actual, - "bundler": output(["bundle", "--version"], cwd=source, env=env), - "jekyll": output(["bundle", "exec", "jekyll", "--version"], cwd=source, env=env), - "doxygen": output(["doxygen", "--version"]), - "graphviz": subprocess.run(["dot", "-V"], capture_output=True, text=True, check=True).stderr.strip()}}) + sources.update( + { + "baseurl": args.baseurl, + "mark_changes": args.mark_changes, + "source_date_epoch": epoch, + "light": True, + "gemfile_lock_sha256": hashlib.sha256( + (source / "Gemfile.lock").read_bytes() + ).hexdigest(), + "versions": { + "python": sys.version.split()[0], + "ruby": actual, + "bundler": output(["bundle", "--version"], cwd=source, env=env), + "jekyll": output( + ["bundle", "exec", "jekyll", "--version"], cwd=source, env=env + ), + "doxygen": output(["doxygen", "--version"]), + "graphviz": subprocess.run( + ["dot", "-V"], capture_output=True, text=True, check=True + ).stderr.strip(), + }, + } + ) (site / "build-info.json").write_text(json.dumps(sources, indent=2) + "\n") - subprocess.run([sys.executable, str(TOOLS / "check_site.py"), "--su2", str(su2), "--source", str(source), - "--site", str(site), "--baseurl", args.baseurl], env=env, check=True) + subprocess.run( + [ + sys.executable, + str(TOOLS / "check_site.py"), + "--su2", + str(su2), + "--source", + str(source), + "--site", + str(site), + "--baseurl", + args.baseurl, + ], + env=env, + check=True, + ) print("Checked website: " + str(site)) diff --git a/Docs/website/tools/build_site.py b/Docs/website/tools/build_site.py index 1b0a03b38655..b9d8c5a70cda 100644 --- a/Docs/website/tools/build_site.py +++ b/Docs/website/tools/build_site.py @@ -42,803 +42,1258 @@ REF_PREFIX = "Option-Reference" # Order of the "How to Add..." pages in the menu, from small to large changes; other "Adding-" pages go last. -HOWTO_ORDER = ["Adding-a-Config-Option", "Adding-Output-Fields", "Adding-a-Boundary-Condition", - "Adding-a-Numerical-Scheme", "Adding-a-Fluid-Model", "Adding-a-Turbulence-Model", - "Adding-a-Transition-Model", "Adding-a-Solver"] +HOWTO_ORDER = [ + "Adding-a-Config-Option", + "Adding-Output-Fields", + "Adding-a-Boundary-Condition", + "Adding-a-Numerical-Scheme", + "Adding-a-Fluid-Model", + "Adding-a-Turbulence-Model", + "Adding-a-Transition-Model", + "Adding-a-Solver", +] # Folders of the website that are only downloads; the test site links to the real site instead. HEAVY_FOLDERS = ["documents", "download", "emails"] # Upper case words followed by "=" in the docs that are not configuration options # (keywords of the SU2 mesh format, etc.). -NOT_OPTIONS = {"NDIME", "NELEM", "NPOIN", "NMARK", "NZONE", "IZONE", "MARKER_TAG", "MARKER_ELEMS", "FFD_NBOX", - "FFD_NLEVEL", "FFD_TAG", "FFD_LEVEL", "FFD_DEGREE_I", "FFD_DEGREE_J", "FFD_DEGREE_K", - "FFD_PARENTS", "FFD_CHILDREN", "FFD_CORNER_POINTS", "FFD_CONTROL_POINTS", "FFD_SURFACE_POINTS", - "NMARK_FFD", "NPERIODIC", "PERIODIC_INDEX", "NELEM_BOUND", "VALUE", "OPTION", - # keys of the Python FSI configuration file (Unsteady_FSI_Python tutorial) - "IMPOSED_MODES", "IMPOSED_PARAMETERS", - # formulas in the comment of RAMP_MUSCL in config_template.cfg - "ITERATION", "SMOOTH_FUNCTION"} +NOT_OPTIONS = { + "NDIME", + "NELEM", + "NPOIN", + "NMARK", + "NZONE", + "IZONE", + "MARKER_TAG", + "MARKER_ELEMS", + "FFD_NBOX", + "FFD_NLEVEL", + "FFD_TAG", + "FFD_LEVEL", + "FFD_DEGREE_I", + "FFD_DEGREE_J", + "FFD_DEGREE_K", + "FFD_PARENTS", + "FFD_CHILDREN", + "FFD_CORNER_POINTS", + "FFD_CONTROL_POINTS", + "FFD_SURFACE_POINTS", + "NMARK_FFD", + "NPERIODIC", + "PERIODIC_INDEX", + "NELEM_BOUND", + "VALUE", + "OPTION", + # keys of the Python FSI configuration file (Unsteady_FSI_Python tutorial) + "IMPOSED_MODES", + "IMPOSED_PARAMETERS", + # formulas in the comment of RAMP_MUSCL in config_template.cfg + "ITERATION", + "SMOOTH_FUNCTION", +} TYPE_NAMES = { - "Double": "real number", "Bool": "YES / NO", "String": "string", "UnsignedLong": "integer", - "UnsignedShort": "integer", "Long": "integer", "DoubleList": "list of real numbers", - "DoubleArray": "fixed-size list of real numbers", "StringList": "list of strings", - "UShortList": "list of integers", "ShortList": "list of integers", "ULongList": "list of integers", - "ULongArray": "fixed-size list of integers", "UShortArray": "fixed-size list of integers", - "StringDoubleList": "list of (string, real number) pairs", "Python": "used by the Python scripts only", + "Double": "real number", + "Bool": "YES / NO", + "String": "string", + "UnsignedLong": "integer", + "UnsignedShort": "integer", + "Long": "integer", + "DoubleList": "list of real numbers", + "DoubleArray": "fixed-size list of real numbers", + "StringList": "list of strings", + "UShortList": "list of integers", + "ShortList": "list of integers", + "ULongList": "list of integers", + "ULongArray": "fixed-size list of integers", + "UShortArray": "fixed-size list of integers", + "StringDoubleList": "list of (string, real number) pairs", + "Python": "used by the Python scripts only", } def slugify(text): - return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") + return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") def split_args(text): - """Split the arguments of a C++ call at the commas that are not inside brackets.""" - args, depth, start = [], 0, 0 - for i, c in enumerate(text): - if c in "([{<": - depth += 1 - elif c in ")]}>": - depth -= 1 - elif c == "," and depth == 0: - args.append(text[start:i].strip()) - start = i + 1 - args.append(text[start:].strip()) - return args + """Split the arguments of a C++ call at the commas that are not inside brackets.""" + args, depth, start = [], 0, 0 + for i, c in enumerate(text): + if c in "([{<": + depth += 1 + elif c in ")]}>": + depth -= 1 + elif c == "," and depth == 0: + args.append(text[start:i].strip()) + start = i + 1 + args.append(text[start:].strip()) + return args def call_body(text, open_paren): - """Return the text between the parenthesis at open_paren and its matching parenthesis.""" - depth = 0 - for i in range(open_paren, len(text)): - if text[i] == "(": - depth += 1 - elif text[i] == ")": - depth -= 1 - if depth == 0: - return text[open_paren + 1:i], i - return text[open_paren + 1:], len(text) + """Return the text between the parenthesis at open_paren and its matching parenthesis.""" + depth = 0 + for i in range(open_paren, len(text)): + if text[i] == "(": + depth += 1 + elif text[i] == ")": + depth -= 1 + if depth == 0: + return text[open_paren + 1 : i], i + return text[open_paren + 1 :], len(text) def read_maps(su2): - """Map name -> list of (key, C++ value) for every MapType in Common/include.""" - maps = {} - pattern = re.compile(r"MapType\s*<\s*std::string\s*,[^>]*>\s+(\w+)\s*=\s*\{(.*?)\};", re.S) - for path in glob.glob(os.path.join(su2, "Common", "include", "**", "*.*pp"), recursive=True): - with open(path, encoding="utf-8", errors="replace") as f: - text = f.read() - # Ignore disabled entries, but keep comment markers inside quoted strings. - text = re.sub(r'"(?:\\.|[^"\\])*"|//[^\n]*|/\*.*?\*/', - lambda m: m[0] if m[0].startswith('"') else " ", text, flags=re.S) - for m in pattern.finditer(text): - pairs = re.findall(r'MakePair\(\s*"([^"]+)"\s*,\s*([^)]*?)\s*\)', m.group(2)) - maps[m.group(1)] = pairs - return maps + """Map name -> list of (key, C++ value) for every MapType in Common/include.""" + maps = {} + pattern = re.compile( + r"MapType\s*<\s*std::string\s*,[^>]*>\s+(\w+)\s*=\s*\{(.*?)\};", re.S + ) + for path in glob.glob( + os.path.join(su2, "Common", "include", "**", "*.*pp"), recursive=True + ): + with open(path, encoding="utf-8", errors="replace") as f: + text = f.read() + # Ignore disabled entries, but keep comment markers inside quoted strings. + text = re.sub( + r'"(?:\\.|[^"\\])*"|//[^\n]*|/\*.*?\*/', + lambda m: m[0] if m[0].startswith('"') else " ", + text, + flags=re.S, + ) + for m in pattern.finditer(text): + pairs = re.findall( + r'MakePair\(\s*"([^"]+)"\s*,\s*([^)]*?)\s*\)', m.group(2) + ) + maps[m.group(1)] = pairs + return maps def clean_comment(raw): - """Turn a /*! ... */ comment of CConfig.cpp into (description, allowed values text, default text).""" - text = raw.replace("\\ingroup Config", "") - text = re.sub(r"\\brief\s+[A-Z0-9_]+\s*", "", text) - text = text.replace("\\brief", "") - text = re.sub(r"\\link\s+(\w+)\s*\\endlink", r"\1", text) - text = re.sub(r"\s*\*\s*", " ", text) if "\n" in text else text - parts = {"DESCRIPTION": "", "OPTIONS": "", "DEFAULT": ""} - key = "DESCRIPTION" - for chunk in re.split(r"\\n", text): - chunk = chunk.strip() - m = re.match(r"(DESCRIPTION|Options|OPTIONS|DEFAULT|Default)\s*:\s*(.*)", chunk, re.S) - if m: - key = m.group(1).upper() - chunk = m.group(2) - if chunk: - parts[key] = (parts[key] + " " + chunk).strip() - return parts["DESCRIPTION"], parts["OPTIONS"], parts["DEFAULT"] + """Turn a /*! ... */ comment of CConfig.cpp into (description, allowed values text, default text).""" + text = raw.replace("\\ingroup Config", "") + text = re.sub(r"\\brief\s+[A-Z0-9_]+\s*", "", text) + text = text.replace("\\brief", "") + text = re.sub(r"\\link\s+(\w+)\s*\\endlink", r"\1", text) + text = re.sub(r"\s*\*\s*", " ", text) if "\n" in text else text + parts = {"DESCRIPTION": "", "OPTIONS": "", "DEFAULT": ""} + key = "DESCRIPTION" + for chunk in re.split(r"\\n", text): + chunk = chunk.strip() + m = re.match( + r"(DESCRIPTION|Options|OPTIONS|DEFAULT|Default)\s*:\s*(.*)", chunk, re.S + ) + if m: + key = m.group(1).upper() + chunk = m.group(2) + if chunk: + parts[key] = (parts[key] + " " + chunk).strip() + return parts["DESCRIPTION"], parts["OPTIONS"], parts["DEFAULT"] def read_options(su2): - """Read every option registered in CConfig::SetConfig_Options, in order, with its category.""" - with open(os.path.join(su2, "Common", "src", "CConfig.cpp"), encoding="utf-8") as f: - text = f.read() - - # Option comments start with "/*!" or "/*"; only the ones with a description or a \brief are kept, - # so that plain code comments such as "/*--- ... ---*/" do not become descriptions. - token = re.compile(r"/\*!?(.*?)\*/|\badd(\w*)Option\s*\(", re.S) - options, category, comment = [], "General", None - pos = 0 - while True: - m = token.search(text, pos) - if not m: - break - if m.group(1) is not None: - body = m.group(1) - cat = re.search(r"\\par\s+CONFIG_CATEGORY\s*:\s*(.*?)\s*\\ingroup", body, re.S) - if cat: - category, comment = " ".join(cat.group(1).split()), None - elif "DESCRIPTION" in body or "\\brief" in body: - comment = body - pos = m.end() - continue - args_text, end = call_body(text, m.end() - 1) - pos = end + 1 - args = split_args(args_text) - name = re.fullmatch(r'"([A-Z0-9_]+)"', args[0]) - if not name: - continue - line = text.count("\n", 0, m.start()) + 1 - options.append({"name": name.group(1), "kind": m.group(2), "args": args, "category": category, - "comment": comment, "line": line}) - comment = None - return options + """Read every option registered in CConfig::SetConfig_Options, in order, with its category.""" + with open(os.path.join(su2, "Common", "src", "CConfig.cpp"), encoding="utf-8") as f: + text = f.read() + + # Option comments start with "/*!" or "/*"; only the ones with a description or a \brief are kept, + # so that plain code comments such as "/*--- ... ---*/" do not become descriptions. + token = re.compile(r"/\*!?(.*?)\*/|\badd(\w*)Option\s*\(", re.S) + options, category, comment = [], "General", None + pos = 0 + while True: + m = token.search(text, pos) + if not m: + break + if m.group(1) is not None: + body = m.group(1) + cat = re.search( + r"\\par\s+CONFIG_CATEGORY\s*:\s*(.*?)\s*\\ingroup", body, re.S + ) + if cat: + category, comment = " ".join(cat.group(1).split()), None + elif "DESCRIPTION" in body or "\\brief" in body: + comment = body + pos = m.end() + continue + args_text, end = call_body(text, m.end() - 1) + pos = end + 1 + args = split_args(args_text) + name = re.fullmatch(r'"([A-Z0-9_]+)"', args[0]) + if not name: + continue + line = text.count("\n", 0, m.start()) + 1 + options.append( + { + "name": name.group(1), + "kind": m.group(2), + "args": args, + "category": category, + "comment": comment, + "line": line, + } + ) + comment = None + return options def read_template(su2): - """Option name -> (comment lines above it, example line) from config_template.cfg.""" - examples = {} - with open(os.path.join(su2, "config_template.cfg"), encoding="utf-8") as f: - lines = f.read().splitlines() - notes = [] - for line in lines: - m = re.match(r"^\s*%?\s*([A-Z][A-Z0-9_]+)\s*=(.*)$", line) - if m and not line.lstrip().startswith("%%"): - examples.setdefault(m.group(1), (list(notes), line.strip())) - notes = [] - elif line.lstrip().startswith("%") and not line.lstrip().startswith("% ---") and line.strip("% -"): - notes.append(line.strip().lstrip("%").strip()) - else: - notes = [] - return examples + """Option name -> (comment lines above it, example line) from config_template.cfg.""" + examples = {} + with open(os.path.join(su2, "config_template.cfg"), encoding="utf-8") as f: + lines = f.read().splitlines() + notes = [] + for line in lines: + m = re.match(r"^\s*%?\s*([A-Z][A-Z0-9_]+)\s*=(.*)$", line) + if m and not line.lstrip().startswith("%%"): + examples.setdefault(m.group(1), (list(notes), line.strip())) + notes = [] + elif ( + line.lstrip().startswith("%") + and not line.lstrip().startswith("% ---") + and line.strip("% -") + ): + notes.append(line.strip().lstrip("%").strip()) + else: + notes = [] + return examples def read_testcases(su2): - """Option name -> list of (cfg path relative to the SU2 root, value) from TestCases/**/*.cfg.""" - usage = {} - setting = re.compile(r"^\s*([A-Z][A-Z0-9_]+)\s*=\s*([^%]*)") - for path in sorted(glob.glob(os.path.join(su2, "TestCases", "**", "*.cfg"), recursive=True)): - rel = os.path.relpath(path, su2) - with open(path, encoding="utf-8", errors="replace") as f: - for line in f: - m = setting.match(line) - if m: - usage.setdefault(m.group(1), []).append((rel, " ".join(m.group(2).split()))) - return usage + """Option name -> list of (cfg path relative to the SU2 root, value) from TestCases/**/*.cfg.""" + usage = {} + setting = re.compile(r"^\s*([A-Z][A-Z0-9_]+)\s*=\s*([^%]*)") + for path in sorted( + glob.glob(os.path.join(su2, "TestCases", "**", "*.cfg"), recursive=True) + ): + rel = os.path.relpath(path, su2) + with open(path, encoding="utf-8", errors="replace") as f: + for line in f: + m = setting.match(line) + if m: + usage.setdefault(m.group(1), []).append( + (rel, " ".join(m.group(2).split())) + ) + return usage def pick_examples(uses, count=3): - """Up to count uses with different values, so that the examples show the range of settings.""" - picked, values = [], set() - for rel, value in uses: - if value and value not in values: - picked.append((rel, value)) - values.add(value) - if len(picked) == count: - break - return picked + """Up to count uses with different values, so that the examples show the range of settings.""" + picked, values = [], set() + for rel, value in uses: + if value and value not in values: + picked.append((rel, value)) + values.add(value) + if len(picked) == count: + break + return picked def read_page_mentions(site, known, skip): - """Option name -> list of (title, url) of the user guide pages and tutorials that name the option.""" - mentions = {} - files = sorted(glob.glob(os.path.join(site, DOCS, "*.md"))) + \ - sorted(glob.glob(os.path.join(site, "_tutorials", "**", "*.md"), recursive=True)) - for path in files: - base = os.path.splitext(os.path.basename(path))[0] - if base.startswith(REF_PREFIX) or base in skip or "unfinished" in path: - continue - with open(path, encoding="utf-8", errors="replace") as f: - text = f.read() - title = re.search(r"^title:\s*(.+)$", text, re.M) - link = re.search(r"^permalink:\s*(\S+)$", text, re.M) - if not (title and link): - continue - title = title.group(1).strip().strip("\"'") - for name in set(re.findall(r"\b[A-Z][A-Z0-9_]{2,}\b", text)) & known: - in_guide = path.startswith(os.path.join(site, DOCS)) - mentions.setdefault(name, []).append((title, link.group(1).strip("\"'"), in_guide)) - for name in mentions: - # User guide pages first, then tutorials. - mentions[name].sort(key=lambda m: (not m[2], m[0])) - return mentions + """Option name -> list of (title, url) of the user guide pages and tutorials that name the option.""" + mentions = {} + files = sorted(glob.glob(os.path.join(site, DOCS, "*.md"))) + sorted( + glob.glob(os.path.join(site, "_tutorials", "**", "*.md"), recursive=True) + ) + for path in files: + base = os.path.splitext(os.path.basename(path))[0] + if base.startswith(REF_PREFIX) or base in skip or "unfinished" in path: + continue + with open(path, encoding="utf-8", errors="replace") as f: + text = f.read() + title = re.search(r"^title:\s*(.+)$", text, re.M) + link = re.search(r"^permalink:\s*(\S+)$", text, re.M) + if not (title and link): + continue + title = title.group(1).strip().strip("\"'") + for name in set(re.findall(r"\b[A-Z][A-Z0-9_]{2,}\b", text)) & known: + in_guide = path.startswith(os.path.join(site, DOCS)) + mentions.setdefault(name, []).append( + (title, link.group(1).strip("\"'"), in_guide) + ) + for name in mentions: + # User guide pages first, then tutorials. + mentions[name].sort(key=lambda m: (not m[2], m[0])) + return mentions def describe_type(opt, maps): - """Return (type text, list of allowed values, default text).""" - kind, args = opt["kind"], opt["args"] - values, default = [], "" - if kind in ("Enum", "EnumList"): - map_name = args[2] if kind == "Enum" else args[3] - values = [k for k, _ in maps.get(map_name, [])] - if kind == "Enum" and len(args) > 3: - default = next((k for k, v in maps.get(map_name, []) if v == args[3]), args[3]) - return ("one of the values below" if kind == "Enum" else "list of the values below"), values, default - if kind == "Bool" and len(args) > 2: - default = {"true": "YES", "false": "NO"}.get(args[2], args[2]) - elif kind in ("Double", "String", "UnsignedLong", "UnsignedShort", "Long") and len(args) > 2: - default = args[2].strip('"') - return TYPE_NAMES.get(kind, "special format, see config_template.cfg"), values, default + """Return (type text, list of allowed values, default text).""" + kind, args = opt["kind"], opt["args"] + values, default = [], "" + if kind in ("Enum", "EnumList"): + map_name = args[2] if kind == "Enum" else args[3] + values = [k for k, _ in maps.get(map_name, [])] + if kind == "Enum" and len(args) > 3: + default = next( + (k for k, v in maps.get(map_name, []) if v == args[3]), args[3] + ) + return ( + ( + "one of the values below" + if kind == "Enum" + else "list of the values below" + ), + values, + default, + ) + if kind == "Bool" and len(args) > 2: + default = {"true": "YES", "false": "NO"}.get(args[2], args[2]) + elif ( + kind in ("Double", "String", "UnsignedLong", "UnsignedShort", "Long") + and len(args) > 2 + ): + default = args[2].strip('"') + return ( + TYPE_NAMES.get(kind, "special format, see config_template.cfg"), + values, + default, + ) def normalize(text): - return re.sub(r"[^a-z0-9]+", " ", text.lower()).strip() + return re.sub(r"[^a-z0-9]+", " ", text.lower()).strip() def same_text(a, b): - """True if b says nothing more than a (same words, or contained in a).""" - return normalize(a) == normalize(b) or normalize(b) in normalize(a) + """True if b says nothing more than a (same words, or contained in a).""" + return normalize(a) == normalize(b) or normalize(b) in normalize(a) def write_reference(su2, site, options, maps, commit, repo, skip): - examples = read_template(su2) - usage = read_testcases(su2) - mentions = read_page_mentions(site, {o["name"] for o in options}, skip) - epoch = os.environ.get("SOURCE_DATE_EPOCH") - date = datetime.datetime.fromtimestamp(int(epoch), datetime.timezone.utc).date() if epoch else datetime.date.today() - stamp = "Generated from SU2 commit `{}` on {}.".format(commit, date.isoformat()) - blob = "https://github.com/{}/blob/{}/".format(repo, commit) - source = blob + "Common/src/CConfig.cpp" - - categories = {} - for opt in options: - categories.setdefault(opt["category"], []).append(opt) - - pages = [] - for cat, opts in categories.items(): - slug = "{}-{}".format(REF_PREFIX, slugify(cat)) - for opt in opts: - opt["page"] = slug - out = ["---", 'title: "{}"'.format(cat.replace('"', "'")), "permalink: /{}/{}/".format(URL, slug), "---", - "", "{% raw %}", "", "> {} Do not edit this page. Edit the comment above the option in " - "[`Common/src/CConfig.cpp`]({}) instead.".format(stamp, source), ""] - for opt in opts: - desc, opt_text, def_text = clean_comment(opt["comment"]) if opt["comment"] else ("", "", "") - notes, tmpl_line = examples.get(opt["name"], ([], "")) - notes = " ".join(notes) - type_text, values, default = describe_type(opt, maps) - uses = usage.get(opt["name"], []) - pages_of = mentions.get(opt["name"], []) - opt.update({"desc_words": len(desc.split()), "has_notes": bool(notes), "uses": len(uses), - "mentions": len(pages_of)}) - - out.append("### {} {{#{}}}".format(opt["name"], opt["name"])) - out.append("") - if desc: - out.append(desc) - elif notes: - out.append(notes + " *(from config_template.cfg)*") - else: - out.append("*No description in the code yet.*") - out.append("") - if desc and notes and not same_text(desc, notes): - out.append("> **More in config_template.cfg:** " + notes) - out.append("") - out.append("- **Type:** {}".format(type_text)) - if values: - out.append("- **Values:** " + ", ".join("`{}`".format(v) for v in values)) - elif opt_text and not opt_text.startswith(tuple(maps)): - out.append("- **Values:** {}".format(opt_text)) - if default or def_text: - out.append("- **Default:** `{}`".format(default if default else def_text)) - elif opt["kind"] in ("DoubleArray", "UShortArray", "ULongArray"): - out.append("- **Default:** *Not extracted; see the code.*") - if tmpl_line: - out.append("- **In config_template.cfg:** `{}`".format(tmpl_line)) - else: - out.append("- **Not in config_template.cfg.**") - if pages_of: - out.append("- **Mentioned in:** " + ", ".join( - "[{}]({})".format(title, url) for title, url, _ in pages_of[:6]) + - (" and {} more".format(len(pages_of) - 6) if len(pages_of) > 6 else "")) - if uses: - files = len({rel for rel, _ in uses}) - out.append("- **Used in {} test case file{}**, for example: ".format(files, "s" if files > 1 else "") + - ", ".join("[`{}`]({}{}) (`{}`)".format(os.path.basename(rel), blob, rel, value or "empty") - for rel, value in pick_examples(uses))) - out.append("- **Code:** [`CConfig.cpp` line {}]({}#L{})".format(opt["line"], source, opt["line"])) - out.append("") - out += ["{% endraw %}", ""] - with open(os.path.join(site, DOCS, slug + ".md"), "w", encoding="utf-8") as f: - f.write("\n".join(out)) - pages.append((slug, cat, len(opts))) - - missing_desc = sum(1 for o in options if not o["desc_words"]) - missing_both = sum(1 for o in options if not o["desc_words"] and not o["has_notes"]) - missing_tmpl = sum(1 for o in options if o["name"] not in examples) - out = ["---", "title: Option Reference", "permalink: /{}/{}/".format(URL, REF_PREFIX), "---", "", - "{% raw %}", "", - "This reference lists the options registered in the SU2 source code. {}".format(stamp), "", - "Defaults initialized outside the registration calls may not be extracted. " - "The template lines are examples and may differ from the defaults. " - "Allowed keywords can have additional restrictions for a particular solver or option combination.", "", - "Each option shows the description in the code, the text of `config_template.cfg` when it adds " - "something, the pages of this website that mention it, and real test case files that use it.", "", - "- Options: {}".format(len(options)), - "- Options without a description in the code: {} ({} without any text at all)".format( - missing_desc, missing_both), - "- Options not in `config_template.cfg`: {}".format(missing_tmpl), - "- The [Docs Check](../Docs-Check/) page lists the options that need better documentation.", "", - "## Groups", ""] - out += ["- [{}](../{}/) ({} options)".format(cat, slug, n) for slug, cat, n in pages] - out += ["", "## All options A-Z", ""] - for opt in sorted(options, key=lambda o: o["name"]): - out.append("[`{0}`](../{1}/#{0}) ".format(opt["name"], opt["page"])) - out += ["", "{% endraw %}", ""] - with open(os.path.join(site, DOCS, REF_PREFIX + ".md"), "w", encoding="utf-8") as f: - f.write("\n".join(out)) - return [REF_PREFIX] + [p[0] for p in pages] + examples = read_template(su2) + usage = read_testcases(su2) + mentions = read_page_mentions(site, {o["name"] for o in options}, skip) + epoch = os.environ.get("SOURCE_DATE_EPOCH") + date = ( + datetime.datetime.fromtimestamp(int(epoch), datetime.timezone.utc).date() + if epoch + else datetime.date.today() + ) + stamp = "Generated from SU2 commit `{}` on {}.".format(commit, date.isoformat()) + blob = "https://github.com/{}/blob/{}/".format(repo, commit) + source = blob + "Common/src/CConfig.cpp" + + categories = {} + for opt in options: + categories.setdefault(opt["category"], []).append(opt) + + pages = [] + for cat, opts in categories.items(): + slug = "{}-{}".format(REF_PREFIX, slugify(cat)) + for opt in opts: + opt["page"] = slug + out = [ + "---", + 'title: "{}"'.format(cat.replace('"', "'")), + "permalink: /{}/{}/".format(URL, slug), + "---", + "", + "{% raw %}", + "", + "> {} Do not edit this page. Edit the comment above the option in " + "[`Common/src/CConfig.cpp`]({}) instead.".format(stamp, source), + "", + ] + for opt in opts: + desc, opt_text, def_text = ( + clean_comment(opt["comment"]) if opt["comment"] else ("", "", "") + ) + notes, tmpl_line = examples.get(opt["name"], ([], "")) + notes = " ".join(notes) + type_text, values, default = describe_type(opt, maps) + uses = usage.get(opt["name"], []) + pages_of = mentions.get(opt["name"], []) + opt.update( + { + "desc_words": len(desc.split()), + "has_notes": bool(notes), + "uses": len(uses), + "mentions": len(pages_of), + } + ) + + out.append("### {} {{#{}}}".format(opt["name"], opt["name"])) + out.append("") + if desc: + out.append(desc) + elif notes: + out.append(notes + " *(from config_template.cfg)*") + else: + out.append("*No description in the code yet.*") + out.append("") + if desc and notes and not same_text(desc, notes): + out.append("> **More in config_template.cfg:** " + notes) + out.append("") + out.append("- **Type:** {}".format(type_text)) + if values: + out.append( + "- **Values:** " + ", ".join("`{}`".format(v) for v in values) + ) + elif opt_text and not opt_text.startswith(tuple(maps)): + out.append("- **Values:** {}".format(opt_text)) + if default or def_text: + out.append( + "- **Default:** `{}`".format(default if default else def_text) + ) + elif opt["kind"] in ("DoubleArray", "UShortArray", "ULongArray"): + out.append("- **Default:** *Not extracted; see the code.*") + if tmpl_line: + out.append("- **In config_template.cfg:** `{}`".format(tmpl_line)) + else: + out.append("- **Not in config_template.cfg.**") + if pages_of: + out.append( + "- **Mentioned in:** " + + ", ".join( + "[{}]({})".format(title, url) for title, url, _ in pages_of[:6] + ) + + ( + " and {} more".format(len(pages_of) - 6) + if len(pages_of) > 6 + else "" + ) + ) + if uses: + files = len({rel for rel, _ in uses}) + out.append( + "- **Used in {} test case file{}**, for example: ".format( + files, "s" if files > 1 else "" + ) + + ", ".join( + "[`{}`]({}{}) (`{}`)".format( + os.path.basename(rel), blob, rel, value or "empty" + ) + for rel, value in pick_examples(uses) + ) + ) + out.append( + "- **Code:** [`CConfig.cpp` line {}]({}#L{})".format( + opt["line"], source, opt["line"] + ) + ) + out.append("") + out += ["{% endraw %}", ""] + with open(os.path.join(site, DOCS, slug + ".md"), "w", encoding="utf-8") as f: + f.write("\n".join(out)) + pages.append((slug, cat, len(opts))) + + missing_desc = sum(1 for o in options if not o["desc_words"]) + missing_both = sum(1 for o in options if not o["desc_words"] and not o["has_notes"]) + missing_tmpl = sum(1 for o in options if o["name"] not in examples) + out = [ + "---", + "title: Option Reference", + "permalink: /{}/{}/".format(URL, REF_PREFIX), + "---", + "", + "{% raw %}", + "", + "This reference lists the options registered in the SU2 source code. {}".format( + stamp + ), + "", + "Defaults initialized outside the registration calls may not be extracted. " + "The template lines are examples and may differ from the defaults. " + "Allowed keywords can have additional restrictions for a particular solver or option combination.", + "", + "Each option shows the description in the code, the text of `config_template.cfg` when it adds " + "something, the pages of this website that mention it, and real test case files that use it.", + "", + "- Options: {}".format(len(options)), + "- Options without a description in the code: {} ({} without any text at all)".format( + missing_desc, missing_both + ), + "- Options not in `config_template.cfg`: {}".format(missing_tmpl), + "- The [Docs Check](../Docs-Check/) page lists the options that need better documentation.", + "", + "## Groups", + "", + ] + out += [ + "- [{}](../{}/) ({} options)".format(cat, slug, n) for slug, cat, n in pages + ] + out += ["", "## All options A-Z", ""] + for opt in sorted(options, key=lambda o: o["name"]): + out.append("[`{0}`](../{1}/#{0}) ".format(opt["name"], opt["page"])) + out += ["", "{% endraw %}", ""] + with open(os.path.join(site, DOCS, REF_PREFIX + ".md"), "w", encoding="utf-8") as f: + f.write("\n".join(out)) + return [REF_PREFIX] + [p[0] for p in pages] def read_code_names(su2): - """Upper case identifiers of the C++ code (enums, constants), to tell them apart from options.""" - names = set() - for folder in ("Common", "SU2_CFD"): - for path in glob.glob(os.path.join(su2, folder, "**", "*.[ch]pp"), recursive=True): - with open(path, encoding="utf-8", errors="replace") as f: - names.update(re.findall(r"\b[A-Z][A-Z0-9_]{2,}\b", f.read())) - return names + """Upper case identifiers of the C++ code (enums, constants), to tell them apart from options.""" + names = set() + for folder in ("Common", "SU2_CFD"): + for path in glob.glob( + os.path.join(su2, folder, "**", "*.[ch]pp"), recursive=True + ): + with open(path, encoding="utf-8", errors="replace") as f: + names.update(re.findall(r"\b[A-Z][A-Z0-9_]{2,}\b", f.read())) + return names def check_docs(site, known, code_names=frozenset()): - """Find options in the docs, tutorials and V&V pages that the code does not know. - Only code blocks that look like an SU2 configuration file (at least half known options) - and inline code such as `OPTION= VALUE` are checked.""" - found = [] - setting = re.compile(r"^\s*%?\s*([A-Z][A-Z0-9_]{2,})\s*=") - files = sorted(glob.glob(os.path.join(site, DOCS, "*.md")) + - glob.glob(os.path.join(site, "_tutorials", "**", "*.md"), recursive=True) + - glob.glob(os.path.join(site, "_vandv", "*.md"))) - for path in files: - if os.path.basename(path).startswith(REF_PREFIX): - continue - with open(path, encoding="utf-8", errors="replace") as f: - lines = f.read().splitlines() - hits, block, code = set(), None, False - for num, line in enumerate(lines, 1): - if line.lstrip().startswith("```"): - if block is None: - block = [] - # Blocks of source code (C++, Python, shell) are not configuration files. - code = line.strip("` \t").lower() in ("cpp", "c++", "c", "hpp", "python", "py", "bash", "sh", "shell") - else: - names = [(n, m.group(1)) for n, m in block if m] - if not code and names and 2 * sum(name in known for _, name in names) >= len(names): - hits.update((n, name) for n, name in names) - block = None - continue - if block is not None: - block.append((num, setting.match(line))) - else: - # Inline code such as `MAXNVAR = 4` can be C++; only names unknown to the code are reported. - hits.update((num, name) for name in re.findall(r"`\s*([A-Z][A-Z0-9_]{2,})\s*=", line) - if name not in code_names) - for num, name in sorted(hits): - if name not in known and name not in NOT_OPTIONS: - found.append((os.path.relpath(path, site), num, name, lines[num - 1].strip())) - return found + """Find options in the docs, tutorials and V&V pages that the code does not know. + Only code blocks that look like an SU2 configuration file (at least half known options) + and inline code such as `OPTION= VALUE` are checked.""" + found = [] + setting = re.compile(r"^\s*%?\s*([A-Z][A-Z0-9_]{2,})\s*=") + files = sorted( + glob.glob(os.path.join(site, DOCS, "*.md")) + + glob.glob(os.path.join(site, "_tutorials", "**", "*.md"), recursive=True) + + glob.glob(os.path.join(site, "_vandv", "*.md")) + ) + for path in files: + if os.path.basename(path).startswith(REF_PREFIX): + continue + with open(path, encoding="utf-8", errors="replace") as f: + lines = f.read().splitlines() + hits, block, code = set(), None, False + for num, line in enumerate(lines, 1): + if line.lstrip().startswith("```"): + if block is None: + block = [] + # Blocks of source code (C++, Python, shell) are not configuration files. + code = line.strip("` \t").lower() in ( + "cpp", + "c++", + "c", + "hpp", + "python", + "py", + "bash", + "sh", + "shell", + ) + else: + names = [(n, m.group(1)) for n, m in block if m] + if ( + not code + and names + and 2 * sum(name in known for _, name in names) >= len(names) + ): + hits.update((n, name) for n, name in names) + block = None + continue + if block is not None: + block.append((num, setting.match(line))) + else: + # Inline code such as `MAXNVAR = 4` can be C++; only names unknown to the code are reported. + hits.update( + (num, name) + for name in re.findall(r"`\s*([A-Z][A-Z0-9_]{2,})\s*=", line) + if name not in code_names + ) + for num, name in sorted(hits): + if name not in known and name not in NOT_OPTIONS: + found.append( + (os.path.relpath(path, site), num, name, lines[num - 1].strip()) + ) + return found # Output classes, in the order of the page, with the solvers that use them. OUTPUT_CLASSES = [ - ("COutput", "All solvers"), ("CFVMOutput", "All finite volume solvers"), - ("CFlowOutput", - "All flow solvers (compressible, incompressible, NEMO), also for their turbulence and species fields"), - ("CFlowCompOutput", "Compressible flow"), ("CFlowIncOutput", "Incompressible flow"), - ("CNEMOCompOutput", "Non-equilibrium flow (NEMO)"), ("CFlowCompFEMOutput", "Finite element flow (DG-FEM)"), - ("CTurboOutput", "Turbomachinery"), ("CHeatOutput", "Heat equation"), ("CElasticityOutput", "Structural analysis"), - ("CAdjFlowOutput", "All adjoint flow solvers"), ("CAdjFlowCompOutput", "Compressible adjoint"), - ("CAdjFlowIncOutput", "Incompressible adjoint"), ("CAdjHeatOutput", "Heat adjoint"), - ("CAdjElasticityOutput", "Structural adjoint"), ("CMultizoneOutput", "Multizone problems"), - ("CMeshOutput", "Mesh conversion (SU2_DEF, SU2_SOL)"), ("CBaselineOutput", "Solution conversion (SU2_SOL)"), + ("COutput", "All solvers"), + ("CFVMOutput", "All finite volume solvers"), + ( + "CFlowOutput", + "All flow solvers (compressible, incompressible, NEMO), also for their turbulence and species fields", + ), + ("CFlowCompOutput", "Compressible flow"), + ("CFlowIncOutput", "Incompressible flow"), + ("CNEMOCompOutput", "Non-equilibrium flow (NEMO)"), + ("CFlowCompFEMOutput", "Finite element flow (DG-FEM)"), + ("CTurboOutput", "Turbomachinery"), + ("CHeatOutput", "Heat equation"), + ("CElasticityOutput", "Structural analysis"), + ("CAdjFlowOutput", "All adjoint flow solvers"), + ("CAdjFlowCompOutput", "Compressible adjoint"), + ("CAdjFlowIncOutput", "Incompressible adjoint"), + ("CAdjHeatOutput", "Heat adjoint"), + ("CAdjElasticityOutput", "Structural adjoint"), + ("CMultizoneOutput", "Multizone problems"), + ("CMeshOutput", "Mesh conversion (SU2_DEF, SU2_SOL)"), + ("CBaselineOutput", "Solution conversion (SU2_SOL)"), ] def literal(arg): - """The text of a C++ string literal, or None if the argument is an expression.""" - m = re.fullmatch(r'"((?:[^"\\]|\\.)*)"', arg.strip()) - return m.group(1) if m else None + """The text of a C++ string literal, or None if the argument is an expression.""" + m = re.fullmatch(r'"((?:[^"\\]|\\.)*)"', arg.strip()) + return m.group(1) if m else None def read_outputs(su2): - """Output class -> list of (kind, name, group, description, line) of the fields it registers.""" - calls = {"AddHistoryOutput": ("history", 3, 4), "AddHistoryOutputPerSurface": ("history, per surface", 3, None), - "AddVolumeOutput": ("volume", 2, 3)} - token = re.compile(r"\b(" + "|".join(calls) + r")\s*\(") - fields = {} - for path in sorted(glob.glob(os.path.join(su2, "SU2_CFD", "src", "output", "*.cpp"))): - cls = os.path.splitext(os.path.basename(path))[0] - with open(path, encoding="utf-8", errors="replace") as f: - text = f.read() - seen = set() - for m in token.finditer(text): - body, _ = call_body(text, m.end() - 1) - args = split_args(body) - kind, igroup, idesc = calls[m.group(1)] - name = literal(args[0]) - shown = name if name is not None else " ".join(args[0].split()) - if (kind, shown) in seen: - continue - seen.add((kind, shown)) - group = literal(args[igroup]) if len(args) > igroup else None - desc = literal(args[idesc]) if idesc is not None and len(args) > idesc else None - fields.setdefault(cls, []).append({"kind": kind, "name": shown, "literal": name is not None, - "group": group if group is not None else "", - "desc": desc or "", "line": text.count("\n", 0, m.start()) + 1}) - return fields + """Output class -> list of (kind, name, group, description, line) of the fields it registers.""" + calls = { + "AddHistoryOutput": ("history", 3, 4), + "AddHistoryOutputPerSurface": ("history, per surface", 3, None), + "AddVolumeOutput": ("volume", 2, 3), + } + token = re.compile(r"\b(" + "|".join(calls) + r")\s*\(") + fields = {} + for path in sorted( + glob.glob(os.path.join(su2, "SU2_CFD", "src", "output", "*.cpp")) + ): + cls = os.path.splitext(os.path.basename(path))[0] + with open(path, encoding="utf-8", errors="replace") as f: + text = f.read() + seen = set() + for m in token.finditer(text): + body, _ = call_body(text, m.end() - 1) + args = split_args(body) + kind, igroup, idesc = calls[m.group(1)] + name = literal(args[0]) + shown = name if name is not None else " ".join(args[0].split()) + if (kind, shown) in seen: + continue + seen.add((kind, shown)) + group = literal(args[igroup]) if len(args) > igroup else None + desc = ( + literal(args[idesc]) + if idesc is not None and len(args) > idesc + else None + ) + fields.setdefault(cls, []).append( + { + "kind": kind, + "name": shown, + "literal": name is not None, + "group": group if group is not None else "", + "desc": desc or "", + "line": text.count("\n", 0, m.start()) + 1, + } + ) + return fields def cell(text): - return text.replace("|", "\\|").replace("\n", " ") + return text.replace("|", "\\|").replace("\n", " ") def write_output_page(site, fields, commit, blob): - name = "Output-Fields-Reference" - out = ["---", "title: Output Fields Reference", "permalink: /{}/{}/".format(URL, name), "---", "", "{% raw %}", "", - "This page lists the screen, history and volume output fields that each output class of SU2 can write. " - "It is made from the `AddHistoryOutput` and `AddVolumeOutput` calls in `SU2_CFD/src/output/`. " - "Generated from SU2 commit `{}`.".format(commit), "", - "- A field is written only when the case needs it (for example turbulence fields only with a turbulence " - "model). For the exact list of your case, run a dry run: `SU2_CFD -d your_config.cfg`. It prints " - "\"Available screen/history output fields\" and \"Available volume output fields\".", - "- How to select fields: [Custom Output](../Custom-Output/). How to add one: " - "[Adding Output Fields](../Adding-Output-Fields/).", - "- Names in *italics* are built at run time (for example one field per species or per marker).", ""] - order = [c for c, _ in OUTPUT_CLASSES if c in fields] + sorted(c for c in fields if c not in dict(OUTPUT_CLASSES)) - used = dict(OUTPUT_CLASSES) - for cls in order: - path = "SU2_CFD/src/output/{}.cpp".format(cls) - out += ["## {} {{#{}}}".format(cls, cls), "", "Used by: {}. Source: [`{}`]({}{}).".format( - used.get(cls, "see the code"), path, blob, path), ""] - for kind in ("history", "history, per surface", "volume"): - rows = [f for f in fields[cls] if f["kind"] == kind] - if not rows: - continue - out += ["**{} fields ({})**".format(kind.capitalize(), len(rows)), "", "| Field | Group | Description |", - "|---|---|---|"] - for f in rows: - shown = "`{}`".format(f["name"]) if f["literal"] else "*{}*".format(cell(f["name"])) - out.append("| [{}]({}{}#L{}) | {} | {} |".format(shown, blob, path, f["line"], cell(f["group"]), - cell(f["desc"]) or "*no description*")) - out.append("") - out += ["{% endraw %}", ""] - with open(os.path.join(site, DOCS, name + ".md"), "w", encoding="utf-8") as f: - f.write("\n".join(out)) - return name + name = "Output-Fields-Reference" + out = [ + "---", + "title: Output Fields Reference", + "permalink: /{}/{}/".format(URL, name), + "---", + "", + "{% raw %}", + "", + "This page lists the screen, history and volume output fields that each output class of SU2 can write. " + "It is made from the `AddHistoryOutput` and `AddVolumeOutput` calls in `SU2_CFD/src/output/`. " + "Generated from SU2 commit `{}`.".format(commit), + "", + "- A field is written only when the case needs it (for example turbulence fields only with a turbulence " + "model). For the exact list of your case, run a dry run: `SU2_CFD -d your_config.cfg`. It prints " + '"Available screen/history output fields" and "Available volume output fields".', + "- How to select fields: [Custom Output](../Custom-Output/). How to add one: " + "[Adding Output Fields](../Adding-Output-Fields/).", + "- Names in *italics* are built at run time (for example one field per species or per marker).", + "", + ] + order = [c for c, _ in OUTPUT_CLASSES if c in fields] + sorted( + c for c in fields if c not in dict(OUTPUT_CLASSES) + ) + used = dict(OUTPUT_CLASSES) + for cls in order: + path = "SU2_CFD/src/output/{}.cpp".format(cls) + out += [ + "## {} {{#{}}}".format(cls, cls), + "", + "Used by: {}. Source: [`{}`]({}{}).".format( + used.get(cls, "see the code"), path, blob, path + ), + "", + ] + for kind in ("history", "history, per surface", "volume"): + rows = [f for f in fields[cls] if f["kind"] == kind] + if not rows: + continue + out += [ + "**{} fields ({})**".format(kind.capitalize(), len(rows)), + "", + "| Field | Group | Description |", + "|---|---|---|", + ] + for f in rows: + shown = ( + "`{}`".format(f["name"]) + if f["literal"] + else "*{}*".format(cell(f["name"])) + ) + out.append( + "| [{}]({}{}#L{}) | {} | {} |".format( + shown, + blob, + path, + f["line"], + cell(f["group"]), + cell(f["desc"]) or "*no description*", + ) + ) + out.append("") + out += ["{% endraw %}", ""] + with open(os.path.join(site, DOCS, name + ".md"), "w", encoding="utf-8") as f: + f.write("\n".join(out)) + return name def find_stale_pages(su2, pages_dir): - """Developer pages whose named code files changed since the commit in their "Last checked" line.""" - checked = re.compile(r"Last checked against SU2 \w+ `([0-9a-f]{7,40})`") - named = re.compile(r"`((?:Common|SU2_CFD|SU2_DEF|SU2_DOT|SU2_GEO|SU2_SOL|SU2_PY|UnitTests|TestCases|externals|" - r"meson_scripts|Docs|\.github)/[^`\s:]+|config_template\.cfg|meson\.build|meson_options\.txt|" - r"doxyfile)") - report = [] - for path in sorted(glob.glob(os.path.join(pages_dir, "*.md"))): - with open(path, encoding="utf-8") as f: - text = f.read() - page = os.path.splitext(os.path.basename(path))[0] - m = checked.search(text) - if not m: - report.append((page, None, "no \"Last checked against\" line", [])) - continue - files = sorted({p for p in named.findall(text) if os.path.exists(os.path.join(su2, p))}) - try: - out = subprocess.check_output(["git", "-C", su2, "log", "--format=%h", "--name-only", m.group(1) + "..HEAD", - "--"] + files, text=True, stderr=subprocess.DEVNULL) - except (OSError, subprocess.CalledProcessError): - report.append((page, m.group(1), "commit not found in the history", [])) - continue - counts = {} - for line in out.splitlines(): - if line in files: - counts[line] = counts.get(line, 0) + 1 - report.append((page, m.group(1), "", sorted(counts.items(), key=lambda c: (-c[1], c[0])))) - return report + """Developer pages whose named code files changed since the commit in their "Last checked" line.""" + checked = re.compile(r"Last checked against SU2 \w+ `([0-9a-f]{7,40})`") + named = re.compile( + r"`((?:Common|SU2_CFD|SU2_DEF|SU2_DOT|SU2_GEO|SU2_SOL|SU2_PY|UnitTests|TestCases|externals|" + r"meson_scripts|Docs|\.github)/[^`\s:]+|config_template\.cfg|meson\.build|meson_options\.txt|" + r"doxyfile)" + ) + report = [] + for path in sorted(glob.glob(os.path.join(pages_dir, "*.md"))): + with open(path, encoding="utf-8") as f: + text = f.read() + page = os.path.splitext(os.path.basename(path))[0] + m = checked.search(text) + if not m: + report.append((page, None, 'no "Last checked against" line', [])) + continue + files = sorted( + {p for p in named.findall(text) if os.path.exists(os.path.join(su2, p))} + ) + try: + out = subprocess.check_output( + [ + "git", + "-C", + su2, + "log", + "--format=%h", + "--name-only", + m.group(1) + "..HEAD", + "--", + ] + + files, + text=True, + stderr=subprocess.DEVNULL, + ) + except (OSError, subprocess.CalledProcessError): + report.append((page, m.group(1), "commit not found in the history", [])) + continue + counts = {} + for line in out.splitlines(): + if line in files: + counts[line] = counts.get(line, 0) + 1 + report.append( + (page, m.group(1), "", sorted(counts.items(), key=lambda c: (-c[1], c[0]))) + ) + return report def doc_coverage(su2): - """Classes in the headers of Common and SU2_CFD: total, with a Doxygen comment, and in a Doxygen group. - Approximate: found with a regular expression, not with a C++ parser.""" - cls = re.compile(r"^\s*(?:template\s*<[^;{]*>\s*)?class\s+(?:\w+\s+)?([A-Z]\w*)\b[^;]*?(?:\{|:[^:])", re.M) - stats = {} - for folder in ("Common/include", "SU2_CFD/include"): - for path in sorted(glob.glob(os.path.join(su2, folder, "**", "*.hpp"), recursive=True)): - with open(path, encoding="utf-8", errors="replace") as f: - text = f.read() - rel = os.path.relpath(path, su2) - seen = set() - for m in cls.finditer(text): - # Skip repeated declarations (specializations) and all-capital helper types of template code. - if m.group(1) in seen or m.group(1).isupper(): - continue - seen.add(m.group(1)) - before = text[:m.start()].rstrip() - # The comment must end just before the class (template lines are part of the match). - comment = before[before.rfind("/*"):] if before.endswith("*/") else "" - key = os.path.dirname(rel) - total, documented, grouped, missing = stats.get(key, (0, 0, 0, [])) - stats[key] = (total + 1, documented + bool(comment), grouped + ("\\ingroup" in comment), - missing + ([m.group(1)] if not comment else [])) - return stats + """Classes in the headers of Common and SU2_CFD: total, with a Doxygen comment, and in a Doxygen group. + Approximate: found with a regular expression, not with a C++ parser.""" + cls = re.compile( + r"^\s*(?:template\s*<[^;{]*>\s*)?class\s+(?:\w+\s+)?([A-Z]\w*)\b[^;]*?(?:\{|:[^:])", + re.M, + ) + stats = {} + for folder in ("Common/include", "SU2_CFD/include"): + for path in sorted( + glob.glob(os.path.join(su2, folder, "**", "*.hpp"), recursive=True) + ): + with open(path, encoding="utf-8", errors="replace") as f: + text = f.read() + rel = os.path.relpath(path, su2) + seen = set() + for m in cls.finditer(text): + # Skip repeated declarations (specializations) and all-capital helper types of template code. + if m.group(1) in seen or m.group(1).isupper(): + continue + seen.add(m.group(1)) + before = text[: m.start()].rstrip() + # The comment must end just before the class (template lines are part of the match). + comment = before[before.rfind("/*") :] if before.endswith("*/") else "" + key = os.path.dirname(rel) + total, documented, grouped, missing = stats.get(key, (0, 0, 0, [])) + stats[key] = ( + total + 1, + documented + bool(comment), + grouped + ("\\ingroup" in comment), + missing + ([m.group(1)] if not comment else []), + ) + return stats def read_doxygen_log(path): - """Doxygen warnings: total, by kind and by folder.""" - kinds, folders, total = {}, {}, 0 - if not path or not os.path.isfile(path): - return None - with open(path, encoding="utf-8", errors="replace") as f: - for line in f: - m = re.match(r"(.*?):\d+: warning: (.*)", line) - if not m: - continue - total += 1 - kind = re.sub(r"'[^']*'|\"[^\"]*\"|\b\w+::\w+(::\w+)*\b", "X", m.group(2)) - kind = re.sub(r"\(.*", "", kind).strip()[:80] - kinds[kind] = kinds.get(kind, 0) + 1 - parts = re.split(r"[\\/]", m.group(1)) - top = next((i for i, p in enumerate(parts) if p in ("Common", "SU2_CFD", "SU2_DEF", "SU2_DOT", "SU2_GEO", - "SU2_SOL", "Docs")), None) - folder = "/".join(parts[top:top + 2]) if top is not None else "other" - folders[folder] = folders.get(folder, 0) + 1 - return total, kinds, folders + """Doxygen warnings: total, by kind and by folder.""" + kinds, folders, total = {}, {}, 0 + if not path or not os.path.isfile(path): + return None + with open(path, encoding="utf-8", errors="replace") as f: + for line in f: + m = re.match(r"(.*?):\d+: warning: (.*)", line) + if not m: + continue + total += 1 + kind = re.sub(r"'[^']*'|\"[^\"]*\"|\b\w+::\w+(::\w+)*\b", "X", m.group(2)) + kind = re.sub(r"\(.*", "", kind).strip()[:80] + kinds[kind] = kinds.get(kind, 0) + 1 + parts = re.split(r"[\\/]", m.group(1)) + top = next( + ( + i + for i, p in enumerate(parts) + if p + in ( + "Common", + "SU2_CFD", + "SU2_DEF", + "SU2_DOT", + "SU2_GEO", + "SU2_SOL", + "Docs", + ) + ), + None, + ) + folder = "/".join(parts[top : top + 2]) if top is not None else "other" + folders[folder] = folders.get(folder, 0) + 1 + return total, kinds, folders def quality_sections(stale, coverage, doxy): - """Extra sections of the Docs Check page: stale pages and code documentation coverage.""" - out = ["", "## Developer pages that may be out of date", "", - "Each developer page ends with \"Last checked against SU2 develop ``\". These pages name code " - "files that changed since that commit. Read the changes, update the page if needed, then update the " - "commit in its last line.", ""] - flagged = [r for r in stale if r[2] or r[3]] - out.append("Pages to review: {} of {}.".format(len(flagged), len(stale))) - out.append("") - for page, commit, problem, changed in flagged: - out.append("- [{}](../{}/){}".format(page.replace("-", " "), page, ": " + problem if problem else "")) - for path, n in changed: - out.append(" - `{}`: {} commit{}".format(path, n, "s" if n > 1 else "")) - total = sum(v[0] for v in coverage.values()) - documented = sum(v[1] for v in coverage.values()) - grouped = sum(v[2] for v in coverage.values()) - out += ["", "## Code documentation coverage", "", - "This section counts the classes declared in the headers of `Common/include` and `SU2_CFD/include`. " - "The count uses a simple text search, so the numbers are approximate. `Docs/docmain.hpp` asks that " - "every class has a Doxygen comment with `\\brief` and `\\ingroup`.", "", - "- Classes: {}".format(total), - "- With a Doxygen comment: {} ({:.0f}%)".format(documented, 100.0 * documented / max(total, 1)), - "- In a Doxygen group (`\\ingroup`): {} ({:.0f}%)".format(grouped, 100.0 * grouped / max(total, 1)), "", - "| Folder | Classes | With comment | In a group |", "|---|---|---|---|"] - for folder, (t, d, g, _) in sorted(coverage.items()): - out.append("| `{}` | {} | {} | {} |".format(folder, t, d, g)) - out += ["", '
Classes without a Doxygen comment', ""] - for folder, (_, _, _, missing) in sorted(coverage.items()): - if missing: - out.append("- `{}`: {}".format(folder, ", ".join("`{}`".format(c) for c in missing))) - out += ["", "
", ""] - if doxy: - total, kinds, folders = doxy - out += ["", "## Doxygen warnings", "", "Doxygen found {} problems in the code comments.".format(total), "", - "| Kind | Count |", "|---|---|"] - out += ["| {} | {} |".format(cell(k), n) for k, n in sorted(kinds.items(), key=lambda k: -k[1])[:12]] - out += ["", "| Folder | Count |", "|---|---|"] - out += ["| `{}` | {} |".format(k, n) for k, n in sorted(folders.items(), key=lambda k: -k[1])] - return out + """Extra sections of the Docs Check page: stale pages and code documentation coverage.""" + out = [ + "", + "## Developer pages that may be out of date", + "", + 'Each developer page ends with "Last checked against SU2 develop ``". These pages name code ' + "files that changed since that commit. Read the changes, update the page if needed, then update the " + "commit in its last line.", + "", + ] + flagged = [r for r in stale if r[2] or r[3]] + out.append("Pages to review: {} of {}.".format(len(flagged), len(stale))) + out.append("") + for page, commit, problem, changed in flagged: + out.append( + "- [{}](../{}/){}".format( + page.replace("-", " "), page, ": " + problem if problem else "" + ) + ) + for path, n in changed: + out.append(" - `{}`: {} commit{}".format(path, n, "s" if n > 1 else "")) + total = sum(v[0] for v in coverage.values()) + documented = sum(v[1] for v in coverage.values()) + grouped = sum(v[2] for v in coverage.values()) + out += [ + "", + "## Code documentation coverage", + "", + "This section counts the classes declared in the headers of `Common/include` and `SU2_CFD/include`. " + "The count uses a simple text search, so the numbers are approximate. `Docs/docmain.hpp` asks that " + "every class has a Doxygen comment with `\\brief` and `\\ingroup`.", + "", + "- Classes: {}".format(total), + "- With a Doxygen comment: {} ({:.0f}%)".format( + documented, 100.0 * documented / max(total, 1) + ), + "- In a Doxygen group (`\\ingroup`): {} ({:.0f}%)".format( + grouped, 100.0 * grouped / max(total, 1) + ), + "", + "| Folder | Classes | With comment | In a group |", + "|---|---|---|---|", + ] + for folder, (t, d, g, _) in sorted(coverage.items()): + out.append("| `{}` | {} | {} | {} |".format(folder, t, d, g)) + out += [ + "", + '
Classes without a Doxygen comment', + "", + ] + for folder, (_, _, _, missing) in sorted(coverage.items()): + if missing: + out.append( + "- `{}`: {}".format( + folder, ", ".join("`{}`".format(c) for c in missing) + ) + ) + out += ["", "
", ""] + if doxy: + total, kinds, folders = doxy + out += [ + "", + "## Doxygen warnings", + "", + "Doxygen found {} problems in the code comments.".format(total), + "", + "| Kind | Count |", + "|---|---|", + ] + out += [ + "| {} | {} |".format(cell(k), n) + for k, n in sorted(kinds.items(), key=lambda k: -k[1])[:12] + ] + out += ["", "| Folder | Count |", "|---|---|"] + out += [ + "| `{}` | {} |".format(k, n) + for k, n in sorted(folders.items(), key=lambda k: -k[1]) + ] + return out def check_template(su2, known): - """Options written in config_template.cfg (active or commented out) that the code does not know.""" - found = [] - with open(os.path.join(su2, "config_template.cfg"), encoding="utf-8", errors="replace") as f: - for num, line in enumerate(f, 1): - m = re.match(r"^\s*%?\s*([A-Z][A-Z0-9_]{2,})\s*=", line) - if m and m.group(1) not in known and m.group(1) not in NOT_OPTIONS: - found.append((num, m.group(1))) - return found + """Options written in config_template.cfg (active or commented out) that the code does not know.""" + found = [] + with open( + os.path.join(su2, "config_template.cfg"), encoding="utf-8", errors="replace" + ) as f: + for num, line in enumerate(f, 1): + m = re.match(r"^\s*%?\s*([A-Z][A-Z0-9_]{2,})\s*=", line) + if m and m.group(1) not in known and m.group(1) not in NOT_OPTIONS: + found.append((num, m.group(1))) + return found def read_parser_renames(su2): - """Old option name -> message of CConfig::SetConfig_Parsing for options that were renamed or removed.""" - with open(os.path.join(su2, "Common", "src", "CConfig.cpp"), encoding="utf-8") as f: - text = f.read() - found = {} - pattern = re.compile(r'if\s*\(((?:\s*(?:\|\|)?\s*!option_name\.compare\("\w+"\))+)\s*\)\s*' - r'newString\.append\("((?:[^"\\]|\\.)*)"\)') - for m in pattern.finditer(text): - message = m.group(2).replace("\\n", " ").strip() - for name in re.findall(r'compare\("(\w+)"\)', m.group(1)): - found[name] = message - return found + """Old option name -> message of CConfig::SetConfig_Parsing for options that were renamed or removed.""" + with open(os.path.join(su2, "Common", "src", "CConfig.cpp"), encoding="utf-8") as f: + text = f.read() + found = {} + pattern = re.compile( + r'if\s*\(((?:\s*(?:\|\|)?\s*!option_name\.compare\("\w+"\))+)\s*\)\s*' + r'newString\.append\("((?:[^"\\]|\\.)*)"\)' + ) + for m in pattern.finditer(text): + message = m.group(2).replace("\\n", " ").strip() + for name in re.findall(r'compare\("(\w+)"\)', m.group(1)): + found[name] = message + return found def option_history(su2): - """Option name -> last release tag that had it, for options that are no longer in the code.""" - tags = subprocess.run(["git", "-C", su2, "tag", "-l", "v7.*", "v8.*"], capture_output=True, text=True).stdout.split() - tags.sort(key=lambda t: [int(x) if x.isdigit() else x for x in re.split(r"[.v-]", t) if x]) - last_seen = {} - for tag in tags: - for path in ("Common/src/CConfig.cpp", "Common/src/config_structure.cpp"): - show = subprocess.run(["git", "-C", su2, "show", "{}:{}".format(tag, path)], capture_output=True, text=True) - if show.returncode == 0: - for name in re.findall(r'add\w*Option\s*\(\s*"([A-Z0-9_]+)"', show.stdout): - last_seen[name] = tag - break - return last_seen, tags + """Option name -> last release tag that had it, for options that are no longer in the code.""" + tags = subprocess.run( + ["git", "-C", su2, "tag", "-l", "v7.*", "v8.*"], capture_output=True, text=True + ).stdout.split() + tags.sort( + key=lambda t: [int(x) if x.isdigit() else x for x in re.split(r"[.v-]", t) if x] + ) + last_seen = {} + for tag in tags: + for path in ("Common/src/CConfig.cpp", "Common/src/config_structure.cpp"): + show = subprocess.run( + ["git", "-C", su2, "show", "{}:{}".format(tag, path)], + capture_output=True, + text=True, + ) + if show.returncode == 0: + for name in re.findall( + r'add\w*Option\s*\(\s*"([A-Z0-9_]+)"', show.stdout + ): + last_seen[name] = tag + break + return last_seen, tags def write_renamed_page(su2, site, known, commit): - name = "Renamed-Options" - renames = read_parser_renames(su2) - last_seen, tags = option_history(su2) - # Parser messages for names that no release ever had are left out (they would only confuse readers). - removed = sorted(n for n in last_seen if n not in known) if tags else sorted(n for n in renames if n not in known) - out = ["---", "title: Renamed and Removed Options", "permalink: /{}/{}/".format(URL, name), "---", "", - "{% raw %}", "", - "This page lists the options that SU2 had in a release since v7.0.0 but no longer has in commit `{}`. " - "It is made from the release history and from the messages SU2 prints when it reads an old option. " - "If your config file stops with \"invalid option name\", look for the option here.".format(commit), ""] - if not tags: - out.append("*The release history was not available for this build.*") - out += ["", "| Option | Last release with it | What SU2 says |", "|---|---|---|"] - for opt in removed: - out.append("| `{}` | {} | {} |".format(opt, last_seen.get(opt, "-"), cell(renames.get(opt, "")))) - out += ["", "{% endraw %}", ""] - with open(os.path.join(site, DOCS, name + ".md"), "w", encoding="utf-8") as f: - f.write("\n".join(out)) - return name + name = "Renamed-Options" + renames = read_parser_renames(su2) + last_seen, tags = option_history(su2) + # Parser messages for names that no release ever had are left out (they would only confuse readers). + removed = ( + sorted(n for n in last_seen if n not in known) + if tags + else sorted(n for n in renames if n not in known) + ) + out = [ + "---", + "title: Renamed and Removed Options", + "permalink: /{}/{}/".format(URL, name), + "---", + "", + "{% raw %}", + "", + "This page lists the options that SU2 had in a release since v7.0.0 but no longer has in commit `{}`. " + "It is made from the release history and from the messages SU2 prints when it reads an old option. " + 'If your config file stops with "invalid option name", look for the option here.'.format( + commit + ), + "", + ] + if not tags: + out.append("*The release history was not available for this build.*") + out += ["", "| Option | Last release with it | What SU2 says |", "|---|---|---|"] + for opt in removed: + out.append( + "| `{}` | {} | {} |".format( + opt, last_seen.get(opt, "-"), cell(renames.get(opt, "")) + ) + ) + out += ["", "{% endraw %}", ""] + with open(os.path.join(site, DOCS, name + ".md"), "w", encoding="utf-8") as f: + f.write("\n".join(out)) + return name def read_error_messages(su2): - """List of (file, line, message) for every SU2_MPI::Error call; message pieces built at run time become '...'.""" - token = re.compile(r"\bSU2_MPI::Error\s*\(") - found = [] - for folder in ("Common", "SU2_CFD", "SU2_DEF", "SU2_DOT", "SU2_GEO", "SU2_SOL"): - for path in sorted(glob.glob(os.path.join(su2, folder, "**", "*.*"), recursive=True)): - if not path.endswith((".cpp", ".hpp", ".inl")): - continue - with open(path, encoding="utf-8", errors="replace") as f: - text = f.read() - for m in token.finditer(text): - body, _ = call_body(text, m.end() - 1) - first = split_args(body)[0] - pieces = re.findall(r'"((?:[^"\\]|\\.)*)"', first) - if not pieces: - continue - message = re.sub(r"\\n|\\t", " ", " ... ".join(p for p in pieces if p.strip())) - message = re.sub(r"\s+", " ", message.replace('\\"', '"')).strip() - found.append((os.path.relpath(path, su2), text.count("\n", 0, m.start()) + 1, message)) - return found + """List of (file, line, message) for every SU2_MPI::Error call; message pieces built at run time become '...'.""" + token = re.compile(r"\bSU2_MPI::Error\s*\(") + found = [] + for folder in ("Common", "SU2_CFD", "SU2_DEF", "SU2_DOT", "SU2_GEO", "SU2_SOL"): + for path in sorted( + glob.glob(os.path.join(su2, folder, "**", "*.*"), recursive=True) + ): + if not path.endswith((".cpp", ".hpp", ".inl")): + continue + with open(path, encoding="utf-8", errors="replace") as f: + text = f.read() + for m in token.finditer(text): + body, _ = call_body(text, m.end() - 1) + first = split_args(body)[0] + pieces = re.findall(r'"((?:[^"\\]|\\.)*)"', first) + if not pieces: + continue + message = re.sub( + r"\\n|\\t", " ", " ... ".join(p for p in pieces if p.strip()) + ) + message = re.sub(r"\s+", " ", message.replace('\\"', '"')).strip() + found.append( + ( + os.path.relpath(path, su2), + text.count("\n", 0, m.start()) + 1, + message, + ) + ) + return found def write_errors_page(site, errors, options, commit, blob): - name = "Error-Messages" - pages = {o["name"]: o["page"] for o in options} - groups = {} - for path, line, message in errors: - if path == "Common/src/CConfig.cpp": - group = "Configuration checks" - else: - group = "/".join(path.split("/")[:-1]) - groups.setdefault(group, []).append((path, line, message)) - out = ["---", "title: Error Messages", "permalink: /{}/{}/".format(URL, name), "---", "", "{% raw %}", "", - "This page lists the {} error messages that can stop SU2 in commit `{}` (the `SU2_MPI::Error` calls " - "in the code). Use the search of your browser (Ctrl+F) with a few words of the message you see. " - "Parts of a message that SU2 fills in at run time are shown as \"...\".".format(len(errors), commit), "", - "Each message links to the code that prints it, and to the options it names.", "", - "See also: [Renamed and Removed Options](../Renamed-Options/).", ""] - order = ["Configuration checks"] + sorted(g for g in groups if g != "Configuration checks") - for group in order: - if group not in groups: - continue - out += ["## {} ({})".format(group, len(groups[group])), "", "| Message | Options | Code |", "|---|---|---|"] - for path, line, message in groups[group]: - named = sorted({w for w in re.findall(r"\b[A-Z][A-Z0-9_]{2,}\b", message) if w in pages}) - links = ", ".join("[`{0}`](../{1}/#{0})".format(n, pages[n]) for n in named) - out.append("| {} | {} | [{}:{}]({}{}#L{}) |".format(cell(message), links, os.path.basename(path), line, - blob, path, line)) - out.append("") - out += ["{% endraw %}", ""] - with open(os.path.join(site, DOCS, name + ".md"), "w", encoding="utf-8") as f: - f.write("\n".join(out)) - return name + name = "Error-Messages" + pages = {o["name"]: o["page"] for o in options} + groups = {} + for path, line, message in errors: + if path == "Common/src/CConfig.cpp": + group = "Configuration checks" + else: + group = "/".join(path.split("/")[:-1]) + groups.setdefault(group, []).append((path, line, message)) + out = [ + "---", + "title: Error Messages", + "permalink: /{}/{}/".format(URL, name), + "---", + "", + "{% raw %}", + "", + "This page lists the {} error messages that can stop SU2 in commit `{}` (the `SU2_MPI::Error` calls " + "in the code). Use the search of your browser (Ctrl+F) with a few words of the message you see. " + 'Parts of a message that SU2 fills in at run time are shown as "...".'.format( + len(errors), commit + ), + "", + "Each message links to the code that prints it, and to the options it names.", + "", + "See also: [Renamed and Removed Options](../Renamed-Options/).", + "", + ] + order = ["Configuration checks"] + sorted( + g for g in groups if g != "Configuration checks" + ) + for group in order: + if group not in groups: + continue + out += [ + "## {} ({})".format(group, len(groups[group])), + "", + "| Message | Options | Code |", + "|---|---|---|", + ] + for path, line, message in groups[group]: + named = sorted( + { + w + for w in re.findall(r"\b[A-Z][A-Z0-9_]{2,}\b", message) + if w in pages + } + ) + links = ", ".join("[`{0}`](../{1}/#{0})".format(n, pages[n]) for n in named) + out.append( + "| {} | {} | [{}:{}]({}{}#L{}) |".format( + cell(message), links, os.path.basename(path), line, blob, path, line + ) + ) + out.append("") + out += ["{% endraw %}", ""] + with open(os.path.join(site, DOCS, name + ".md"), "w", encoding="utf-8") as f: + f.write("\n".join(out)) + return name def write_check_page(site, found, commit, options, extra=()): - out = ["---", "title: Docs Check", "permalink: /{}/Docs-Check/".format(URL), "---", "", "{% raw %}", "", - "This page is made at each build. It lists what needs work in the documentation of SU2 commit `{}`." - .format(commit), "", - "## Options named in the docs that do not exist", "", - "These names are not options of the code: the option was renamed or removed, or the name has a typo. " - "Fix the page in the website repository.", "", - "Found: {}".format(len(found)), ""] - last = None - for path, num, name, line in found: - if path != last: - out += ["", "### `{}`".format(path), ""] - last = path - out.append("- line {}: `{}`".format(num, name)) - - def link(opt): - return "[`{0}`](../{1}/#{0})".format(opt["name"], opt["page"]) - - checks = [ - ("Options without any description", - "No description in `Common/src/CConfig.cpp` and no comment in `config_template.cfg`.", - lambda o: not o["desc_words"] and not o["has_notes"]), - ("Options with a description only in config_template.cfg", - "Move or copy the text into the comment above the option in `Common/src/CConfig.cpp`.", - lambda o: not o["desc_words"] and o["has_notes"]), - ("Options with a very short description", - "The comment in `Common/src/CConfig.cpp` has 6 words or fewer.", - lambda o: 0 < o["desc_words"] <= 6), - ("Options with no example and no explanation", - "No test case uses the option and no page of this website names it.", - lambda o: not o["uses"] and not o["mentions"]), - ] - for title, text, test in checks: - selected = [o for o in options if test(o)] - out += ["", "## {} ({})".format(title, len(selected)), "", text, ""] - groups = {} - for opt in selected: - groups.setdefault(opt["category"], []).append(link(opt)) - for cat, links in groups.items(): - out.append("- **{}:** {}".format(cat, ", ".join(links))) - out += list(extra) - out += ["", "{% endraw %}", ""] - with open(os.path.join(site, DOCS, "Docs-Check.md"), "w", encoding="utf-8") as f: - f.write("\n".join(out)) + out = [ + "---", + "title: Docs Check", + "permalink: /{}/Docs-Check/".format(URL), + "---", + "", + "{% raw %}", + "", + "This page is made at each build. It lists what needs work in the documentation of SU2 commit `{}`.".format( + commit + ), + "", + "## Options named in the docs that do not exist", + "", + "These names are not options of the code: the option was renamed or removed, or the name has a typo. " + "Fix the page in the website repository.", + "", + "Found: {}".format(len(found)), + "", + ] + last = None + for path, num, name, line in found: + if path != last: + out += ["", "### `{}`".format(path), ""] + last = path + out.append("- line {}: `{}`".format(num, name)) + + def link(opt): + return "[`{0}`](../{1}/#{0})".format(opt["name"], opt["page"]) + + checks = [ + ( + "Options without any description", + "No description in `Common/src/CConfig.cpp` and no comment in `config_template.cfg`.", + lambda o: not o["desc_words"] and not o["has_notes"], + ), + ( + "Options with a description only in config_template.cfg", + "Move or copy the text into the comment above the option in `Common/src/CConfig.cpp`.", + lambda o: not o["desc_words"] and o["has_notes"], + ), + ( + "Options with a very short description", + "The comment in `Common/src/CConfig.cpp` has 6 words or fewer.", + lambda o: 0 < o["desc_words"] <= 6, + ), + ( + "Options with no example and no explanation", + "No test case uses the option and no page of this website names it.", + lambda o: not o["uses"] and not o["mentions"], + ), + ] + for title, text, test in checks: + selected = [o for o in options if test(o)] + out += ["", "## {} ({})".format(title, len(selected)), "", text, ""] + groups = {} + for opt in selected: + groups.setdefault(opt["category"], []).append(link(opt)) + for cat, links in groups.items(): + out.append("- **{}:** {}".format(cat, ", ".join(links))) + out += list(extra) + out += ["", "{% endraw %}", ""] + with open(os.path.join(site, DOCS, "Docs-Check.md"), "w", encoding="utf-8") as f: + f.write("\n".join(out)) def copy_code_notes(su2, site): - """Copy the Markdown pages kept in the code folders (e.g. Docs/Aeroacoustics.md) into the user guide.""" - folders = ["Docs", "Common", "SU2_CFD", "SU2_DEF", "SU2_DOT", "SU2_GEO", "SU2_SOL", "SU2_PY"] - names = [] - for folder in folders: - for path in sorted(glob.glob(os.path.join(su2, folder, "**", "*.md"), recursive=True)): - if os.path.join("Docs", "website") in path: - continue - with open(path, encoding="utf-8", errors="replace") as f: - text = f.read() - heading = re.search(r"^#\s+(.+)$", text, re.M) - title = heading.group(1).strip() if heading else os.path.basename(path) - if heading: - text = text[:heading.start()] + text[heading.end():] - name = "Code-Notes-" + slugify(os.path.splitext(os.path.basename(path))[0]) - rel = os.path.relpath(path, su2) - page = ["---", 'title: "{}"'.format(title.replace('"', "'")), "permalink: /{}/{}/".format(URL, name), "---", - "", "> This page is kept in the SU2 code repository: `{}`.".format(rel), "", - "{% raw %}", text.strip(), "{% endraw %}", ""] - with open(os.path.join(site, DOCS, name + ".md"), "w", encoding="utf-8") as f: - f.write("\n".join(page)) - names.append(name) - return names + """Copy the Markdown pages kept in the code folders (e.g. Docs/Aeroacoustics.md) into the user guide.""" + folders = [ + "Docs", + "Common", + "SU2_CFD", + "SU2_DEF", + "SU2_DOT", + "SU2_GEO", + "SU2_SOL", + "SU2_PY", + ] + names = [] + for folder in folders: + for path in sorted( + glob.glob(os.path.join(su2, folder, "**", "*.md"), recursive=True) + ): + if os.path.join("Docs", "website") in path: + continue + with open(path, encoding="utf-8", errors="replace") as f: + text = f.read() + heading = re.search(r"^#\s+(.+)$", text, re.M) + title = heading.group(1).strip() if heading else os.path.basename(path) + if heading: + text = text[: heading.start()] + text[heading.end() :] + name = "Code-Notes-" + slugify(os.path.splitext(os.path.basename(path))[0]) + rel = os.path.relpath(path, su2) + page = [ + "---", + 'title: "{}"'.format(title.replace('"', "'")), + "permalink: /{}/{}/".format(URL, name), + "---", + "", + "> This page is kept in the SU2 code repository: `{}`.".format(rel), + "", + "{% raw %}", + text.strip(), + "{% endraw %}", + "", + ] + with open( + os.path.join(site, DOCS, name + ".md"), "w", encoding="utf-8" + ) as f: + f.write("\n".join(page)) + names.append(name) + return names def add_to_section(nav, title, pages): - """Append pages to a section of the menu, unless they are listed already.""" - sec = re.search(r"- title: {}\n docs_v7:\n((?: - .*\n)+)".format(re.escape(title)), nav) - if not sec: - return nav - added = "".join(" - {}\n".format(p) for p in pages if " - {}\n".format(p) not in sec.group(1)) - return nav[:sec.end(1)] + added + nav[sec.end(1):] + """Append pages to a section of the menu, unless they are listed already.""" + sec = re.search( + r"- title: {}\n docs_v7:\n((?: - .*\n)+)".format(re.escape(title)), nav + ) + if not sec: + return nav + added = "".join( + " - {}\n".format(p) for p in pages if " - {}\n".format(p) not in sec.group(1) + ) + return nav[: sec.end(1)] + added + nav[sec.end(1) :] def patch_nav(site, ref_pages, extra_dev_pages, user_pages): - path = os.path.join(site, NAV) - with open(path, encoding="utf-8") as f: - nav = f.read() - section = "- title: Option Reference\n docs_v7:\n" + "".join(" - {}\n".format(p) for p in ref_pages) + "\n" - nav = nav.replace("- title: Developer Docs", section + "- title: Developer Docs", 1) - # The "Adding-..." pages get their own section, after the developer docs. - howto = sorted((p for p in extra_dev_pages if p.startswith("Adding-")), - key=lambda p: (HOWTO_ORDER.index(p) if p in HOWTO_ORDER else len(HOWTO_ORDER), p)) - if howto: - dev = re.search(r"- title: Developer Docs\n docs_v7:\n(?: - .*\n)+", nav) - block = "- title: How to Add...\n docs_v7:\n" + "".join(" - {}\n".format(p) for p in howto) - nav = nav[:dev.end()] + block + nav[dev.end():] if dev else nav - nav = add_to_section(nav, "Developer Docs", [p for p in extra_dev_pages if p not in howto]) - nav = add_to_section(nav, "Users Guide", user_pages) - with open(path, "w", encoding="utf-8") as f: - f.write(nav) + path = os.path.join(site, NAV) + with open(path, encoding="utf-8") as f: + nav = f.read() + section = ( + "- title: Option Reference\n docs_v7:\n" + + "".join(" - {}\n".format(p) for p in ref_pages) + + "\n" + ) + nav = nav.replace("- title: Developer Docs", section + "- title: Developer Docs", 1) + # The "Adding-..." pages get their own section, after the developer docs. + howto = sorted( + (p for p in extra_dev_pages if p.startswith("Adding-")), + key=lambda p: ( + HOWTO_ORDER.index(p) if p in HOWTO_ORDER else len(HOWTO_ORDER), + p, + ), + ) + if howto: + dev = re.search(r"- title: Developer Docs\n docs_v7:\n(?: - .*\n)+", nav) + block = "- title: How to Add...\n docs_v7:\n" + "".join( + " - {}\n".format(p) for p in howto + ) + nav = nav[: dev.end()] + block + nav[dev.end() :] if dev else nav + nav = add_to_section( + nav, "Developer Docs", [p for p in extra_dev_pages if p not in howto] + ) + nav = add_to_section(nav, "Users Guide", user_pages) + with open(path, "w", encoding="utf-8") as f: + f.write(nav) UPSTREAM_SITE = "https://github.com/su2code/su2code.github.io.git" @@ -875,109 +1330,166 @@ def patch_nav(site, ref_pages, extra_dev_pages, user_pages): def changed_site_pages(site): - """Permalinks of the website pages that differ from the official website (su2code/su2code.github.io).""" - try: - subprocess.check_call(["git", "-C", site, "fetch", "-q", "--depth=50", UPSTREAM_SITE, "master"], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + """Permalinks of the website pages that differ from the official website (su2code/su2code.github.io).""" try: - base = subprocess.check_output(["git", "-C", site, "merge-base", "FETCH_HEAD", "HEAD"], text=True).strip() - except subprocess.CalledProcessError: - base = "FETCH_HEAD" - files = subprocess.check_output(["git", "-C", site, "diff", "--name-only", base, "HEAD"], text=True).split() - except (OSError, subprocess.CalledProcessError): - print("Could not compare the website with the official one; only the generated pages are marked.") - return [] - urls = [] - for rel in files: - path = os.path.join(site, rel) - if not rel.endswith(".md") or not os.path.isfile(path): - continue - with open(path, encoding="utf-8", errors="replace") as f: - link = re.search(r"^permalink:\s*(\S+)$", f.read(), re.M) - if link: - urls.append(link.group(1).strip("\"'")) - return urls + subprocess.check_call( + ["git", "-C", site, "fetch", "-q", "--depth=50", UPSTREAM_SITE, "master"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + try: + base = subprocess.check_output( + ["git", "-C", site, "merge-base", "FETCH_HEAD", "HEAD"], text=True + ).strip() + except subprocess.CalledProcessError: + base = "FETCH_HEAD" + files = subprocess.check_output( + ["git", "-C", site, "diff", "--name-only", base, "HEAD"], text=True + ).split() + except (OSError, subprocess.CalledProcessError): + print( + "Could not compare the website with the official one; only the generated pages are marked." + ) + return [] + urls = [] + for rel in files: + path = os.path.join(site, rel) + if not rel.endswith(".md") or not os.path.isfile(path): + continue + with open(path, encoding="utf-8", errors="replace") as f: + link = re.search(r"^permalink:\s*(\S+)$", f.read(), re.M) + if link: + urls.append(link.group(1).strip("\"'")) + return urls def mark_changes(site, urls, sections): - """Debug: add a script to every page that colours the changed pages and menu sections in red.""" - os.makedirs(os.path.join(site, "js"), exist_ok=True) - with open(os.path.join(site, "js", "debug-changes.js"), "w", encoding="utf-8") as f: - f.write(DEBUG_JS % (json.dumps(sorted(set(urls))), json.dumps(sections))) - layout = os.path.join(site, "_layouts", "default.html") - with open(layout, encoding="utf-8") as f: - text = f.read() - tag = '\n' - if tag not in text: - with open(layout, "w", encoding="utf-8") as f: - f.write(text.replace("", tag + "", 1)) + """Debug: add a script to every page that colours the changed pages and menu sections in red.""" + os.makedirs(os.path.join(site, "js"), exist_ok=True) + with open(os.path.join(site, "js", "debug-changes.js"), "w", encoding="utf-8") as f: + f.write(DEBUG_JS % (json.dumps(sorted(set(urls))), json.dumps(sections))) + layout = os.path.join(site, "_layouts", "default.html") + with open(layout, encoding="utf-8") as f: + text = f.read() + tag = '\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 + 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()) + sys.exit(main()) diff --git a/Docs/website/tools/check_site.py b/Docs/website/tools/check_site.py index 80f297501697..b5d69ceeb65a 100644 --- a/Docs/website/tools/check_site.py +++ b/Docs/website/tools/check_site.py @@ -33,7 +33,7 @@ def local_target(root, page, href, baseurl): path = unquote(link.path) if base and path != base and not path.startswith(base + "/"): return root / "__outside_baseurl__", path, "" - path = path[len(base):].lstrip("/") + path = path[len(base) :].lstrip("/") target = (root / path).resolve() if root != target and root not in target.parents: return root / "__outside_site__", path, "" @@ -63,55 +63,100 @@ def parse(path): reason = "" if not target.is_file(): reason = "missing file" - elif fragment and target.suffix == ".html" and fragment not in parse(target).ids: + 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)) + 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")} + 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"): + 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): + 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): + 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)) + 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))] + 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"])) + 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 @@ -119,8 +164,13 @@ 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"))) + 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: @@ -138,18 +188,33 @@ def check_search(root, source, su2, baseurl): 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))) + 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] + 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( + "--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()) @@ -159,15 +224,24 @@ def main(): 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) + 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 = { + "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"]))) + 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) diff --git a/Docs/website/tools/rebase_links.py b/Docs/website/tools/rebase_links.py index 96704840b652..467c0239c8c5 100644 --- a/Docs/website/tools/rebase_links.py +++ b/Docs/website/tools/rebase_links.py @@ -35,37 +35,37 @@ 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*["\'])/(?!/)') + 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 + "/" + 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 + 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()) + sys.exit(main()) diff --git a/Docs/website/tools/test_build_site.py b/Docs/website/tools/test_build_site.py index aaa4aacd6239..10d596153304 100644 --- a/Docs/website/tools/test_build_site.py +++ b/Docs/website/tools/test_build_site.py @@ -12,14 +12,18 @@ 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 = { + (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")]) +};""" + ) + 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] @@ -33,8 +37,11 @@ def test_current_disabled_model_values(self): 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")] + 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"]): diff --git a/Docs/website/tools/test_docs_checks.py b/Docs/website/tools/test_docs_checks.py index 821522349263..52764057b44f 100644 --- a/Docs/website/tools/test_docs_checks.py +++ b/Docs/website/tools/test_docs_checks.py @@ -19,9 +19,11 @@ def test_links_under_baseurl_with_escaped_anchor_and_relative_asset(self): page = root / "docs_v7/example/index.html" page.parent.mkdir(parents=True) (root / "asset.png").touch() - page.write_text('

Title

ok' - 'broken' - 'brokenexternal') + page.write_text( + '

Title

ok' + 'broken' + 'brokenexternal' + ) problems, checked = check_site.check_links(root, "/SU2") self.assertEqual(checked, 4) self.assertEqual(len(problems), 2) @@ -34,23 +36,43 @@ def test_search_detects_missing_page_and_output_field(self): source = root / "source" (source / "_docs_v7").mkdir(parents=True) (source / "_docs_v7/example.md").write_text( - "---\ntitle: Example\npermalink: /docs_v7/example/\n---\nContent") + "---\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}]}): + 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": []}) + 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): @@ -59,9 +81,18 @@ def test_config_enum_values_ignore_source_code_and_check_disabled_variants(self) (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")]}) + "```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))) @@ -71,8 +102,15 @@ def test_config_enum_checks_hyphenated_keywords(self): (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"]}] + "```\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) @@ -88,7 +126,10 @@ def test_rebase_links_handles_root_and_subpath_without_double_prefix(self): 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) + self.assertIn( + 'href="https://su2code.github.io/documents/example.pdf"', + expected, + ) rebase_links.main() self.assertEqual(page.read_text(), expected) @@ -98,10 +139,21 @@ def test_link_to_check_report_exists_during_its_own_validation(self): 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)): + 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"], []) @@ -115,7 +167,9 @@ def test_build_refuses_sources_and_unmarked_nonempty_directory(self): work.mkdir() (work / "keep.txt").write_text("keep") for destination in (source, source / "build", root, work): - with self.subTest(destination=destination), self.assertRaises(ValueError): + with self.subTest(destination=destination), self.assertRaises( + ValueError + ): build_docs.prepare_directory(destination, (source,)) self.assertEqual((work / "keep.txt").read_text(), "keep") From a9d05f8a077fd0a55cf424130f575b44ebc4ad6f Mon Sep 17 00:00:00 2001 From: rois1995 Date: Mon, 26 Oct 2026 11:36:02 +0100 Subject: [PATCH 24/25] docs: address CodeQL parser and text warnings --- Docs/website/tools/build_site.py | 25 ++++++++------- Docs/website/tools/test_build_site.py | 46 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/Docs/website/tools/build_site.py b/Docs/website/tools/build_site.py index b9d8c5a70cda..0730b6e7b308 100644 --- a/Docs/website/tools/build_site.py +++ b/Docs/website/tools/build_site.py @@ -496,11 +496,11 @@ def write_reference(su2, site, options, maps, commit, repo, skip): ), "", "Defaults initialized outside the registration calls may not be extracted. " - "The template lines are examples and may differ from the defaults. " - "Allowed keywords can have additional restrictions for a particular solver or option combination.", + + "The template lines are examples and may differ from the defaults. " + + "Allowed keywords can have additional restrictions for a particular solver or option combination.", "", "Each option shows the description in the code, the text of `config_template.cfg` when it adds " - "something, the pages of this website that mention it, and real test case files that use it.", + + "something, the pages of this website that mention it, and real test case files that use it.", "", "- Options: {}".format(len(options)), "- Options without a description in the code: {} ({} without any text at all)".format( @@ -691,10 +691,10 @@ def write_output_page(site, fields, commit, blob): "Generated from SU2 commit `{}`.".format(commit), "", "- A field is written only when the case needs it (for example turbulence fields only with a turbulence " - "model). For the exact list of your case, run a dry run: `SU2_CFD -d your_config.cfg`. It prints " - '"Available screen/history output fields" and "Available volume output fields".', + + "model). For the exact list of your case, run a dry run: `SU2_CFD -d your_config.cfg`. It prints " + + '"Available screen/history output fields" and "Available volume output fields".', "- How to select fields: [Custom Output](../Custom-Output/). How to add one: " - "[Adding Output Fields](../Adding-Output-Fields/).", + + "[Adding Output Fields](../Adding-Output-Fields/).", "- Names in *italics* are built at run time (for example one field per species or per marker).", "", ] @@ -873,8 +873,8 @@ def quality_sections(stale, coverage, doxy): "## Developer pages that may be out of date", "", 'Each developer page ends with "Last checked against SU2 develop ``". These pages name code ' - "files that changed since that commit. Read the changes, update the page if needed, then update the " - "commit in its last line.", + + "files that changed since that commit. Read the changes, update the page if needed, then update the " + + "commit in its last line.", "", ] flagged = [r for r in stale if r[2] or r[3]] @@ -896,8 +896,8 @@ def quality_sections(stale, coverage, doxy): "## Code documentation coverage", "", "This section counts the classes declared in the headers of `Common/include` and `SU2_CFD/include`. " - "The count uses a simple text search, so the numbers are approximate. `Docs/docmain.hpp` asks that " - "every class has a Doxygen comment with `\\brief` and `\\ingroup`.", + + "The count uses a simple text search, so the numbers are approximate. `Docs/docmain.hpp` asks that " + + "every class has a Doxygen comment with `\\brief` and `\\ingroup`.", "", "- Classes: {}".format(total), "- With a Doxygen comment: {} ({:.0f}%)".format( @@ -967,7 +967,8 @@ def read_parser_renames(su2): text = f.read() found = {} pattern = re.compile( - r'if\s*\(((?:\s*(?:\|\|)?\s*!option_name\.compare\("\w+"\))+)\s*\)\s*' + r'if\s*\(\s*(!option_name\.compare\("\w+"\)' + r'(?:\s*\|\|\s*!option_name\.compare\("\w+"\))*)\s*\)\s*' r'newString\.append\("((?:[^"\\]|\\.)*)"\)' ) for m in pattern.finditer(text): @@ -1152,7 +1153,7 @@ def write_check_page(site, found, commit, options, extra=()): "## Options named in the docs that do not exist", "", "These names are not options of the code: the option was renamed or removed, or the name has a typo. " - "Fix the page in the website repository.", + + "Fix the page in the website repository.", "", "Found: {}".format(len(found)), "", diff --git a/Docs/website/tools/test_build_site.py b/Docs/website/tools/test_build_site.py index 10d596153304..5829efcbfbb5 100644 --- a/Docs/website/tools/test_build_site.py +++ b/Docs/website/tools/test_build_site.py @@ -1,6 +1,8 @@ """Run with python3 -m unittest discover -s Docs/website/tools.""" from pathlib import Path +import subprocess +import sys import tempfile import unittest @@ -47,6 +49,50 @@ def test_array_storage_is_not_a_default(self): 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(), "{}") + if __name__ == "__main__": unittest.main() From 4990ea47210a709d35edcc45920d6ae31ee9dd76 Mon Sep 17 00:00:00 2001 From: rois1995 Date: Mon, 26 Oct 2026 12:24:50 +0100 Subject: [PATCH 25/25] docs: correct C++ reference extraction and string defaults --- Docs/website/README.md | 2 +- Docs/website/pages/Documentation-Status.md | 1 + Docs/website/tools/build_site.py | 54 ++++++++++----- Docs/website/tools/test_build_site.py | 76 ++++++++++++++++++++++ 4 files changed, 114 insertions(+), 19 deletions(-) diff --git a/Docs/website/README.md b/Docs/website/README.md index 7c82614a9dd2..6d8610add6ff 100644 --- a/Docs/website/README.md +++ b/Docs/website/README.md @@ -21,7 +21,7 @@ Developer pages live in `pages/`, with `title` and `permalink: /docs_v7/` 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. The reference also links to relevant pages and regression configurations. +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. diff --git a/Docs/website/pages/Documentation-Status.md b/Docs/website/pages/Documentation-Status.md index fb17991d5292..6e31ce268a47 100644 --- a/Docs/website/pages/Documentation-Status.md +++ b/Docs/website/pages/Documentation-Status.md @@ -127,6 +127,7 @@ These are proposed next priorities. The page records completed work; it does not | 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 diff --git a/Docs/website/tools/build_site.py b/Docs/website/tools/build_site.py index 0730b6e7b308..571c3089cfe9 100644 --- a/Docs/website/tools/build_site.py +++ b/Docs/website/tools/build_site.py @@ -117,31 +117,42 @@ def slugify(text): return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-") +CPP_TEXT = r'"(?:\\.|[^"\\])*"' + r"|'(?:\\.|[^'\\])*'" + r"|//[^\n]*|/\*.*?\*/" +CPP_TOKENS = re.compile(CPP_TEXT + r"|[()[\]{},]", re.S) + + +def cpp_code(text): + """Mask ordinary C++ literals and comments, preserving offsets and line numbers.""" + return re.sub(CPP_TEXT, lambda m: re.sub(r"[^\n]", " ", m[0]), text, flags=re.S) + + def split_args(text): - """Split the arguments of a C++ call at the commas that are not inside brackets.""" + """Split C++ call arguments outside literals, comments and (), [] or {} groups.""" + # shortcut: template-argument commas are unsupported; extend if registrations use them. args, depth, start = [], 0, 0 - for i, c in enumerate(text): - if c in "([{<": + for m in CPP_TOKENS.finditer(text): + token = m[0] + if token in ("(", "[", "{"): depth += 1 - elif c in ")]}>": + elif token in (")", "]", "}"): depth -= 1 - elif c == "," and depth == 0: - args.append(text[start:i].strip()) - start = i + 1 + elif token == "," and depth == 0: + args.append(text[start : m.start()].strip()) + start = m.end() args.append(text[start:].strip()) return args def call_body(text, open_paren): - """Return the text between the parenthesis at open_paren and its matching parenthesis.""" + """Return a C++ call body, ignoring parentheses in literals and comments.""" depth = 0 - for i in range(open_paren, len(text)): - if text[i] == "(": + for m in CPP_TOKENS.finditer(text, open_paren): + if m[0] == "(": depth += 1 - elif text[i] == ")": + elif m[0] == ")": depth -= 1 if depth == 0: - return text[open_paren + 1 : i], i + return text[open_paren + 1 : m.start()], m.start() return text[open_paren + 1 :], len(text) @@ -341,10 +352,17 @@ def describe_type(opt, maps): ) if kind == "Bool" and len(args) > 2: default = {"true": "YES", "false": "NO"}.get(args[2], args[2]) - elif ( - kind in ("Double", "String", "UnsignedLong", "UnsignedShort", "Long") - and len(args) > 2 - ): + elif kind == "String" and len(args) > 2: + constructor = re.fullmatch( + r'(?:std::)?string\s*\(\s*("(?:\\.|[^"\\])*")\s*\)', args[2] + ) + value = literal(constructor[1] if constructor else args[2]) + default = ( + "not extracted (C++ expression: {})".format(args[2]) + if value is None + else value or '""' + ) + elif kind in ("Double", "UnsignedLong", "UnsignedShort", "Long") and len(args) > 2: default = args[2].strip('"') return ( TYPE_NAMES.get(kind, "special format, see config_template.cfg"), @@ -644,7 +662,7 @@ def read_outputs(su2): with open(path, encoding="utf-8", errors="replace") as f: text = f.read() seen = set() - for m in token.finditer(text): + for m in token.finditer(cpp_code(text)): body, _ = call_body(text, m.end() - 1) args = split_args(body) kind, igroup, idesc = calls[m.group(1)] @@ -1055,7 +1073,7 @@ def read_error_messages(su2): continue with open(path, encoding="utf-8", errors="replace") as f: text = f.read() - for m in token.finditer(text): + for m in token.finditer(cpp_code(text)): body, _ = call_body(text, m.end() - 1) first = split_args(body)[0] pieces = re.findall(r'"((?:[^"\\]|\\.)*)"', first) diff --git a/Docs/website/tools/test_build_site.py b/Docs/website/tools/test_build_site.py index 5829efcbfbb5..9e4162780433 100644 --- a/Docs/website/tools/test_build_site.py +++ b/Docs/website/tools/test_build_site.py @@ -93,6 +93,82 @@ def test_parser_renames_reject_long_malformed_conditions_promptly(self): ) 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()