Project
Loading...
Searching...
No Matches
XRayTransport.h
Go to the documentation of this file.
1// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
13
26
27#ifndef ALICEO2_BASE_XRAYTRANSPORT_H_
28#define ALICEO2_BASE_XRAYTRANSPORT_H_
29
31
32#include "TGeoShape.h"
33
34#include <algorithm>
35#include <array>
36#include <cmath>
37#include <string>
38#include <vector>
39
40namespace o2
41{
42namespace cad
43{
44namespace xray
45{
46
48
51struct Crossing {
52 double t = 0.;
53 int kind = 0;
54};
55
58struct Robustness {
59 long long rays = 0;
60 long long raysWithCrossings = 0;
61 long long crossings = 0;
62 long long steps = 0;
63 long long zeroLengthSteps = 0;
64 long long nonAdvancingSteps = 0;
65 long long unstickPushes = 0;
66 long long iterationCapHits = 0;
67 long long unterminated = 0;
68 long long oddCrossingLists = 0;
71 long long nonAlternating = 0;
72 long long duplicateCrossings = 0;
82 long long originInside = 0;
87 long long originOutsideWorld = 0;
88 double insideLength = 0.;
89 double seconds = 0.;
90};
91
92struct StepConfig {
98 double push = 1.e-9;
100 double zeroStep = 1.e-9;
103 double unstickPush = 1.e-6;
104 int maxIter = 512;
107 double matchTolerance = 1.e-6;
108};
109
110// ------------------------------------------------------------------------------------------
111// Mode (a): the direct shape-API stepping loop
112// ------------------------------------------------------------------------------------------
113//
114// Contains() to establish the starting state, then alternating DistFromOutside/DistFromInside,
115// advancing the point along the ray, until the accumulated distance leaves the raster window.
116// `stepmax` is deliberately NOT used to bound the query: its semantics differ between shape
117// implementations (some return the crossing, some return stepmax, some return Big), and this loop
118// must be a measurement of the crossing list rather than of that convention. The window is
119// enforced on the returned crossing distance instead.
120
128template <typename ContainsFn, typename DistOutFn, typename DistInFn>
129std::vector<Crossing> stepCrossingsWithKernels(const Point3D& origin, const Point3D& dir,
130 double tMax, const StepConfig& cfg,
131 Robustness& stats, ContainsFn contains,
132 DistOutFn distFromOutside, DistInFn distFromInside)
133{
134 std::vector<Crossing> crossings;
135 double point[3] = {origin[0], origin[1], origin[2]};
136 bool inside = contains(point);
137 if (inside) {
138 ++stats.originInside;
139 }
140 double t = 0.;
141 int iter = 0;
142 for (; iter < cfg.maxIter; ++iter) {
143 const double step = inside ? distFromInside(point, dir.data()) : distFromOutside(point, dir.data());
144 ++stats.steps;
145 if (!(step < TGeoShape::Big())) {
146 break; // no further crossing along this ray
147 }
148 const double tCross = t + step;
149 if (tCross > tMax) {
150 break; // beyond the raster window: not this ray's business
151 }
152 if (step <= cfg.zeroStep) {
153 ++stats.zeroLengthSteps;
154 }
155 crossings.push_back({tCross, inside ? -1 : +1});
156 inside = !inside;
157 double advance = step + cfg.push;
158 if (!(advance > 0.)) {
159 ++stats.nonAdvancingSteps;
160 advance = cfg.unstickPush;
161 ++stats.unstickPushes;
162 } else if (step <= cfg.zeroStep) {
163 advance = step + cfg.unstickPush;
164 ++stats.unstickPushes;
165 }
166 t += advance;
167 if (t > tMax) {
168 break;
169 }
170 for (int k = 0; k < 3; ++k) {
171 point[k] = origin[k] + t * dir[k];
172 }
173 }
174 if (iter >= cfg.maxIter) {
175 ++stats.iterationCapHits;
176 }
177 if (inside) {
178 ++stats.unterminated;
179 }
180 return crossings;
181}
182
184inline std::vector<Crossing> stepWithShapeApi(const TGeoShape* shape, const Point3D& origin,
185 const Point3D& dir, double tMax,
186 const StepConfig& cfg, Robustness& stats)
187{
189 origin, dir, tMax, cfg, stats, [shape](const double* p) { return shape->Contains(p); },
190 [shape](const double* p, const double* d) {
191 return shape->DistFromOutside(p, d, 3, TGeoShape::Big(), nullptr);
192 },
193 [shape](const double* p, const double* d) {
194 return shape->DistFromInside(p, d, 3, TGeoShape::Big(), nullptr);
195 });
196}
197
201inline void auditCrossingList(const std::vector<Crossing>& crossings, const TGeoShape* shape,
202 const Point3D& origin, const Point3D& dir, double tMax,
203 const StepConfig& cfg, Robustness& stats)
204{
205 ++stats.rays;
206 stats.crossings += static_cast<long long>(crossings.size());
207 if (!crossings.empty()) {
208 ++stats.raysWithCrossings;
209 }
210 if (crossings.size() % 2 != 0) {
211 ++stats.oddCrossingLists;
212 }
213 for (size_t i = 1; i < crossings.size(); ++i) {
214 if (crossings[i].kind == crossings[i - 1].kind) {
215 ++stats.nonAlternating;
216 }
217 if (std::fabs(crossings[i].t - crossings[i - 1].t) <= cfg.matchTolerance) {
218 ++stats.duplicateCrossings;
219 }
220 }
221 // The inside-segment length: the chord integral's contribution from this ray.
222 for (size_t i = 0; i + 1 < crossings.size(); i += 2) {
223 if (crossings[i].kind == +1 && crossings[i + 1].kind == -1) {
224 stats.insideLength += crossings[i + 1].t - crossings[i].t;
225 }
226 }
227 // The independent check. Both stepping modes produce an alternating list *by construction*, so
228 // `nonAlternating` above can never fire on them; asking the shape's own Contains() at the
229 // midpoint of every interval is the only way this instrument can contradict itself.
230 if (shape != nullptr) {
231 std::vector<double> edges;
232 edges.push_back(0.);
233 for (const auto& c : crossings) {
234 edges.push_back(c.t);
235 }
236 edges.push_back(tMax);
237 bool expectInside = false;
238 for (size_t i = 0; i + 1 < edges.size(); ++i) {
239 const double mid = 0.5 * (edges[i] + edges[i + 1]);
240 if (edges[i + 1] - edges[i] > 8. * cfg.matchTolerance) {
241 double p[3];
242 for (int k = 0; k < 3; ++k) {
243 p[k] = origin[k] + mid * dir[k];
244 }
245 const bool actuallyInside = shape->Contains(p);
246 if (actuallyInside != expectInside) {
247 // Classify before counting. A midpoint within the match tolerance of the boundary has no
248 // defined answer on either side, exactly as the sample gate's `nNoVerdict` points do;
249 // counting it as a contradiction would manufacture defects out of near-tangency.
250 // Safety() is only paid for on a mismatch, which is rare.
251 // Safety() must be asked with the state the shape ITSELF reports; asking it with the
252 // state the crossing list expects makes a plain outside point look like a boundary
253 // point (TGeoBBox::Safety(p, in=true) goes negative there) and silently excuses every
254 // real contradiction. That mistake made this counter read 0 on a deliberately
255 // truncated list.
256 if (shape->Safety(p, actuallyInside ? kTRUE : kFALSE) <= cfg.matchTolerance) {
257 ++stats.parityMismatchNearBoundary;
258 } else {
259 ++stats.parityMismatchIntervals;
260 }
261 }
262 }
263 expectInside = !expectInside;
264 }
265 }
266}
267
275 long long rays = 0;
276 long long raysIdentical = 0;
277 long long raysStructural = 0;
278 long long matched = 0;
279 long long displaced = 0;
280 long long missing = 0;
281 long long extra = 0;
282 long long kindMismatch = 0;
283 double worstDeltaT = 0.;
284 Point3D worstOrigin{};
285 Point3D worstDir{};
286 std::string worstReason;
287};
288
289inline void compareLists(const std::vector<Crossing>& candidate, const std::vector<Crossing>& reference,
290 const Point3D& origin, const Point3D& dir, double tolerance, ListComparison& out)
291{
292 ++out.rays;
293 bool sameShape = candidate.size() == reference.size();
294 for (size_t i = 0; sameShape && i < candidate.size(); ++i) {
295 sameShape = candidate[i].kind == reference[i].kind;
296 }
297 if (sameShape) {
298 // Same number of crossings in the same order with the same senses: every difference is a
299 // position, so report the positions and never manufacture a missing/extra pair out of one
300 // displaced crossing.
301 bool identical = true;
302 for (size_t i = 0; i < candidate.size(); ++i) {
303 const double delta = std::fabs(candidate[i].t - reference[i].t);
304 ++out.matched;
305 if (delta > tolerance) {
306 ++out.displaced;
307 identical = false;
308 }
309 if (delta > out.worstDeltaT) {
310 out.worstDeltaT = delta;
311 out.worstOrigin = origin;
312 out.worstDir = dir;
313 out.worstReason = delta > tolerance ? "displaced crossing" : "deltaT";
314 }
315 }
316 out.raysIdentical += identical;
317 return;
318 }
319
320 // Structurally different: walk both lists and attribute each unpaired crossing to the side it
321 // came from. This is the branch that names a LOST wall.
322 ++out.raysStructural;
323 size_t i = 0;
324 size_t j = 0;
325 while (i < candidate.size() && j < reference.size()) {
326 const double delta = candidate[i].t - reference[j].t;
327 if (std::fabs(delta) <= tolerance) {
328 ++out.matched;
329 if (candidate[i].kind != reference[j].kind) {
330 ++out.kindMismatch;
331 }
332 if (std::fabs(delta) > out.worstDeltaT) {
333 out.worstDeltaT = std::fabs(delta);
334 }
335 ++i;
336 ++j;
337 } else if (delta < 0.) {
338 ++out.extra;
339 ++i;
340 } else {
341 ++out.missing;
342 ++j;
343 }
344 }
345 out.extra += static_cast<long long>(candidate.size() - i);
346 out.missing += static_cast<long long>(reference.size() - j);
347 if (out.worstReason.empty() || out.worstReason == "deltaT" ||
348 out.worstReason == "displaced crossing") {
349 out.worstOrigin = origin;
350 out.worstDir = dir;
351 out.worstReason = reference.size() > candidate.size() ? "MISSING crossing" : "EXTRA crossing";
352 }
353}
354
355struct RayDef {
356 Point3D origin{};
357 Point3D dir{};
358 double tMax = 0.;
359 int beam = 0;
360};
361
366struct Beam {
367 Point3D dir{};
368 Point3D u{};
369 Point3D v{};
370 std::string label;
371};
372
373struct Raster {
374 int n = 0;
375 std::vector<Beam> beams;
376 std::vector<double> cellArea;
381 std::vector<double> windowExcess;
382 std::vector<RayDef> rays;
383 double transverseMargin = 0.;
384 Point3D windowMin{};
385 Point3D windowMax{};
386};
387
388inline double dot3(const Point3D& a, const Point3D& b)
389{
390 return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
391}
392
393inline Point3D normalize3(const Point3D& a)
394{
395 const double norm = std::sqrt(dot3(a, a));
396 return {a[0] / norm, a[1] / norm, a[2] / norm};
397}
398
399inline Point3D cross3(const Point3D& a, const Point3D& b)
400{
401 return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]};
402}
403
419inline std::vector<Beam> buildFanBeams(int count)
420{
421 std::vector<Beam> beams;
422 const double golden = 3.14159265358979323846 * (3. - std::sqrt(5.));
423 for (int i = 0; i < count; ++i) {
424 // Only the upper hemisphere is needed: a beam and its reverse sample the same lines.
425 const double z = (count == 1) ? 1. : 1. - static_cast<double>(i) / static_cast<double>(count);
426 const double radius = std::sqrt(std::max(0., 1. - z * z));
427 const double theta = golden * i;
428 Beam beam;
429 beam.dir = normalize3({radius * std::cos(theta), radius * std::sin(theta), z});
430 // A transverse frame: Gram-Schmidt off whichever axis the beam is least aligned with.
431 int least = 0;
432 for (int k = 1; k < 3; ++k) {
433 if (std::fabs(beam.dir[k]) < std::fabs(beam.dir[least])) {
434 least = k;
435 }
436 }
437 Point3D seed{};
438 seed[least] = 1.;
439 const double projection = dot3(seed, beam.dir);
440 beam.u = normalize3({seed[0] - projection * beam.dir[0], seed[1] - projection * beam.dir[1],
441 seed[2] - projection * beam.dir[2]});
442 beam.v = cross3(beam.dir, beam.u);
443 beam.label = "f" + std::to_string(i);
444 beams.push_back(std::move(beam));
445 }
446 return beams;
447}
448
449inline std::vector<Beam> buildBeams(const std::string& axesSpec, double tiltDegrees)
450{
451 std::vector<Beam> beams;
452 const double t = std::tan(tiltDegrees * 3.14159265358979323846 / 180.);
453 for (const char c : axesSpec) {
454 int axis = -1;
455 if (c == 'x' || c == 'X') {
456 axis = 0;
457 } else if (c == 'y' || c == 'Y') {
458 axis = 1;
459 } else if (c == 'z' || c == 'Z') {
460 axis = 2;
461 } else {
462 continue;
463 }
464 const int iu = (axis + 1) % 3;
465 const int iv = (axis + 2) % 3;
466 Point3D w{};
467 Point3D u{};
468 Point3D v{};
469 w[axis] = 1.;
470 u[iu] = 1.;
471 v[iv] = 1.;
472 Beam beam;
473 if (t == 0.) {
474 beam.dir = w;
475 beam.u = u;
476 beam.v = v;
477 beam.label = std::string(1, "xyz"[axis]);
478 } else {
479 Point3D dir{w[0] + t * u[0] + 0.618 * t * v[0], w[1] + t * u[1] + 0.618 * t * v[1],
480 w[2] + t * u[2] + 0.618 * t * v[2]};
481 beam.dir = normalize3(dir);
482 // Gram-Schmidt the transverse frame off the original in-plane axis.
483 Point3D uu{u[0] - dot3(u, beam.dir) * beam.dir[0], u[1] - dot3(u, beam.dir) * beam.dir[1],
484 u[2] - dot3(u, beam.dir) * beam.dir[2]};
485 beam.u = normalize3(uu);
486 beam.v = cross3(beam.dir, beam.u);
487 beam.label = std::string(1, "xyz"[axis]) + "+t";
488 }
489 beams.push_back(std::move(beam));
490 }
491 return beams;
492}
493
503inline Raster buildRaster(const Point3D& bboxMin, const Point3D& bboxMax, int n,
504 const std::vector<Beam>& beams, double transverseMargin)
505{
506 Raster raster;
507 raster.n = n;
508 raster.beams = beams;
509 raster.transverseMargin = transverseMargin;
510 for (int k = 0; k < 3; ++k) {
511 raster.windowMin[k] = bboxMin[k] - transverseMargin;
512 raster.windowMax[k] = bboxMax[k] + transverseMargin;
513 }
514 for (const auto& beam : beams) {
515 // Project the eight bounding-box corners into the beam frame; the window is their extent.
516 double lo[3] = {1.e300, 1.e300, 1.e300};
517 double hi[3] = {-1.e300, -1.e300, -1.e300};
518 for (int corner = 0; corner < 8; ++corner) {
519 const Point3D p{(corner & 1) ? bboxMax[0] : bboxMin[0], (corner & 2) ? bboxMax[1] : bboxMin[1],
520 (corner & 4) ? bboxMax[2] : bboxMin[2]};
521 const double coordinate[3] = {dot3(p, beam.u), dot3(p, beam.v), dot3(p, beam.dir)};
522 for (int k = 0; k < 3; ++k) {
523 lo[k] = std::min(lo[k], coordinate[k]);
524 hi[k] = std::max(hi[k], coordinate[k]);
525 }
526 }
527 const double uLo = lo[0] - transverseMargin;
528 const double vLo = lo[1] - transverseMargin;
529 const double du = (hi[0] - lo[0] + 2. * transverseMargin) / n;
530 const double dv = (hi[1] - lo[1] + 2. * transverseMargin) / n;
531 raster.cellArea.push_back(du * dv);
532 const double bboxArea = (hi[0] - lo[0]) * (hi[1] - lo[1]);
533 raster.windowExcess.push_back(bboxArea > 0. ? (du * dv * n * n) / bboxArea - 1. : 0.);
534 const double extent = hi[2] - lo[2];
535 const double lead = 0.05 * extent + 1.e-3;
536 const double wStart = lo[2] - lead;
537 const int index = static_cast<int>(raster.cellArea.size()) - 1;
538 for (int i = 0; i < n; ++i) {
539 for (int j = 0; j < n; ++j) {
540 const double uu = uLo + (i + 0.5) * du;
541 const double vv = vLo + (j + 0.5) * dv;
542 RayDef ray;
543 ray.beam = index;
544 for (int k = 0; k < 3; ++k) {
545 ray.origin[k] = uu * beam.u[k] + vv * beam.v[k] + wStart * beam.dir[k];
546 ray.dir[k] = beam.dir[k];
547 }
548 ray.tMax = extent + 2. * lead;
549 raster.rays.push_back(ray);
550 }
551 }
552 }
553 return raster;
554}
555
558inline double chordVolume(const Raster& raster, const std::vector<double>& insideLengthPerBeam)
559{
560 double sum = 0.;
561 size_t used = 0;
562 for (size_t i = 0; i < raster.beams.size() && i < insideLengthPerBeam.size(); ++i) {
563 sum += insideLengthPerBeam[i] * raster.cellArea[i];
564 ++used;
565 }
566 return used > 0 ? sum / static_cast<double>(used) : 0.;
567}
568
569} // namespace xray
570} // namespace cad
571} // namespace o2
572
573#endif
header::DataOrigin origin
int32_t i
Validation and timing harness for TGeoShape navigation, typed on plain TGeoShape*.
std::vector< SidecarEdge > edges
uint32_t j
Definition RawData.h:0
uint32_t c
Definition RawData.h:2
float sum(float s, o2::dcs::DataPointValue v)
Definition dcs-ccdb.cxx:39
GLdouble n
Definition glcorearb.h:1982
GLint GLsizei count
Definition glcorearb.h:399
const GLdouble * v
Definition glcorearb.h:832
GLuint index
Definition glcorearb.h:781
GLboolean GLboolean GLboolean b
Definition glcorearb.h:1233
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
GLubyte GLubyte GLubyte GLubyte w
Definition glcorearb.h:852
GLdouble GLdouble GLdouble z
Definition glcorearb.h:843
std::array< double, 3 > Point3D
std::vector< Crossing > stepWithShapeApi(const TGeoShape *shape, const Point3D &origin, const Point3D &dir, double tMax, const StepConfig &cfg, Robustness &stats)
Mode (a): the same loop driven by the ordinary TGeoShape virtuals.
void compareLists(const std::vector< Crossing > &candidate, const std::vector< Crossing > &reference, const Point3D &origin, const Point3D &dir, double tolerance, ListComparison &out)
double chordVolume(const Raster &raster, const std::vector< double > &insideLengthPerBeam)
std::vector< Crossing > stepCrossingsWithKernels(const Point3D &origin, const Point3D &dir, double tMax, const StepConfig &cfg, Robustness &stats, ContainsFn contains, DistOutFn distFromOutside, DistInFn distFromInside)
void auditCrossingList(const std::vector< Crossing > &crossings, const TGeoShape *shape, const Point3D &origin, const Point3D &dir, double tMax, const StepConfig &cfg, Robustness &stats)
double dot3(const Point3D &a, const Point3D &b)
Point3D cross3(const Point3D &a, const Point3D &b)
std::vector< Beam > buildFanBeams(int count)
Raster buildRaster(const Point3D &bboxMin, const Point3D &bboxMax, int n, const std::vector< Beam > &beams, double transverseMargin)
std::vector< Beam > buildBeams(const std::string &axesSpec, double tiltDegrees)
Point3D normalize3(const Point3D &a)
a couple of static helper functions to create timestamp values for CCDB queries or override obsolete ...
std::string to_string(gsl::span< T, Size > span)
Definition common.h:52
long long displaced
same position in both lists, more than tolerance apart
long long raysStructural
the lists have different lengths or senses
long long raysIdentical
the whole ordered list matched, position and sense
double worstDeltaT
max |dt| over positionally matched crossings, cm
long long missing
in the reference, absent from the candidate
long long extra
in the candidate, absent from the reference
Point3D windowMin
the part bbox plus the margin, in world coordinates (the world box)
std::vector< RayDef > rays
std::vector< double > windowExcess
std::vector< Beam > beams
std::vector< double > cellArea
int beam
index into Raster::beams
long long zeroLengthSteps
a step at or below zeroStep (default 1e-9 cm)
long long unterminated
the ray ended INSIDE the solid: entered and never left
long long iterationCapHits
the loop hit maxIter without leaving the window
long long unstickPushes
a stalled step that had to be nudged to continue
long long originInside
a raster ray whose origin was not outside the solid
double insideLength
summed inside-segment length, cm (the chord integral)
long long nonAlternating
two consecutive crossings of the same kind
long long nonAdvancingSteps
the accumulated distance did not increase
double zeroStep
A step at or below this is a stall, not progress.