Implementing variable density for unsteady incompressible flow#2641
Implementing variable density for unsteady incompressible flow#2641tkiymaz wants to merge 107 commits into
Conversation
|
@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.
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.
|
@Cristopher-Morales I guess the changes to the preconditioner can be removed from this PR since we now have your implementation |
Removed unnecessary blank lines in SetPrimVar function.
Removed unnecessary blank lines in CVariable.cpp.
Removed debug print statement from SetPrimVar function.
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…/su2 into fix_inc_unsteady_density
| inline su2double GetDensity_time_n(unsigned long iPoint) const override { | ||
| return Density_time_n.size() > 0 ? Density_time_n(iPoint) : GetDensity(iPoint); | ||
| } |
|
@tkiymaz @pcarruscag |
| 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| SU2_OMP_ATOMIC | ||
| ErrorCounter += SetPrimitive_Variables(solver_container, config); | ||
|
|
||
| /*--- Provide a valid initial Density_time_n for restart or step 0 since |
There was a problem hiding this comment.
Three related problems with this initialization strategy:
- Multigrid: the
iMesh == MESH_0guard means the coarse-grid variable objects (which also allocateDensity_time_n = 0) are never initialized, andSetResidual_DualTimeruns on all MG levels — soMGLEVEL > 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. - 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 guardsDUAL_TIME_STEPPING-2ND_ORDER). - The
== 0.0sentinel 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.
There was a problem hiding this comment.
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.
| /*! | ||
| * \brief Virtual function returning whether this is the species solver. | ||
| */ | ||
| inline virtual bool IsSpeciesSolver() const { return false; } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
Two things here:
- These override the
CVariableversions, so per the style guide they should be markedoverride(and then don't needvirtual). - More fundamentally, the base declarations on
CVariableare not needed at all. The only call site that goes throughCVariable*isCScalarSolver::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 onCFlowVariable(compressible default) with theCIncEulerVariableoverride, and the dangerous silent-0.0default onCVariabledisappears — which is exactly what makes the uninitialized coarse-MG case fail silently instead of loudly.
There was a problem hiding this comment.
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.
| * \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; } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
|
||
| #include "../../../Common/include/geometry/CGeometry.hpp" | ||
| #include "../../include/solvers/CSolver.hpp" | ||
| #include "../../include/variables/CIncEulerVariable.hpp" |
There was a problem hiding this comment.
This include appears to be unused — nothing else in this file changed and no CIncEulerVariable symbol is referenced.
| * 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. |
There was a problem hiding this comment.
| * 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). ---*/ |
There was a problem hiding this comment.
This type of AI comment is utterly useless (the strong words are for Claude to pay attention).
| /*--- The bounded-scalar correction (set from the derived solver constructor). ---*/ |
| /*--- 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. ---*/ | ||
|
|
There was a problem hiding this comment.
This is not the place to document another class hierarchy.
| /*--- 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 { |
There was a problem hiding this comment.
| 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 { |
There was a problem hiding this comment.
| inline su2double GetDensity_time_n1(unsigned long iPoint) const override { | |
| inline su2double GetDensity_time_n1(unsigned long iPoint) const final { |
There was a problem hiding this comment.
changes in this file are no longer needed, revert
|
|
||
| /*--- 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); | ||
| } |
There was a problem hiding this comment.
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.
| /*--- 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. ---*/ |
There was a problem hiding this comment.
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?
| /*--- 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. ---*/ |
| available on this level, leave the history untouched (it is maintained | ||
| by MG restriction of the primitive field). ---*/ |
| /*--- 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); | ||
| } |
There was a problem hiding this comment.
| /*--- 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); | |
| } |
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
pre-commit run --allto format old commits.