Skip to content

Fix the second-order convection of turbulence and species - #2956

Open
rois1995 wants to merge 26 commits into
developfrom
fix_scalar_muscl
Open

rois1995 wants to merge 26 commits into
developfrom
fix_scalar_muscl

Conversation

@rois1995

@rois1995 rois1995 commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Proposed Changes

Fixes for MUSCL_TURB, MUSCL_SPECIES and the bounded scalar scheme. One commit per fix; the test cases follow in the comments.

  • Limiter lost after LIMITER_ITER: the turbulence and species were then reconstructed with no limiter at all. Now the last limiter values are kept, as in the flow.
  • VAN_ALBADA_EDGE stopped the run for turbulence and species. It now works, as in the flow.
  • The flow's VAN_ALBADA_EDGE was ignored for the velocity that carries the turbulence. Now it is applied.
  • Reconstruction gradient computed without MUSCL: with MUSCL_TURB= NO (or MUSCL_SPECIES= NO), the turbulence and species still computed a separate reconstruction gradient when the flow used one. Now only when they reconstruct.
  • Bounded scheme with density different from 1: its correction did not match the flux it corrects, and at sliding interfaces it was added once per interpolation donor. Both now match the flux.
  • Values outside the allowed range: the reconstruction could make k or omega negative at a face, which can stop convergence. Such a face now falls back to first order, as the flow does for negative pressure.

New test turb_naca0012_sst_muscl_edge. Reference values of rae2822_sa, rae2822_sst, the venturi and flamelet species tests and bars_SST_2D will be updated from the CI.

Related Work

