Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ vaft/data/wheel_samples/** text=auto eol=lf

# POSIX shell scripts must keep LF even in a Windows checkout.
*.sh text eol=lf

# GACODE fixed-format text fixtures are compared field-by-field against a
# real NEO run, so a CRLF checkout must not change them.
test/data/gacode/** text=auto eol=lf
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,17 @@ export CHEASEHOME=/path/to/chease
export EFITHOME=/path/to/efit
export TESHOME=/path/to/tes
export NUBEAMHOME=/path/to/nubeam
export GACODEHOME=/path/to/gacode
export GACODE_PLATFORM=GFORTRAN_OSX_BREW
```

`GACODEHOME` is the GACODE checkout itself: the suite builds in place, so there is no
separate prefix, and each member carries its own `bin` (`neo/bin/neo`). `GACODE_PLATFORM`
names the tag it was built with. VAFT sets GACODE's own `GACODE_ROOT` and
`GACODE_PLATFORM` for the subprocess from these rather than redefining them, and falls
back to `GACODE_ROOT` when `GACODEHOME` is unset. Build it through
[`external/gacode/`](external/gacode/) and verify with `python install/check_gacode.py`.

`NUBEAMHOME` also supplies the PREACT and ADAS reaction databases NUBEAM cannot
run without, at `share/preact` and `share/adas`. VAFT builds NUBEAM through
[`external/nubeam/`](external/nubeam/) rather than vendoring it: NTCC requires each
Expand Down
92 changes: 92 additions & 0 deletions docs/_guide/Formula_reference_neoclassical.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
---
title: "Formula reference: neoclassical"
author: VEST team
date: 2026-09-07 17:20
category: guide
layout: post
permalink: /reference/formula/neoclassical/
guide:
architecture: Generated per-function reference for vaft.formula.neoclassical, read from the standardized docstrings (issue 248).
prerequisites: None.
expected: Definition, units, conventions, validity, limitations and literature references for every public function of the submodule.
related:
api: [formula]
---

{% assign category = site.data.formula_catalog.categories | where: "name", "neoclassical" | first %}
{% assign entries = site.data.formula_catalog.formulas | where: "category", "neoclassical" %}

This page is generated from the docstrings of
[`vaft/formula/neoclassical.py`](https://github.com/VEST-Tokamak/vaft/blob/develop/vaft/formula/neoclassical.py):
{{ entries.size }} public functions. The category overview and notation come from the module
docstring; every entry below is what `vaft.formula.describe("neoclassical.<name>")` prints.
Back to the [formula reference index]({{ site.baseurl }}/reference/formula/).

## Overview

{{ category.overview }}

{% if category.notation.size > 0 %}<table class="formula-table">
<thead><tr><th>Symbol</th><th>Meaning</th><th>Unit</th></tr></thead>
<tbody>
{% for row in category.notation %}<tr><td>{{ row.symbol | escape }}</td><td>{{ row.description | escape }}</td><td>{{ row.unit | escape }}</td></tr>
{% endfor %}</tbody>
</table>

{% endif %}{% if category.conventions != "" %}{{ category.conventions }}

{% endif %}## Functions

<ul class="formula-index">
{% for f in entries %} <li><a href="#{{ f.name }}"><code>{{ f.name }}</code></a> &mdash; {{ f.summary | markdownify | remove: "<p>" | remove: "</p>" }}</li>
{% endfor %}</ul>

{% for f in entries %}
### `{{ f.name }}` {#{{ f.name }}}

<p class="formula-signature"><code>{{ f.name }}{{ f.signature }}</code>{% if f.aliases.size > 0 %} &mdash; aliases {% for alias in f.aliases %}<code>{{ alias }}</code>{% unless forloop.last %}, {% endunless %}{% endfor %}{% endif %}</p>

{% if f.empirical or f.convention_sensitive or f.deprecated or f.shadowed_by %}<p>{% if f.empirical %}<strong>Empirical fit.</strong> {% endif %}{% if f.convention_sensitive %}<strong>Convention-sensitive.</strong> {% endif %}{% if f.deprecated %}<strong>Deprecated.</strong> {% endif %}{% if f.shadowed_by %}<em><code>vaft.formula.{{ f.name }}</code> resolves to the <code>{{ f.shadowed_by }}</code> copy; reach this one as <code>vaft.formula.{{ f.category }}.{{ f.name }}</code>.</em>{% endif %}</p>

{% endif %}{{ f.summary }}

{% if f.description != "" %}{{ f.description }}

{% endif %}{% if f.parameters.size > 0 %}<table class="formula-table">
<thead><tr><th>Parameter</th><th>Type</th><th>Unit</th><th>Description</th></tr></thead>
<tbody>
{% for p in f.parameters %}<tr><td><code>{{ p.name }}</code></td><td>{{ p.type }}</td><td>{{ p.unit }}</td><td>{{ p.description | markdownify }}</td></tr>
{% endfor %}</tbody>
</table>

{% endif %}{% if f.returns.size > 0 %}<table class="formula-table">
<thead><tr><th>Returns</th><th>Type</th><th>Unit</th><th>Description</th></tr></thead>
<tbody>
{% for r in f.returns %}<tr><td>{% if r.name %}<code>{{ r.name }}</code>{% endif %}</td><td>{{ r.type }}</td><td>{{ r.unit }}</td><td>{{ r.description | markdownify }}</td></tr>
{% endfor %}</tbody>
</table>

{% endif %}{% for s in f.sections %}<p><strong>{{ s.title }}.</strong></p>

{{ s.text }}

{% endfor %}{% if f.references.size > 0 %}<p><strong>References.</strong></p>

<ol class="formula-references">
{% for ref in f.references %} <li>{{ ref.text | markdownify | remove: "<p>" | remove: "</p>" }}</li>
{% endfor %}</ol>

{% endif %}{% endfor %}

## Refreshing this snapshot

From a checkout of the `develop` branch, run:

```bash
python -m vaft.formula.catalog --output /path/to/vaft-gh/_data/formula_catalog.yml
```

The snapshot records the SHA-256 of every `vaft/formula/*.py` source file; documentation
validation compares them when `VAFT_REGISTRY_SOURCE` points to the corresponding source checkout.
The same text is available offline as `vaft.formula.describe("<name>")`,
`vaft.formula.search("<text>")` and `vaft.formula.list_formulas(category="<category>")`.
70 changes: 70 additions & 0 deletions external/gacode/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# GACODE: build and verify

GACODE is the General Atomics code suite for kinetic and transport modelling.
This directory builds it; `vaft.code.gacode` then runs it. NEO, the
drift-kinetic neoclassical solver, is the first backend VAFT drives; TGLF and
CGYRO share the same profile and runtime layer and are tracked in
[issue #553](https://github.com/VEST-Tokamak/vaft/issues/553).

**The source is not here, deliberately.** VAFT owns the build recipe and the
adapter contract; the source stays external, obtained from
[gafusion/gacode](https://github.com/gafusion/gacode). Every script takes
`--gacode-root` naming a tree you already hold and writes nothing into the VAFT
checkout.

**GACODE builds in place.** There is no separate installation prefix: the
executables land inside the source tree (`neo/src/neo`, launched through
`neo/bin/neo`). `$GACODEHOME` therefore points at the checkout itself, which is
why this code has no `<root>/local` the way NUBEAM does.

**macOS / Apple Silicon.** Linux and Windows are not covered here. None of this
runs in CI; the VAFT test suite passes with GACODE absent.

| File | Purpose |
| --- | --- |
| `macos.sh` | Installs the Homebrew dependencies, builds the shared and `f2py` libraries and the requested suite members, and optionally runs the NEO `reg18` regression case. |

## Usage

```bash
bash external/gacode/macos.sh --gacode-root ~/git/gacode --check
export GACODEHOME=~/git/gacode
python install/check_gacode.py --source ~/git/gacode
```

## The environment contract, and why VAFT does not replace it

GACODE's own build and run scripts read two variables:

| Variable | Meaning |
| --- | --- |
| `GACODE_ROOT` | the suite tree |
| `GACODE_PLATFORM` | selects `platform/build/make.inc.$GACODE_PLATFORM` for the build and `platform/exec/exec.$GACODE_PLATFORM` for the run |

VAFT adds `GACODEHOME` to match the `$XHOME` convention every other external
code in this repository uses (`GPECHOME`, `CHEASEHOME`, `EFITHOME`,
`NUBEAMHOME`), and **derives** `GACODE_ROOT` and `GACODE_PLATFORM` from it for
the subprocess rather than redefining them. A tree built here therefore stays
usable from a plain shell that sources `shared/bin/gacode_setup`, and
`vaft.code.gacode` accepts a pre-set `GACODE_ROOT` as a compatibility fallback
when `GACODEHOME` is unset.

## Two failure modes worth knowing before you hit them

**The launcher needs `pygacode` on `PYTHONPATH`.** `neo/bin/neo` shells out to
`neo_parse.py`, which imports `gacodeinput` from `f2py/pygacode`. When that
import fails the launcher does *not* stop -- it carries on, and NEO then aborts
with a Fortran runtime error about a missing `./input.neo.gen`, which points at
the wrong thing entirely. `vaft.code.gacode` always sets `PYTHONPATH` itself
for this reason.

**`GACODE_PLATFORM` must match the build.** `neo/bin/neo` executes
`platform/exec/exec.$GACODE_PLATFORM`; an unset or wrong value fails deep inside
a shell script without naming the variable. `vaft.code.gacode` resolves it
explicitly and lists the available platforms when it cannot.

## Verified

Built against `gafusion/gacode` `6357db30` (2026-07-22) with Homebrew
gfortran 15.2 and Open MPI on macOS/arm64. The NEO `reg18` regression case
reproduces its shipped `out.neo.prec` value `0.12268957E+02` exactly.
148 changes: 148 additions & 0 deletions external/gacode/macos.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env bash
# Build the GACODE suite (NEO first) natively on Apple Silicon macOS.
#
# Usage:
# bash external/gacode/macos.sh --gacode-root PATH [--codes neo,tglf] [--check]
#
# VAFT does not vendor the GACODE source. This script owns the reproducible
# build recipe and operates on a GACODE tree you already hold, named by
# --gacode-root. Unlike NUBEAM, GACODE builds in place: there is no separate
# installation prefix, so $GACODEHOME points at the checkout itself.
#
# GACODE's own build contract is $GACODE_ROOT plus $GACODE_PLATFORM, which
# selects platform/build/make.inc.$GACODE_PLATFORM. VAFT does not redefine
# either -- it sets both from $GACODEHOME rather than replacing them, so a tree
# built here stays usable from a plain shell with shared/bin/gacode_setup.
#
# macOS/Apple Silicon only. Linux and Windows are not covered here.

set -euo pipefail
IFS=$'\n\t'

GACODE_SOURCE="${GACODE_SOURCE_DIR:-}"
CODES="neo"
RUN_CHECK=0

usage() {
cat <<'EOF'
Usage: bash external/gacode/macos.sh --gacode-root PATH [--codes neo,tglf] [--check]

--gacode-root PATH the GACODE source tree to build (or set GACODE_SOURCE_DIR)
--codes LIST comma-separated suite members to build; default "neo"
--check after building, run the NEO reg18 regression case

Environment overrides:
GACODE_SOURCE_DIR=/absolute/path default for --gacode-root
GACODE_PLATFORM=NAME default GFORTRAN_OSX_BREW

The build happens in place. Afterwards, export:

export GACODEHOME=<the tree you passed>

which is what vaft.code.gacode reads. Nothing is written into the VAFT checkout.
EOF
}

while [ $# -gt 0 ]; do
case "$1" in
--gacode-root) GACODE_SOURCE="${2:-}" ; shift 2 ;;
--codes) CODES="${2:-}" ; shift 2 ;;
--check) RUN_CHECK=1 ; shift ;;
-h|--help) usage ; exit 0 ;;
*) echo "unknown argument: $1" >&2 ; usage >&2 ; exit 2 ;;
esac
done

if [ -z "$GACODE_SOURCE" ]; then
echo "error: --gacode-root is required (or set GACODE_SOURCE_DIR)" >&2
usage >&2
exit 2
fi

GACODE_SOURCE="$(cd "$GACODE_SOURCE" && pwd -P)"

for marker in Makefile shared/bin/gacode_setup platform/build neo/src; do
if [ ! -e "$GACODE_SOURCE/$marker" ]; then
echo "error: $GACODE_SOURCE is missing $marker, so it is not a GACODE tree" >&2
exit 1
fi
done

if ! command -v brew >/dev/null 2>&1; then
echo "error: Homebrew is required. See https://brew.sh" >&2
exit 1
fi

# gcc supplies gfortran; open-mpi supplies the mpif90 wrapper the makefiles call
# unconditionally, even for the serial build. fftw and netcdf are linked by the
# suite makefiles whether or not NEO itself uses them.
MISSING=()
for formula in gcc open-mpi netcdf netcdf-fortran fftw; do
brew --prefix "$formula" >/dev/null 2>&1 || MISSING+=("$formula")
done
if [ ${#MISSING[@]} -gt 0 ]; then
echo "Installing missing dependencies: ${MISSING[*]}"
brew install "${MISSING[@]}"
fi

export GACODE_ROOT="$GACODE_SOURCE"
export GACODE_PLATFORM="${GACODE_PLATFORM:-GFORTRAN_OSX_BREW}"
export FFTW_INC="$(brew --prefix fftw)/include"
export BREW_LIB="$(brew --prefix)/lib"
export PATH="$GACODE_ROOT/shared/bin:$PATH"

MAKE_INC="$GACODE_ROOT/platform/build/make.inc.$GACODE_PLATFORM"
if [ ! -f "$MAKE_INC" ]; then
echo "error: no platform file $MAKE_INC" >&2
echo "Available platforms:" >&2
ls "$GACODE_ROOT/platform/build" | sed 's/^make\.inc\./ /' >&2
exit 1
fi

echo "GACODE_ROOT = $GACODE_ROOT"
echo "GACODE_PLATFORM = $GACODE_PLATFORM"

# Order matters: the per-code makefiles link shared/*/*.a and f2py/*/*.a as
# EXTRA_LIBS, so both must exist before any suite member is built.
echo "==> shared libraries"
make -C "$GACODE_ROOT/shared"
echo "==> f2py libraries (expro, geo)"
make -C "$GACODE_ROOT/f2py"

IFS=',' read -r -a CODE_LIST <<< "$CODES"
for code in "${CODE_LIST[@]}"; do
if [ ! -d "$GACODE_ROOT/$code" ]; then
echo "error: no suite member '$code' in $GACODE_ROOT" >&2
exit 1
fi
echo "==> $code"
make -C "$GACODE_ROOT/$code"
done

echo
echo "Build complete. Export:"
echo
echo " export GACODEHOME=$GACODE_ROOT"
echo

if [ "$RUN_CHECK" -eq 1 ]; then
echo "==> NEO reg18 regression"
# The neo launcher shells out to neo_parse.py, which imports gacodeinput from
# f2py/pygacode. Without it on PYTHONPATH the parse step fails silently and
# NEO then aborts on a missing input.neo.gen -- see install/check_gacode.py.
export PYTHONPATH="$GACODE_ROOT/f2py:$GACODE_ROOT/f2py/pygacode:${PYTHONPATH:-}"
export PATH="$GACODE_ROOT/neo/bin:$PATH"
SCRATCH="$(mktemp -d "${TMPDIR:-/tmp}/vaft-gacode-reg18.XXXXXX")"
trap 'rm -rf "$SCRATCH"' EXIT
cp -R "$GACODE_ROOT/neo/tools/input/reg18" "$SCRATCH/reg18"
EXPECTED="$(tr -d '[:space:]' < "$SCRATCH/reg18/out.neo.prec")"
rm -f "$SCRATCH/reg18/out.neo.prec"
( cd "$SCRATCH" && neo -e reg18 -n 1 >/dev/null )
ACTUAL="$(tr -d '[:space:]' < "$SCRATCH/reg18/out.neo.prec")"
if [ "$ACTUAL" = "$EXPECTED" ]; then
echo "reg18 PASS: $ACTUAL"
else
echo "reg18 FAIL: got $ACTUAL, expected $EXPECTED" >&2
exit 1
fi
fi
23 changes: 23 additions & 0 deletions install/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,29 @@ NTCC dependency modules only after you pass `-AcceptNtccTerms`. Everything it
generates stays inside your NUBEAM source tree. See
[`external/nubeam/README.md`](../external/nubeam/README.md).

### GACODE

GACODE has its own entry point, [`external/gacode/macos.sh`](../external/gacode/macos.sh),
which installs the Homebrew dependencies, builds the shared and `f2py` libraries and the
requested suite members, and can run NEO's shipped `reg18` regression case in the same
invocation:

```bash
bash external/gacode/macos.sh --gacode-root ~/git/gacode --check
export GACODEHOME=~/git/gacode
export GACODE_PLATFORM=GFORTRAN_OSX_BREW
python install/check_gacode.py --source ~/git/gacode
```

Two things about it differ from every other code here. It **builds in place**, so
`GACODEHOME` is the checkout rather than a separate prefix; and each suite member carries
its own `bin`, so the executable is `neo/bin/neo`, not `bin/neo`. It also needs
`GACODE_PLATFORM`, which selects `platform/exec/exec.$GACODE_PLATFORM` at run time --
`vaft.code.gacode` resolves it up front and lists the available tags, because a wrong
value otherwise fails inside a shell script without naming itself. See
[`external/gacode/README.md`](../external/gacode/README.md). macOS/Apple Silicon only for
now.

### Linux and macOS

CHEASE and GPEC are not yet automated — tracked in
Expand Down
Loading
Loading