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, ¶ms](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