ParticleCollector.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) 2012-2024 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 "ParticleCollector.H"
27 #include "Pstream.H"
28 #include "surfaceWriter.H"
29 #include "randomGenerator.H"
30 #include "triangle.H"
31 #include "axesRotation.H"
32 #include "OSspecific.H"
33 
34 // * * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * //
35 
36 template<class CloudType>
38 (
39  const faceList& faces,
40  const Field<point>& points,
41  const Field<scalar>& area
42 )
43 {
44  // Create the output file if not already created
45  if (log_)
46  {
47  if (debug)
48  {
49  Info<< "Creating output file" << endl;
50  }
51 
52  if (Pstream::master())
53  {
54  // Create directory if does not exist
55  mkDir(this->writeTimeDir());
56 
57  // Open new file at start up
58  outputFilePtr_.reset
59  (
60  new OFstream(this->writeTimeDir()/(type() + ".dat"))
61  );
62 
63  outputFilePtr_()
64  << "# Source : " << type() << nl
65  << "# Bins : " << faces.size() << nl
66  << "# Total area : " << sum(area) << nl;
67 
68  outputFilePtr_()
69  << "# Geometry :" << nl
70  << '#'
71  << tab << "Bin"
72  << tab << "(Centre_x Centre_y Centre_z)"
73  << tab << "Area"
74  << nl;
75 
76  forAll(faces, i)
77  {
78  outputFilePtr_()
79  << '#'
80  << tab << i
81  << tab << faces[i].centre(points)
82  << tab << area[i]
83  << nl;
84  }
85 
86  outputFilePtr_()
87  << '#' << nl
88  << "# Output format:" << nl;
89 
90  forAll(faces, i)
91  {
92  word id = Foam::name(i);
93  word binId = "bin_" + id;
94 
95  outputFilePtr_()
96  << '#'
97  << tab << "Time"
98  << tab << binId
99  << tab << "mass[" << id << "]"
100  << tab << "massFlowRate[" << id << "]"
101  << endl;
102  }
103  }
104  }
105 }
106 
107 
108 template<class CloudType>
110 (
111  const List<Field<point>>& polygons
112 )
113 {
114  mode_ = mtPolygon;
115 
116  label nPoints = 0;
117  forAll(polygons, polyI)
118  {
119  label np = polygons[polyI].size();
120  if (np < 3)
121  {
122  FatalIOErrorInFunction(this->coeffDict())
123  << "polygons must consist of at least 3 points"
124  << exit(FatalIOError);
125  }
126 
127  nPoints += np;
128  }
129 
130  label pointOffset = 0;
131  points_.setSize(nPoints);
132  faces_.setSize(polygons.size());
133  area_.setSize(polygons.size());
134  forAll(faces_, facei)
135  {
136  const Field<point>& polyPoints = polygons[facei];
137  face f(identityMap(polyPoints.size()) + pointOffset);
138  UIndirectList<point>(points_, f) = polyPoints;
139  area_[facei] = f.mag(points_);
140  faces_[facei].transfer(f);
141 
142  pointOffset += polyPoints.size();
143  }
144 }
145 
146 
147 template<class CloudType>
149 {
150  mode_ = mtConcentricCircle;
151 
152  vector origin(this->coeffDict().lookup("origin"));
153 
154  this->coeffDict().lookup("radius") >> radius_;
155  nSector_ = this->coeffDict().template lookup<label>("nSector");
156 
157  label nS = nSector_;
158 
159  vector refDir;
160  if (nSector_ > 1)
161  {
162  refDir = this->coeffDict().lookup("refDir");
163  refDir -= normal_[0]*(normal_[0] & refDir);
164  refDir /= mag(refDir);
165  }
166  else
167  {
168  // Set 4 quadrants for single sector cases
169  nS = 4;
170  refDir = normalised(perpendicular(normal_[0]));
171  }
172 
173  scalar dTheta = degToRad(5.0);
174  scalar dThetaSector = degToRad(360.0)/scalar(nS);
175  label intervalPerSector = max(1, ceil(dThetaSector/dTheta));
176  dTheta = dThetaSector/scalar(intervalPerSector);
177 
178  label nPointPerSector = intervalPerSector + 1;
179 
180  label nPointPerRadius = nS*(nPointPerSector - 1);
181  label nPoint = radius_.size()*nPointPerRadius;
182  label nFace = radius_.size()*nS;
183 
184  // Add origin
185  nPoint++;
186 
187  points_.setSize(nPoint);
188  faces_.setSize(nFace);
189  area_.setSize(nFace);
190 
191  coordSys_ =
192  coordinateSystems::cylindrical("coordSys", origin, normal_[0], refDir);
193 
194  List<label> ptIDs(identityMap(nPointPerRadius));
195 
196  points_[0] = origin;
197 
198  // Points
199  forAll(radius_, radI)
200  {
201  label pointOffset = radI*nPointPerRadius + 1;
202 
203  for (label i = 0; i < nPointPerRadius; i++)
204  {
205  label pI = i + pointOffset;
206  point pCyl(radius_[radI], i*dTheta, 0.0);
207  points_[pI] = coordSys_.globalPosition(pCyl);
208  }
209  }
210 
211  // Faces
212  DynamicList<label> facePts(2*nPointPerSector);
213  forAll(radius_, radI)
214  {
215  if (radI == 0)
216  {
217  for (label secI = 0; secI < nS; secI++)
218  {
219  facePts.clear();
220 
221  // Append origin point
222  facePts.append(0);
223 
224  for (label ptI = 0; ptI < nPointPerSector; ptI++)
225  {
226  label i = ptI + secI*(nPointPerSector - 1);
227  label id = ptIDs.fcIndex(i - 1) + 1;
228  facePts.append(id);
229  }
230 
231  label facei = secI + radI*nS;
232 
233  faces_[facei] = face(facePts);
234  area_[facei] = faces_[facei].mag(points_);
235  }
236  }
237  else
238  {
239  for (label secI = 0; secI < nS; secI++)
240  {
241  facePts.clear();
242 
243  label offset = (radI - 1)*nPointPerRadius + 1;
244 
245  for (label ptI = 0; ptI < nPointPerSector; ptI++)
246  {
247  label i = ptI + secI*(nPointPerSector - 1);
248  label id = offset + ptIDs.fcIndex(i - 1);
249  facePts.append(id);
250  }
251  for (label ptI = nPointPerSector-1; ptI >= 0; ptI--)
252  {
253  label i = ptI + secI*(nPointPerSector - 1);
254  label id = offset + nPointPerRadius + ptIDs.fcIndex(i - 1);
255  facePts.append(id);
256  }
257 
258  label facei = secI + radI*nS;
259 
260  faces_[facei] = face(facePts);
261  area_[facei] = faces_[facei].mag(points_);
262  }
263  }
264  }
265 }
266 
267 
268 template<class CloudType>
270 (
271  const point& p1,
272  const point& p2
273 ) const
274 {
275  forAll(faces_, facei)
276  {
277  const label facePoint0 = faces_[facei][0];
278 
279  const point& pf = points_[facePoint0];
280 
281  const scalar d1 = normal_[facei] & (p1 - pf);
282  const scalar d2 = normal_[facei] & (p2 - pf);
283 
284  if (sign(d1) == sign(d2))
285  {
286  // Did not cross polygon plane
287  continue;
288  }
289 
290  // Intersection point
291  const point pIntersect = p1 + (d1/(d1 - d2))*(p2 - p1);
292 
293  // Identify if point is within the bounds of the face. Create triangles
294  // between the intersection point and each edge of the face. If all the
295  // triangle normals point in the same direction as the face normal, then
296  // the particle is within the face. Note that testing for pointHits on
297  // the face's decomposed triangles does not work due to ambiguity along
298  // the diagonals.
299  const face& f = faces_[facei];
300  const vector a = f.area(points_);
301  bool inside = true;
302  for (label i = 0; i < f.size(); ++ i)
303  {
304  const label j = f.fcIndex(i);
305  const triPointRef t(pIntersect, points_[f[i]], points_[f[j]]);
306  if ((a & t.area()) < 0)
307  {
308  inside = false;
309  break;
310  }
311  }
312 
313  // Add to the list of hits
314  if (inside)
315  {
316  hitFaceIndices_.append(facei);
317  }
318  }
319 }
320 
321 
322 template<class CloudType>
324 (
325  const point& p1,
326  const point& p2
327 ) const
328 {
329  label secI = -1;
330 
331  const scalar d1 = normal_[0] & (p1 - coordSys_.origin());
332  const scalar d2 = normal_[0] & (p2 - coordSys_.origin());
333 
334  if (sign(d1) == sign(d2))
335  {
336  // Did not cross plane
337  return;
338  }
339 
340  // Intersection point in cylindrical co-ordinate system
341  const point pCyl = coordSys_.localPosition(p1 + (d1/(d1 - d2))*(p2 - p1));
342 
343  scalar r = pCyl[0];
344 
345  if (r < radius_.last())
346  {
347  label radI = 0;
348  while (r > radius_[radI])
349  {
350  radI++;
351  }
352 
353  if (nSector_ == 1)
354  {
355  secI = 4*radI;
356  }
357  else
358  {
359  scalar theta = pCyl[1] + constant::mathematical::pi;
360 
361  secI =
362  nSector_*radI
363  + floor
364  (
365  scalar(nSector_)*theta/constant::mathematical::twoPi
366  );
367  }
368  }
369 
370  if (secI != -1)
371  {
372  hitFaceIndices_.append(secI);
373  }
374 }
375 
376 
377 // * * * * * * * * * * * * Protected Member Functions * * * * * * * * * * * //
378 
379 template<class CloudType>
381 {
382  const fvMesh& mesh = this->owner().mesh();
383  const Time& time = mesh.time();
384  scalar timeNew = time.value();
385  scalar timeElapsed = timeNew - timeOld_;
386 
387  totalTime_ += timeElapsed;
388 
389  const scalar alpha = (totalTime_ - timeElapsed)/totalTime_;
390  const scalar beta = timeElapsed/totalTime_;
391 
392  forAll(faces_, facei)
393  {
394  massFlowRate_[facei] =
395  alpha*massFlowRate_[facei] + beta*mass_[facei]/timeElapsed;
396  massTotal_[facei] += mass_[facei];
397  }
398 
399  const label proci = Pstream::myProcNo();
400 
401  Info<< type() << " output:" << nl;
402 
403  Field<scalar> faceMassTotal(mass_.size(), 0.0);
404  this->getModelProperty("massTotal", faceMassTotal);
405 
406  Field<scalar> faceMassFlowRate(massFlowRate_.size(), 0.0);
407  this->getModelProperty("massFlowRate", faceMassFlowRate);
408 
409 
410  scalar sumTotalMass = 0.0;
411  scalar sumAverageMFR = 0.0;
412  forAll(faces_, facei)
413  {
414  scalarList allProcMass(Pstream::nProcs());
415  allProcMass[proci] = massTotal_[facei];
416  Pstream::gatherList(allProcMass);
417  faceMassTotal[facei] += sum(allProcMass);
418 
419  scalarList allProcMassFlowRate(Pstream::nProcs());
420  allProcMassFlowRate[proci] = massFlowRate_[facei];
421  Pstream::gatherList(allProcMassFlowRate);
422  faceMassFlowRate[facei] += sum(allProcMassFlowRate);
423 
424  sumTotalMass += faceMassTotal[facei];
425  sumAverageMFR += faceMassFlowRate[facei];
426 
427  if (outputFilePtr_.valid())
428  {
429  outputFilePtr_()
430  << time.name()
431  << tab << facei
432  << tab << faceMassTotal[facei]
433  << tab << faceMassFlowRate[facei]
434  << endl;
435  }
436  }
437 
438  Info<< " sum(total mass) = " << sumTotalMass << nl
439  << " sum(average mass flow rate) = " << sumAverageMFR << nl
440  << endl;
441 
442 
443  if (surfaceFormat_ != "none")
444  {
445  if (Pstream::master())
446  {
448  (
449  surfaceWriter::New(surfaceFormat_, this->coeffDict())
450  );
451 
452  writer->write
453  (
454  this->writeTimeDir(),
455  "collector",
456  points_,
457  faces_,
458  false,
459  "massTotal",
460  faceMassTotal,
461  "massFlowRate",
462  faceMassFlowRate
463  );
464  }
465  }
466 
467 
468  if (resetOnWrite_)
469  {
470  Field<scalar> dummy(faceMassTotal.size(), 0.0);
471  this->setModelProperty("massTotal", dummy);
472  this->setModelProperty("massFlowRate", dummy);
473 
474  timeOld_ = timeNew;
475  totalTime_ = 0.0;
476  }
477  else
478  {
479  this->setModelProperty("massTotal", faceMassTotal);
480  this->setModelProperty("massFlowRate", faceMassFlowRate);
481  }
482 
483  forAll(faces_, facei)
484  {
485  mass_[facei] = 0.0;
486  massTotal_[facei] = 0.0;
487  massFlowRate_[facei] = 0.0;
488  }
489 }
490 
491 
492 // * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * //
493 
494 template<class CloudType>
496 (
497  const dictionary& dict,
498  CloudType& owner,
499  const word& modelName
500 )
501 :
502  CloudFunctionObject<CloudType>(dict, owner, modelName, typeName),
503  mode_(mtUnknown),
504  parcelType_(this->coeffDict().lookupOrDefault("parcelType", -1)),
505  removeCollected_(this->coeffDict().lookup("removeCollected")),
506  points_(),
507  faces_(),
508  nSector_(0),
509  radius_(),
510  coordSys_("coordSys", vector::zero, axesRotation(sphericalTensor::I)),
511  normal_(),
512  negateParcelsOppositeNormal_
513  (
514  readBool(this->coeffDict().lookup("negateParcelsOppositeNormal"))
515  ),
516  surfaceFormat_(this->coeffDict().lookup("surfaceFormat")),
517  resetOnWrite_(this->coeffDict().lookup("resetOnWrite")),
518  totalTime_(0.0),
519  mass_(),
520  massTotal_(),
521  massFlowRate_(),
522  log_(this->coeffDict().lookup("log")),
523  outputFilePtr_(),
524  timeOld_(owner.mesh().time().value()),
525  hitFaceIndices_()
526 {
527  normal_ /= mag(normal_);
528 
529  word mode(this->coeffDict().lookup("mode"));
530  if (mode == "polygon")
531  {
532  List<Field<point>> polygons(this->coeffDict().lookup("polygons"));
533 
534  initPolygons(polygons);
535 
536  vector n0(this->coeffDict().lookup("normal"));
537  normal_ = vectorField(faces_.size(), n0);
538  }
539  else if (mode == "polygonWithNormal")
540  {
541  List<Tuple2<Field<point>, vector>> polygonAndNormal
542  (
543  this->coeffDict().lookup("polygons")
544  );
545 
546  List<Field<point>> polygons(polygonAndNormal.size());
547  normal_.setSize(polygonAndNormal.size());
548 
549  forAll(polygons, polyI)
550  {
551  polygons[polyI] = polygonAndNormal[polyI].first();
552  normal_[polyI] = polygonAndNormal[polyI].second();
553  normal_[polyI] /= mag(normal_[polyI]) + rootVSmall;
554  }
555 
556  initPolygons(polygons);
557  }
558  else if (mode == "concentricCircle")
559  {
560  vector n0(this->coeffDict().lookup("normal"));
561  normal_ = vectorField(1, n0);
562 
563  initConcentricCircles();
564  }
565  else
566  {
568  << "Unknown mode " << mode << ". Available options are "
569  << "polygon, polygonWithNormal and concentricCircle"
570  << exit(FatalIOError);
571  }
572 
573  mass_.setSize(faces_.size(), 0.0);
574  massTotal_.setSize(faces_.size(), 0.0);
575  massFlowRate_.setSize(faces_.size(), 0.0);
576 
577  makeLogFile(faces_, points_, area_);
578 }
579 
580 
581 template<class CloudType>
583 (
585 )
586 :
588  mode_(pc.mode_),
589  parcelType_(pc.parcelType_),
590  removeCollected_(pc.removeCollected_),
591  points_(pc.points_),
592  faces_(pc.faces_),
593  nSector_(pc.nSector_),
594  radius_(pc.radius_),
595  coordSys_(pc.coordSys_),
596  normal_(pc.normal_),
597  negateParcelsOppositeNormal_(pc.negateParcelsOppositeNormal_),
598  surfaceFormat_(pc.surfaceFormat_),
599  resetOnWrite_(pc.resetOnWrite_),
600  totalTime_(pc.totalTime_),
601  mass_(pc.mass_),
602  massTotal_(pc.massTotal_),
603  massFlowRate_(pc.massFlowRate_),
604  log_(pc.log_),
605  outputFilePtr_(),
606  timeOld_(0.0),
607  hitFaceIndices_()
608 {}
609 
610 
611 // * * * * * * * * * * * * * * * * Destructor * * * * * * * * * * * * * * * //
612 
613 template<class CloudType>
615 {}
616 
617 
618 // * * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * //
619 
620 template<class CloudType>
622 (
623  parcelType& p,
624  const scalar dt,
625  const point& position0,
626  bool& keepParticle
627 )
628 {
629  if ((parcelType_ != -1) && (parcelType_ != p.typeId()))
630  {
631  return;
632  }
633 
634  hitFaceIndices_.clear();
635 
636  switch (mode_)
637  {
638  case mtPolygon:
639  {
640  collectParcelPolygon
641  (
642  position0,
643  p.position(this->owner().mesh())
644  );
645  break;
646  }
647  case mtConcentricCircle:
648  {
649  collectParcelConcentricCircles
650  (
651  position0,
652  p.position(this->owner().mesh())
653  );
654  break;
655  }
656  default:
657  {}
658  }
659 
660  forAll(hitFaceIndices_, i)
661  {
662  label facei = hitFaceIndices_[i];
663  scalar m = p.nParticle()*p.mass();
664 
665  if (negateParcelsOppositeNormal_)
666  {
667  scalar Unormal = 0;
668  vector Uhat = p.U();
669  switch (mode_)
670  {
671  case mtPolygon:
672  {
673  Unormal = Uhat & normal_[facei];
674  break;
675  }
676  case mtConcentricCircle:
677  {
678  Unormal = Uhat & normal_[0];
679  break;
680  }
681  default:
682  {}
683  }
684 
685  Uhat /= mag(Uhat) + rootVSmall;
686 
687  if (Unormal < 0)
688  {
689  m = -m;
690  }
691  }
692 
693  // Add mass contribution
694  mass_[facei] += m;
695 
696  if (nSector_ == 1)
697  {
698  mass_[facei + 1] += m;
699  mass_[facei + 2] += m;
700  mass_[facei + 3] += m;
701  }
702 
703  if (removeCollected_)
704  {
705  keepParticle = false;
706  }
707  }
708 }
709 
710 
711 // ************************************************************************* //
Functions used by OpenFOAM that are specific to POSIX compliant operating systems and need to be repl...
#define forAll(list, i)
Loop across all elements in list.
Definition: UList.H:433
Templated cloud function object base class.
Templated base class for dsmc cloud.
Definition: DSMCCloud.H:80
void size(const label)
Override size to be inconsistent with allocated storage.
Definition: ListI.H:164
void setSize(const label)
Reset size of List.
Definition: List.C:281
Function object to collect the parcel mass- and mass flow rate over a set of polygons....
virtual void postMove(parcelType &p, const scalar dt, const point &position0, bool &keepParticle)
Post-move hook.
virtual ~ParticleCollector()
Destructor.
ParticleCollector(const dictionary &dict, CloudType &owner, const word &modelName)
Construct from dictionary.
void write()
Write post-processing info.
Templated 3D SphericalTensor derived from VectorSpace adding construction from 1 component,...
Class to control time during OpenFOAM simulations that is also the top-level objectRegistry.
Definition: Time.H:76
T & first()
Return the first element of the list.
Definition: UListI.H:114
An auto-pointer similar to the STL auto_ptr but with automatic casting to a reference to the type and...
Definition: autoPtr.H:51
A coordinate rotation specified using global axis.
Definition: axesRotation.H:67
A list of keywords followed by any number of values (e.g. words and numbers) or sub-dictionaries.
Definition: dictionary.H:162
const Type & value() const
Return const reference to value.
const word & name() const
Return const reference to name.
Mesh data needed to do the Finite Volume discretisation.
Definition: fvMesh.H:96
const Time & time() const
Return the top-level database.
Definition: fvMesh.H:420
const polyMesh & mesh() const
Return reference to polyMesh.
Definition: fvMesh.H:443
const dictionary & coeffDict() const
Return const access to the coefficients dictionary.
Definition: subModelBase.C:128
A class for handling words, derived from string.
Definition: word.H:62
A class representing the concept of 0 used to avoid unnecessary manipulations for objects that are kn...
Definition: zero.H:50
Foam::fvMesh mesh(Foam::IOobject(regionName, runTime.name(), runTime, Foam::IOobject::MUST_READ), false)
#define FatalIOErrorInFunction(ios)
Report an error message using Foam::FatalIOError.
Definition: error.H:346
const pointField & points
label nPoints
volScalarField alpha(IOobject("alpha", runTime.name(), mesh, IOobject::READ_IF_PRESENT, IOobject::AUTO_WRITE), lambda *max(Ua &U, zeroSensitivity))
autoPtr< CompressibleMomentumTransportModel > New(const volScalarField &rho, const volVectorField &U, const surfaceScalarField &phi, const viscosity &viscosity)
const scalar twoPi(2 *pi)
errorManipArg< error, int > exit(error &err, const int errNo=1)
Definition: errorManip.H:124
bool readBool(Istream &)
Definition: boolIO.C:66
mode_t mode(const fileName &, const bool checkVariants=true, const bool followLink=true)
Return the file mode.
Definition: POSIX.C:461
scalar degToRad(const scalar deg)
Convert degrees to radians.
dimensionedScalar sign(const dimensionedScalar &ds)
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
Ostream & endl(Ostream &os)
Add newline and flush stream.
Definition: Ostream.H:258
static const char tab
Definition: Ostream.H:266
messageStream Info
static const Identity< scalar > I
Definition: Identity.H:93
vector point
Point is a vector.
Definition: point.H:41
void mag(LagrangianPatchField< scalar > &f, const LagrangianPatchField< Type > &f1)
Vector< scalar > vector
A scalar version of the templated Vector.
Definition: vector.H:49
dimensionSet normalised(const dimensionSet &)
Definition: dimensionSet.C:507
dimensioned< Type > sum(const DimensionedField< Type, GeoMesh, PrimitiveField > &df)
layerAndWeight max(const layerAndWeight &a, const layerAndWeight &b)
Field< vector > vectorField
Specialisation of Field<T> for vector.
IOerror FatalIOError
word name(const LagrangianState state)
Return a string representation of a Lagrangian state enumeration.
triangle< point, const point & > triPointRef
Definition: triPointRef.H:44
labelList identityMap(const label len)
Create identity map (map[i] == i) of given length.
Definition: ListOps.C:104
dimensionSet perpendicular(const dimensionSet &)
Definition: dimensionSet.C:513
void offset(label &lst, const label o)
List< face > faceList
Definition: faceListFwd.H:41
static const char nl
Definition: Ostream.H:267
fileType type(const fileName &, const bool checkVariants=true, const bool followLink=true)
Return the file type: directory or file.
Definition: POSIX.C:488
labelList f(nPoints)
dictionary dict
volScalarField & p
mkDir(pdfPath)