PR Checklist

  • I am submitting my contribution to the develop branch.
  • My contribution generates no new compiler warnings (try with --warnlevel=3 when using meson).
  • My contribution is commented and consistent with SU2 style (https://su2code.github.io/docs_v7/Style-Guide/).
  • I used the pre-commit hook to prevent dirty commits and used pre-commit run --all to format old commits.
  • I have added a test case that demonstrates my contribution, if necessary.
  • I have updated appropriate documentation (Tutorials, Docs Page, config_template.cpp), if necessary.

🤖 Generated with Claude Code

rois1995 and others added 8 commits October 1, 2026 07:09
After LIMITER_ITER the scalar flux kernel set its limiter type to NONE, so the turbulence and species
reconstruction was no longer limited at all, while the flow solvers keep applying the last stored
limiter. The scalar limiter is now frozen in the same way: CScalarSolver stops recomputing it and the
kernel keeps applying it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6
The scalar flux kernel already applies the edge limiter, but CScalarSolver still computed point
limiters for it and stopped with "Unknown limiter type". The point limiter and its volume outputs are
now skipped for VAN_ALBADA_EDGE, as in the flow solvers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6
…structs

GetReconstructionGradientRequired is true for the whole case as soon as the flow uses a separate
reconstruction gradient, so e.g. SST with MUSCL_TURB= NO computed an unused least-squares gradient at
every iteration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6
…e flow does

The velocity that convects the scalars was reconstructed without any limiter when the flow used
VAN_ALBADA_EDGE. The edge limiter needs no stored values, so it is now applied as in the flow.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6
The divergence correction of BOUNDED_SCALAR cancels the part of the upwind flux due to the discrete
divergence of the mass flux, so it must use the coefficients of that flux:
- SA is not conservative: its flux uses the volume flux mdot+/rho_i + mdot-/rho_j, while the
  correction used the mass flux, so the correction was rho times too large (e.g. ~1000 for water);
- SST, transition and species are conservative: the residual was right, but the Jacobian with respect
  to rho*phi must be -mdot/rho_i, not -mdot.
With rho = 1 nothing changes. With dimensional water (rho = 998.2) the bounded SA flat plate stalled
and gave a CD 5% off, now it converges like the non-dimensional case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6
…bounds

The MUSCL reconstruction of the turbulence and species variables was not kept within the bounds that
clip the solution (e.g. k, omega > 0). In regions where a variable is small and varies a lot, like the
decaying omega of the far field on coarse cells, the face values became negative, omega was clipped to
its floor and the eddy viscosity grew without limit. The SST NACA0012 at 10 deg with MUSCL_TURB and
VENKATAKRISHNAN_WANG did not converge (CD 0.47 after 10000 iterations instead of 0.0158).
A face value outside [lowerlimit, upperlimit] of its variable now falls back to the point value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6
SST NACA0012 from the converged restart, with MUSCL_TURB and VAN_ALBADA_EDGE for both the flow and the
turbulence, and LIMITER_ITER = 3. It stops with "Unknown limiter type" on develop, and its values change
if the limiter is switched off after LIMITER_ITER again or if the flow edge limiter is not applied to
the velocity of the turbulence flux.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6
…nterfaces

At a fluid interface the scalar flux of each donor is weighted by its interpolation weight, but the
bounded scalar correction was added with weight 1 for every donor, so with several donors
(e.g. KIND_INTERPOLATION= WEIGHTED_AVERAGE) it was counted about as many times as there are donors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LEL91DW5WPbPwgFtCvHga6
@rois1995

rois1995 commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Test cases

Files (configurations, histories, plot script) for each case: scalarMuscl. Meshes are from the TestCases repository. 2 MPI ranks, "develop" = a6b7496b74.

MUSCL fixes

1. Values outside the allowed range: rans/naca0012/turb_NACA0012_sst.cfg, 10°, from the free stream, MUSCL_TURB= YES.

iterations CD
develop, first order 447 0.015775
develop, MUSCL 10000, not converged 0.4717
this PR, MUSCL 527 0.015743

2. Bounded scheme with density ≠ 1: incomp_rans/rough_flatplate/rough_flatplate_incomp.cfg without roughness, BOUNDED_SCALAR, water density (998.2) with the viscosity scaled to keep the Reynolds number. It must match the run with density 1.

converges CD
develop, density 1 yes 0.0028593
develop, density 998.2 no, stalls 0.0030075
this PR, density 998.2 yes 0.0028593

3. Limiter lost after LIMITER_ITER: rans/naca0012/turb_NACA0012_sa.cfg, restart, turbulence limiter VENKATAKRISHNAN_WANG, LIMITER_ITER= 50.

CL
develop, no limiter 1.0832985
develop, limiter never frozen 1.0824923
develop, LIMITER_ITER= 50 1.0832986, the same as no limiter
this PR, LIMITER_ITER= 50 1.0824915, the same as never frozen

4. VAN_ALBADA_EDGE for turbulence: rans/flatplate/turb_SST_flatplate.cfg, MUSCL_TURB= YES. Develop stops with "Unknown limiter type"; this PR converges (CD 0.0028093).

5. Flow VAN_ALBADA_EDGE on the turbulence velocity: case 3 with SLOPE_LIMITER_FLOW= VAN_ALBADA_EDGE. CL 1.0876936 on develop, 1.0878103 with this PR.

6. Sliding interfaces: sliding_interface/bars_SST_2D with BOUNDED_SCALAR and KIND_INTERPOLATION= WEIGHTED_AVERAGE. With this PR the bounded scheme follows SCALAR_UPWIND: rms[Rho] in zone 1 after 500 iterations is -4.399, against -4.397 for upwind and -4.207 before.

7. Reconstruction gradient without MUSCL: case 4 with first-order turbulence and NUM_METHOD_GRAD_RECON= LEAST_SQUARES, 1 rank. The histories are identical; 0.238 s per iteration instead of 0.296 s.

New regression test turb_naca0012_sst_muscl_edge: develop stops with "Unknown limiter type". The test values change if fix 3 or fix 5 is undone.

@bigfooted

Copy link
Copy Markdown
Contributor

can you check that the changed options are used in regression tests? I don't think we use van_albada_edge for instance for any species case or bounded scalar scheme with varying density?

@bigfooted bigfooted assigned bigfooted and unassigned bigfooted Oct 9, 2026
Comment thread TestCases/rans/naca0012/turb_NACA0012_sst_muscl_edge.cfg
@rois1995

Copy link
Copy Markdown
Contributor Author

@bigfooted added the missing species VAN_ALBADA_EDGE regression in 5aeb5f4, using variable density and BOUNDED_SCALAR in the venturi mixing case. I also added a stored turbulence limiter case that runs beyond LIMITER_ITER=3. The existing species2_primitiveVenturi_mixingmodel_boundedscalar already covers variable-density bounded transport. All four focused cases pass with two MPI ranks; the 44 unit tests pass too.

bigfooted added a commit that referenced this pull request Oct 13, 2026
…and limiters (#2961)

## Proposed Changes

This is one of **seven related periodic-boundary bug-fix PRs**.

| PR | Scope |
|---|---|
| #2961 (this PR) | Rotational Jacobian, multigrid velocity rotation and
limiters |
| #2963 | Axis points, slip-wall normals and moving donor volumes |
| #2964 | Periodic volumes, neighbor counts, sensors, stencils and
coarse flags |
| #2965 | Streamwise periodicity, auxiliary gradients and flamelet
metadata |
| #2966 | Inverse affine transforms in the parser and FEM |
| #2968 | Diagnostics for unsupported periodic solver combinations |
| #2969 | Translated wall-distance images |

The other bug-fix PRs are drafts with their remaining validation and
implementation limits documented. Their grouping does not imply a linear
merge order; #2963 depends on this PR.

The fully coupled implicit operator proposed in #2967 is a separate
experimental enhancement. It has been closed and deferred because no
overall benefit has been established and convergence/validation issues
remain. This PR retains the existing implicit approximation.

Three fixes for rotational periodic boundaries, one commit each. The
test cases are in the comment below.

- **Implicit runs:** the Jacobian block that a periodic point gets from
its match was rotated on one side only. It is now rotated on both sides.
- **Multigrid:** the coarse levels did not rotate the velocity, so the
run did not converge. The velocity is now rotated on all levels.
- **Limiters:** the limiters of the velocity components were rotated
like a vector, but they are separate numbers. On the periodic boundary
one velocity component became first order, and limiters could be
negative. Each side now sends the velocity bounds and reconstruction
increments in the frame of the other side, so each limiter uses the
complete stencil. Nothing changes for a translation.

New tests `periodic2d_no_limiter` and `periodic2d_multigrid`;
`periodic2d` is now also in the MPI regression. Their x86 reference
values pass in CI.

Existing reference values that change: `aachen_turbine_restart`,
`jones_turbocharger_restart` and `multi_interface` (rotational periodic,
the Jacobian block), and the hybrid `periodic2d` (the Jacobian block and
the limiters). The x86 periodic tests and the other changed cases pass
in CI; the Jones restart residual references were refreshed from the
serial, MPI and hybrid CI logs. The aarch64 references still need the CI
runs.

No conflict with #2945 and #2959. #2956 also changes the reference
values of `jones_turbocharger_restart`; whichever is merged second takes
them from the CI.

## Related Work

- #652 added the present implementation of the periodic boundaries.
- #1563 fixed the rotation in 2D and added the `periodic2D` case used
here.
- #1467, #763 and #1585 discuss the incomplete linear system at periodic
points; this PR keeps the existing approximation.

## PR Checklist

- [x] I am submitting my contribution to the develop branch.
- [x] My contribution generates no new compiler warnings (try with
--warnlevel=3 when using meson).
- [x] My contribution is commented and consistent with SU2 style
(https://su2code.github.io/docs_v7/Style-Guide/).
- [x] I used the pre-commit hook to prevent dirty commits and used
`pre-commit run --all` to format old commits.
- [x] I have added a test case that demonstrates my contribution, if
necessary.
- [ ] I have updated appropriate documentation (Tutorials, Docs Page,
config_template.cpp), if necessary.

---------

Co-authored-by: rois1995 <rauxa1995@hotmail.it>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Josh Kelly <81244680+joshkellyjak@users.noreply.github.com>
Co-authored-by: Nijso <bigfootedrockmidget@hotmail.com>
@bigfooted

Copy link
Copy Markdown
Contributor

@rois1995 if you can fix the issues in the regression tests then we can finalize this.

@rois1995

Copy link
Copy Markdown
Contributor Author

@bigfooted There's still a problem with eigen that is outside the scope of this PR.

@bigfooted

Copy link
Copy Markdown
Contributor

@bigfooted There's still a problem with eigen that is outside the scope of this PR.

that's probably the server being overloaded, it tries to get external packages but fails due to time-out:

fatal: remote error: GitLab is currently unable to handle this request due to load (ID a49f7cf55aa93bb9-LAX).

@bigfooted

Copy link
Copy Markdown
Contributor

@rois1995 can you check this:

  1. The scalar VAN_ALBADA_EDGE is not the flow's van Albada, and for turbulence it barely limits anything.
    The scalar kernel calls musclMusclLimiter::EDGE in SU2_CFD/include/numerics/util.hpp:

lim = (Δ² + 1e-6 + p·Δ) / (p² + Δ² + 1e-6)
The flow solvers use LimiterHelpers::vanAlbadaFunction(p, Δ, EPS), with EPS = 1e-16:

lim = Δ(p + Δ) / (p² + Δ² + 1e-16)
These differ in two ways:

The fixed 1e-6 also sits in the numerator, so as Δ→0 the limiter goes to 1 instead of 0. At an extremum the slope is left unlimited.
1e-6 is an absolute value. Non-dimensional ν̃ or k (e.g. with FREESTREAM_PRESS_EQ_ONE) is around 1e-8 to 1e-5, so Δ² ≪ 1e-6 and lim ≈ 1. The new turb_naca0012_sst_muscl_edge test therefore runs k with almost no limiting.
The PR's third fix applies this same kernel to the flow velocity that carries the turbulence. Δu is about 1e-3, so Δ² is about the size of the epsilon. The scalar face velocity is then still not the flow solver's own face velocity, which was the point of that fix.
Fix: call vanAlbadaFunction with EPS in the EDGE branch, or use an epsilon scaled to the variable. This changes the new test's reference values.

Comment on lines +350 to +351
* Both the scalar and the flow limiters are frozen after LIMITER_ITER by no longer
* recomputing them: the last stored values keep being applied.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For VAN_ALBADA_EDGE, it is recomputed on every edge and never frozen.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified in eef97f4: stored point limiters keep their last values after LIMITER_ITER; VAN_ALBADA_EDGE is recomputed on every edge and never frozen. The regression-script comment and config-template explanation now make the same distinction.

@bigfooted

Copy link
Copy Markdown
Contributor

My Claude analysis found something:
The fix is to build the boundary mass flux with the upwind density, the same way the face itself is upwinded. Then the volume flux is u·n on both inflow and outflow, and the conservative flux takes the ghost density on inflow.

What's wrong now. BoundedScalarBCFlux always builds the mass flux from the interior density:


const su2double edgeMassFlux = density * DotProduct(nDim, velocity, normal); // ρ_i · (u_ghost·n)
This value is returned and passed to ComputeFlux, which turns it into volume fluxes by dividing by the density on each side:


a0 = fmax(0.0, massFlux) / rho.i; // outflow: ρ_i u·n / ρ_i = u·n ✓
a1 = fmin(0.0, massFlux) / rho.j; // inflow: ρ_i u·n / ρ_ghost = (ρ_i/ρ_ghost) u·n ✗
Inflow with a non-conservative scalar: the face carries (ρ_i/ρ_ghost)·u·n instead of u·n.
Inflow with a conservative scalar: the face carries ρ_i·u·n instead of ρ_ghost·u·n.
The PR's new BoundedScalarDivergenceFlux reproduces the same error, because it takes this mass flux as its input.

The fix, in CScalarSolver.hpp:


inline su2double BoundedScalarBCFlux(unsigned long iPoint, bool implicit, const su2double& density,
const su2double* velocity, const su2double* normal,
const su2double* densityGhost = nullptr, su2double weight = 1.0) {
const su2double rhoGhost = densityGhost ? densityGhost : density;
const su2double volumeFlux = GeometryToolbox::DotProduct(nDim, velocity, normal);
/
--- Upwind density: the interior on outflow, the ghost on inflow. ---*/
const su2double edgeMassFlux = volumeFlux * (volumeFlux > 0.0 ? density : rhoGhost);
const su2double q = weight * BoundedScalarDivergenceFlux(edgeMassFlux, density, rhoGhost);
LinSysRes.AddBlock(iPoint, nodes->GetSolution(iPoint), -q);
if (implicit) Jacobian.AddVal2Diag(iPoint, -q / (Conservative ? density : 1.0));
return edgeMassFlux;
}

@bigfooted

Copy link
Copy Markdown
Contributor

Gradient_Aux is the second gradient array a scalar solver keeps for MUSCL reconstruction. The PR stopped computing it when the scalar doesn't reconstruct, but each scalar variable still allocates it.

The fix. Make the variable use the same condition that CommonPreprocessing now uses. In the constructor, the generic config->GetMUSCL() is not set yet (it's a per-solver global parameter set during the iteration), so each derived class passes its own flag:

// CScalarVariable.hpp / .cpp
CScalarVariable(unsigned long npoint, unsigned long ndim, unsigned long nvar, const CConfig* config, bool muscl);

CScalarVariable::CScalarVariable(..., const CConfig* config, bool muscl)
: CVariable(npoint, ndim, nvar, config),
Gradient_Reconstruction(muscl && config->GetReconstructionGradientRequired() ? Gradient_Aux : Gradient) {
Gradient.resize(nPoint, nVar, nDim, 0.0);
if (muscl && config->GetReconstructionGradientRequired()) {
Gradient_Aux.resize(nPoint, nVar, nDim, 0.0);
}

@rois1995

rois1995 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

@bigfooted addressed the October 14 findings in eef97f4:

  • Scalar/SIMD VAN_ALBADA_EDGE now calls the flow's vanAlbadaFunction with EPS. Added checks for extrema and small jumps; refreshed the SST and species edge-test references.
  • Bounded boundary transport now upwinds density: interior on outflow, ghost on inflow. All callers pass the ghost density, and sliding donors retain their weights. Tests cover conservative/non-conservative corrections, explicit/implicit updates, zero flux and donor weights.
  • Scalar variables allocate the auxiliary reconstruction gradient only when their own turbulence/species/heat MUSCL flag requires it. Poisson variables use the main gradient. Tests cover the constructor flags and shared/separate gradients.
  • Clarified that only stored point limiters freeze after LIMITER_ITER; edge limiters are recomputed on every edge. The requested co-author attribution was already added in 5aeb5f4158.

Validation: 50 supported normal unit cases pass (75,383 assertions); the three new scalar tests pass on two MPI ranks and with two OpenMP threads (380 assertions per rank/run). All four focused MPI regressions pass. Formatting hooks pass. The local build disables MLPCpp, so its existing ANN fluid unit was excluded; CI will run the complete configured matrix.

The separate epsilon improvement is now open as #2978, with the priority label. It includes scaling, near-zero-reference, derivative and SA/SST regression tests, and targets this branch until #2956 merges.

@rois1995

rois1995 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

@bigfooted the remaining eight x86_64 V&V/adjoint failures were reproduced with the binaries from CI run 38595536164. The shared edge helper also serves SIMD flow reconstruction, so its van Albada correction changes those cases. Updated their references in 48e871b; all eight pass with those same CI binaries, without changing tolerances. New unit files also carry author attribution (a3968c9).

The variable-relative scalar limiter epsilon follow-up is now #2978, labeled priority. Its scaling/zero-reference tests pass in serial, MPI and OpenMP; reverse derivatives match finite differences, and both new SA/SST MPI regressions pass. CI for the latest #2956 head is running. #2978 passes formatting; the regression workflow only triggers for PRs targeting develop/master, so its full matrix will run after retargeting it to develop following this merge.

@bigfooted

Copy link
Copy Markdown
Contributor

ok, so this one goes first after updating regressions, then #2978

rois1995 and others added 2 commits October 15, 2026 11:32
The scalar review change replaced the VAN_ALBADA_EDGE form in the shared
muscl helper, which also changed the SIMD flow reconstruction and stopped
the 30P30N and DSMA661 V&V cases from converging. The flow form
(delta^2 + 1e-6) is restored for the flow solver and for the flow velocity
and density that the scalar convection reconstructs, so both see the same
face values. Only the transported scalars use the van Albada function of
the point limiters, selected with MusclLimiter::EDGE_SCALAR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the flow van Albada edge limiter restored, 30P30N, DSMA661, the flat
plate, bump and SWBLI V&V cases and the axial stage adjoint return to the
references they had before the scalar review change. The NACA0012 MUSCL
edge test now uses the flow form for the flow and the point-limiter van
Albada function for the turbulence variables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rois1995

Copy link
Copy Markdown
Contributor Author

@bigfooted correction to my previous comment: the van Albada change in eef97f4 also changed the flow solver's VAN_ALBADA_EDGE, because the SIMD flow kernels use the same helper. 30P30N and dsma661 stopped converging, and updating their references in 48e871b hid that. This is fixed in 75621f9. The flow keeps its original limiter, and only the turbulence and species variables use vanAlbadaFunction with EPS. The V&V and axial_stage_2D references are back to their original values (30c52aa). The remaining species and rae2822_sst changes come from the boundary density upwinding. I will update those references from the next CI run.

rois1995 and others added 3 commits October 15, 2026 14:47
Values from CI run 38717278593 for rae2822_sst (serial, hybrid), species2_primitiveVenturi_JST,
species2_primitiveVenturi_Lax_Friedrich and species_active_transport_temp_limits (parallel).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One flow-limiter face cancels to zero in exact arithmetic, so its value is only rounding, which
depends on the compiler. Use a margin far above that rounding and a tight relative tolerance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r2956_flow_vanalbada

# Conflicts:
#	TestCases/hybrid_regression.py
#	TestCases/parallel_regression.py
#	TestCases/serial_regression.py
#	TestCases/tutorials.py
#	TestCases/vandv.py
@bigfooted bigfooted self-assigned this Oct 16, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants