Skip to content

Implementing variable density for unsteady incompressible flow#2641

Open
tkiymaz wants to merge 107 commits into
developfrom
fix_inc_unsteady_density
Open

Implementing variable density for unsteady incompressible flow#2641
tkiymaz wants to merge 107 commits into
developfrom
fix_inc_unsteady_density

Conversation

@tkiymaz

@tkiymaz tkiymaz commented Dec 10, 2025

Copy link
Copy Markdown

Proposed Changes

Implements variable density treatment for unsteady incompressible flow simulations. Currently, SU2 uses constant density in transient simulations, which is inaccurate for combustion cases where density varies significantly due to heat release and species composition changes. This contribution enables proper density updates during time-stepping for flamelet-based combustion modeling. For now, only 1st order time marching is implemented

Related Work

Related to incompressible flow solver and flamelet combustion modeling. No specific issue linked yet.

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.

Tahsin Berk Kiymaz and others added 3 commits June 17, 2025 10:28
@pcarruscag

Copy link
Copy Markdown
Member

@Cristopher-Morales can you review this pr please?

Removed commented-out code for setting density at local point.
Updated the comment for density retrieval to reflect changes for transient density handling.
Removed unnecessary blank lines in CIncEulerVariable.hpp
Removed unnecessary blank lines in CVariable.hpp
Removed unnecessary blank lines to improve code readability.
@Cristopher-Morales

Copy link
Copy Markdown
Contributor

@Cristopher-Morales can you review this pr please?

Hi @pcarruscag !

Yes, I can help reviewing this PR.

Please let me know if you need something else from my side

Removed commented-out code and updated density calculation.
@bigfooted

Copy link
Copy Markdown
Contributor

@Cristopher-Morales I guess the changes to the preconditioner can be removed from this PR since we now have your implementation

Comment thread SU2_CFD/include/variables/CVariable.hpp Outdated
Removed debug print statement from SetPrimVar function.
Comment thread SU2_CFD/include/output/COutput.hpp Outdated
Comment thread SU2_CFD/include/variables/CIncEulerVariable.hpp Outdated
Comment thread SU2_CFD/src/output/CFlowIncOutput.cpp Fixed
bigfooted and others added 3 commits July 14, 2026 22:12
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Comment thread SU2_CFD/include/solvers/CScalarSolver.inl Outdated
Comment thread SU2_CFD/include/solvers/CScalarSolver.inl
Comment thread SU2_CFD/include/variables/CFlowVariable.hpp Outdated
Comment thread SU2_CFD/include/variables/CFlowVariable.hpp Outdated
Comment thread SU2_CFD/include/variables/CIncEulerVariable.hpp Outdated
Comment on lines +311 to +313
inline su2double GetDensity_time_n(unsigned long iPoint) const override {
return Density_time_n.size() > 0 ? Density_time_n(iPoint) : GetDensity(iPoint);
}

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.

@pcarruscag like this?

@bigfooted

Copy link
Copy Markdown
Contributor

@tkiymaz @pcarruscag
Done from my side, please have a look...

Comment thread SU2_CFD/src/solvers/CIncEulerSolver.cpp Outdated
it was 0.0 before SetPrimitive_Variables evaluated the exact field.
Checking GetDensity_time_n(0) == 0.0 ensures we only evaluate this
once when the history arrays are uninitialized. ---*/
if (dual_time && nPoint > 0 && nodes->GetDensity_time_n(0) == 0.0 && iMesh == MESH_0) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not OpenMP-safe: CommonPreprocessing runs inside the solver's parallel region, and this worksharing loop sits behind a branch whose condition is invalidated by the loop itself. A fast thread can enter the omp for and write point 0 while a slower thread has not evaluated the condition yet; the slow thread then sees a nonzero value and skips the construct. A worksharing construct encountered by only some threads of a team is undefined behavior (in practice a hang, and at minimum a TSan report in the CI). The decision to initialize must be made in a single-threaded context (BEGIN_SU2_OMP_SAFE_GLOBAL_ACCESS, or a flag set once outside the parallel region), not by probing the data from all threads.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Folded into the fix in abe9e19. The initialization is now guarded by InnerIter == 0, which is a uniform predicate, so the worksharing loop inside is entered by every thread of the team.

