1 #include "post/Analysis.h"
2 #include "io/Column.h"
3 #include "io/Logger.h"
4 #include "io/Output.h"
5 #include "objects/finders/BruteForceFinder.h"
6 #include "objects/finders/UniformGrid.h"
7 #include "objects/geometry/Box.h"
8 #include "objects/utility/Algorithm.h"
9 #include "objects/utility/IteratorAdapters.h"
10 #include "post/MarchingCubes.h"
11 #include "post/Point.h"
12 #include "post/TwoBody.h"
13 #include "quantities/Storage.h"
14 #include "quantities/Utility.h"
15 #include "sph/initial/MeshDomain.h"
16 #include "sph/kernel/Kernel.h"
17 #include "system/Factory.h"
18 #include "thread/Scheduler.h"
19 #include <numeric>
20 #include <set>
21 
22 NAMESPACE_SPH_BEGIN
23 
findNeighborCounts(const Storage & storage,const Float particleRadius)24 Array<Size> Post::findNeighborCounts(const Storage& storage, const Float particleRadius) {
25     Array<NeighborRecord> neighs;
26     AutoPtr<IBasicFinder> finder = Factory::getFinder(RunSettings::getDefaults());
27     ArrayView<const Vector> r = storage.getValue<Vector>(QuantityId::POSITION);
28     finder->build(SEQUENTIAL, r);
29     Array<Size> counts(r.size());
30     for (Size i = 0; i < r.size(); ++i) {
31         counts[i] = finder->findAll(i, r[i][H] * particleRadius, neighs);
32     }
33     return counts;
34 }
35 
36 // Checks if two particles belong to the same component
37 struct ComponentChecker : public Polymorphic {
belongComponentChecker38     virtual bool belong(const Size UNUSED(i), const Size UNUSED(j)) {
39         // by default, any two particles within the search radius belong to the same component
40         return true;
41     }
42 };
43 
findComponentsImpl(ComponentChecker & checker,ArrayView<const Vector> r,const Float radius,Array<Size> & indices)44 static Size findComponentsImpl(ComponentChecker& checker,
45     ArrayView<const Vector> r,
46     const Float radius,
47     Array<Size>& indices) {
48     // initialize stuff
49     indices.resize(r.size());
50     const Size unassigned = Size(-1);
51     indices.fill(unassigned);
52     Size componentIdx = 0;
53 
54     Array<Size> stack;
55     Array<NeighborRecord> neighs;
56 
57     AutoPtr<IBasicFinder> finder = Factory::getFinder(RunSettings::getDefaults());
58     // the build time is probably negligible compared to the actual search of components, so let's just use
59     // the sequential execution here
60     finder->build(SEQUENTIAL, r);
61 
62     for (Size i = 0; i < r.size(); ++i) {
63         if (indices[i] == unassigned) {
64             indices[i] = componentIdx;
65             stack.push(i);
66             // find new neigbours recursively until we find all particles in the component
67             while (!stack.empty()) {
68                 const Size index = stack.pop();
69                 finder->findAll(index, r[index][H] * radius, neighs);
70                 for (auto& n : neighs) {
71                     if (!checker.belong(index, n.index)) {
72                         // do not count as neighbors
73                         continue;
74                     }
75                     if (indices[n.index] == unassigned) {
76                         indices[n.index] = componentIdx;
77                         stack.push(n.index);
78                     }
79                 }
80             }
81             componentIdx++;
82         }
83     }
84 
85     return componentIdx;
86 }
87 
findComponents(const Storage & storage,const Float radius,const Flags<ComponentFlag> flags,Array<Size> & indices)88 Size Post::findComponents(const Storage& storage,
89     const Float radius,
90     const Flags<ComponentFlag> flags,
91     Array<Size>& indices) {
92     SPH_ASSERT(radius > 0._f);
93 
94     AutoPtr<ComponentChecker> checker = makeAuto<ComponentChecker>();
95 
96     if (flags.has(ComponentFlag::SEPARATE_BY_FLAG)) {
97         class FlagComponentChecker : public ComponentChecker {
98             ArrayView<const Size> flag;
99 
100         public:
101             explicit FlagComponentChecker(const Storage& storage) {
102                 flag = storage.getValue<Size>(QuantityId::FLAG);
103             }
104             virtual bool belong(const Size i, const Size j) override {
105                 return flag[i] == flag[j];
106             }
107         };
108         checker = makeAuto<FlagComponentChecker>(storage);
109     }
110 
111     ArrayView<const Vector> r = storage.getValue<Vector>(QuantityId::POSITION);
112     Size componentCnt = findComponentsImpl(*checker, r, radius, indices);
113 
114     if (flags.has(ComponentFlag::ESCAPE_VELOCITY)) {
115         // now we have to merge components with relative velocity lower than the (mutual) escape velocity
116 
117         // first, compute the total mass and the average velocity of each component
118         Array<Float> masses(componentCnt);
119         Array<Vector> positions(componentCnt);
120         Array<Vector> velocities(componentCnt);
121         Array<Float> volumes(componentCnt);
122 
123         masses.fill(0._f);
124         positions.fill(Vector(0._f));
125         velocities.fill(Vector(0._f));
126         volumes.fill(0._f);
127 
128         ArrayView<const Float> m = storage.getValue<Float>(QuantityId::MASS);
129         ArrayView<const Vector> v = storage.getDt<Vector>(QuantityId::POSITION);
130 
131         for (Size i = 0; i < r.size(); ++i) {
132             masses[indices[i]] += m[i];
133             positions[indices[i]] += m[i] * r[i];
134             velocities[indices[i]] += m[i] * v[i];
135             volumes[indices[i]] += pow<3>(r[i][H]);
136         }
137         for (Size k = 0; k < componentCnt; ++k) {
138             SPH_ASSERT(masses[k] > 0._f);
139             positions[k] /= masses[k];
140             positions[k][H] = cbrt(3._f * volumes[k] / (4._f * PI));
141             velocities[k] /= masses[k];
142         }
143 
144         // Helper checker connecting components with relative velocity lower than v_esc
145         struct EscapeVelocityComponentChecker : public ComponentChecker {
146             ArrayView<const Vector> r;
147             ArrayView<const Vector> v;
148             ArrayView<const Float> m;
149             Float radius;
150 
151             virtual bool belong(const Size i, const Size j) override {
152                 const Float dv = getLength(v[i] - v[j]);
153                 const Float dr = getLength(r[i] - r[j]);
154                 const Float m_tot = m[i] + m[j];
155                 const Float v_esc = sqrt(2._f * Constants::gravity * m_tot / dr);
156                 return dv < v_esc;
157             }
158         };
159         EscapeVelocityComponentChecker velocityChecker;
160         velocityChecker.r = positions;
161         velocityChecker.v = velocities;
162         velocityChecker.m = masses;
163         velocityChecker.radius = radius;
164 
165         // run the component finder again, this time for the components found in the first step
166         Array<Size> velocityIndices;
167         componentCnt = findComponentsImpl(velocityChecker, positions, 50._f, velocityIndices);
168 
169         // We should keep merging the components, as now we could have created a new component that was
170         // previously undetected (three bodies bound to their center of gravity, where each two bodies move
171         // faster than the escape velocity of those two bodies). That is not very probable, though, so we end
172         // the process here.
173 
174         // Last thing - we have to reindex the components found in the first step, using the indices from the
175         // second step.
176         SPH_ASSERT(r.size() == indices.size());
177         for (Size i = 0; i < r.size(); ++i) {
178             indices[i] = velocityIndices[indices[i]];
179         }
180     }
181 
182 #ifdef SPH_DEBUG
183     std::set<Size> uniqueIndices;
184     for (Size i : indices) {
185         uniqueIndices.insert(i);
186     }
187     SPH_ASSERT(uniqueIndices.size() == componentCnt);
188     Size counter = 0;
189     for (Size i : uniqueIndices) {
190         SPH_ASSERT(i == counter);
191         counter++;
192     }
193 
194 #endif
195 
196     if (flags.has(ComponentFlag::SORT_BY_MASS)) {
197         Array<Float> componentMass(componentCnt);
198         componentMass.fill(0._f);
199         ArrayView<const Float> m = storage.getValue<Float>(QuantityId::MASS);
200         for (Size i = 0; i < indices.size(); ++i) {
201             componentMass[indices[i]] += m[i];
202         }
203         Order mapping(componentCnt);
204         mapping.shuffle([&componentMass](Size i, Size j) { return componentMass[i] > componentMass[j]; });
205         mapping = mapping.getInverted();
206 
207         Array<Size> realIndices(indices.size());
208         for (Size i = 0; i < indices.size(); ++i) {
209             realIndices[i] = mapping[indices[i]];
210         }
211 
212         indices = copyable(realIndices);
213     }
214 
215     return componentCnt;
216 }
217 
findLargestComponent(const Storage & storage,const Float particleRadius,const Flags<ComponentFlag> flags)218 Array<Size> Post::findLargestComponent(const Storage& storage,
219     const Float particleRadius,
220     const Flags<ComponentFlag> flags) {
221     Array<Size> componentIdxs;
222     Post::findComponents(storage, particleRadius, flags | ComponentFlag::SORT_BY_MASS, componentIdxs);
223 
224     // get the indices of the largest component (with index 0)
225     Array<Size> idxs;
226     for (Size i = 0; i < componentIdxs.size(); ++i) {
227         if (componentIdxs[i] == 0) {
228             idxs.push(i);
229         }
230     }
231     return idxs;
232 }
233 
234 
235 /*static Storage clone(const Storage& storage) {
236     Storage cloned;
237     const Array<Vector>& r = storage.getValue<Vector>(QuantityId::POSITION);
238     cloned.insert<Vector>(QuantityId::POSITION, OrderEnum::FIRST, r.clone());
239 
240     const Array<Vector>& v = storage.getDt<Vector>(QuantityId::POSITION);
241     cloned.getDt<Vector>(QuantityId::POSITION) = v.clone();
242 
243     if (storage.has(QuantityId::MASS)) {
244         const Array<Float>& m = storage.getValue<Float>(QuantityId::MASS);
245         cloned.insert<Float>(QuantityId::MASS, OrderEnum::ZERO, m.clone());
246     } else {
247         ArrayView<const Float> rho = storage.getValue<Float>(QuantityId::DENSITY);
248         Float rhoAvg = 0._f;
249         for (Size i = 0; i < r.size(); ++i) {
250             rhoAvg += rho[i];
251         }
252         rhoAvg /= r.size();
253 
254         /// \todo ASSUMING 10km body!
255         const Float m = sphereVolume(5.e3_f) * rhoAvg / r.size();
256         cloned.insert<Float>(QuantityId::MASS, OrderEnum::ZERO, m);
257     }
258 
259     return cloned;
260 }*/
261 
262 /*Storage Post::findFutureBodies2(const Storage& storage, ILogger& logger) {
263     Array<Vector> r = storage.getValue<Vector>(QuantityId::POSITION).clone();
264     Array<Vector> v = storage.getDt<Vector>(QuantityId::POSITION).clone();
265     const Float m = sphereVolume(5.e3_f) * 2700.f / r.size();
266     Float W_tot = 0._f;
267     for (Size i = 0; i < r.size(); ++i) {
268         for (Size j = i + 1; j < r.size(); ++j) {
269             W_tot += Constants::gravity * sqr(m) / getLength(r[i] - r[j]);
270             SPH_ASSERT(isReal(W_tot));
271         }
272     }
273 
274     Size iteration = 0;
275     while (true) {
276         // find velocity of COM
277         Vector v0(0._f);
278         for (Size i = 0; i < v.size(); ++i) {
279             v0 += v[i];
280         }
281         v0 /= v.size();
282 
283         // find kinetic energies
284         Float K_tot = 0._f;
285         Float K_largest = 0._f;
286         Size idx_largest = 0;
287         for (Size i = 0; i < r.size(); ++i) {
288             const Float k = 0.5_f * m * getSqrLength(v[i] - v0);
289             K_tot += k;
290             if (k > K_largest) {
291                 K_largest = k;
292                 idx_largest = i;
293             }
294         }
295 
296         logger.write("Iteration ", iteration++, ", W = ", W_tot, " / K = ", K_tot);
297         if (K_tot > W_tot) {
298             for (Size i = 0; i < r.size(); ++i) {
299                 if (i != idx_largest) {
300                     W_tot -= Constants::gravity * sqr(m) / getLength(r[i] - r[idx_largest]);
301                 }
302                 SPH_ASSERT(W_tot > 0._f);
303             }
304 
305             r.remove(idx_largest);
306             v.remove(idx_largest);
307         } else {
308             break;
309         }
310     }
311 
312     logger.write("Find largest remnant with ", r.size(), " particles");
313     return clone(storage);
314 }
315 
316 Storage Post::findFutureBodies(const Storage& storage, const Float particleRadius, ILogger& logger) {
317     Storage cloned = clone(storage);
318     Size numComponents = 0, prevNumComponents;
319     Size iter = 0;
320     do {
321         Array<Size> indices;
322         prevNumComponents = numComponents;
323 
324         logger.write(
325             "Iteration ", iter, ": number of bodies: ", iter == 0 ? storage.getParticleCnt() : numComponents);
326 
327         // do merging the first iteration, the follow with energy considerations
328         ComponentConnectivity connectivity =
329             (iter == 0) ? ComponentConnectivity::OVERLAP : ComponentConnectivity::ESCAPE_VELOCITY;
330         numComponents = findComponents(cloned, particleRadius, connectivity, indices);
331 
332         Array<Vector> r_new(numComponents);
333         Array<Vector> v_new(numComponents);
334         Array<Float> h_new(numComponents);
335         Array<Float> m_new(numComponents);
336         r_new.fill(Vector(0._f));
337         v_new.fill(Vector(0._f));
338         h_new.fill(0._f);
339         m_new.fill(0._f);
340 
341 
342         ArrayView<const Vector> r = cloned.getValue<Vector>(QuantityId::POSITION);
343         ArrayView<const Vector> v = cloned.getDt<Vector>(QuantityId::POSITION);
344         ArrayView<const Float> m = cloned.getValue<Float>(QuantityId::MASS);
345 
346         for (Size i = 0; i < r.size(); ++i) {
347             m_new[indices[i]] += m[i];
348             r_new[indices[i]] += m[i] * r[i];
349             h_new[indices[i]] += pow<3>(r[i][H]);
350             v_new[indices[i]] += m[i] * v[i];
351         }
352         for (Size i = 0; i < numComponents; ++i) {
353             SPH_ASSERT(m_new[i] != 0._f);
354             r_new[i] /= m_new[i];
355             r_new[i][H] = root<3>(h_new[i]);
356             v_new[i] /= m_new[i];
357         }
358 
359         cloned.getValue<Vector>(QuantityId::POSITION) = std::move(r_new);
360         cloned.getDt<Vector>(QuantityId::POSITION) = std::move(v_new);
361         cloned.getValue<Float>(QuantityId::MASS) = std::move(m_new);
362 
363         iter++;
364     } while (numComponents != prevNumComponents);
365 
366     return cloned;
367 }*/
368 
findMoons(const Storage & storage,const Float radius,const Float limit)369 Array<Post::MoonEnum> Post::findMoons(const Storage& storage, const Float radius, const Float limit) {
370     // first, find the larget one
371     ArrayView<const Float> m = storage.getValue<Float>(QuantityId::MASS);
372     const auto largestIter = findMax(m);
373     const Float largestM = *largestIter;
374     const Size largestIdx = Size(largestIter - m.begin());
375 
376     Array<MoonEnum> statuses(m.size());
377 #ifdef SPH_DEBUG
378     statuses.fill(MoonEnum(-1));
379 #endif
380     statuses[largestIdx] = MoonEnum::LARGEST_FRAGMENT;
381 
382     // find the ellipse for all bodies
383     ArrayView<const Vector> r = storage.getValue<Vector>(QuantityId::POSITION);
384     ArrayView<const Vector> v = storage.getDt<Vector>(QuantityId::POSITION);
385     for (Size i = 0; i < m.size(); ++i) {
386         if (i == largestIdx) {
387             continue;
388         }
389 
390         // check for observability
391         if (r[i][H] < limit * r[largestIdx][H]) {
392             statuses[i] = MoonEnum::UNOBSERVABLE;
393             continue;
394         }
395 
396         // compute the orbital elements
397         Optional<Kepler::Elements> elements = Kepler::computeOrbitalElements(
398             m[i] + largestM, m[i] * largestM / (m[i] + largestM), r[i] - r[largestIdx], v[i] - v[largestIdx]);
399 
400         if (!elements) {
401             // not bound, mark as ejected body
402             statuses[i] = MoonEnum::RUNAWAY;
403         } else {
404             // if the pericenter is closer than the sum of radii, mark as impactor
405             if (elements->pericenterDist() < radius * (r[i][H] + r[largestIdx][H])) {
406                 statuses[i] = MoonEnum::IMPACTOR;
407             } else {
408                 // bound and not on collisional trajectory
409                 statuses[i] = MoonEnum::MOON;
410             }
411         }
412     }
413 
414     return statuses;
415 }
416 
findMoonCount(ArrayView<const Float> m,ArrayView<const Vector> r,ArrayView<const Vector> v,const Size i,const Float radius,const Float limit)417 Size Post::findMoonCount(ArrayView<const Float> m,
418     ArrayView<const Vector> r,
419     ArrayView<const Vector> v,
420     const Size i,
421     const Float radius,
422     const Float limit) {
423     SPH_ASSERT(std::is_sorted(m.begin(), m.end(), std::greater<Float>{}));
424     SPH_ASSERT(r.size() == m.size());
425 
426     Size count = 0;
427     for (Size j = i + 1; j < r.size(); ++j) {
428         if (m[j] < limit * m[i]) {
429             break;
430         }
431 
432         Optional<Kepler::Elements> elements = Kepler::computeOrbitalElements(
433             m[i] + m[j], m[i] * m[j] / (m[i] + m[j]), r[i] - r[j], v[i] - v[j]);
434 
435         if (elements && elements->pericenterDist() > radius * (r[i][H] + r[j][H])) {
436             count++;
437         }
438     }
439 
440     return count;
441 }
442 
findTumblers(const Storage & storage,const Float limit)443 Array<Post::Tumbler> Post::findTumblers(const Storage& storage, const Float limit) {
444     Array<Tumbler> tumblers;
445     ArrayView<const Vector> omega = storage.getValue<Vector>(QuantityId::ANGULAR_FREQUENCY);
446     ArrayView<const SymmetricTensor> I = storage.getValue<SymmetricTensor>(QuantityId::MOMENT_OF_INERTIA);
447 
448     for (Size i = 0; i < omega.size(); ++i) {
449         const Vector L = I[i] * omega[i];
450         if (omega[i] == Vector(0._f)) {
451             continue;
452         }
453         const Float cosBeta = dot(L, omega[i]) / (getLength(L) * getLength(omega[i]));
454         SPH_ASSERT(cosBeta >= -1._f && cosBeta <= 1._f);
455         const Float beta = acos(cosBeta);
456         if (beta > limit) {
457             tumblers.push(Tumbler{ i, beta });
458         }
459     }
460     return tumblers;
461 }
462 
getCenterOfMass(ArrayView<const Float> m,ArrayView<const Vector> r,ArrayView<const Size> idxs)463 Vector Post::getCenterOfMass(ArrayView<const Float> m,
464     ArrayView<const Vector> r,
465     ArrayView<const Size> idxs) {
466     Vector r_com(0._f);
467     Float m_tot = 0._f;
468     auto functor = [&m_tot, &r_com, m, r](Size i) {
469         r_com += m[i] * r[i];
470         m_tot += m[i];
471     };
472     if (idxs) {
473         for (Size i : idxs) {
474             functor(i);
475         }
476     } else {
477         for (Size i = 0; i < r.size(); ++i) {
478             functor(i);
479         }
480     }
481     r_com /= m_tot;
482     r_com[H] = 0._f;
483     return r_com;
484 }
485 
getInertiaTensor(ArrayView<const Float> m,ArrayView<const Vector> r,const Vector & r0,ArrayView<const Size> idxs)486 SymmetricTensor Post::getInertiaTensor(ArrayView<const Float> m,
487     ArrayView<const Vector> r,
488     const Vector& r0,
489     ArrayView<const Size> idxs) {
490     SymmetricTensor I = SymmetricTensor::null();
491 
492     auto functor = [&I, r, m, &r0](Size i) {
493         const Vector dr = r[i] - r0;
494         I += m[i] * (SymmetricTensor::identity() * getSqrLength(dr) - symmetricOuter(dr, dr));
495     };
496     if (idxs) {
497         for (Size i : idxs) {
498             functor(i);
499         }
500     } else {
501         for (Size i = 0; i < r.size(); ++i) {
502             functor(i);
503         }
504     }
505     return I;
506 }
507 
getInertiaTensor(ArrayView<const Float> m,ArrayView<const Vector> r,ArrayView<const Size> idxs)508 SymmetricTensor Post::getInertiaTensor(ArrayView<const Float> m,
509     ArrayView<const Vector> r,
510     ArrayView<const Size> idxs) {
511 
512     const Vector r_com = getCenterOfMass(m, r, idxs);
513     return getInertiaTensor(m, r, r_com, idxs);
514 }
515 
getAngularFrequency(ArrayView<const Float> m,ArrayView<const Vector> r,ArrayView<const Vector> v,const Vector & r0,const Vector & v0,ArrayView<const Size> idxs)516 Vector Post::getAngularFrequency(ArrayView<const Float> m,
517     ArrayView<const Vector> r,
518     ArrayView<const Vector> v,
519     const Vector& r0,
520     const Vector& v0,
521     ArrayView<const Size> idxs) {
522     SymmetricTensor I = getInertiaTensor(m, r, r0, idxs);
523     Vector L(0._f);
524     auto functor = [&L, m, r, v, &r0, &v0](const Size i) { //
525         L += m[i] * cross(r[i] - r0, v[i] - v0);
526     };
527 
528     if (idxs) {
529         for (Size i : idxs) {
530             functor(i);
531         }
532     } else {
533         for (Size i = 0; i < r.size(); ++i) {
534             functor(i);
535         }
536     }
537     // L = I * omega => omega = I^-1 * L)
538     const SymmetricTensor I_inv = I.inverse();
539     SPH_ASSERT(isReal(I_inv));
540     return I_inv * L;
541 }
542 
getAngularFrequency(ArrayView<const Float> m,ArrayView<const Vector> r,ArrayView<const Vector> v,ArrayView<const Size> idxs)543 Vector Post::getAngularFrequency(ArrayView<const Float> m,
544     ArrayView<const Vector> r,
545     ArrayView<const Vector> v,
546     ArrayView<const Size> idxs) {
547     const Vector r_com = getCenterOfMass(m, r, idxs);
548     const Vector v_com = getCenterOfMass(m, v, idxs);
549     return getAngularFrequency(m, r, v, r_com, v_com, idxs);
550 }
551 
getSphericity(IScheduler & scheduler,const Storage & storage,const Float resolution)552 Float Post::getSphericity(IScheduler& scheduler, const Storage& storage, const Float resolution) {
553     const Box boundingBox = getBoundingBox(storage);
554     McConfig config;
555     config.gridResolution = resolution * maxElement(boundingBox.size());
556     config.surfaceLevel = 0.15_f;
557     Array<Triangle> mesh = getSurfaceMesh(scheduler, storage, config);
558     Float area = 0._f;
559     for (const Triangle& triangle : mesh) {
560         area += triangle.area();
561     }
562     SPH_ASSERT(area > 0._f);
563 
564     MeshParams params;
565     params.precomputeInside = false;
566     MeshDomain domain(scheduler, std::move(mesh), params);
567     const Float volume = domain.getVolume();
568     SPH_ASSERT(volume > 0._f);
569 
570     // https://en.wikipedia.org/wiki/Sphericity
571     return pow(PI * sqr(6._f * volume), 1._f / 3._f) / area;
572 }
573 
operator ==(const HistPoint & other) const574 bool Post::HistPoint::operator==(const HistPoint& other) const {
575     return value == other.value && count == other.count;
576 }
577 
578 /// \brief Filters the input values using cut-offs specified in params.
579 template <typename TValueFunctor>
processParticleCutoffs(const Storage & storage,const Post::HistogramParams & params,const TValueFunctor & functor)580 static Array<Float> processParticleCutoffs(const Storage& storage,
581     const Post::HistogramParams& params,
582     const TValueFunctor& functor) {
583     ArrayView<const Float> m;
584     ArrayView<const Vector> v;
585 
586     // only require mass/velocity if the correpsonding cutoff is specified
587     if (params.massCutoff > 0._f) {
588         m = storage.getValue<Float>(QuantityId::MASS);
589     }
590     if (params.velocityCutoff < INFTY) {
591         v = storage.getDt<Vector>(QuantityId::POSITION);
592     }
593     Array<Float> filtered;
594     for (Size i = 0; i < storage.getParticleCnt(); ++i) {
595         if (m && m[i] < params.massCutoff) {
596             continue;
597         }
598         if (v && getLength(v[i]) > params.velocityCutoff) {
599             continue;
600         }
601         if (params.validator(i)) {
602             filtered.push(functor(i));
603         }
604     }
605     return filtered;
606 }
607 
608 /// \brief Returns the particle values corresponding to given histogram quantity.
getParticleValues(const Storage & storage,const Post::HistogramParams & params,const Post::HistogramId id)609 static Array<Float> getParticleValues(const Storage& storage,
610     const Post::HistogramParams& params,
611     const Post::HistogramId id) {
612 
613     switch (id) {
614     case Post::HistogramId::RADII: {
615         ArrayView<const Vector> r = storage.getValue<Vector>(QuantityId::POSITION);
616         return processParticleCutoffs(storage, params, [r](Size i) { return r[i][H]; });
617     }
618     case Post::HistogramId::EQUIVALENT_MASS_RADII: {
619         ArrayView<const Float> m = storage.getValue<Float>(QuantityId::MASS);
620         return processParticleCutoffs(storage, params, [m, &params](Size i) {
621             return root<3>(3._f * m[i] / (params.referenceDensity * 4._f * PI));
622         });
623     }
624     case Post::HistogramId::VELOCITIES: {
625         ArrayView<const Vector> v = storage.getDt<Vector>(QuantityId::POSITION);
626         return processParticleCutoffs(storage, params, [v](Size i) { return getLength(v[i]); });
627     }
628     case Post::HistogramId::ROTATIONAL_PERIOD: {
629         if (!storage.has(QuantityId::ANGULAR_FREQUENCY)) {
630             return {};
631         }
632         ArrayView<const Vector> omega = storage.getValue<Vector>(QuantityId::ANGULAR_FREQUENCY);
633         return processParticleCutoffs(storage, params, [omega](Size i) {
634             const Float w = getLength(omega[i]);
635             if (w == 0._f) {
636                 // placeholder for zero frequency to avoid division by zero
637                 return 0._f;
638             } else {
639                 return 2._f * PI / (3600._f * w);
640             }
641         });
642     }
643     case Post::HistogramId::ROTATIONAL_FREQUENCY: {
644         if (!storage.has(QuantityId::ANGULAR_FREQUENCY)) {
645             return {};
646         }
647         ArrayView<const Vector> omega = storage.getValue<Vector>(QuantityId::ANGULAR_FREQUENCY);
648         return processParticleCutoffs(storage, params, [omega](Size i) {
649             const Float w = getLength(omega[i]);
650             return 3600._f * 24._f * w / (2._f * PI);
651         });
652     }
653     case Post::HistogramId::ROTATIONAL_AXIS: {
654         if (!storage.has(QuantityId::ANGULAR_FREQUENCY)) {
655             return {};
656         }
657         ArrayView<const Vector> omega = storage.getValue<Vector>(QuantityId::ANGULAR_FREQUENCY);
658         return processParticleCutoffs(storage, params, [omega](Size i) {
659             const Float w = getLength(omega[i]);
660             if (w == 0._f) {
661                 return 0._f; /// \todo what here??
662             } else {
663                 return acos(omega[i][Z] / w);
664             }
665         });
666     }
667     default:
668         const QuantityId quantityId = QuantityId(id);
669         SPH_ASSERT((int)quantityId >= 0);
670         /// \todo allow also other types
671         Array<Float> values = storage.getValue<Float>(quantityId).clone();
672         return processParticleCutoffs(storage, params, [&values](Size i) { return values[i]; });
673     }
674 }
675 
676 /// \brief Returns indices of components to remove from the histogram.
processComponentCutoffs(const Storage & storage,ArrayView<const Size> components,const Size numComponents,const Post::HistogramParams & params)677 static Array<Size> processComponentCutoffs(const Storage& storage,
678     ArrayView<const Size> components,
679     const Size numComponents,
680     const Post::HistogramParams& params) {
681     ArrayView<const Float> m = storage.getValue<Float>(QuantityId::MASS);
682     ArrayView<const Vector> v = storage.getDt<Vector>(QuantityId::POSITION);
683     Array<Vector> velocities(numComponents);
684     Array<Float> masses(numComponents);
685     velocities.fill(Vector(0._f));
686     masses.fill(0._f);
687 
688     for (Size i = 0; i < m.size(); ++i) {
689         velocities[components[i]] += m[i] * v[i];
690         masses[components[i]] += m[i];
691     }
692 
693     Array<Size> toRemove;
694     for (Size idx = 0; idx < numComponents; ++idx) {
695         SPH_ASSERT(masses[idx] > 0._f);
696         velocities[idx] /= masses[idx];
697 
698         if (masses[idx] < params.massCutoff || getLength(velocities[idx]) > params.velocityCutoff) {
699             toRemove.push(idx);
700         }
701     }
702     return toRemove;
703 }
704 
705 /// \todo move directly to Storage?
706 struct MissingQuantityException : public Exception {
707 public:
MissingQuantityExceptionMissingQuantityException708     explicit MissingQuantityException(const QuantityId id)
709         : Exception("Attempting to access missing quantity " + getMetadata(id).quantityName) {}
710 };
711 
712 /// \brief Returns the component values corresponding to given histogram quantity.
getComponentValues(const Storage & storage,const Post::HistogramParams & params,const Post::HistogramId id)713 static Array<Float> getComponentValues(const Storage& storage,
714     const Post::HistogramParams& params,
715     const Post::HistogramId id) {
716 
717     Array<Size> components;
718     const Size numComponents =
719         findComponents(storage, params.components.radius, params.components.flags, components);
720 
721     Array<Size> toRemove = processComponentCutoffs(storage, components, numComponents, params);
722 
723     switch (id) {
724     case Post::HistogramId::EQUIVALENT_MASS_RADII:
725     case Post::HistogramId::RADII: {
726         // compute volume of the body
727         ArrayView<const Float> m = storage.getValue<Float>(QuantityId::MASS);
728         ArrayView<const Float> rho;
729         if (id == Post::HistogramId::RADII) {
730             if (storage.has(QuantityId::DENSITY)) {
731                 rho = storage.getValue<Float>(QuantityId::DENSITY);
732             } else {
733                 throw MissingQuantityException(QuantityId::DENSITY);
734             }
735         }
736 
737         Array<Float> values(numComponents);
738         values.fill(0._f);
739         for (Size i = 0; i < m.size(); ++i) {
740             Float density;
741             if (id == Post::HistogramId::EQUIVALENT_MASS_RADII) {
742                 density = params.referenceDensity;
743             } else {
744                 density = rho[i];
745             }
746             SPH_ASSERT(m[i] > 0._f && density > 0._f);
747             values[components[i]] += m[i] / density;
748         }
749 
750         // remove the components we cut off (in reverse to avoid invalidating indices)
751         values.remove(toRemove);
752 
753         // compute equivalent radii from volumes
754         Array<Float> radii(values.size());
755         for (Size i = 0; i < values.size(); ++i) {
756             radii[i] = root<3>(3._f * values[i] / (4._f * PI));
757             SPH_ASSERT(isReal(radii[i]) && radii[i] > 0._f, values[i]);
758         }
759         return radii;
760     }
761     case Post::HistogramId::VELOCITIES: {
762         // compute velocity as weighted average
763         ArrayView<const Float> m = storage.getValue<Float>(QuantityId::MASS);
764         ArrayView<const Vector> v = storage.getDt<Vector>(QuantityId::POSITION);
765         Array<Vector> sumV(numComponents);
766         Array<Float> weights(numComponents);
767         sumV.fill(Vector(0._f));
768         weights.fill(0._f);
769         for (Size i = 0; i < m.size(); ++i) {
770             const Size componentIdx = components[i];
771             sumV[componentIdx] += m[i] * v[i];
772             weights[componentIdx] += m[i];
773         }
774 
775         // remove the components we cut off (in reverse to avoid invalidating indices)
776         sumV.remove(toRemove);
777         weights.remove(toRemove);
778 
779         Array<Float> velocities(sumV.size());
780         for (Size i = 0; i < sumV.size(); ++i) {
781             SPH_ASSERT(weights[i] != 0._f);
782             velocities[i] = getLength(sumV[i] / weights[i]);
783         }
784         return velocities;
785     }
786     default:
787         NOT_IMPLEMENTED;
788     }
789 }
790 
791 /// \brief Returns the values of particles or components of particles
getValues(const Storage & storage,const Post::ExtHistogramId id,const Post::HistogramSource source,const Post::HistogramParams & params)792 static Array<Float> getValues(const Storage& storage,
793     const Post::ExtHistogramId id,
794     const Post::HistogramSource source,
795     const Post::HistogramParams& params) {
796     Array<Float> values;
797     if (source == Post::HistogramSource::PARTICLES) {
798         values = getParticleValues(storage, params, id);
799         SPH_ASSERT(values.size() <= storage.getParticleCnt()); // can be < due to cutoffs
800     } else {
801         values = getComponentValues(storage, params, id);
802         SPH_ASSERT(values.size() <= storage.getParticleCnt());
803     }
804     return values;
805 }
806 
getCumulativeHistogram(const Storage & storage,const ExtHistogramId id,const HistogramSource source,const Post::HistogramParams & params)807 Array<Post::HistPoint> Post::getCumulativeHistogram(const Storage& storage,
808     const ExtHistogramId id,
809     const HistogramSource source,
810     const Post::HistogramParams& params) {
811 
812     Array<Float> values = getValues(storage, id, source, params);
813     if (values.empty()) {
814         return {}; // no values, trivially empty histogram
815     }
816     std::sort(values.begin(), values.end());
817 
818     Interval range = params.range;
819     if (range.empty()) {
820         for (Size i = 0; i < values.size(); ++i) {
821             range.extend(values[i]);
822         }
823     }
824     SPH_ASSERT(!range.empty());
825 
826     Array<HistPoint> histogram;
827     Size count = 1;
828     Float lastR = INFTY;
829 
830     // iterate in reverse order - from largest radii to smallest ones
831     for (int i = values.size() - 1; i >= 0; --i) {
832         if (values[i] < lastR) {
833             if (range.contains(values[i])) {
834                 histogram.push(HistPoint{ values[i], count });
835             }
836             lastR = values[i];
837         }
838         count++;
839     }
840     SPH_ASSERT(histogram.size() > 0);
841 
842     return histogram;
843 }
844 
getDifferentialHistogram(const Storage & storage,const ExtHistogramId id,const HistogramSource source,const HistogramParams & params)845 Array<Post::HistPoint> Post::getDifferentialHistogram(const Storage& storage,
846     const ExtHistogramId id,
847     const HistogramSource source,
848     const HistogramParams& params) {
849 
850     Array<Float> values = getValues(storage, id, source, params);
851     return getDifferentialHistogram(values, params);
852 }
853 
getDifferentialHistogram(ArrayView<const Float> values,const HistogramParams & params)854 Array<Post::HistPoint> Post::getDifferentialHistogram(ArrayView<const Float> values,
855     const HistogramParams& params) {
856 
857     Interval range = params.range;
858     if (range.empty()) {
859         for (Size i = 0; i < values.size(); ++i) {
860             range.extend(values[i]);
861         }
862         // extend slightly, so that the min/max value is strictly inside the interval
863         range.extend(range.lower() - EPS * range.size());
864         range.extend(range.upper() + EPS * range.size());
865     }
866     SPH_ASSERT(!range.empty());
867     SPH_ASSERT(isReal(range.lower()) && isReal(range.upper()));
868 
869     Size binCnt = params.binCnt;
870     if (binCnt == 0) {
871         // estimate initial bin count as sqrt of component count
872         binCnt = Size(0.5 * sqrt(Float(values.size())));
873     }
874 
875     Array<Size> sfd(binCnt);
876     sfd.fill(0);
877     // check for case where only one body/particle exists
878     const bool singular = range.size() == 0;
879     for (Size i = 0; i < values.size(); ++i) {
880         // get bin index
881         Size binIdx;
882         if (singular) {
883             binIdx = 0; // just add everything into the first bin to get some reasonable output
884         } else {
885             const Float floatIdx = binCnt * (values[i] - range.lower()) / range.size();
886             if (floatIdx >= 0._f && floatIdx < binCnt) {
887                 binIdx = Size(floatIdx);
888             } else {
889                 // out of range, skip
890                 // this should not happen if the range was determined
891                 SPH_ASSERT(!params.range.empty(), floatIdx, binCnt);
892                 continue;
893             }
894         }
895         sfd[binIdx]++;
896     }
897     // convert to HistPoints
898     Array<HistPoint> histogram(binCnt);
899     for (Size i = 0; i < binCnt; ++i) {
900         const Float centerIdx = i + int(params.centerBins) * 0.5_f;
901         histogram[i] = { range.lower() + (centerIdx * range.size()) / binCnt, sfd[i] };
902         SPH_ASSERT(isReal(histogram[i].value), sfd[i], range);
903     }
904     return histogram;
905 }
906 
getLinearFit(ArrayView<const PlotPoint> points)907 Post::LinearFunction Post::getLinearFit(ArrayView<const PlotPoint> points) {
908     SPH_ASSERT(points.size() >= 2);
909     Float x = 0._f, x2 = 0._f;
910     Float y = 0._f, y2 = 0._f;
911     Float xy = 0._f;
912     for (PlotPoint p : points) {
913         x += p.x;
914         x2 += sqr(p.x);
915         y += p.y;
916         y2 += sqr(p.y);
917         xy += p.x * p.y;
918     }
919 
920     const Size n = points.size();
921     const Float denom = n * x2 - sqr(x);
922     SPH_ASSERT(denom > 0._f);
923     const Float b = (y * x2 - x * xy) / denom;
924     const Float a = (n * xy - x * y) / denom;
925     return LinearFunction(a, b);
926 }
927 
getQuadraticFit(ArrayView<const PlotPoint> points)928 Post::QuadraticFunction Post::getQuadraticFit(ArrayView<const PlotPoint> points) {
929     SPH_ASSERT(points.size() >= 3);
930     Array<Vector> xs(points.size());
931     Array<Float> ys(points.size());
932     for (Size k = 0; k < xs.size(); ++k) {
933         xs[k] = Vector(1._f, points[k].x, sqr(points[k].x));
934         ys[k] = points[k].y;
935     }
936     AffineMatrix xTx = AffineMatrix::null();
937     Vector xTy = Vector(0._f);
938 
939     for (Size k = 0; k < xs.size(); ++k) {
940         for (Size i = 0; i < 3; ++i) {
941             for (Size j = 0; j < 3; ++j) {
942                 xTx(i, j) += xs[k][j] * xs[k][i];
943             }
944             xTy[i] += xs[k][i] * ys[k];
945         }
946     }
947     SPH_ASSERT(xTx.determinant() != 0._f);
948 
949     const Vector result = xTx.inverse() * xTy;
950     return QuadraticFunction(result[2], result[1], result[0]);
951 }
952 
953 NAMESPACE_SPH_END
954