forked from AliceO2Group/AliceO2
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFT3Module.cxx
More file actions
1438 lines (1274 loc) · 66.6 KB
/
Copy pathFT3Module.cxx
File metadata and controls
1438 lines (1274 loc) · 66.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2019-2020 CERN and copyright holders of ALICE O2.
// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
// All rights not expressly granted are reserved.
//
// This software is distributed under the terms of the GNU General Public
// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
//
// In applying this license CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.
/// \file FT3Module.cxx
/// \brief Implementation of the FT3Module class
#include "FT3Simulation/FT3Module.h"
#include "FT3Base/FT3BaseParam.h"
#include <TGeoManager.h>
#include <TGeoMaterial.h>
#include <TGeoMedium.h>
#include <TGeoBBox.h>
#include <TGeoXtru.h>
#include <TGeoMatrix.h>
#include <TGeoCompositeShape.h>
#include <Framework/Logger.h>
#include <cmath>
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#include <utility>
TGeoMaterial* FT3Module::siliconMat = nullptr;
TGeoMedium* FT3Module::siliconMed = nullptr;
TGeoMaterial* FT3Module::copperMat = nullptr;
TGeoMedium* FT3Module::copperMed = nullptr;
TGeoMaterial* FT3Module::kaptonMat = nullptr;
TGeoMedium* FT3Module::kaptonMed = nullptr;
TGeoMaterial* FT3Module::epoxyMat = nullptr;
TGeoMedium* FT3Module::epoxyMed = nullptr;
TGeoMaterial* FT3Module::AluminumMat = nullptr;
TGeoMedium* FT3Module::AluminumMed = nullptr;
TGeoMaterial* FT3Module::carbonFiberMat = nullptr;
TGeoMedium* FT3Module::carbonFiberMed = nullptr;
void FT3Module::initialize_materials()
{
LOG(debug) << "FT3Module: initialize_materials";
if (siliconMat) {
return;
}
TGeoManager* geoManager = gGeoManager;
auto* itsH = new TGeoElement("FT3_H", "Hydrogen", 1, 1.00794);
auto* itsC = new TGeoElement("FT3_C", "Carbon", 6, 12.0107);
auto* itsO = new TGeoElement("FT3_O", "Oxygen", 8, 15.994);
siliconMat = new TGeoMaterial("FT3_Silicon", 28.0855, 14, 2.33);
siliconMed = new TGeoMedium("FT3_Silicon", 1, siliconMat);
copperMat = new TGeoMaterial("FT3_Copper", 63.546, 29, 8.96);
copperMed = new TGeoMedium("FT3_Copper", 2, copperMat);
kaptonMat = new TGeoMaterial("FT3_Kapton", 13.84, 6.88, 1.346);
kaptonMed = new TGeoMedium("FT3_Kapton", 3, kaptonMat);
// TODO: Check with Rene the exact type of carbon fiber
carbonFiberMat = new TGeoMaterial("FT3_Carbon", 12.0107, 6, 1.8);
carbonFiberMed = new TGeoMedium("FT3_Carbon", 6, carbonFiberMat);
// Epoxy: C18 H19 O3
auto* itsEpoxy = new TGeoMixture("FT3_Epoxy", 3);
itsEpoxy->AddElement(itsC, 18);
itsEpoxy->AddElement(itsH, 19);
itsEpoxy->AddElement(itsO, 3);
itsEpoxy->SetDensity(2.186);
epoxyMed = new TGeoMedium("FT3_Epoxy", 4, itsEpoxy);
epoxyMat = epoxyMed->GetMaterial();
AluminumMat = new TGeoMaterial("Aluminum", 26.98, 13, 2.7);
AluminumMed = new TGeoMedium("Aluminum", 5, AluminumMat);
LOG(debug) << "FT3Module: done initialize_materials";
}
double calculate_y_circle(double x, double radius)
{
return (x * x < radius * radius) ? std::sqrt(radius * radius - x * x) : 0;
}
std::pair<double, double> calculate_y_range(
double x_left, double x_right, double Rin, double Rout)
{
double max_y_abs;
double min_y_abs;
/*
* Have 5 cases:
* (1) Stave wholly on the left of inner radius
* (2) Stave wholly on the left, but within inner radius
* (3) Stave crosses the middle x=0
* (4) Stave wholly on the right, but within inner radius
* (5) Stave wholly on the right of inner radius
*/
if (x_right < -Rin) {
// Stave is completely on the left of inner radius
min_y_abs = 0;
max_y_abs = calculate_y_circle(x_left, Rout);
} else if (x_left < -Constants::sensor2x1_width) {
// Stave is completely on the left, but within inner radius
min_y_abs = calculate_y_circle(x_right, Rin);
max_y_abs = calculate_y_circle(x_left, Rout);
} else if (x_left < 0) {
// Stave crosses the middle x=0
min_y_abs = Rin;
// x_right should be > 0, but might have FLP issues, so do abs nonetheless
max_y_abs = calculate_y_circle(std::max(std::abs(x_left), std::abs(x_right)), Rout);
} else if (x_left < Rin) {
// Stave is completely on the right, but within inner radius
min_y_abs = calculate_y_circle(x_left, Rin);
max_y_abs = calculate_y_circle(x_right, Rout);
} else {
// Stave is completely on the right of inner radius
min_y_abs = 0.;
max_y_abs = calculate_y_circle(x_right, Rout);
}
return {min_y_abs, max_y_abs};
}
/*
* This function is a helper function which will pad out the stave with sensors
* until there is no more space available.
*
* Arguments:
* y_positions: a pair of vectors, where each vector contains pairs of
* y position and stack height for the positive and negative y positions respectively.
* This argument will be appended with the new sensor positions and stack heights.
* Rout: the outer radius of the layer
* Rin: the inner radius of the layer
* x_left: the x position of the left edge of the sensor to be placed
* kSensorStack: the number of sensors to be stacked on top of each other
* y_ranges: the y positions to start and end placing sensors,
* for positive and negative y respectively
* absAllowedYRange: the absolute y range allowed for placing sensors,
* used to cut placement if they go past allowed tolerances
*/
void FT3Module::fill_stave(PosNegPositionTypes& y_positions, double Rin, double Rout,
double x_left, unsigned kSensorStack, PositionRangeType y_ranges,
std::pair<double, double>& absAllowedYRange)
{
// start with upper half of the stave, then mirror to the bottom half
// add the height of kSensorStack sensors + the gaps in between them
double sensorStackHeight = Constants::getStackHeight(kSensorStack);
double sensorAbsStackYShift = sensorStackHeight + Constants::stackGap;
// in case a big tolerance is given, cut on the given range instead
double max_sensor_y_abs = std::min(absAllowedYRange.second, y_ranges.first.second);
double y_top; // top half of the xy grid, y>0
// either start at given value (adjusted for tolerance), or at last placed sensors
if (!y_positions.first.empty()) { // sensors already placed
double previousStackHeight = Constants::getStackHeight(y_positions.first.back().second);
y_top = y_positions.first.back().first + previousStackHeight + Constants::stackGap;
} else if (absAllowedYRange.first > 0) {
// there is a minimum inner value --> start at the max of the two
y_top = std::max(absAllowedYRange.first, y_ranges.first.first);
} else {
// No inner minimum value, start at given value
y_top = y_ranges.first.first;
}
// fill positive y sensor positions
while ((y_top + sensorStackHeight) <= max_sensor_y_abs) {
y_positions.first.emplace_back(y_top, kSensorStack);
y_top += sensorAbsStackYShift;
}
// now we do the same for the negative y positions
// they do not have to be exactly mirrored, hence done separately
double y_bottom;
if (!y_positions.second.empty()) {
// subtract instead to move further down
double previousStackHeight = Constants::getStackHeight(y_positions.second.back().second);
y_bottom = y_positions.second.back().first - previousStackHeight - Constants::stackGap;
} else if (absAllowedYRange.first > 0) {
// there is a minimum inner value --> start at the min of the two
y_bottom = std::min(-absAllowedYRange.first, y_ranges.second.first);
} else {
// No inner minimum value, start at given value
y_bottom = y_ranges.second.first;
}
// fill in the sensors on negative y
while ((y_bottom - sensorStackHeight) >= -max_sensor_y_abs) {
y_positions.second.emplace_back(y_bottom, kSensorStack);
y_bottom -= sensorAbsStackYShift;
}
}
/*
* Create the vertices of the triangles that make up the stave cross section
*
* Each array of 3 corresponds to x or z values of the 3 triangle vertices,
* and the outer array corresponds to which triangle:
*
* [x_outer, z_outer, x_inner, z_inner], each of which has three values
*/
std::array<std::array<double, 3>, 4> buildStaveTriangle(int direction)
{
// Set some constants for readability
double d = Constants::effectiveCarbonThickness_Stave;
double H = Constants::staveTriangleHeight;
/*
* Inner and outer vertices of the stave cross section triangle
* all vertices are at y_mid, we simply extend the triangle into y dir.
* We work in the local coordinate system of the stave, but still
* call the coordinates x and z for readability.
*
* 1. Get all local coordinates of the two triangle vertices
* 2. Extrude a volume from the subtracted triangle cross section area
* 3. Rotate the volume around the x-axis since it is by default in xy,
* and extruded in z. Rotate by -90 for xz -> xy, otherwise xz -> x(-y)
* 4. Translate the volume to the given position (arguments)
*
*/
std::array<double, 3> xv_inner, xv_outer, zv_inner, zv_outer;
// calculate the coordinates of the triangle vertices
// Top/bottom vertex (apex)
xv_outer[0] = 0;
zv_outer[0] = (direction == 1) ? -H
: H;
;
// right
xv_outer[1] = Constants::sensor2x1_width / 2 + Constants::staveSensorGap;
zv_outer[1] = 0;
// left
xv_outer[2] = -xv_outer[1];
zv_outer[2] = 0;
// now get inner vertices, shifted inwards by effective carbon thickness
xv_inner[0] = xv_outer[0];
double z_shift_inner = d / Constants::sinTheta;
zv_inner[0] = (direction == 1) ? zv_outer[0] + z_shift_inner
: zv_outer[0] - z_shift_inner;
// face vertices, first right
zv_inner[1] = (direction == 1) ? zv_outer[1] - d
: zv_outer[1] + d;
double x_shift_abs = d / TMath::Tan(Constants::alpha / 2);
xv_inner[1] = xv_outer[1] - x_shift_abs;
// left
zv_inner[2] = zv_inner[1];
xv_inner[2] = -xv_inner[1];
return {xv_outer, zv_outer, xv_inner, zv_inner};
}
/*
* This function creates a carbon fibre volume for the stave,
* onto which the sensor and its support will be glued.
*/
void FT3Module::addStaveVolume(
TGeoVolume* motherVolume, std::string volumeName, int direction,
unsigned* volume_count, double staveLength,
std::array<std::array<double, 3>, 4> staveTriangles,
std::pair<double, double>& absAllowedYRange,
double x_mid, double y_mid, double z_stave_shift_forward)
{
// The allowed y range is assumed to be non-negative.
if (absAllowedYRange.first < 0 || absAllowedYRange.second < 0 ||
absAllowedYRange.first >= absAllowedYRange.second) {
LOG(error) << "Invalid allowed y range in addStaveVolume(): ("
<< absAllowedYRange.first << ", " << absAllowedYRange.second
<< "). Both values must be non-negative and the first "
<< "value must be less than the second value.";
return;
}
// Set the lower and upper y values of the stave:
double y_lower = y_mid - staveLength / 2;
double y_upper = y_mid + staveLength / 2;
bool splitStave = false;
if (y_lower > 0) { // This stave is fully above x-axis
y_lower = std::max(y_lower, absAllowedYRange.first);
y_upper = std::min(y_upper, absAllowedYRange.second);
} else if (y_upper < 0) { // stave entirely below x-axis
y_lower = std::max(y_lower, -absAllowedYRange.second);
y_upper = std::min(y_upper, -absAllowedYRange.first);
} else { // Full range stave that goes across x-axis
// Here we might have to cut the stave up into two pieces
if (absAllowedYRange.first > 0) {
// There is a minimum inner value --> Split stave
splitStave = true;
y_lower = absAllowedYRange.first;
} else {
// regular stave, use full length, but don't forget outer cut
y_lower = std::max(y_lower, -absAllowedYRange.second);
}
y_upper = std::min(y_upper, absAllowedYRange.second);
}
double staveLengthToUse = y_upper - y_lower;
/*
* create the extruded volumes from z=0 (later y=0 after rotation) to stave length
* and not from midpoint - staveLength/2 to midpoint + staveLength/2, translate later
*
* Note also that we first need to check if the length is allowed given the inner
* and outer radius of the layer.
*/
TGeoXtru* staveFull = new TGeoXtru(2);
staveFull->SetName((volumeName + "_Xtru_outer").c_str());
staveFull->DefinePolygon(3, staveTriangles[0].data(), staveTriangles[1].data());
staveFull->DefineSection(0, 0);
staveFull->DefineSection(1, staveLengthToUse);
TGeoXtru* staveInner = new TGeoXtru(2);
staveInner->SetName((volumeName + "_Xtru_inner").c_str());
staveInner->DefinePolygon(3, staveTriangles[2].data(), staveTriangles[3].data());
staveInner->DefineSection(0, 0);
staveInner->DefineSection(1, staveLengthToUse);
TGeoCompositeShape* staveShape = new TGeoCompositeShape(
(volumeName + "_shape").c_str(),
Form("%s - %s", staveFull->GetName(), staveInner->GetName()));
TGeoVolume* staveVolume = new TGeoVolume(
(volumeName).c_str(),
staveShape,
carbonFiberMed);
staveVolume->SetLineColor(Constants::carbonFiberColor);
staveVolume->SetFillColorAlpha(Constants::carbonFiberColor, 0.4);
TGeoRotation* rot = new TGeoRotation();
rot->RotateX(-90); // lift from xy plane into xz plane
/*
* After rotations the face of the stave lies in the xy-plane,
* facing downwards for direction == 1 and upwards for direction == 0.
* We still need to shift it in z to get the right staggered layout.
* This means moving the staves that must be shifted in the opposite
* direction they are facing: up for direction 1, and down for direction 0.
*
* Unlike a regular node placement, we have to put the stave at its
* starting point in y, not the midpoint. Hence, if we have the mirror,
* the starting point is the upper y value, since that is the bottom
* of the mirrored stave -- by the outer radius
*/
double z_shift = (direction == 1) ? z_stave_shift_forward : -z_stave_shift_forward;
TGeoCombiTrans* combiTrans =
new TGeoCombiTrans(x_mid, y_lower, z_shift, rot);
motherVolume->AddNode(staveVolume,
*volume_count,
combiTrans);
(*volume_count)++;
// if the stave needs to be split, reuse the same volume on opposite side
if (splitStave) {
TGeoCombiTrans* combiTransSplit =
new TGeoCombiTrans(x_mid, -y_upper, z_shift, rot);
motherVolume->AddNode(staveVolume,
*volume_count,
combiTransSplit);
(*volume_count)++;
}
}
/*
* Generic helper function that adds a box at the given position with
* the given dimensions to the given mother volume, with the given color and name.
*/
void FT3Module::addDetectorVolume(
TGeoVolume* motherVolume, std::string volumeName, int color,
unsigned volume_count, double x_mid, double y_mid, double z_mid,
double x_half_length, double y_half_length, double z_half_length)
{
TGeoManager* geoManager = gGeoManager;
TGeoVolume* volume = geoManager->MakeBox(volumeName.c_str(), siliconMed, x_half_length,
y_half_length, z_half_length);
volume->SetLineColor(color);
volume->SetFillColorAlpha(color, 0.4);
motherVolume->AddNode(
volume,
volume_count,
new TGeoTranslation( // midpoint of box to add
x_mid,
y_mid,
z_mid) // TGeoTranslation
); // addNode
}
/*
* This function adds a glue volume between two element layers,
* immediately for a whole 2x1 layout, under both the active and inactive region.
*/
void FT3Module::add2x1GlueVolume(
TGeoVolume* motherVolume, int layerNumber, int direction, unsigned stave_idx,
unsigned volume_count, double x_mid, double y_mid, double z_mid,
std::string element_glued_to)
{
std::string glue_name = "FT3glue_" + element_glued_to + "_" + std::to_string(direction) + "_" + std::to_string(layerNumber) + "_" + std::to_string(stave_idx) + "_" + std::to_string(volume_count);
addDetectorVolume(
motherVolume, glue_name, Constants::glueColor, volume_count,
x_mid, y_mid, z_mid,
Constants::sensor2x1_width / 2, Constants::sensor2x1_height / 2, Constants::epoxyThickness / 2);
}
/*
* This function adds a copper volume onto which the silicon sensor is glued.
* As with the glue, this is a whole 2x1 layout volume.
*/
void FT3Module::add2x1CopperVolume(
TGeoVolume* motherVolume, int layerNumber, int direction, unsigned stave_idx,
unsigned volume_count, double x_mid, double y_mid, double z_mid)
{
std::string copper_name = "FT3Copper_" + std::to_string(direction) + "_" + std::to_string(layerNumber) + "_" + std::to_string(stave_idx) + "_" + std::to_string(volume_count);
addDetectorVolume(
motherVolume, copper_name, Constants::CuColor, volume_count,
x_mid, y_mid, z_mid,
Constants::sensor2x1_width / 2, Constants::sensor2x1_height / 2, Constants::copperThickness / 2);
}
/*
* This function adds a kapton volume behind the copper, which represents the ???
* As with copper and glue, this is a whole 2x1 layout volume.
*/
void FT3Module::add2x1KaptonVolume(
TGeoVolume* motherVolume, int layerNumber, int direction, unsigned stave_idx,
unsigned volume_count, double x_mid, double y_mid, double z_mid)
{
std::string kapton_name = "FT3Kapton_" + std::to_string(direction) + "_" + std::to_string(layerNumber) + "_" + std::to_string(stave_idx) + "_" + std::to_string(volume_count);
addDetectorVolume(
motherVolume, kapton_name, Constants::kaptonColor, volume_count,
x_mid, y_mid, z_mid,
Constants::sensor2x1_width / 2, Constants::sensor2x1_height / 2, Constants::kaptonThickness / 2);
}
/*
* This function adds a single sensor (currently 2.5x3.2mm) to the given mother volume
* at the given (x,y,z) position of the module.
*
* Because the sensor has an inactive region of 0.2mm on one side, we also add a
* separate volume for the inactive region, which will be either on the left or
* or right dependent on the if the sensor is on the left or right in a 2x1 layout.
* See FT3Module.h for more details on the layout.
*
* Arguments:
* motherVolume: the volume to which the sensor volume will be added
* layerNumber: the layer number of the sensor, used for naming
* direction: the direction of the sensor (forward or backward eta), used for naming
* x_mid: the x position of the center of the sensor volume
* y_mid: the y position of the center of the sensor volume
* z_mid: the z position of the center of the sensor volume
* isLeft: whether the sensor is on the left or right in the 2x1 layout
*/
void FT3Module::addSingleSensorVolume(
TGeoVolume* motherVolume, int layerNumber, int direction, unsigned stave_idx,
unsigned volume_count, double active_x_mid, double y_mid, double z_mid,
bool isLeft)
{
TGeoVolume* sensor;
TGeoManager* geoManager = gGeoManager;
// ACTIVE AREA
std::string sensor_name = "FT3Sensor_Active_" + std::to_string(direction) + "_" + std::to_string(layerNumber) + "_" + std::to_string(stave_idx) + "_" + std::to_string(volume_count);
sensor = geoManager->MakeBox(sensor_name.c_str(), siliconMed, Constants::active_width / 2,
Constants::single_sensor_height / 2, Constants::siliconThickness / 2);
sensor->SetLineColor(Constants::SiColor);
sensor->SetFillColorAlpha(Constants::SiColor, 0.4);
motherVolume->AddNode(
sensor,
volume_count,
new TGeoTranslation( // midpoint of box to add
active_x_mid,
y_mid,
z_mid) // TGeoTranslation
); // addNode
// INACTIVE STRIP ON LEFT OR RIGHT
double inactive_x_mid = isLeft ? (active_x_mid - Constants::active_width / 2 - Constants::inactive_width / 2)
: (active_x_mid + Constants::active_width / 2 + Constants::inactive_width / 2);
std::string sensor_inactive_name = "FT3Sensor_Inactive_" + std::to_string(direction) + "_" + std::to_string(layerNumber) + "_" + std::to_string(stave_idx) + "_" + std::to_string(volume_count);
sensor = geoManager->MakeBox(sensor_inactive_name.c_str(), siliconMed, Constants::inactive_width / 2,
Constants::single_sensor_height / 2, Constants::siliconThickness / 2);
sensor->SetLineColor(Constants::SiInactiveColor);
sensor->SetFillColorAlpha(Constants::SiInactiveColor, 0.4);
motherVolume->AddNode(
sensor,
volume_count,
new TGeoTranslation( // midpoint of box to add
inactive_x_mid,
y_mid,
z_mid) // TGeoTranslation
); // addNode
(volume_count)++;
}
void FT3Module::create_layout_staveGeo(double mZ, int layerNumber, int direction,
double Rin, double Rout, double z_offset_local,
const Constants::StaveConfig& staveConfig,
TGeoVolume* motherVolume)
{
LOG(debug) << "FT3Module: create_layout_staveGeo - Direction "
<< direction << ", Layer " << layerNumber;
FT3Module::initialize_materials();
auto& ft3Params = o2::ft3::FT3BaseParam::Instance();
// First let's define some constants used throughout
/*
* we build the volume from the outside in, starting with the silicon,
* then glue & materials towards the stave. Depending on direction,
* the distance from the center will be mirrored.
*
* | SILICON SENSOR | GLUE | COPPER | KAPTON | GLUE | CARBON STAVE |
* ----------------------------------------------------------------> z
*
* Naturally, this will be mirrored for layers in the backwards direction,
* such that the face of the sensors always face the interaction region.
*
* Currently, we stipulate that the default stave face is at local z=0,
* that is then shifted by the half air thickness encapsulating the layer
* to avoid overlaps with the air and services. All offsets are
* calculated for backward direction (since that is a positive shift),
* and then flipped for forward. At that point, the innermost/frontmost
* stave face is at the edge of the air volume, so we shift it back a little
* to make space for the sensor materials and a slight margin.
*/
double totalSensorMaterialThickness =
Constants::epoxyThickness + Constants::kaptonThickness + Constants::copperThickness +
Constants::epoxyThickness + Constants::siliconThickness;
double z_offset_to_carbon_face = z_offset_local - totalSensorMaterialThickness - 0.1;
double z_offset_to_glue_Ka =
z_offset_to_carbon_face + Constants::epoxyThickness / 2;
double z_offset_to_kapton =
z_offset_to_carbon_face + Constants::epoxyThickness +
Constants::kaptonThickness / 2;
double z_offset_to_copper =
z_offset_to_carbon_face + Constants::epoxyThickness +
Constants::kaptonThickness + Constants::copperThickness / 2;
double z_offset_to_glue_Si =
z_offset_to_carbon_face + Constants::epoxyThickness + Constants::kaptonThickness +
Constants::copperThickness + Constants::epoxyThickness / 2;
double z_offset_to_silicon =
z_offset_to_carbon_face + Constants::epoxyThickness +
Constants::kaptonThickness + Constants::copperThickness +
Constants::epoxyThickness + Constants::siliconThickness / 2;
// initialise all y_positions, vector over all staves/columns
std::vector<PosNegPositionTypes> y_positionsPosNeg;
// stave triangle cross sections are the same for every stave (direction based)
std::array<std::array<double, 3>, 4> staveTriangles = buildStaveTriangle(direction);
// declare vector with number of 2xn sensor stacks (modules) -- only used for logging
// each entry is a vector, where each entry is the number of modules of that stack height
std::vector<std::vector<unsigned>> nSensorStackCountPerStave(
staveConfig.x_midpoints.size(),
std::vector<unsigned>(Constants::kSensorsPerStack.size(), 0));
std::vector<unsigned> nSensorStackTotal(Constants::kSensorsPerStack.size(), 0);
unsigned staveVolumeCount = 0;
for (unsigned i_stave = 0; i_stave < staveConfig.x_midpoints.size(); i_stave++) {
y_positionsPosNeg.emplace_back(PosNegPositionTypes{PositionTypes{}, PositionTypes{}});
const int staveID = Constants::staveIdxToID(i_stave, staveConfig.x_midpoints.size());
double y_midpoint = 0.;
bool mirrorStaveAroundX = false;
// default positive and negative starting points has a gap around x-axis for symmetry
double stave_half_length = staveConfig.y_lengths[i_stave] / 2;
PositionRangeType y_ranges;
if (ft3Params.placeSensorStackInMiddleOfStave) {
/*
* We want a sensor stack to cross over the x-axis for coverage at y=0
* N.B. not necessarily exactly mirrored, only if stack gap is the same
* as the gap between sensors in a stack. Since we start filling with the
* first value in the kSensorsPerStack vector, we offset the first position
* by half of that.
*
* NOTE: TODO: in case the stave is too short to fit one full stack over the middle,
* then we will not be able to place anything since the bottom right/left point of
* the module will already be outside of acceptable bounds -- killing further placement.
*/
double stackHeight = Constants::getStackHeight(Constants::kSensorsPerStack[0]);
y_ranges = {{-stackHeight / 2, stave_half_length},
{-stackHeight / 2 - Constants::stackGap, -stave_half_length}};
} else {
/*
* Otherwise have a gap around y=0, so sensors are not placed there.
* This means the stave is perfectly mirrored around the x-axis.
*/
y_ranges = {{Constants::stackGap / 2, stave_half_length},
{-Constants::stackGap / 2, -stave_half_length}};
}
auto y_midpoint_it = staveConfig.staveID_to_y_midpoint.find(staveID);
if (y_midpoint_it != staveConfig.staveID_to_y_midpoint.end()) {
// there is a defined midpoint for this stave, use this for starting points
y_midpoint = y_midpoint_it->second.first; // avoid double map lookup
mirrorStaveAroundX = y_midpoint_it->second.second;
y_ranges.first = {y_midpoint - stave_half_length, y_midpoint + stave_half_length};
y_ranges.second = {-y_midpoint + stave_half_length, -y_midpoint - stave_half_length};
}
// Define tolerances for cutting staves and placing sensors
double tolerance_inner, tolerance_outer;
if (staveConfig.isML) {
tolerance_inner = ft3Params.staveTolMLInner;
tolerance_outer = ft3Params.staveTolMLOuter;
} else {
tolerance_inner = ft3Params.staveTolOTInner;
tolerance_outer = ft3Params.staveTolOTOuter;
}
// cut staves on nominal inner radius if specified
if (tolerance_inner > staveConfig.maxToleranceInner) {
tolerance_inner = staveConfig.maxToleranceInner;
}
if (tolerance_outer > staveConfig.maxToleranceOuter) {
tolerance_outer = staveConfig.maxToleranceOuter;
}
/*
* There are two cases in which we want to mirror the stave around the x-axis,
* which correspond to the stave not going fully from + to - Rout in y.
*
* (1) The inner tolerance is 0 (or negative)
* a) AND either x_left or x_right lies within the inner radius
* (2) The inner tolerance is large enough to allow stave placement as wished
* a) AND the given stave midpoint is above the inner radius
*/
double x_left = staveConfig.x_midpoints[i_stave] - Constants::sensor2x1_width / 2;
double x_right = x_left + Constants::sensor2x1_width;
std::pair<double, double> absAllowedYRange =
calculate_y_range(x_left, x_right, Rin, Rout);
/*
* Shift allowed range by tolerance. Note that both values in the range must
* be non-negative, and if the inner is not, then set it to 0. This just means
* that there is no lower limit. The upper limit must however be larger than 0,
* if it is not, then skip this stave and give a warning.
*/
absAllowedYRange.first -= tolerance_inner;
absAllowedYRange.second += tolerance_outer;
if (absAllowedYRange.first < 0) {
absAllowedYRange.first = 0;
}
if (absAllowedYRange.second <= 0) {
LOG(warning) << "For stave " << i_stave << " in layer " << layerNumber
<< " with direction " << direction << ": no space to place sensors after applying tolerances, skipping stave.";
continue;
}
// Get whether the stave is shifted backward or not before creating
double z_stave_shift_abs = staveConfig.staveOnFront[i_stave] ? 0 : Constants::z_offsetStave(staveConfig.x_midpoint_spacing);
double z_stave_shift_forward = // move staves more inward to fit in layer volume
-z_offset_to_carbon_face + z_stave_shift_abs;
std::string stave_volume_name =
"FT3_Stave_" + std::to_string(direction) + "_" + std::to_string(layerNumber) +
"_" + std::to_string(i_stave);
// Create the stave volumes and fill the y positions where to put sensors on the stave
addStaveVolume(
motherVolume, stave_volume_name, direction, &staveVolumeCount,
staveConfig.y_lengths[i_stave], staveTriangles, absAllowedYRange,
staveConfig.x_midpoints[i_stave], y_midpoint, z_stave_shift_forward);
// Now create the mirrored stave
if (mirrorStaveAroundX) {
addStaveVolume(
motherVolume, stave_volume_name + "_mirrored", direction, &staveVolumeCount,
staveConfig.y_lengths[i_stave], staveTriangles, absAllowedYRange,
staveConfig.x_midpoints[i_stave], -y_midpoint, z_stave_shift_forward);
}
// now add the sensor positions on the stave
for (unsigned i_kSens = 0; i_kSens < Constants::kSensorsPerStack.size(); i_kSens++) {
unsigned nModulesCurr = y_positionsPosNeg.back().first.size() + y_positionsPosNeg.back().second.size();
fill_stave(y_positionsPosNeg.back(), Rin, Rout, x_left,
Constants::kSensorsPerStack[i_kSens], y_ranges,
absAllowedYRange);
unsigned nModulesAdded = y_positionsPosNeg.back().first.size() + y_positionsPosNeg.back().second.size() - nModulesCurr;
nSensorStackCountPerStave[i_stave][i_kSens] = nModulesAdded;
nSensorStackTotal[i_kSens] += nModulesAdded;
}
std::string moduleDebugStr = "Module size counts for layer " + std::to_string(layerNumber) + " in direction " + std::to_string(direction) + ":\n";
for (unsigned i_kSens = 0; i_kSens < Constants::kSensorsPerStack.size(); i_kSens++) {
moduleDebugStr += "\t" + std::to_string(nSensorStackCountPerStave[i_stave][i_kSens]) + " modules with " + std::to_string(Constants::kSensorsPerStack[i_kSens]) + " sensors stacked\n";
}
LOG(debug) << moduleDebugStr;
}
std::string totalModuleInfoStr =
"Total module size counts for layer " + std::to_string(layerNumber) +
" in direction " + std::to_string(direction) + ":\n";
for (unsigned i_kSens = 0; i_kSens < Constants::kSensorsPerStack.size(); i_kSens++) {
totalModuleInfoStr += "\t" + std::to_string(nSensorStackTotal[i_kSens]) + " modules with " + std::to_string(Constants::kSensorsPerStack[i_kSens]) + " sensors stacked\n";
}
LOG(info) << totalModuleInfoStr;
// Create volumes for the sensors and the support materials on top of the stave
for (unsigned i_stave = 0; i_stave < staveConfig.x_midpoints.size(); i_stave++) {
double x_mid = staveConfig.x_midpoints[i_stave];
int staveID = Constants::staveIdxToID(i_stave, staveConfig.x_midpoints.size());
/*
* Declare an offset multiplier for the z offsets, used for distinguishing
* sensors facing either forward or backward.
*
* In the stave layout, all sensors face inward, and isFront
* refers to whether a stave is shifted backwards or not. Thus,
* we decide the offset multiplier only with direction, to
* keep the face facing inwards.
*/
bool isFront;
if (direction == 1) { // direction = 1 is forward
isFront = staveConfig.staveOnFront[i_stave];
} else {
isFront = !(staveConfig.staveOnFront[i_stave]);
}
int z_offset_multiplier = (direction == 1) ? -1 : 1;
// Get whether the stave is shifted for staggering or not
double z_stave_shift = 0;
if (!staveConfig.staveOnFront[i_stave]) {
// in forward direction, shifting backwards means +z shift
z_stave_shift = (direction == 1) ? Constants::z_offsetStave(staveConfig.x_midpoint_spacing)
: -Constants::z_offsetStave(staveConfig.x_midpoint_spacing);
}
unsigned sensor_count = 0; // reset for each stave
for (int y_sign = -1; y_sign < 2; y_sign += 2) {
// place sensors at positive and negative y
const auto& positions = (y_sign == 1) ? y_positionsPosNeg[i_stave].first
: y_positionsPosNeg[i_stave].second;
// define starting midpoint: y = y_start +- distance to middle of sensor
for (unsigned i_y_pos = 0; i_y_pos < positions.size(); i_y_pos++) {
double y_mid = positions[i_y_pos].first + y_sign * Constants::sensor2x1_height / 2;
for (unsigned i_sens = 0; i_sens < positions[i_y_pos].second; i_sens++) {
TGeoVolume* sensor;
// ------------ (1) Silicon sensor ------------
// left single sensor of the 2x1
double z_mid = z_offset_to_silicon * z_offset_multiplier + z_stave_shift;
addSingleSensorVolume(
motherVolume, layerNumber, direction, i_stave, sensor_count,
x_mid - Constants::active_width / 2, y_mid, z_mid, true);
// right single sensor of the 2x1
addSingleSensorVolume(
motherVolume, layerNumber, direction, i_stave, sensor_count,
x_mid + Constants::active_width / 2, y_mid, z_mid, false);
// ------------ (2) Epoxy glue layer between silicon and copper (FPC) ------------
z_mid = z_offset_to_glue_Si * z_offset_multiplier + z_stave_shift;
add2x1GlueVolume(
motherVolume, layerNumber, direction, i_stave, sensor_count,
x_mid, y_mid, z_mid, "SiCu");
// ------------ (3) Copper layer (FPC) ------------
z_mid = z_offset_to_copper * z_offset_multiplier + z_stave_shift;
add2x1CopperVolume(
motherVolume, layerNumber, direction, i_stave, sensor_count,
x_mid, y_mid, z_mid);
// ------------ (4) Kapton layer (FPC) ------------
z_mid = z_offset_to_kapton * z_offset_multiplier + z_stave_shift;
add2x1KaptonVolume(
motherVolume, layerNumber, direction, i_stave, sensor_count,
x_mid, y_mid, z_mid);
// ------------ (5) Epoxy glue layer between stave and Kapton ------------
z_mid = z_offset_to_glue_Ka * z_offset_multiplier + z_stave_shift;
add2x1GlueVolume(
motherVolume, layerNumber, direction, i_stave, sensor_count,
x_mid, y_mid, z_mid, "CarbonKapton");
// increment to next sensor: (height + gap of one sensor)
y_mid += y_sign * (Constants::sensor2x1_height + Constants::sensor2x1_gap);
sensor_count++; // same count for each material in the glued stack of materials
} // sensors in stack
} // for y_sign (writing of positive or negative y positions)
} // i_y_pos
} // i_stave
}
void FT3Module::create_layout(double mZ, int layerNumber, int direction, double Rin, double Rout, double overlap, const std::string& face, const std::string& layout_type, TGeoVolume* motherVolume)
{
LOG(debug) << "FT3Module: create_layout - Layer " << layerNumber << ", Direction " << direction << ", Face " << face;
TGeoManager* geoManager = gGeoManager;
FT3Module::initialize_materials();
// double sensor_width = 2.5;
// double sensor_height = 9.6;
// double active_width = 2.3;
// double active_height = 9.6;
double sensor_width = 5.0;
double sensor_height = 9.6;
double inactive_width = 0.2; // per side
double active_width = 4.6;
double active_height = 9.6;
double silicon_thickness = 0.01;
double copper_thickness = 0.006;
double kapton_thickness = 0.03;
double epoxy_thickness = 0.0012;
double carbonFiberThickness = 0.01;
double foamSpacingThickness = 1.0;
int dist_offset = 0;
double x_offset;
double y_offset;
double z_offset = (face == "front") ? -foamSpacingThickness / 2.0 - carbonFiberThickness : foamSpacingThickness / 2.0 + carbonFiberThickness;
// offset correction
if (sensor_height == 3.2 && sensor_width == 2.5) {
x_offset = 0.8;
y_offset = 1.5;
} else if (sensor_height == 19.2 && sensor_width == 5) {
x_offset = 0.7;
y_offset = 9;
} else {
x_offset = sensor_width / 2;
y_offset = sensor_height / 2;
}
double x_condition_min = 0;
double x_condition_max = 0;
double offset_Rin_lower = 0;
double offset_Rin_upper = 0;
bool adjust_bottom_y_pos = false;
bool adjust_bottom_y_neg = false;
double x_adjust_bottom_y_pos = 0;
double bottom_y_pos_value = 0;
double bottom_y_neg_value = 0;
double Rin_offset = (sensor_height == 19.2) ? 1 : 0;
double Rout_offset = (sensor_height == 19.2) ? 1 : 0;
if (Rin == 7 && sensor_height == 9.6 && sensor_width == 5) {
x_condition_min = -Rin - 2;
x_condition_max = Rin;
dist_offset = 2;
adjust_bottom_y_pos = true;
adjust_bottom_y_neg = true;
x_adjust_bottom_y_pos = 3.5;
bottom_y_pos_value = 3.5;
bottom_y_neg_value = -3.5;
} else if (Rin == 5 && sensor_height == 9.6 && sensor_width == 5) {
x_condition_min = -Rin - 6;
x_condition_max = Rin;
adjust_bottom_y_pos = true;
adjust_bottom_y_neg = true;
x_adjust_bottom_y_pos = 3.5;
bottom_y_pos_value = 3.5;
bottom_y_neg_value = -3.5;
} else if ((Rin == 5 || Rin == 7) && sensor_height == 19.2) {
x_condition_min = -Rin - 3;
x_condition_max = Rin - 0.2;
dist_offset = 2;
adjust_bottom_y_pos = false;
adjust_bottom_y_neg = false;
} else if (Rin == 5 && sensor_height == 3.2) {
x_condition_min = -(Rin + 2.6);
x_condition_max = Rin + 1.5;
adjust_bottom_y_pos = true;
adjust_bottom_y_neg = true;
x_adjust_bottom_y_pos = 3.5;
bottom_y_pos_value = 3.5;
bottom_y_neg_value = -3.5;
} else if (Rin == 7 && sensor_height == 3.2) {
x_condition_min = -Rin - 1;
x_condition_max = Rin - 0.2;
adjust_bottom_y_pos = true;
adjust_bottom_y_neg = true;
x_adjust_bottom_y_pos = 3.5;
bottom_y_pos_value = 3.5;
bottom_y_neg_value = -3.5;
} else if (Rin == 5 && sensor_height == 9.6 && sensor_width == 2.5) {
x_condition_min = -(Rin + 2.6);
x_condition_max = Rin;
adjust_bottom_y_pos = true;
adjust_bottom_y_neg = true;
x_adjust_bottom_y_pos = 3.5;
bottom_y_pos_value = 3.5;
bottom_y_neg_value = -3.5;
} else if (Rin == 7 && sensor_height == 9.6 && sensor_width == 2.5) {
x_condition_min = -Rin - 2.6;
x_condition_max = Rin + 1;
dist_offset = 2;
adjust_bottom_y_pos = true;
adjust_bottom_y_neg = true;
x_adjust_bottom_y_pos = 5.5;
bottom_y_pos_value = 3.5;
bottom_y_neg_value = -3.5;
} else if (Rin == 10 && sensor_height == 9.6 && sensor_width == 5.0) {
x_condition_min = -Rin - 4;
x_condition_max = Rin;
dist_offset = 2;
adjust_bottom_y_pos = false;
adjust_bottom_y_neg = false;
x_adjust_bottom_y_pos = 3.5;
bottom_y_pos_value = 3.5;
bottom_y_neg_value = -3.5;
} else if (Rin == 20 && sensor_height == 9.6 && sensor_width == 5.0) {
x_condition_min = -Rin - 4;
x_condition_max = Rin;
dist_offset = 2;
adjust_bottom_y_pos = false;
adjust_bottom_y_neg = false;
x_adjust_bottom_y_pos = 3.5;
bottom_y_pos_value = 3.5;
bottom_y_neg_value = -3.5;
} else {
LOG(warning) << "Different config - to determine offsets needed for " << "Rin = " << Rin << " ; sensor_height = " << sensor_height << " ; sensor_width = " << sensor_width << " layer " << layerNumber;
x_condition_min = -Rin - sensor_width;
x_condition_max = Rin;
adjust_bottom_y_pos = false;
adjust_bottom_y_neg = false;
}
offset_Rin_lower = Rin - Rin_offset;
offset_Rin_upper = Rout + Rout_offset;
std::set<std::pair<double, double>> placed_sensors;
int sensor_count = 0;
int placementCounter = 0;
bool justSkipped = false;
std::vector<double> X_positions;
std::vector<int> justSkipped1;
if (sensor_width == 2.5) {
// logic for placement - x positions with complete overlap
if (face == "front") {
X_positions = {-63.4, -60.9, -54.2, -51.7, -45.0, -42.5, -35.8, -33.3, -26.6, -24.1, -17.4, -14.9,
-8.2, -5.7, 1.0, 3.5, 10.2, 12.7, 19.4, 21.9, 28.6, 31.1, 37.8, 40.3, 47.0, 49.5,
56.2, 58.7, 65.4};
justSkipped1 = {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1};
} else if (face == "back") {
X_positions = {-65.5, -58.8, -56.3, -49.6, -47.1, -40.4, -37.9, -31.2, -28.7, -22.0, -19.5, -12.8,
-10.3, -3.6, -1.1, 5.6, 8.1, 14.8, 17.3, 24.0, 26.5, 33.2, 35.7, 42.4, 44.9,
51.6, 54.1, 60.8, 63.3};
justSkipped1 = {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0};
}
} else {
if (Rin == 10 || Rin == 20) { // v3 paving, rough attempt
float overlap = 0.3;
// NB: these are left edges
float X_start = -2.0 - 13.5 * (sensor_width - overlap);
float X_start_pos = 2.0 - 0.5 * (sensor_width - overlap);
if (face == "back") {
X_start += (sensor_width - overlap);
X_start_pos += (sensor_width - overlap);
}
while (X_start < -2) {
X_positions.push_back(X_start);
justSkipped1.push_back(1);
X_start += 2 * (sensor_width - overlap);
}
while (X_start_pos < Rout + x_offset - sensor_width) {
X_positions.push_back(X_start_pos);
justSkipped1.push_back(1);
X_start_pos += 2 * (sensor_width - overlap);
}
} else {
// filling for sensors with 2x width, each row skipped
if (face == "front") {
X_positions = {-63.4, -54.2, -45, -35.8, -26.6, -17.4, -8.2, 1., 10.2, 19.4, 28.6, 37.8, 47., 56.2, 65.4};
justSkipped1 = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
} else if (face == "back") {
X_positions = {-58.8, -49.6, -40.4, -31.2, -22, -12.8, -3.6, 5.6, 14.8, 24, 33.2, 42.4, 51.6, 60.8};
justSkipped1 = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
}
}
}
if (layout_type == "rectangular") {
double x_start = -Rout;
double x_end = Rout;
std::vector<double> x_positions;
for (double x = x_start; x <= x_end; x += sensor_width) {
x_positions.push_back(x);
}
int rowCounter = 0;
const int rowsToAlternate = 2;
for (size_t i = 0; i < X_positions.size(); ++i) {
double x = X_positions[i];
bool justSkippedValue = justSkipped1[i];
std::vector<double> y_positions_positive;
std::vector<double> y_positions_negative;
for (double y = -Rout - Rin_offset; y <= Rout + Rin_offset; y += sensor_height) {
std::vector<std::pair<double, double>> corners = {
{x, y},
{x + sensor_width, y},
{x, y + sensor_height},
{x + sensor_width, y + sensor_height}};
bool within_bounds = std::all_of(corners.begin(), corners.end(), [&](const std::pair<double, double>& corner) {
double cx = corner.first;
double cy = corner.second;
return (offset_Rin_lower <= std::sqrt(cx * cx + cy * cy) && std::sqrt(cx * cx + cy * cy) <= offset_Rin_upper);
});