Comment thread SU2_CFD/src/solvers/CIncEulerSolver.cpp Outdated
SU2_OMP_ATOMIC
ErrorCounter += SetPrimitive_Variables(solver_container, config);

/*--- Provide a valid initial Density_time_n for restart or step 0 since

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Three related problems with this initialization strategy:

  1. Multigrid: the iMesh == MESH_0 guard means the coarse-grid variable objects (which also allocate Density_time_n = 0) are never initialized, and SetResidual_DualTime runs on all MG levels — so MGLEVEL > 0 + variable density + dual time uses rho_n = 0 on the coarse levels for the first time step. Either initialize all levels or error out for this combination.
  2. Restart consistency: on restart, rho_n and rho_n-1 are set to the current density rather than densities consistent with Solution_time_n / Solution_time_n1, so the first step after restart is not a true continuation (and the 2nd-order history is wrong — the PR says only 1st order is implemented, but the 2nd-order branches exist and nothing guards DUAL_TIME_STEPPING-2ND_ORDER).
  3. The == 0.0 sentinel probed every inner iteration is fragile even once the race is fixed.

All three disappear with one change: recompute Density_time_n{,1} from the stored solution histories via the fluid model once per time step (after the push-back), instead of lazily cloning the current density. That is correct after restart by construction, needs no sentinel, works on every MG level it runs on, and — if the recomputation sits in the recorded path — the dual-time residual's dependence on rho_n flows back to the already-registered Solution_time_n inputs on the tape, closing the adjoint chain rule that is currently left as a TODO, with no manual extraction needed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in abe9e19. Density_time_n[,1] are now recomputed from Solution_time_n[,1] (plus the species histories on the flamelet path) with the fluid model. It happens once per physical time step in CommonPreprocessing.
I also updated the flamelet regression test_vals because the pre-fix values were tuned with the previous runs.

Comment thread SU2_CFD/include/solvers/CSolver.hpp Outdated
/*!
* \brief Virtual function returning whether this is the species solver.
*/
inline virtual bool IsSpeciesSolver() const { return false; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These two virtuals don't belong on the CSolver base: their only call site is CScalarSolver::SetResidual_DualTime, called on this. A protected bounded_scalar member of CScalarSolver, set by the derived constructors from GetBounded_Species() / GetBounded_Turb() (same pattern as the existing Conservative flag), does the job with zero base-class footprint and no per-call config queries.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in de8eb53. Both virtuals pulled off CSolver. CScalarSolver now stores a const bool BoundedScalar set from the derived constructors, same pattern as Conservative — CSpeciesSolver passes GetBounded_Species(), CTurbSolver passes GetBounded_Turb(), CHeatSolver passes false.

* \param[in] iPoint - Point index.
* \return Density at time level n.
*/
inline virtual su2double GetDensity_time_n(unsigned long iPoint) const {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two things here:

  1. These override the CVariable versions, so per the style guide they should be marked override (and then don't need virtual).
  2. More fundamentally, the base declarations on CVariable are not needed at all. The only call site that goes through CVariable* is CScalarSolver::SetResidual_DualTime, and the idiom for exactly this situation is already used in the same file (CScalarSolver.inl:156): su2staticcast_p<CFlowVariable*>(solver_container[FLOW_SOL]->GetNodes()). With that cast, these getters can be declared here on CFlowVariable (compressible default) with the CIncEulerVariable override, and the dangerous silent-0.0 default on CVariable disappears — which is exactly what makes the uninitialized coarse-MG case fail silently instead of loudly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 8120bda. GetDensity_time_n[,1] now live on CFlowVariable with the CIncEulerVariable override, and CScalarSolver::SetResidual_DualTime casts to CFlowVariable* using the same su2staticcast_p<> idiom. Thanks for pointing at the existing pattern.

Comment thread SU2_CFD/include/variables/CVariable.hpp Outdated
* \param[in] iPoint - Point index.
* \return Density at time n; 0 for compressible nodes.
*/
inline virtual su2double GetDensity_time_n(unsigned long /*iPoint*/) const { return 0.0; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

See the comment on CFlowVariable — these declarations can be removed from CVariable entirely by casting to CFlowVariable* at the scalar-solver call site. A base default that silently returns 0.0 for the density turns any missed override or missed initialization into a silently zeroed unsteady term. The doc comments here ("incompressible only; 0 for compressible nodes") also contradict the CFlowVariable behavior.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in same commit (8120bda). Silent-0 defaults are gone — a missed override would now fail to compile instead of quietly zeroing the unsteady term.

* \param[in] iPoint - Point index.
* \return Adjoint of the density at time n.
*/
inline su2double GetAdjointDensity_time_n(unsigned long iPoint) const override {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

GetAdjointDensity_time_n{,1} are never called anywhere — the only references are the TODO comments in CDiscAdjSolver.cpp. Please remove them (here and the CVariable defaults), along with the RegisterSolution_time_n{,1} overrides that register inputs whose adjoints are never extracted, and add them together with the actual chain-rule extraction in a follow-up (or better, avoid the manual chain rule entirely by recomputing the density histories on tape — see the comment in CIncEulerSolver.cpp). Until the adjoint is closed, an error for unsteady discrete adjoint + non-constant INC_DENSITY_MODEL would prevent silently wrong gradients.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 324d94d — GetAdjointDensity_time_n[,1], the RegisterSolution_time_n[,1] density overrides, and the CVariable defaults are all gone, along with the two TODOs in CDiscAdjSolver::ExtractAdjoint_Solution. Added an SU2_MPI::Error in the discrete-adjoint constructor for unsteady + non-CONSTANT INC_DENSITY_MODEL.

Comment thread SU2_CFD/src/output/CFlowIncOutput.cpp Outdated

#include "../../../Common/include/geometry/CGeometry.hpp"
#include "../../include/solvers/CSolver.hpp"
#include "../../include/variables/CIncEulerVariable.hpp"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This include appears to be unused — nothing else in this file changed and no CIncEulerVariable symbol is referenced.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 12fd1f6.

Comment on lines +95 to +96
* state at time n (also after restart), on every MG level where the inputs
* exist, without the fragile per-iteration sentinel that used to seed it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
* state at time n (also after restart), on every MG level where the inputs
* exist, without the fragile per-iteration sentinel that used to seed it.
* state at time n (also after restart).

const bool incompressible = (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE);

/*--- Flow solution, needed to get density. ---*/
/*--- The bounded-scalar correction (set from the derived solver constructor). ---*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This type of AI comment is utterly useless (the strong words are for Claude to pay attention).

Suggested change
/*--- The bounded-scalar correction (set from the derived solver constructor). ---*/

Comment on lines +629 to +633
/*--- Flow solution, needed to get density. The static cast to CFlowVariable*
exposes GetDensity_time_n[,1] (declared on CFlowVariable, overridden by
CIncEulerVariable) without a silent 0.0-returning base on CVariable.
Same idiom as Upwind_Residual above. ---*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is not the place to document another class hierarchy.

Suggested change
/*--- Flow solution, needed to get density. The static cast to CFlowVariable*
exposes GetDensity_time_n[,1] (declared on CFlowVariable, overridden by
CIncEulerVariable) without a silent 0.0-returning base on CVariable.
Same idiom as Upwind_Residual above. ---*/
/*--- Flow solution, needed to get density. ---*/

* \param[in] iPoint - Point index.
* \return Density at time level n.
*/
inline su2double GetDensity_time_n(unsigned long iPoint) const override {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
inline su2double GetDensity_time_n(unsigned long iPoint) const override {
inline su2double GetDensity_time_n(unsigned long iPoint) const final {

* \param[in] iPoint - Point index.
* \return Density at time level n-1.
*/
inline su2double GetDensity_time_n1(unsigned long iPoint) const override {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
inline su2double GetDensity_time_n1(unsigned long iPoint) const override {
inline su2double GetDensity_time_n1(unsigned long iPoint) const final {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

changes in this file are no longer needed, revert

Comment on lines +114 to +123

/*--- The discrete adjoint chain rule through the time-history of the incompressible density
is not yet implemented. Bail out until it is, to avoid silently wrong gradients. ---*/
if (KindDirect_Solver == RUNTIME_FLOW_SYS &&
config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE &&
config->GetTime_Marching() != TIME_MARCHING::STEADY &&
config->GetKind_DensityModel() != INC_DENSITYMODEL::CONSTANT) {
SU2_MPI::Error("Unsteady discrete adjoint with non-constant INC_DENSITY_MODEL is not yet supported.",
CURRENT_FUNCTION);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Probably not needed anymore since you are recomputing densities like I suggested in the first place.
Either way settings validation is done in CConfig not in the solvers so that dry run mode can catch issues like these.

Comment on lines +979 to +986
/*--- Recompute the dual-time density history from the stored solution history
via the fluid model, once per physical time step after the push-back.
This is consistent with the primitive state at time n by construction
(also after restart, since Solution_time_n[,1] are the loaded histories)
and needs no sentinel. The InnerIter == 0 guard is a uniform, non
data-dependent condition, so the worksharing loop inside is entered by
every thread of the team -- OpenMP-safe, unlike the old probe of
GetDensity_time_n(0) == 0.0. ---*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What is the purpose of repeating the function documentation?
What is the purpose of making a reference to an iteration of the code that will not be kept after merging into develop?

Suggested change
/*--- Recompute the dual-time density history from the stored solution history
via the fluid model, once per physical time step after the push-back.
This is consistent with the primitive state at time n by construction
(also after restart, since Solution_time_n[,1] are the loaded histories)
and needs no sentinel. The InnerIter == 0 guard is a uniform, non
data-dependent condition, so the worksharing loop inside is entered by
every thread of the team -- OpenMP-safe, unlike the old probe of
GetDensity_time_n(0) == 0.0. ---*/

Comment on lines +1110 to +1111
available on this level, leave the history untouched (it is maintained
by MG restriction of the primitive field). ---*/

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

no it's not

Comment on lines +63 to +71
/*--- For constant-density flows, save memory by using GetDensity() on demand. ---*/
const bool variable_density = (config->GetKind_DensityModel() != INC_DENSITYMODEL::CONSTANT);

if (variable_density) {
/*--- Allocate density storage for time levels.
Note: Actual density values will be set after SetPrimVar is called in Preprocessing. ---*/
Density_time_n.resize(nPoint) = su2double(0.0);
Density_time_n1.resize(nPoint) = su2double(0.0);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
/*--- For constant-density flows, save memory by using GetDensity() on demand. ---*/
const bool variable_density = (config->GetKind_DensityModel() != INC_DENSITYMODEL::CONSTANT);
if (variable_density) {
/*--- Allocate density storage for time levels.
Note: Actual density values will be set after SetPrimVar is called in Preprocessing. ---*/
Density_time_n.resize(nPoint) = su2double(0.0);
Density_time_n1.resize(nPoint) = su2double(0.0);
}
if (config->GetKind_DensityModel() != INC_DENSITYMODEL::CONSTANT) {
Density_time_n.resize(nPoint) = su2double(0.0);
Density_time_n1.resize(nPoint) = su2double(0.0);
}

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.

5 participants