ParticleCollector.C
Go to the documentation of this file.
1/*---------------------------------------------------------------------------*\
2 ========= |
3 \\ / F ield | OpenFOAM: The Open Source CFD Toolbox
4 \\ / O peration |
5 \\ / A nd | www.openfoam.com
6 \\/ M anipulation |
7-------------------------------------------------------------------------------
8 Copyright (C) 2012-2017 OpenFOAM Foundation
9 Copyright (C) 2015-2021 OpenCFD Ltd.
10-------------------------------------------------------------------------------
11License
12 This file is part of OpenFOAM.
13
14 OpenFOAM is free software: you can redistribute it and/or modify it
15 under the terms of the GNU General Public License as published by
16 the Free Software Foundation, either version 3 of the License, or
17 (at your option) any later version.
18
19 OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
20 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
21 FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
22 for more details.
23
24 You should have received a copy of the GNU General Public License
25 along with OpenFOAM. If not, see <http://www.gnu.org/licenses/>.
26
27\*---------------------------------------------------------------------------*/
28
29#include "ParticleCollector.H"
30#include "Pstream.H"
31#include "surfaceWriter.H"
32#include "unitConversion.H"
33#include "Random.H"
34#include "triangle.H"
35#include "cloud.H"
36
37// * * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * //
38
39template<class CloudType>
41(
42 const faceList& faces,
43 const Field<point>& points,
44 const Field<scalar>& area
45)
46{
47 // Create the output file if not already created
48 if (log_)
49 {
50 if (debug)
51 {
52 Info<< "Creating output file" << endl;
53 }
54
55 if (Pstream::master())
56 {
57 // Create directory if does not exist
58 mkDir(this->writeTimeDir());
59
60 // Open new file at start up
61 outputFilePtr_.reset
62 (
63 new OFstream(this->writeTimeDir()/(type() + ".dat"))
64 );
65
66 outputFilePtr_()
67 << "# Source : " << type() << nl
68 << "# Bins : " << faces.size() << nl
69 << "# Total area : " << sum(area) << nl;
70
71 outputFilePtr_()
72 << "# Geometry :" << nl
73 << '#'
74 << tab << "Bin"
75 << tab << "(Centre_x Centre_y Centre_z)"
76 << tab << "Area"
77 << nl;
78
79 forAll(faces, i)
80 {
81 outputFilePtr_()
82 << '#'
83 << tab << i
84 << tab << faces[i].centre(points)
85 << tab << area[i]
86 << nl;
87 }
88
89 outputFilePtr_()
90 << '#' << nl
91 << "# Output format:" << nl;
92
93 forAll(faces, i)
94 {
95 word id = Foam::name(i);
96 word binId = "bin_" + id;
97
98 outputFilePtr_()
99 << '#'
100 << tab << "Time"
101 << tab << binId
102 << tab << "mass[" << id << "]"
103 << tab << "massFlowRate[" << id << "]"
104 << endl;
105 }
106 }
107 }
108}
109
110
111template<class CloudType>
113(
114 const List<Field<point>>& polygons
115)
116{
117 mode_ = mtPolygon;
118
119 label nPoints = 0;
120 forAll(polygons, polyI)
121 {
122 label np = polygons[polyI].size();
123 if (np < 3)
124 {
125 FatalIOErrorInFunction(this->coeffDict())
126 << "polygons must consist of at least 3 points"
127 << exit(FatalIOError);
128 }
129
130 nPoints += np;
131 }
132
133 label pointOffset = 0;
134 points_.setSize(nPoints);
135 faces_.setSize(polygons.size());
136 faceTris_.setSize(polygons.size());
137 area_.setSize(polygons.size());
138 forAll(faces_, facei)
139 {
140 const Field<point>& polyPoints = polygons[facei];
141 face f(identity(polyPoints.size(), pointOffset));
142 UIndirectList<point>(points_, f) = polyPoints;
143 area_[facei] = f.mag(points_);
144
145 DynamicList<face> tris;
146 f.triangles(points_, tris);
147 faceTris_[facei].transfer(tris);
148
149 faces_[facei].transfer(f);
150
151 pointOffset += polyPoints.size();
152 }
153}
154
155
156template<class CloudType>
158{
159 mode_ = mtConcentricCircle;
160
161 vector origin(this->coeffDict().lookup("origin"));
162
163 this->coeffDict().readEntry("radius", radius_);
164 this->coeffDict().readEntry("nSector", nSector_);
165
166 label nS = nSector_;
167
168 vector refDir;
169 if (nSector_ > 1)
170 {
171 this->coeffDict().readEntry("refDir", refDir);
172
173 refDir -= normal_[0]*(normal_[0] & refDir);
174 refDir.normalise();
175 }
176 else
177 {
178 // Set 4 quadrants for single sector cases
179 nS = 4;
180
181 vector tangent = Zero;
182 scalar magTangent = 0.0;
183
184 Random& rnd = this->owner().rndGen();
185 while (magTangent < SMALL)
186 {
187 vector v = rnd.sample01<vector>();
188
189 tangent = v - (v & normal_[0])*normal_[0];
190 magTangent = mag(tangent);
191 }
192
193 refDir = tangent/magTangent;
194 }
195
196 scalar dTheta = 5.0;
197 scalar dThetaSector = 360.0/scalar(nS);
198 label intervalPerSector = max(1, ceil(dThetaSector/dTheta));
199 dTheta = dThetaSector/scalar(intervalPerSector);
200
201 label nPointPerSector = intervalPerSector + 1;
202
203 label nPointPerRadius = nS*(nPointPerSector - 1);
204 label nPoint = radius_.size()*nPointPerRadius;
205 label nFace = radius_.size()*nS;
206
207 // Add origin
208 nPoint++;
209
210 points_.setSize(nPoint);
211 faces_.setSize(nFace);
212 area_.setSize(nFace);
213
214 coordSys_ = coordSystem::cylindrical(origin, normal_[0], refDir);
215
216 List<label> ptIDs(identity(nPointPerRadius));
217
218 points_[0] = origin;
219
220 // Points
221 forAll(radius_, radI)
222 {
223 label pointOffset = radI*nPointPerRadius + 1;
224
225 for (label i = 0; i < nPointPerRadius; i++)
226 {
227 label pI = i + pointOffset;
228 point pCyl(radius_[radI], degToRad(i*dTheta), 0.0);
229 points_[pI] = coordSys_.globalPosition(pCyl);
230 }
231 }
232
233 // Faces
234 DynamicList<label> facePts(2*nPointPerSector);
235 forAll(radius_, radI)
236 {
237 if (radI == 0)
238 {
239 for (label secI = 0; secI < nS; secI++)
240 {
241 facePts.clear();
242
243 // Append origin point
244 facePts.append(0);
245
246 for (label ptI = 0; ptI < nPointPerSector; ptI++)
247 {
248 label i = ptI + secI*(nPointPerSector - 1);
249 label id = ptIDs.fcIndex(i - 1) + 1;
250 facePts.append(id);
251 }
252
253 label facei = secI + radI*nS;
254
255 faces_[facei] = face(facePts);
256 area_[facei] = faces_[facei].mag(points_);
257 }
258 }
259 else
260 {
261 for (label secI = 0; secI < nS; secI++)
262 {
263 facePts.clear();
264
265 label offset = (radI - 1)*nPointPerRadius + 1;
266
267 for (label ptI = 0; ptI < nPointPerSector; ptI++)
268 {
269 label i = ptI + secI*(nPointPerSector - 1);
270 label id = offset + ptIDs.fcIndex(i - 1);
271 facePts.append(id);
272 }
273 for (label ptI = nPointPerSector-1; ptI >= 0; ptI--)
274 {
275 label i = ptI + secI*(nPointPerSector - 1);
276 label id = offset + nPointPerRadius + ptIDs.fcIndex(i - 1);
277 facePts.append(id);
278 }
279
280 label facei = secI + radI*nS;
281
282 faces_[facei] = face(facePts);
283 area_[facei] = faces_[facei].mag(points_);
284 }
285 }
286 }
287}
288
289
290template<class CloudType>
292(
293 const point& p1,
294 const point& p2
295) const
296{
297 forAll(faces_, facei)
298 {
299 const label facePoint0 = faces_[facei][0];
300
301 const point& pf = points_[facePoint0];
302
303 const scalar d1 = normal_[facei] & (p1 - pf);
304 const scalar d2 = normal_[facei] & (p2 - pf);
305
306 if (sign(d1) == sign(d2))
307 {
308 // Did not cross polygon plane
309 continue;
310 }
311
312 // Intersection point
313 const point pIntersect = p1 + (d1/(d1 - d2))*(p2 - p1);
314
315 // Identify if point is within the bounds of the face. Create triangles
316 // between the intersection point and each edge of the face. If all the
317 // triangle normals point in the same direction as the face normal, then
318 // the particle is within the face. Note that testing for pointHits on
319 // the face's decomposed triangles does not work due to ambiguity along
320 // the diagonals.
321 const face& f = faces_[facei];
322 const vector areaNorm = f.areaNormal(points_);
323 bool inside = true;
324 for (label i = 0; i < f.size(); ++i)
325 {
326 const label j = f.fcIndex(i);
327 const triPointRef t(pIntersect, points_[f[i]], points_[f[j]]);
328
329 if ((areaNorm & t.areaNormal()) < 0)
330 {
331 inside = false;
332 break;
333 }
334 }
335
336 // Add to the list of hits
337 if (inside)
338 {
339 hitFaceIDs_.append(facei);
340 }
341 }
342}
343
344
345template<class CloudType>
347(
348 const point& p1,
349 const point& p2
350) const
351{
352 label secI = -1;
353
354 const scalar d1 = normal_[0] & (p1 - coordSys_.origin());
355 const scalar d2 = normal_[0] & (p2 - coordSys_.origin());
356
357 if (sign(d1) == sign(d2))
358 {
359 // Did not cross plane
360 return;
361 }
362
363 // Intersection point in cylindrical coordinate system
364 const point pCyl = coordSys_.localPosition(p1 + (d1/(d1 - d2))*(p2 - p1));
365
366 scalar r = pCyl[0];
367
368 if (r < radius_.last())
369 {
370 label radI = 0;
371 while (r > radius_[radI])
372 {
373 radI++;
374 }
375
376 if (nSector_ == 1)
377 {
378 secI = 4*radI;
379 }
380 else
381 {
382 scalar theta = pCyl[1] + constant::mathematical::pi;
383
384 secI =
385 nSector_*radI
386 + floor
387 (
388 scalar(nSector_)*theta/constant::mathematical::twoPi
389 );
390 }
391 }
392
393 if (secI != -1)
394 {
395 hitFaceIDs_.append(secI);
396 }
397}
398
399
400// * * * * * * * * * * * * Protected Member Functions * * * * * * * * * * * //
401
402template<class CloudType>
404{
405 const fvMesh& mesh = this->owner().mesh();
406 const Time& time = mesh.time();
407 scalar timeNew = time.value();
408 scalar timeElapsed = timeNew - timeOld_;
409
410 totalTime_ += timeElapsed;
411
412 const scalar alpha = (totalTime_ - timeElapsed)/totalTime_;
413 const scalar beta = timeElapsed/totalTime_;
414
415 forAll(faces_, facei)
416 {
417 massFlowRate_[facei] =
418 alpha*massFlowRate_[facei] + beta*mass_[facei]/timeElapsed;
419 massTotal_[facei] += mass_[facei];
420 }
421
422 Info<< type() << " output:" << nl;
423
424 Field<scalar> faceMassTotal(mass_.size(), Zero);
425 this->getModelProperty("massTotal", faceMassTotal);
426
427 Field<scalar> faceMassFlowRate(massFlowRate_.size(), Zero);
428 this->getModelProperty("massFlowRate", faceMassFlowRate);
429
430
431 scalar sumTotalMass = 0.0;
432 scalar sumAverageMFR = 0.0;
433 forAll(faces_, facei)
434 {
435 faceMassTotal[facei] +=
436 returnReduce(massTotal_[facei], sumOp<scalar>());
437
438 faceMassFlowRate[facei] +=
439 returnReduce(massFlowRate_[facei], sumOp<scalar>());
440
441 sumTotalMass += faceMassTotal[facei];
442 sumAverageMFR += faceMassFlowRate[facei];
443
444 if (outputFilePtr_)
445 {
446 outputFilePtr_()
447 << time.timeName()
448 << tab << facei
449 << tab << faceMassTotal[facei]
450 << tab << faceMassFlowRate[facei]
451 << endl;
452 }
453 }
454
455 Info<< " sum(total mass) = " << sumTotalMass << nl
456 << " sum(average mass flow rate) = " << sumAverageMFR << nl
457 << endl;
458
459
460 if (surfaceFormat_ != "none" && Pstream::master())
461 {
462 auto writer = surfaceWriter::New
463 (
464 surfaceFormat_,
465 this->coeffDict().subOrEmptyDict("formatOptions")
466 .subOrEmptyDict(surfaceFormat_)
467 );
468
469 if (debug)
470 {
471 writer->verbose(true);
472 }
473
474 writer->open
475 (
476 points_,
477 faces_,
478 (this->writeTimeDir() / "collector"),
479 false // serial - already merged
480 );
481
482 writer->nFields(2); // Legacy VTK
483 writer->write("massFlowRate", faceMassFlowRate);
484 writer->write("massTotal", faceMassTotal);
485 }
486
487
488 if (resetOnWrite_)
489 {
490 Field<scalar> dummy(faceMassTotal.size(), Zero);
491 this->setModelProperty("massTotal", dummy);
492 this->setModelProperty("massFlowRate", dummy);
493
494 timeOld_ = timeNew;
495 totalTime_ = 0.0;
496 }
497 else
498 {
499 this->setModelProperty("massTotal", faceMassTotal);
500 this->setModelProperty("massFlowRate", faceMassFlowRate);
501 }
502
503 forAll(faces_, facei)
504 {
505 mass_[facei] = 0.0;
506 massTotal_[facei] = 0.0;
507 massFlowRate_[facei] = 0.0;
508 }
509}
510
511
512// * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * //
513
514template<class CloudType>
516(
517 const dictionary& dict,
518 CloudType& owner,
519 const word& modelName
520)
521:
522 CloudFunctionObject<CloudType>(dict, owner, modelName, typeName),
523 mode_(mtUnknown),
524 parcelType_(this->coeffDict().getOrDefault("parcelType", -1)),
525 removeCollected_(this->coeffDict().getBool("removeCollected")),
526 resetOnWrite_(this->coeffDict().getBool("resetOnWrite")),
527 log_(this->coeffDict().getBool("log")),
528 points_(),
529 faces_(),
530 faceTris_(),
531 nSector_(0),
532 radius_(),
533 coordSys_(),
534 normal_(),
535 negateParcelsOppositeNormal_
536 (
537 this->coeffDict().getBool("negateParcelsOppositeNormal")
538 ),
539 surfaceFormat_(this->coeffDict().lookup("surfaceFormat")),
540 totalTime_(0.0),
541 mass_(),
542 massTotal_(),
543 massFlowRate_(),
544 outputFilePtr_(),
545 timeOld_(owner.mesh().time().value()),
546 hitFaceIDs_()
547{
548 normal_ /= mag(normal_);
549
550 word mode(this->coeffDict().lookup("mode"));
551 if (mode == "polygon")
552 {
553 List<Field<point>> polygons(this->coeffDict().lookup("polygons"));
554
555 initPolygons(polygons);
556
557 vector n0(this->coeffDict().lookup("normal"));
558 normal_ = vectorField(faces_.size(), n0);
559 }
560 else if (mode == "polygonWithNormal")
561 {
562 List<Tuple2<Field<point>, vector>> polygonAndNormal
563 (
564 this->coeffDict().lookup("polygons")
565 );
566
567 List<Field<point>> polygons(polygonAndNormal.size());
568 normal_.setSize(polygonAndNormal.size());
569
570 forAll(polygons, polyI)
571 {
572 polygons[polyI] = polygonAndNormal[polyI].first();
573 normal_[polyI] = normalised(polygonAndNormal[polyI].second());
574 }
575
576 initPolygons(polygons);
577 }
578 else if (mode == "concentricCircle")
579 {
580 vector n0(this->coeffDict().lookup("normal"));
581 normal_ = vectorField(1, n0);
582
583 initConcentricCircles();
584 }
585 else
586 {
588 << "Unknown mode " << mode << ". Available options are "
589 << "polygon, polygonWithNormal and concentricCircle"
590 << exit(FatalIOError);
591 }
592
593 mass_.setSize(faces_.size(), 0.0);
594 massTotal_.setSize(faces_.size(), 0.0);
595 massFlowRate_.setSize(faces_.size(), 0.0);
596
597 makeLogFile(faces_, points_, area_);
598}
599
600
601template<class CloudType>
603(
605)
606:
608 mode_(pc.mode_),
609 parcelType_(pc.parcelType_),
610 removeCollected_(pc.removeCollected_),
611 resetOnWrite_(pc.resetOnWrite_),
612 log_(pc.log_),
613 points_(pc.points_),
614 faces_(pc.faces_),
615 faceTris_(pc.faceTris_),
616 nSector_(pc.nSector_),
617 radius_(pc.radius_),
618 coordSys_(pc.coordSys_),
619 area_(pc.area_),
620 normal_(pc.normal_),
621 negateParcelsOppositeNormal_(pc.negateParcelsOppositeNormal_),
622 surfaceFormat_(pc.surfaceFormat_),
623 totalTime_(pc.totalTime_),
624 mass_(pc.mass_),
625 massTotal_(pc.massTotal_),
626 massFlowRate_(pc.massFlowRate_),
627 outputFilePtr_(),
628 timeOld_(0.0),
629 hitFaceIDs_()
630{}
631
632
633// * * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * //
634
635template<class CloudType>
637(
638 parcelType& p,
639 const scalar dt,
640 const point& position0,
641 bool& keepParticle
642)
643{
644 if ((parcelType_ != -1) && (parcelType_ != p.typeId()))
645 {
646 return;
647 }
648
649 hitFaceIDs_.clear();
650
651 switch (mode_)
652 {
653 case mtPolygon:
654 {
655 collectParcelPolygon(position0, p.position());
656 break;
657 }
658 case mtConcentricCircle:
659 {
660 collectParcelConcentricCircles(position0, p.position());
661 break;
662 }
663 default:
664 {}
665 }
666
667 forAll(hitFaceIDs_, i)
668 {
669 label facei = hitFaceIDs_[i];
670 scalar m = p.nParticle()*p.mass();
671
672 if (negateParcelsOppositeNormal_)
673 {
674 scalar Unormal = 0;
675 vector Uhat = p.U();
676 switch (mode_)
677 {
678 case mtPolygon:
679 {
680 Unormal = Uhat & normal_[facei];
681 break;
682 }
683 case mtConcentricCircle:
684 {
685 Unormal = Uhat & normal_[0];
686 break;
687 }
688 default:
689 {}
690 }
691
692 Uhat /= mag(Uhat) + ROOTVSMALL;
693
694 if (Unormal < 0)
695 {
696 m = -m;
697 }
698 }
699
700 // Add mass contribution
701 mass_[facei] += m;
702
703 if (nSector_ == 1)
704 {
705 mass_[facei + 1] += m;
706 mass_[facei + 2] += m;
707 mass_[facei + 3] += m;
708 }
709
710 if (removeCollected_)
711 {
712 keepParticle = false;
713 }
714 }
715}
716
717
718// ************************************************************************* //
Y[inertIndex] max(0.0)
vtk::internalMeshWriter writer(topoMesh, topoCells, vtk::formatType::INLINE_ASCII, runTime.path()/"blockTopology")
Templated cloud function object base class.
Templated base class for dsmc cloud.
Definition: DSMCCloud.H:75
Generic templated field type.
Definition: Field.H:82
A 1D array of objects of type <T>, where the size of the vector is known and used for subscript bound...
Definition: List.H:77
void setSize(const label n)
Alias for resize()
Definition: List.H:218
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.
void write()
Write post-processing info.
Class to control time during OpenFOAM simulations that is also the top-level objectRegistry.
Definition: Time.H:80
static word timeName(const scalar t, const int precision=precision_)
Definition: Time.C:780
T & first()
Return the first element of the list.
Definition: UListI.H:202
void size(const label n)
Older name for setAddressableSize.
Definition: UList.H:114
A list of keyword definitions, which are a keyword followed by a number of values (eg,...
Definition: dictionary.H:126
const Type & value() const
Return const reference to value.
Mesh data needed to do the Finite Volume discretisation.
Definition: fvMesh.H:91
Lookup type of boundary radiation properties.
Definition: lookup.H:66
const dictionary & coeffDict() const
Return const access to the coefficients dictionary.
Definition: subModelBase.C:131
A Vector of values with scalar precision, where scalar is float/double depending on the compilation f...
A class for handling words, derived from Foam::string.
Definition: word.H:68
volScalarField & p
dynamicFvMesh & mesh
#define FatalIOErrorInFunction(ios)
Report an error message using Foam::FatalIOError.
Definition: error.H:473
const pointField & points
label nPoints
const wordList area
Standard area field types (scalar, vector, tensor, etc)
type
Types of root.
Definition: Roots.H:55
labelList identity(const label len, label start=0)
Return an identity map of the given length with (map[i] == i)
Definition: labelList.C:38
dimensionedScalar sign(const dimensionedScalar &ds)
quaternion normalised(const quaternion &q)
Return the normalised (unit) quaternion of the given quaternion.
Definition: quaternionI.H:680
dimensioned< Type > sum(const DimensionedField< Type, GeoMesh > &df)
messageStream Info
Information stream (stdout output on master, null elsewhere)
mode_t mode(const fileName &name, const bool followLink=true)
Return the file mode, normally following symbolic links.
Definition: MSwindows.C:572
vector point
Point is a vector.
Definition: point.H:43
constexpr scalar degToRad(const scalar deg) noexcept
Conversion from degrees to radians.
Ostream & endl(Ostream &os)
Add newline and flush stream.
Definition: Ostream.H:372
dimensioned< typename typeOfMag< Type >::type > mag(const dimensioned< Type > &dt)
triangle< point, const point & > triPointRef
A triangle using referred points.
Definition: triangle.H:71
Field< vector > vectorField
Specialisation of Field<T> for vector.
IOerror FatalIOError
word name(const expressions::valueTypeCode typeCode)
A word representation of a valueTypeCode. Empty for INVALID.
Definition: exprTraits.C:59
T returnReduce(const T &value, const BinaryOp &bop, const int tag=UPstream::msgType(), const label comm=UPstream::worldComm)
Reduce (copy) and return value.
errorManipArg< error, int > exit(error &err, const int errNo=1)
Definition: errorManip.H:130
constexpr char nl
The newline '\n' character (0x0a)
Definition: Ostream.H:53
constexpr char tab
The tab '\t' character(0x09)
Definition: Ostream.H:52
labelList f(nPoints)
volScalarField & alpha
dictionary dict
dimensionedScalar beta("beta", dimless/dimTemperature, laminarTransport)
#define forAll(list, i)
Loop across all elements in list.
Definition: stdFoam.H:333
Unit conversion functions.
mkDir(pdfPath)