collisionPhaseTransfer.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 "collisionPhaseTransfer.H"
28 #include "carried.H"
29 #include "grouped.H"
30 #include "massive.H"
31 #include "standardNormal.H"
32 #include "LagrangianmSp.H"
33 
34 // * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
35 
36 namespace Foam
37 {
38 namespace Lagrangian
39 {
42  (
46  );
47 }
48 }
49 
50 
51 // * * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * //
52 
53 void Foam::Lagrangian::collisionPhaseTransfer::addSupType
54 (
55  const LagrangianSubScalarField& deltaT,
56  const LagrangianSubScalarSubField& vOrM,
57  LagrangianEqn<scalar>& eqn
58 ) const
59 {
60  const LagrangianSubMesh& subMesh = deltaT.mesh();
61  const SubList<LagrangianState> subStates = subMesh.sub(mesh().states());
62 
63  if (eqn.isPsi(vOrM))
64  {
65  // Add the implicit coefficient to the models' total
66  if (sumDeltaTSp_.valid()) sumDeltaTSp_.ref() += PhitPtr_();
67 
68  // Add an implicit source to the cloud
69  eqn.deltaTSp -= PhitPtr_();
70  }
71  else
72  {
73  // Calculate the rate of volume/mass consumption
74  tmp<LagrangianSubScalarField> deltaTSu = PhitPtr_()*vOrM;
75 
76  // Modify the rate of consumption to fully consume removed particles
77  forAll(subMesh, subi)
78  {
79  if (subStates[subi] == LagrangianState::toBeRemoved)
80  {
81  deltaTSu.ref()[subi] *= 1 + 1/max(sumDeltaTSp_()[subi], small);
82  }
83  }
84 
85  // Add an explicit source to the carrier
86  eqn.deltaTSu -= deltaTSu;
87  }
88 }
89 
90 
91 template<class Type>
92 void Foam::Lagrangian::collisionPhaseTransfer::addSupType
93 (
94  const LagrangianSubScalarField& deltaT,
95  const LagrangianSubSubField<Type>& field,
96  LagrangianEqn<Type>& eqn
97 ) const
98 {
100  << "The Lagrangian model '" << name() << "' of cloud '"
101  << mesh().name() << "' cannot add a transfer for the field '"
102  << field.name() << "' because the equation is not volume or "
103  << "mass-weighted" << exit(FatalError);
104 }
105 
106 
107 template<class Type>
108 void Foam::Lagrangian::collisionPhaseTransfer::addSupType
109 (
110  const LagrangianSubScalarField& deltaT,
111  const LagrangianSubScalarSubField& vOrM,
112  const LagrangianSubSubField<Type>& field,
113  LagrangianEqn<Type>& eqn
114 ) const
115 {
116  const LagrangianSubMesh& subMesh = deltaT.mesh();
117  const SubList<LagrangianState> subStates = subMesh.sub(mesh().states());
118 
119  if (eqn.isPsi(field))
120  {
121  // Add an implicit source to the cloud
122  eqn.deltaTSp -= PhitPtr_()*vOrM;
123  }
124  else
125  {
126  // Calculate the rate of consumption
127  tmp<LagrangianSubField<Type>> deltaTSu = PhitPtr_()*vOrM*field;
128 
129  // Modify the rate of consumption to fully consume removed particles
130  forAll(subMesh, subi)
131  {
132  if (subStates[subi] == LagrangianState::toBeRemoved)
133  {
134  deltaTSu.ref()[subi] *= 1 + 1/max(sumDeltaTSp_()[subi], small);
135  }
136  }
137 
138  // Add an explicit source to the carrier
139  eqn.deltaTSu -= deltaTSu;
140  }
141 }
142 
143 
144 // * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * //
145 
147 (
148  const word& name,
149  const LagrangianMesh& mesh,
150  const dictionary& modelDict,
151  const dictionary& stateDict
152 )
153 :
155  cloudLagrangianModel(static_cast<const LagrangianModel&>(*this)),
156  minFraction_
157  (
158  isCloud<clouds::grouped>()
159  ? modelDict.lookup<scalar>("minFraction", units::fraction)
160  : NaN
161  ),
162  alphac_
163  (
164  cloud<clouds::carried>().carrierField
165  (
166  mesh.poly().lookupObject<volScalarField>
167  (
168  IOobject::groupName
169  (
170  "alpha",
171  cloud<clouds::carried>().carrierPhaseName()
172  )
173  )
174  )
175  ),
176  rndGen_("rndGen", stateDict, name, false),
177  PhitPtr_(nullptr),
178  sumDeltaTSp_()
179 {}
180 
181 
182 // * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * * //
183 
186 {
187  const bool isGrouped = isCloud<clouds::grouped>();
188 
189  wordList result(isGrouped + 1, word::null);
190 
191  result[isGrouped ? 1 : 0] =
192  isCloud<clouds::massive>()
195 
196  return result;
197 }
198 
199 
201 (
202  const word& fieldName,
203  const word& eqnFieldName
204 ) const
205 {
206  const word eqnPhaseName = IOobject::group(eqnFieldName);
207  const word eqnMember = IOobject::member(eqnFieldName);
208 
209  const bool isCarrierEqn = eqnMember[eqnMember.size() - 1] == 'c';
210 
211  return
212  !isCarrierEqn
213  || eqnPhaseName == word::null
214  || eqnPhaseName == cloud<clouds::carried>().phaseName();
215 }
216 
217 
219 (
220  const LagrangianSubScalarField& deltaT,
221  const bool final
222 )
223 {
224  const LagrangianSubMesh& subMesh = deltaT.mesh();
225  SubList<LagrangianState> subStates = subMesh.sub(mesh().states());
226 
227  auto sampleUniform01 = [&]()
228  {
229  return
231  (
232  "sampleUniform01",
233  subMesh,
234  dimless,
235  rndGen_.sample01<scalar>(subMesh.size())
236  );
237  };
238 
239  auto sampleStandardNormal = [&]()
240  {
241  return
243  (
244  "sampleStandardNormal",
245  subMesh,
246  dimless,
248  (
249  rndGen_.sampleAB(labelMin, labelMax),
250  false
251  ).sample(subMesh.size())
252  );
253  };
254 
255  // Length scale for the containing cells
258  (
259  "L",
260  subMesh,
261  dimLength,
262  cbrt
263  (
265  (
266  subMesh.mesh().poly().cellVolumes(),
267  subMesh.sub(subMesh.mesh().celli())
268  )
269  )
270  );
271 
272  // This is a simple model for the probability that a particle hits the
273  // interface during this step
274  PhitPtr_.set
275  (
276  (
277  max(mag(alphac_.grad(subMesh)), rootVSmall/L)
278  *mag(cloud().U(subMesh))
279  /max(alphac_(subMesh), rootVSmall)
280  *deltaT
281  ).ptr()
282  );
283  LagrangianSubScalarField& Phit = PhitPtr_();
284 
285  // Sample to randomise the collisions
286  if (!isCloud<clouds::grouped>())
287  {
288  Phit = pos(Phit - sampleUniform01());
289  }
290  else
291  {
292  const LagrangianSubScalarSubField number
293  (
294  cloud<clouds::grouped>().number(subMesh)
295  );
296 
297  // The hit probability is thus far uniform across all particles within
298  // the parcel. Sampling each particle individually and summing up the
299  // successes to get the number is equivalent to sampling a normal
300  // distribution for the number. So sample the normal distribution in
301  // order to randomise the collisions for the entire parcel.
302  Phit.maxMin(scalar(0), scalar(1));
303  Phit += sqrt(Phit*(1 - Phit)/number)*sampleStandardNormal();
304  Phit.maxMin(scalar(0), scalar(1));
305 
306  // Clamp the probability to zero if the fraction is below the minimum
307  // permitted. This prevents small transfers and positive feedback in
308  // regions with negligible phase fraction.
309  Phit *= pos(Phit - minFraction_/max(number, scalar(1)));
310  }
311 
312  // If this is not the final iteration then rewind the generator so that the
313  // same numbers are generated in the next iteration
314  rndGen_.start(!final);
315 
316  if (!final) return;
317 
318  // Mark any particles with high probabilities as to be removed
319  forAll(subMesh, subi)
320  {
321  if (Phit[subi] == 1)
322  {
323  subStates[subi] = LagrangianState::toBeRemoved;
324  }
325  }
326 }
327 
328 
331 (
332  const word& fieldName,
333  const LagrangianSubMesh& subMesh
334 )
335 const
336 {
338  return tmp<LagrangianSubScalarField>(nullptr);
339 }
340 
341 
343 (
344  const LagrangianSubScalarField& deltaT,
345  const bool final
346 )
347 {
348  if (!final) return;
349 
350  const LagrangianSubMesh& subMesh = deltaT.mesh();
351  const SubList<LagrangianState> subStates = subMesh.sub(mesh().states());
352 
353  // If particles are being removed then create/lookup the sum of the
354  // implicit coefficients
355  if (findIndex(subStates, LagrangianState::toBeRemoved) != -1)
356  {
357  sumDeltaTSp_.set
358  (
359  IOobject
360  (
361  "sumDeltaTSpvOrM",
362  mesh().time().name(),
363  mesh()
364  ),
365  subMesh,
366  dimensionedScalar(dimless, scalar(0))
367  );
368  }
369 }
370 
371 
373 (
374  const LagrangianSubScalarField& deltaT,
376 ) const
377 {
378  eqn.deltaTSp -= PhitPtr_();
379 }
380 
381 
383 (
385  Lagrangian::collisionPhaseTransfer
386 )
387 
388 
390 (
392  Lagrangian::collisionPhaseTransfer
393 )
394 
395 
397 (
398  const LagrangianSubScalarField& deltaT,
399  const bool final
400 )
401 {
402  PhitPtr_.clear();
403 
404  if (final) sumDeltaTSp_.clear();
405 }
406 
407 
409 (
410  Ostream& os
411 ) const
412 {
414 
415  writeEntry(os, "rndGen", rndGen_);
416 }
417 
418 
419 // ************************************************************************* //
#define IMPLEMENT_LAGRANGIAN_MODEL_ADD_FIELD_SUP(Type, modelType)
#define IMPLEMENT_LAGRANGIAN_MODEL_ADD_V_OR_M_FIELD_SUP(Type, modelType)
Functions for calculating sources in a Lagrangian equation.
#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,.
void maxMin(const dimensioned< Type > &minDt, const dimensioned< Type > &maxDt)
Clip the field to between two values.
const GeoMesh & mesh() const
Return mesh.
Generic GeometricField class.
IOobject defines the attributes of an object for which implicit objectRegistry management is supporte...
Definition: IOobject.H:99
word group() const
Return group (extension part of name)
Definition: IOobject.C:321
word member() const
Return member (name without the extension)
Definition: IOobject.C:327
This class stores the coefficients of a Lagrangian equation, and facilitates solving that equation an...
Definition: LagrangianEqn.H:56
LagrangianCoeff< scalar, true > deltaTSp
Implicit time-coefficient.
Class containing Lagrangian geometry and topology.
const labelIODynamicField & celli() const
Access the cell indices.
const polyMesh & poly() const
Access the poly mesh.
Base class for Lagrangian models.
const LagrangianMesh & mesh() const
The mesh.
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 size() const
Return size.
const LagrangianMesh & mesh() const
Return the mesh.
word sub(const word &fieldName) const
Return the name of a field corresponding to this sub-mesh.
Model to represent the absorption of droplets or bubbles into a corresponding Eulerian phase as a res...
virtual void addSup(const LagrangianSubScalarField &deltaT, LagrangianEqn< scalar > &eqn) const
Add a fractional source term.
virtual wordList addSupFields() const
Return the name of the volume or mass field.
virtual bool addsSupToField(const word &fieldName, const word &eqnFieldName) const
Return true if this is a Lagrangian field or a field of the.
virtual tmp< LagrangianSubScalarField > source(const word &fieldName, const LagrangianSubMesh &subMesh) const
Return the source value.
collisionPhaseTransfer(const word &name, const LagrangianMesh &mesh, const dictionary &modelDict, const dictionary &stateDict)
Construct from components.
virtual void writeProcessorState(Ostream &os) const
Write state.
virtual void preAddSup(const LagrangianSubScalarField &deltaT, const bool final)
Hook before source evaluation.
virtual void postAddSup(const LagrangianSubScalarField &deltaT, const bool final)
Add a source term to an equation.
virtual void calculate(const LagrangianSubScalarField &deltaT, const bool final)
Update the transfer rate and remove any consumed particles.
An Ostream is an abstract base class for all output systems (streams, files, token lists,...
Definition: Ostream.H:57
A List obtained as a section of another List.
Definition: SubList.H:56
Mix-in for Lagrangian models that refer to a cloud.
Base class for clouds. Provides a basic evolution algorithm, models, and a database for caching deriv...
Definition: cloud.H:61
static const word mName
Name of mass fields.
Definition: massive.H:69
static const word vName
Name of volume fields.
Definition: shaped.H:84
A list of keywords followed by any number of values (e.g. words and numbers) or sub-dictionaries.
Definition: dictionary.H:162
Standard normal distribution. Not selectable.
virtual scalar sample() const
Sample the distribution.
const word & name() const
Return reference to name.
Definition: fvMesh.H:447
const scalarField & cellVolumes() const
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
static const word null
An empty word.
Definition: word.H:78
Foam::fvMesh mesh(Foam::IOobject(regionName, runTime.name(), runTime, Foam::IOobject::MUST_READ), false)
#define NotImplemented
Issue a FatalErrorIn for a function not currently implemented.
Definition: error.H:381
#define FatalErrorInFunction
Report an error message using Foam::FatalError.
Definition: error.H:334
U
Definition: pEqn.H:72
defineTypeNameAndDebug(collisionPhaseTransfer, 0)
addToRunTimeSelectionTable(LagrangianModel, collisionPhaseTransfer, dictionary)
const dimensionSet time
const unitSet fraction
const unitSet & lookup(const word &unitName)
Lookup and return the named unit from the table.
Definition: units.C:346
Namespace for OpenFOAM.
errorManipArg< error, int > exit(error &err, const int errNo=1)
Definition: errorManip.H:124
dimensionedScalar pos(const dimensionedScalar &ds)
const dimensionSet & dimless
Definition: dimensions.C:138
const dimensionSet & dimLength
Definition: dimensions.C:141
FOR_ALL_FIELD_TYPES(makeDimensionedPointFieldFunctions)
void cbrt(LagrangianPatchField< scalar > &f, const LagrangianPatchField< scalar > &f1)
LagrangianSubSubField< scalar > LagrangianSubScalarSubField
word name(const LagrangianState state)
Return a string representation of a Lagrangian state enumeration.
tmp< DimensionedField< scalar, GeoMesh, Field > > mag(const DimensionedField< Type, GeoMesh, PrimitiveField > &df)
label findIndex(const ListType &, typename ListType::const_reference, const label start=0)
Find first occurrence of given element and return index,.
error FatalError
static const label labelMax
Definition: label.H:62
LagrangianSubField< scalar > LagrangianSubScalarField
void sqrt(LagrangianPatchField< scalar > &f, const LagrangianPatchField< scalar > &f1)
void writeEntry(Ostream &os, const word &key, const DimensionedFieldFunction< DimensionedFieldType > &f)
static const label labelMin
Definition: label.H:61
dimensioned< Type > max(const DimensionedField< Type, GeoMesh, PrimitiveField > &df)
dimensioned< scalar > dimensionedScalar
Dimensioned scalar obtained from generic dimensioned type.