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