turbulentDispersion.C
Go to the documentation of this file.
1 /*---------------------------------------------------------------------------*\
2  ========= |
3  \\ / F ield | OpenFOAM: The Open Source CFD Toolbox
4  \\ / O peration | Website: https://openfoam.org
5  \\ / A nd | Copyright (C) 2025-2026 OpenFOAM Foundation
6  \\/ M anipulation |
7 -------------------------------------------------------------------------------
8 License
9  This file is part of OpenFOAM.
10 
11  OpenFOAM is free software: you can redistribute it and/or modify it
12  under the terms of the GNU General Public License as published by
13  the Free Software Foundation, either version 3 of the License, or
14  (at your option) any later version.
15 
16  OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
17  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
18  FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
19  for more details.
20 
21  You should have received a copy of the GNU General Public License
22  along with OpenFOAM. If not, see <http://www.gnu.org/licenses/>.
23 
24 \*---------------------------------------------------------------------------*/
25 
26 #include "turbulentDispersion.H"
29 #include "coupledToFluid.H"
30 #include "standardNormal.H"
31 #include "wallPolyPatch.H"
35 
36 // * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
37 
38 namespace Foam
39 {
40 namespace Lagrangian
41 {
44  (
48  );
49 }
50 }
51 
52 // * * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * //
53 
54 template<class Type>
56 Foam::Lagrangian::turbulentDispersion::initialiseTurbField
57 (
58  const word& name,
59  const dimensionSet& dims,
60  const Type& value
61 )
62 {
63  return
65  (
67  (
68  IOobject
69  (
70  name,
71  mesh().time().name(),
72  mesh(),
75  ),
76  mesh(),
77  dimensioned<Type>(name, dims, value)
78  )
79  );
80 }
81 
82 
83 template<class InjectionFieldSourceType, class Type>
84 void Foam::Lagrangian::turbulentDispersion::completeTurbField
85 (
87 )
88 {
89  if (turbField.headerOk()) return;
90 
91  LagrangianModels& modelList = cloud().LagrangianModels();
92 
93  turbField.sourcesRef().table().transfer
94  (
96  (
97  turbField,
99  <
101  InjectionFieldSourceType,
104  >(),
106  ).table()
107  );
108 }
109 
110 
111 // * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * //
112 
114 (
115  const word& name,
116  const LagrangianMesh& mesh,
117  const dictionary& modelDict,
118  const dictionary& stateDict
119 )
120 :
122  cloudLagrangianModel(static_cast<const LagrangianModel&>(*this)),
123  dragPtr_(nullptr),
124  momentumTransportModel_(mesh.poly().lookupType<momentumTransportModel>()),
125  Cmu75_(pow(dimensionedScalar("Cmu", dimless, modelDict, 0.09), 0.75)),
126  maxDiscreteEddies_
127  (
128  modelDict.lookupOrDefault<label>("maxDiscreteEddies", 32)
129  ),
130  kc_
131  (
132  cloud<clouds::carried>().carrierField<scalar>
133  (
134  clouds::carried::nameToCarrierName
135  (
136  momentumTransportModel_.k()().name()
137  ),
138  [&]()
139  {
140  return momentumTransportModel_.k();
141  }
142  )
143  ),
144  epsilonc_
145  (
146  cloud<clouds::carried>().carrierField<scalar>
147  (
149  (
150  momentumTransportModel_.epsilon()().name()
151  ),
152  [&]()
153  {
154  return momentumTransportModel_.epsilon();
155  }
156  )
157  ),
158  fractionTurb_(initialiseTurbField("fractionTurb", dimless, vGreat)),
159  tTurb_(initialiseTurbField("tTurb", dimTime, NaN)),
160  Uturb_
161  (
162  initialiseTurbField("Uturb", dimensions::velocity, vector::uniform(NaN))
163  ),
164  rndGen_("rndGen", stateDict, name, false),
165  avgUturbPtr_(nullptr)
166 {}
167 
168 
169 // * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * * //
170 
172 {
173  return wordList({cloud().U.name()});
174 }
175 
176 
178 (
179  const word& fieldName,
180  const word& eqnFieldName
181 ) const
182 {
183  return
184  fieldName == cloud().U.name()
185  && (
186  eqnFieldName == cloud().U.name()
187  || eqnFieldName == cloud<clouds::carried>().Uc.name()
188  );
189 }
190 
191 
193 {
194  LagrangianModels& modelList = cloud().LagrangianModels();
195 
196  dragPtr_ = nullptr;
197 
198  forAll(modelList, i)
199  {
200  if (!isA<drag>(modelList[i])) continue;
201 
202  if (dragPtr_ != nullptr)
203  {
205  << "Multiple drag models found. Turbulent dispersion "
206  << "requires exactly one drag model."
207  << exit(FatalError);
208  }
209 
210  dragPtr_ = &refCast<const drag>(modelList[i]);
211  }
212 
213  if (dragPtr_ == nullptr)
214  {
216  << "No drag models found. Turbulent dispersion "
217  << "requires exactly one drag model."
218  << exit(FatalError);
219  }
220 
221  completeTurbField<maxLagrangianScalarFieldSource>(fractionTurb_);
222  completeTurbField<NaNLagrangianScalarFieldSource>(tTurb_);
223  completeTurbField<NaNLagrangianVectorFieldSource>(Uturb_);
224 }
225 
226 
228 (
229  const LagrangianSubScalarField& deltaT,
230  const bool final
231 )
232 {
233  const LagrangianSubMesh& subMesh = deltaT.mesh();
234 
235  // References to the evolving fields
236  LagrangianSubScalarSubField& fractionTurb = fractionTurb_.ref(subMesh);
237  LagrangianSubScalarSubField& tTurb = tTurb_.ref(subMesh);
238  LagrangianSubVectorSubField& Uturb = Uturb_.ref(subMesh);
239  const LagrangianSubScalarField& fractionTurb0 = fractionTurb.oldTime();
240  const LagrangianSubVectorField& Uturb0 = Uturb.oldTime();
241 
242  // Update the eddy time-scale
243  const LagrangianSubScalarField magUrel
244  (
245  mag(cloud().U(subMesh) - cloud<clouds::carried>().Uc(subMesh))
246  );
247  static const dimensionedScalar rootVSmallEpsilon
248  (
250  rootVSmall
251  );
252  static const dimensionedScalar rootVSmallEpsilonU
253  (
255  rootVSmall
256  );
257  tTurb =
258  min
259  (
260  kc_(subMesh)/max(epsilonc_(subMesh), rootVSmallEpsilon),
261  Cmu75_*kc_(subMesh)*sqrt(kc_(subMesh))
262  /max(epsilonc_(subMesh)*magUrel, rootVSmallEpsilonU)
263  );
264 
265  // Velocity fluctuation magnitude
266  LagrangianSubScalarField magUturb(sqrt(2.0/3.0*kc_(subMesh)));
267 
268  // Modify particles on the walls to account for the turbulent kinetic
269  // energy being constrained to zero. This prevents a turbulent velocity
270  // fluctuation from pointing through a wall and causing a particle to
271  // vibrate as the drag model and the rebound model fight each other.
272  const PackedBoolList patchIsWall
273  (
274  mesh().poly().boundary().findIndices<wallPolyPatch>().toc()
275  );
276  forAll(subMesh, subi)
277  {
278  const label i = subi + subMesh.start();
279 
280  if (mesh().state(i) < LagrangianState::onPatchZero) continue;
281 
282  const label patchi =
283  static_cast<label>(mesh().state(i))
284  - static_cast<label>(LagrangianState::onPatchZero);
285 
286  if (!patchIsWall[patchi]) continue;
287 
288  tTurb[subi] = rootVSmall;
289 
290  magUturb[subi] = 0;
291  }
292 
293  // Initialise the average turbulent velocities
294  avgUturbPtr_.set
295  (
297  (
298  "avgUturb",
299  subMesh,
301  ).ptr()
302  );
303 
304  // Consider each particle in turn
305  forAll(subMesh, subi)
306  {
307  // Create independent generators for each particle. That way if the
308  // sequence grows or shrinks across iterations, that effect doesn't
309  // propagate and affect other particles' sequences
311  (
312  rndGen_.sampleAB(labelMin, labelMax),
313  false
314  );
316  (
317  rndGen_.sampleAB(labelMin, labelMax),
318  false
319  );
320 
321  // Generate a random direction with a normally distributed magnitude
322  auto rndDir = [&rndGen, &stdNormal]()
323  {
324  const scalar theta =
326  const scalar z = 2*rndGen.scalar01() - 1;
327  const scalar r = sqrt(1 - sqr(z));
328  return stdNormal.sample()*vector(r*cos(theta), r*sin(theta), z);
329  };
330 
331  // Set up sub-stepping
332  const scalar Dt = max(deltaT[subi], rootVSmall);
333  scalar dt = 0;
334 
335  // Continue/complete the previous eddy
336  if (fractionTurb0[subi] < 1)
337  {
338  dt += tTurb[subi]*(1 - fractionTurb0[subi]);
339 
340  avgUturbPtr_()[subi] += min(dt/Dt, 1)*Uturb0[subi];
341  }
342 
343  // Add new eddies across the time-step
344  const scalar nEddies = Dt/tTurb[subi];
345  if (nEddies < maxDiscreteEddies_)
346  {
347  while (dt < Dt)
348  {
349  // Create a turbulent velocity fluctuation for the new eddy
350  Uturb[subi] = rndDir()*magUturb[subi];
351 
352  // Add this eddy to the average
353  avgUturbPtr_()[subi] +=
354  (min(dt + tTurb[subi], Dt) - dt)/Dt*Uturb[subi];
355 
356  // Increment the time
357  dt += tTurb[subi];
358  }
359 
360  // Set the fraction to where we got to in the current eddy
361  fractionTurb[subi] = 1 - (dt - Dt)/tTurb[subi];
362  }
363  else
364  {
365  // Create a turbulent velocity fluctuation for the new eddy
366  Uturb[subi] = rndDir()*magUturb[subi];
367 
368  // Add the combined effect of all the eddies to the average
369  avgUturbPtr_()[subi] +=
370  (Dt - dt)/Dt
371  *sqrt(nEddies/3)
372  *stdNormal.sample<vector>()
373  *magUturb[subi];
374 
375  // Set the fraction to indicate that the eddies are finished
376  fractionTurb[subi] = 1;
377  }
378  }
379 
380  // If this is not the final iteration then rewind the generator so that the
381  // same numbers are generated in the next iteration
382  rndGen_.start(!final);
383 }
384 
385 
387 (
388  const LagrangianSubScalarField& deltaT,
391 ) const
392 {
393  assertCloud<clouds::coupledToConstantDensityFluid>();
394 
395  eqn.Su += dragPtr_->D(deltaT.mesh())*avgUturbPtr_();
396 }
397 
398 
400 (
401  const LagrangianSubScalarField& deltaT,
402  const LagrangianSubScalarSubField& vOrM,
405 ) const
406 {
407  assertCloud
408  <
411  >();
412 
413  eqn.Su += dragPtr_->D(deltaT.mesh())*avgUturbPtr_();
414 }
415 
416 
418 (
419  const LagrangianSubScalarField& deltaT,
420  const bool final
421 )
422 {
423  avgUturbPtr_.clear();
424 }
425 
426 
428 (
429  Ostream& os
430 ) const
431 {
433 
434  writeEntry(os, "rndGen", rndGen_);
435 }
436 
437 
438 // ************************************************************************* //
label k
#define forAll(list, i)
Loop across all elements in list.
Definition: UList.H:449
Macros for easy insertion into run-time selection tables.
Field with dimensions and associated with geometry type GeoMesh which is used to size the field and a...
static tmp< DimensionedField< Type, GeoMesh, PrimitiveField > > New(const word &name, const GeoMesh &mesh, const dimensionSet &, const PrimitiveField< Type > &)
Return a temporary field constructed from name, mesh,.
const GeoMesh & mesh() const
Return mesh.
Part of a geometric field used for setting the values associated with optional sources.
const HashPtrTable< Source > & table() const
Access the underlying field table.
Generic GeometricField class.
Sources & sourcesRef()
Return a reference to the sources.
IOobject defines the attributes of an object for which implicit objectRegistry management is supporte...
Definition: IOobject.H:99
const word & name() const
Return name.
Definition: IOobject.H:307
This class stores the coefficients of a Lagrangian equation, and facilitates solving that equation an...
Definition: LagrangianEqn.H:56
LagrangianCoeff< Type, false > Su
Explicit coefficient.
Base class for Lagrangian injections. Minimal wrapper over LagrangianSource. Implements some utility ...
Class containing Lagrangian geometry and topology.
Base class for Lagrangian models.
const Time & time() const
Return time.
const LagrangianMesh & mesh() const
The mesh.
const word & name() const
The source name.
List of Lagrangian models, constructed as a (Lagrangian) mesh object. Provides similar functions to t...
HashTable< word > modelTypeFieldSourceTypes() const
Return a table of field source types that are chosen to match given.
Base class for Lagrangian sources. Minimal wrapper over LagrangianModel that provides an interface to...
Mesh that relates to a sub-section of a Lagrangian mesh. This is used to construct fields that relate...
label start() const
Return start.
Model for turbulent dispersion. This model creates a random turbulent velocity fluctuation based on t...
virtual wordList addSupFields() const
Return the name of the velocity field.
virtual bool addsSupToField(const word &fieldName, const word &eqnFieldName) const
Return true for the velocity or carrier velocity field.
virtual void postConstruct()
Do post construction steps which require access to other models.
virtual void addSup(const LagrangianSubScalarField &deltaT, const LagrangianSubVectorSubField &U, LagrangianEqn< vector > &eqn) const
Add a source term to the velocity equation.
virtual void writeProcessorState(Ostream &os) const
Write state.
virtual void preAddSup(const LagrangianSubScalarField &deltaT, const bool final)
Hook before source evaluation.
turbulentDispersion(const word &name, const LagrangianMesh &mesh, const dictionary &modelDict, const dictionary &stateDict)
Construct from components.
virtual void postAddSup(const LagrangianSubScalarField &deltaT, const bool final)
Hook after source evaluation.
const Field0Type & oldTime() const
Return the old-time field.
Definition: OldTimeField.C:322
An Ostream is an abstract base class for all output systems (streams, files, token lists,...
Definition: Ostream.H:57
A bit-packed bool list.
static Form uniform(const Cmpt &s)
Return a VectorSpace with all elements = s.
Definition: VectorSpaceI.H:168
Mix-in for Lagrangian models that refer to a cloud.
A cloud is a collection of lagrangian particles.
Definition: cloud.H:61
Foam::LagrangianModels & LagrangianModels() const
Access the models.
Definition: cloud.C:597
CloudStateField< vector > U
Velocity.
Definition: cloud.H:209
static word nameToCarrierName(const word &name)
Convert a name to its disambiguated carrier equivalent name. I.e.,.
Definition: carried.C:273
Base class for clouds which are coupled to a constant density fluid.
Base class for clouds which are coupled to a variable density fluid.
A list of keywords followed by any number of values (e.g. words and numbers) or sub-dictionaries.
Definition: dictionary.H:162
Dimension set for the base types.
Definition: dimensionSet.H:125
Generic dimensioned Type class.
Standard normal distribution. Not selectable.
virtual scalar sample() const
Sample the distribution.
This source condition retains the internal value.
Abstract base class for momentum transport models (RAS, LES and laminar).
virtual tmp< volScalarField > k() const =0
Return the turbulence kinetic energy.
Random number generator.
bool headerOk()
Read and check header info.
Definition: regIOobject.C:429
virtual void writeProcessorState(Ostream &os) const
Write processor state.
Definition: stateModel.C:132
A class for managing temporary objects.
Definition: tmp.H:55
A class for handling words, derived from string.
Definition: word.H:63
Foam::fvMesh mesh(Foam::IOobject(regionName, runTime.name(), runTime, Foam::IOobject::MUST_READ), false)
#define FatalErrorInFunction
Report an error message using Foam::FatalError.
Definition: error.H:334
label patchi
U
Definition: pEqn.H:72
defineTypeNameAndDebug(collisionPhaseTransfer, 0)
addToRunTimeSelectionTable(LagrangianModel, collisionPhaseTransfer, dictionary)
const scalar twoPi(2 *pi)
HashTable< dimensionSet > table
Table of dimensions.
Definition: dimensions.C:38
const dimensionSet velocity
const dimensionSet dimless
const dimensionSet turbulentEpsilon
Namespace for OpenFOAM.
errorManipArg< error, int > exit(error &err, const int errNo=1)
Definition: errorManip.H:124
static const zero Zero
Definition: zero.H:97
List< word > wordList
A List of words.
Definition: fileName.H:54
intWM_LABEL_SIZE_t label
A label is an int32_t or int64_t as specified by the pre-processor macro WM_LABEL_SIZE.
Definition: label.H:59
dimensionedScalar sin(const dimensionedScalar &ds)
tmp< DimensionedField< typename outerProduct< Type, Type >::type, GeoMesh, Field >> sqr(const DimensionedField< Type, GeoMesh, PrimitiveField > &df)
Vector< scalar > vector
A scalar version of the templated Vector.
Definition: vector.H:49
const dimensionSet & dimTime
Definition: dimensions.C:277
dimensioned< Type > min(const DimensionedField< Type, GeoMesh, PrimitiveField > &df)
dimensioned< vector > dimensionedVector
Dimensioned vector obtained from generic dimensioned type.
word name(const LagrangianState state)
Return a string representation of a Lagrangian state enumeration.
tmp< DimensionedField< typename powProduct< Type, r >::type, GeoMesh, Field > > pow(const DimensionedField< Type, GeoMesh, PrimitiveField > &df, typename powProduct< Type, r >::type)
tmp< DimensionedField< scalar, GeoMesh, Field > > mag(const DimensionedField< Type, GeoMesh, PrimitiveField > &df)
error FatalError
static const label labelMax
Definition: label.H:62
void sqrt(LagrangianPatchField< scalar > &f, const LagrangianPatchField< scalar > &f1)
void writeEntry(Ostream &os, const word &key, const DimensionedFieldFunction< DimensionedFieldType > &f)
dimensionedScalar cos(const dimensionedScalar &ds)
static const label labelMin
Definition: label.H:61
dimensioned< Type > max(const DimensionedField< Type, GeoMesh, PrimitiveField > &df)
faceListList boundary(nPatches)
randomGenerator rndGen(653213)