DUNE-DAQ
DUNE Trigger and Data Acquisition software
Toggle main menu visibility
Loading...
Searching...
No Matches
dunedaq
sourcecode
triggeralgs
src
TAMakerMichelElectronAlgorithm.cpp
Go to the documentation of this file.
1
8
9
#include "
triggeralgs/MichelElectron/TAMakerMichelElectronAlgorithm.hpp
"
10
#include "TRACE/trace.h"
11
#define TRACE_NAME "TAMakerMichelElectronAlgorithm"
12
#include <vector>
13
#include <algorithm>
14
15
using namespace
triggeralgs
;
16
17
using
Logging::TLVL_DEBUG_MEDIUM
;
18
19
void
20
TAMakerMichelElectronAlgorithm::process
(
const
TriggerPrimitive
& input_tp,
21
std::vector<TriggerActivity>& output_ta)
22
{
23
24
// The first time operator() is called, reset the window object.
25
if
(
m_current_window
.is_empty()) {
26
m_current_window
.reset(input_tp);
27
m_primitive_count
++;
28
return
;
29
}
30
31
// If the difference between the current TP's start time and the start of the window
32
// is less than the specified window size, add the TP to the window.
33
if
((input_tp.
time_start
-
m_current_window
.time_start) <
m_window_length
) {
34
m_current_window
.add(input_tp);
35
}
36
37
// Check Michel Candidate ========================================================
38
// We've filled the window, now require a sufficient length track AND that the track
39
// has a potential Bragg P, and then a kink.
40
else
if
(
longest_activity
().size() >
m_adjacency_threshold
) {
41
42
43
// We have a good length acitivity, now search for Bragg peak and kinks
44
std::vector<TriggerPrimitive> trackHits =
longest_activity
();
45
46
if
(
check_bragg_peak
(trackHits)){
47
if
(
check_kinks
(trackHits)){
48
TLOG_DEBUG
(
TLVL_DEBUG_MEDIUM
) <<
"[TAM:ME] Emitting a trigger for candidate Michel event."
;
49
output_ta.push_back(
construct_ta
());
50
m_current_window
.reset(input_tp);
51
}
// Kinks
52
}
// Bragg peak
53
54
}
55
56
// Otherwise, slide the window along using the current TP.
57
else
{
58
m_current_window
.move(input_tp,
m_window_length
);
59
}
60
61
m_primitive_count
++;
62
return
;
63
}
64
65
void
66
TAMakerMichelElectronAlgorithm::configure
(
const
nlohmann::json& config)
67
{
68
TriggerActivityMaker::configure
(config);
69
70
// FIX ME: Use some schema here. Also can't work out how to pass booleans.
71
if
(config.is_object()) {
72
if
(config.contains(
"window_length"
))
73
m_window_length
= config[
"window_length"
];
74
if
(config.contains(
"adjacency_tolerance"
))
75
m_adj_tolerance
= config[
"adjacency_tolerance"
];
76
if
(config.contains(
"adjacency_threshold"
))
77
m_adjacency_threshold
= config[
"adjacency_threshold"
];
78
}
79
80
}
81
82
TriggerActivity
83
TAMakerMichelElectronAlgorithm::construct_ta
()
const
84
{
85
86
TriggerPrimitive
latest_tp_in_window =
m_current_window
.inputs.back();
87
88
TriggerActivity
ta;
89
ta.
time_start
=
m_current_window
.time_start;
90
ta.
time_end
= latest_tp_in_window.
time_start
+ latest_tp_in_window.
samples_over_threshold
* 32;
// FIXME: Replace the hard-coded SOT to TOT scaling.
91
ta.
time_peak
= latest_tp_in_window.
samples_to_peak
* 32 + latest_tp_in_window.
time_start
;
// FIXME: Replace STP to `time_peak` conversion.
92
ta.
time_activity
= ta.
time_peak
;
93
ta.
channel_start
= latest_tp_in_window.
channel
;
94
ta.
channel_end
= latest_tp_in_window.
channel
;
95
ta.
channel_peak
= latest_tp_in_window.
channel
;
96
ta.
adc_integral
=
m_current_window
.adc_integral;
97
ta.
adc_peak
= latest_tp_in_window.
adc_peak
;
98
ta.
detid
= latest_tp_in_window.
detid
;
99
ta.
type
=
TriggerActivity::Type::kTPC
;
100
ta.
algorithm
=
TriggerActivity::Algorithm::kMichelElectron
;
101
ta.
inputs
=
m_current_window
.inputs;
102
103
return
ta;
104
}
105
106
std::vector<TriggerPrimitive>
107
TAMakerMichelElectronAlgorithm::longest_activity
()
const
108
{
109
// This function attempts to return a vector of hits that correspond to the longest
110
// piece of activity in the current window. The logic follows that from the HMA
111
// check_adjacency() function and further details can be found there.
112
std::vector<TriggerPrimitive> trackHits;
113
std::vector<TriggerPrimitive> finalHits;
// The vector of track hits, which we return
114
115
uint16_t adj = 1;
// Initialise adjacency, 1 for the first wire.
116
uint16_t max = 0;
117
unsigned
int
channel = 0;
// Current channel ID
118
unsigned
int
next_channel = 0;
// Next channel ID
119
unsigned
int
next = 0;
// The next position in the hit channels vector
120
unsigned
int
tol_count = 0;
// Tolerance count, should not pass adj_tolerance
121
122
// Generate a channelID ordered list of hit channels for this window
123
std::vector<TriggerPrimitive> hitList;
124
for
(
auto
tp :
m_current_window
.inputs) {
125
hitList.push_back(tp);
126
}
127
std::sort(hitList.begin(), hitList.end(), [](
TriggerPrimitive
a,
TriggerPrimitive
b)
128
{ return a.channel < b.channel; });
129
130
// ADAJACENCY LOGIC ====================================================================
131
// =====================================================================================
132
// Adjcancency Tolerance = Number of times prepared to skip missed hits before resetting
133
// the adjacency count. This accounts for things like dead channels / missed TPs. The
134
// maximum gap is 4 which comes from tuning on December 2021 coldbox data, and June 2022
135
// coldbox runs.
136
for
(
int
i = 0; i < hitList.size(); ++i) {
137
138
next = (i + 1) % hitList.size();
// Loops back when outside of channel list range
139
channel = hitList.at(i).channel;
140
next_channel = hitList.at(next).channel;
// Next channel with a hit
141
142
if
(trackHits.size() == 0 ){ trackHits.push_back(hitList.at(i)); }
143
144
// End of vector condition.
145
if
(next_channel == 0) { next_channel = channel - 1; }
146
147
// Skip same channel hits for adjacency counting, but add to the track!
148
if
(next_channel == channel) {
149
trackHits.push_back(hitList.at(next));
150
continue
; }
151
152
// If next hit is on next channel, increment the adjacency count.
153
else
if
(next_channel == channel + 1){
154
trackHits.push_back(hitList.at(next));
155
++adj; }
156
157
// If next channel is not on the next hit, but the 'second next', increase adjacency
158
// but also tally up with the tolerance counter.
159
else
if
(((next_channel == channel + 2) || (next_channel == channel + 3) ||
160
(next_channel == channel + 4) || (next_channel == channel + 5))
161
&& (tol_count <
m_adj_tolerance
)) {
162
trackHits.push_back(hitList.at(next));
163
++adj;
164
for
(
int
i = 0 ; i < next_channel-channel ; ++i){ ++tol_count; }
165
}
166
167
// If next hit isn't within reach, end the adjacency count and check for a new max.
168
// Reset variables for next iteration.
169
else
{
170
if
(adj > max) {
171
max = adj;
172
finalHits.clear();
// Clear previous track
173
for
(
auto
h : trackHits){
174
finalHits.push_back(h);
175
}
176
}
177
adj = 1;
178
tol_count = 0;
179
trackHits.clear();
180
}
181
}
182
183
return
finalHits;
184
}
185
186
187
// Function that tries to identify a Bragg peak via a running
188
// mean average of the ADC values. We use the running mean as it's less susceptible to
189
// spikes of activity that might trick the algorithm. We establish a baseline, then
190
// count up clusters of charge deposition above that baseline. If the largest is at
191
// one of the ends of that collection, signal a potential Bragg peak.
192
bool
193
TAMakerMichelElectronAlgorithm::check_bragg_peak
(std::vector<TriggerPrimitive> trackHits)
194
{
195
bool
bragg =
false
;
196
std::vector<float> adc_means_list;
197
uint16_t convolve_value = 6;
198
199
// Loop over hits that correspond to high adjacency activity
200
for
(uint16_t i = 0; i < trackHits.size(); ++i){
201
float
adc_sum = 0;
202
float
adc_mean = 0;
203
204
// Calculate running ADC mean of this track
205
for
(uint16_t j = i; j < i+convolve_value; ++j){
206
int
hit = (j) % trackHits.size();
207
adc_sum += trackHits.at(hit).adc_integral;
208
}
209
210
adc_mean = adc_sum / convolve_value;
211
adc_means_list.push_back(adc_mean);
212
adc_sum = 0;
213
}
214
215
// We now have a list of convolved adc means.
216
float
ped = std::accumulate(adc_means_list.begin(), adc_means_list.end(), 0.0) / adc_means_list.size();
217
float
charge = 0;
218
std::vector<float> charge_dumps;
219
220
// Now go through the list, picking up clusters of charge above the baseline/ped
221
for
(
auto
a : adc_means_list){
222
if
(a > ped){
223
charge += a;
224
}
225
else
if
( a < ped && charge !=0 ){
226
charge_dumps.push_back(charge);
227
charge = 0;
228
}
229
}
230
231
// If the maximum of that list of charge dumps is near(at?) either end of it
232
float
max_charge = *max_element(charge_dumps.begin(), charge_dumps.end());
233
if
(max_charge == charge_dumps.front() || max_charge == charge_dumps.back()){ bragg=
true
; }
234
235
return
bragg;
236
}
237
238
bool
239
TAMakerMichelElectronAlgorithm::check_kinks
(std::vector<TriggerPrimitive> finalHits)
240
{
241
bool
kinks =
false
;
// We actually required two kinks in the coldbox, the michel kink and the wes kink
242
std::vector<float> runningGradient;
243
std::vector<float> runningMeanGradient;
244
245
// Choice to be made here. Do we want to scane in collection (z) or time (x) direction when calculating gradient between hits. I
246
// would say if we have already made the request to pass a track of length specific threshold which is longer than the drift
247
// direction for the coldbox, it makes sense to scan across channels a little more.
248
std::sort(finalHits.begin(), finalHits.end(), [](
TriggerPrimitive
a,
TriggerPrimitive
b) { return a.channel < b.channel; });
249
250
// Populate the runningGradient with the track hits. Do this between ith and i+kth TPs, to small scale fluctuations of the track
251
// Yet k should be kept small, so that enough gradient information is preserved at the end of the track to identify kinks
252
for
(
int
i=0 ; i < finalHits.size()-2; i++){
253
254
// Skip same channel hits or if the start times are the same - no div by zero!
255
if
(finalHits.at(i+2).channel == finalHits.at(i).channel || (finalHits.at(i+2).time_start == finalHits.at(i).time_start) ) {
continue
; }
256
257
// Check that the next TP is closeby; enough in space and time directions so as to avoid obtaining a gradient value from
258
// same channel hits at large time difference or vice versa due to kink topology or showers. Clearly we shouldn't be very far in
259
// channel number, but since we might later try to do this check in the time direction, leave the condition in.
260
int
diff = finalHits.at(i+2).time_start - finalHits.at(i).time_start;
261
if
((std::abs(diff) > 1000) || ((std::abs(
channel_diff_t
(finalHits.at(i+2).channel) -
channel_diff_t
(finalHits.at(i).channel)) > 6))) {
continue
; }
262
263
// Gradient is just change in z (collection) over change in x (drift). x is admitedly roughly converted from
264
// hit start time, but I don't think diffusion effects are a huge concern over 20cm. Using mm for readability/visualisation
265
float
dz = (finalHits.at(i+2).channel - finalHits.at(i).channel)*4.67;
// Change in collection wire z to separation in mm
266
long
long
int
dt = finalHits.at(i+2).time_start - finalHits.at(i).time_start;
267
float
dx = dt*0.028;
// Change time to separation in x mm
268
float
g = dz/dx;
269
270
runningGradient.push_back(g);
271
}
272
273
// Require a decent length of the gradients vector. Otherwise some adjacent events are showers and the conditions above mean
274
// we don't get enough entries. In essence, this provides some confidence that it's track-like rather than shower-like. Which
275
// is what we want for a Michel event.
276
if
( runningGradient.size() > 10 ){
277
278
// Now lets take a running mean of the gradients between TPs, less susceptible to wild changes due to deltas/etc
279
for
(
int
g=0 ; g < runningGradient.size()-1 ; g++){
280
float
gsum = runningGradient.at(g) + runningGradient.at(g+1);
281
runningMeanGradient.push_back(gsum/2);
282
}
283
284
// We have a list of gradients, now just demand that the two ends have gradients that differ significantly
285
// from the mean gradient of the activity at both ends. This aims to pick out wesKinks and michelKinks
286
if
(runningMeanGradient.size() > 10 ){
287
288
float
mean = (std::abs(std::accumulate(runningMeanGradient.begin(), runningMeanGradient.end(), 0.0)))/(runningMeanGradient.size());
289
290
// If you're testing on simulation or december data, you won't see the wes kink, so use an || instead of &&
291
if
((std::abs(runningMeanGradient.front()) + mean > 2.5*mean) || ((std::abs(runningMeanGradient.back() + mean)) > 2.5*mean)){ kinks=
true
; }
292
}
293
}
294
295
return
kinks;
296
}
297
298
// ===============================================================================================
299
// ===============================================================================================
300
// Functions below this line are for debugging purposes.
301
// ===============================================================================================
302
303
void
304
TAMakerMichelElectronAlgorithm::add_window_to_record
(
Window
window)
305
{
306
m_window_record
.push_back(window);
307
return
;
308
}
309
310
311
// Function to dump the details of the TA window currently on record
312
void
313
TAMakerMichelElectronAlgorithm::dump_window_record
()
314
{
315
// FIX ME: Need to index this outfile in the name by detid or something similar.
316
std::ofstream outfile;
317
outfile.open(
"window_record_tam.csv"
, std::ios_base::app);
318
319
for
(
auto
window :
m_window_record
) {
320
outfile << window.time_start <<
","
;
321
outfile << window.inputs.back().time_start <<
","
;
322
outfile << window.inputs.back().time_start - window.time_start <<
","
;
// window_length - from TP start times
323
outfile << window.adc_integral <<
","
;
324
outfile << window.n_channels_hit() <<
","
;
// Number of unique channels with hits
325
outfile << window.inputs.size() <<
","
;
// Number of TPs in Window
326
outfile << window.inputs.back().channel <<
","
;
// Last TP Channel ID
327
outfile << window.inputs.front().channel <<
","
;
// First TP Channel ID
328
outfile <<
longest_activity
().size() << std::endl;
// New adjacency value for the window
329
}
330
331
outfile.close();
332
333
m_window_record
.clear();
334
335
return
;
336
}
337
338
// Function to add current TP details to a text file for testing and debugging.
339
void
340
TAMakerMichelElectronAlgorithm::dump_tp
(
TriggerPrimitive
const
& input_tp)
341
{
342
std::ofstream outfile;
343
outfile.open(
"coldbox_tps.txt"
, std::ios_base::app);
344
345
// Output relevant TP information to file
346
outfile << input_tp.
time_start
<<
" "
;
// Start time of TP
347
outfile << input_tp.
samples_over_threshold
<<
" "
;
// in multiples of 25
348
outfile << input_tp.
samples_to_peak
<<
" "
;
//
349
outfile << input_tp.
channel
<<
" "
;
// Offline channel ID
350
outfile << input_tp.
adc_integral
<<
" "
;
// ADC Sum
351
outfile << input_tp.
adc_peak
<<
" "
;
// ADC Peak Value
352
outfile << input_tp.
detid
<<
" "
;
// Det ID - Identifies detector element, APA or PDS part etc...
353
outfile.close();
354
355
return
;
356
}
357
358
/*
359
void
360
TAMakerMichelElectronAlgorithm::flush(timestamp_t, std::vector<TriggerActivity>& output_ta)
361
{
362
// Check the status of the current window, construct TA if conditions are met. Regardless
363
// of whether the conditions are met, reset the window.
364
if(m_current_window.adc_integral > m_adc_threshold && m_trigger_on_adc){
365
//else if(m_current_window.adc_integral > m_conf.adc_threshold && m_conf.trigger_on_adc){
366
//TLOG_DEBUG(TRACE_NAME) << "ADC integral in window is greater than specified threshold.";
367
output_ta.push_back(construct_ta());
368
}
369
else if(m_current_window.n_channels_hit() > m_n_channels_threshold && m_trigger_on_n_channels){
370
//else if(m_current_window.n_channels_hit() > m_conf.n_channels_threshold && m_conf.trigger_on_n_channels){
371
//TLOG_DEBUG(TRACE_NAME) << "Number of channels hit in the window is greater than specified threshold.";
372
output_ta.push_back(construct_ta());
373
}
374
375
//TLOG_DEBUG(TRACE_NAME) << "Clearing the current window, on the arrival of the next input_tp, the window will be
376
reset."; m_current_window.clear();
377
378
return;
379
}*/
380
381
// Register algo in TA Factory
382
REGISTER_TRIGGER_ACTIVITY_MAKER
(
TRACE_NAME
,
TAMakerMichelElectronAlgorithm
)
TRACE_NAME
#define TRACE_NAME
Definition
TimestampEstimatorTimeSync.cpp:17
REGISTER_TRIGGER_ACTIVITY_MAKER
#define REGISTER_TRIGGER_ACTIVITY_MAKER(tam_name, tam_class)
Definition
TriggerActivityFactory.hpp:14
triggeralgs::TAMakerMichelElectronAlgorithm::Window
Definition
TAMakerMichelElectronAlgorithm.hpp:27
triggeralgs::TAMakerMichelElectronAlgorithm
Definition
TAMakerMichelElectronAlgorithm.hpp:18
triggeralgs::TAMakerMichelElectronAlgorithm::longest_activity
std::vector< TriggerPrimitive > longest_activity() const
Definition
TAMakerMichelElectronAlgorithm.cpp:107
triggeralgs::TAMakerMichelElectronAlgorithm::m_current_window
Window m_current_window
Definition
TAMakerMichelElectronAlgorithm.hpp:111
triggeralgs::TAMakerMichelElectronAlgorithm::check_kinks
bool check_kinks(std::vector< TriggerPrimitive > trackHits)
Definition
TAMakerMichelElectronAlgorithm.cpp:239
triggeralgs::TAMakerMichelElectronAlgorithm::m_window_record
std::vector< Window > m_window_record
Definition
TAMakerMichelElectronAlgorithm.hpp:129
triggeralgs::TAMakerMichelElectronAlgorithm::m_adjacency_threshold
uint16_t m_adjacency_threshold
Definition
TAMakerMichelElectronAlgorithm.hpp:117
triggeralgs::TAMakerMichelElectronAlgorithm::construct_ta
TriggerActivity construct_ta() const
Definition
TAMakerMichelElectronAlgorithm.cpp:83
triggeralgs::TAMakerMichelElectronAlgorithm::m_adj_tolerance
uint16_t m_adj_tolerance
Definition
TAMakerMichelElectronAlgorithm.hpp:119
triggeralgs::TAMakerMichelElectronAlgorithm::check_bragg_peak
bool check_bragg_peak(std::vector< TriggerPrimitive > trackHits)
Definition
TAMakerMichelElectronAlgorithm.cpp:193
triggeralgs::TAMakerMichelElectronAlgorithm::m_primitive_count
uint64_t m_primitive_count
Definition
TAMakerMichelElectronAlgorithm.hpp:112
triggeralgs::TAMakerMichelElectronAlgorithm::add_window_to_record
void add_window_to_record(Window window)
Definition
TAMakerMichelElectronAlgorithm.cpp:304
triggeralgs::TAMakerMichelElectronAlgorithm::configure
void configure(const nlohmann::json &config)
Definition
TAMakerMichelElectronAlgorithm.cpp:66
triggeralgs::TAMakerMichelElectronAlgorithm::dump_tp
void dump_tp(TriggerPrimitive const &input_tp)
Definition
TAMakerMichelElectronAlgorithm.cpp:340
triggeralgs::TAMakerMichelElectronAlgorithm::m_window_length
timestamp_t m_window_length
Definition
TAMakerMichelElectronAlgorithm.hpp:123
triggeralgs::TAMakerMichelElectronAlgorithm::dump_window_record
void dump_window_record()
Definition
TAMakerMichelElectronAlgorithm.cpp:313
triggeralgs::TAMakerMichelElectronAlgorithm::process
void process(const TriggerPrimitive &input_tp, std::vector< TriggerActivity > &output_ta)
TP processing function that creates & fills TAs.
Definition
TAMakerMichelElectronAlgorithm.cpp:20
triggeralgs::TriggerActivityMaker::configure
virtual void configure(const nlohmann::json &config)
Definition
TriggerActivityMaker.hpp:111
TLOG_DEBUG
#define TLOG_DEBUG(lvl,...)
Definition
Logging.hpp:112
triggeralgs
Definition
AbstractFactory.hpp:18
triggeralgs::TLVL_DEBUG_MEDIUM
@ TLVL_DEBUG_MEDIUM
Definition
Logging.hpp:23
triggeralgs::channel_diff_t
dunedaq::trgdataformats::channel_diff_t channel_diff_t
Definition
Types.hpp:21
triggeralgs::TriggerPrimitive
dunedaq::trgdataformats::TriggerPrimitive TriggerPrimitive
Definition
TriggerPrimitive.hpp:22
TAMakerMichelElectronAlgorithm.hpp
dunedaq::trgdataformats::TriggerActivityData::algorithm
Algorithm algorithm
Definition
TriggerActivityData.hpp:57
dunedaq::trgdataformats::TriggerActivityData::channel_peak
channel_t channel_peak
Definition
TriggerActivityData.hpp:52
dunedaq::trgdataformats::TriggerActivityData::time_activity
timestamp_t time_activity
Definition
TriggerActivityData.hpp:49
dunedaq::trgdataformats::TriggerActivityData::adc_integral
uint64_t adc_integral
Definition
TriggerActivityData.hpp:53
dunedaq::trgdataformats::TriggerActivityData::Type::kTPC
@ kTPC
Definition
TriggerActivityData.hpp:22
dunedaq::trgdataformats::TriggerActivityData::type
Type type
Definition
TriggerActivityData.hpp:56
dunedaq::trgdataformats::TriggerActivityData::time_peak
timestamp_t time_peak
Definition
TriggerActivityData.hpp:48
dunedaq::trgdataformats::TriggerActivityData::Algorithm::kMichelElectron
@ kMichelElectron
Definition
TriggerActivityData.hpp:33
dunedaq::trgdataformats::TriggerActivityData::adc_peak
uint16_t adc_peak
Definition
TriggerActivityData.hpp:54
dunedaq::trgdataformats::TriggerActivityData::channel_end
channel_t channel_end
Definition
TriggerActivityData.hpp:51
dunedaq::trgdataformats::TriggerActivityData::time_end
timestamp_t time_end
Definition
TriggerActivityData.hpp:47
dunedaq::trgdataformats::TriggerActivityData::channel_start
channel_t channel_start
Definition
TriggerActivityData.hpp:50
dunedaq::trgdataformats::TriggerActivityData::time_start
timestamp_t time_start
Definition
TriggerActivityData.hpp:46
dunedaq::trgdataformats::TriggerActivityData::detid
detid_t detid
Definition
TriggerActivityData.hpp:55
dunedaq::trgdataformats::TriggerPrimitive::detid
uint64_t detid
Definition
TriggerPrimitive.hpp:33
dunedaq::trgdataformats::TriggerPrimitive::channel
uint64_t channel
Definition
TriggerPrimitive.hpp:36
dunedaq::trgdataformats::TriggerPrimitive::samples_over_threshold
uint64_t samples_over_threshold
Definition
TriggerPrimitive.hpp:38
dunedaq::trgdataformats::TriggerPrimitive::adc_peak
uint64_t adc_peak
Definition
TriggerPrimitive.hpp:43
dunedaq::trgdataformats::TriggerPrimitive::time_start
uint64_t time_start
Definition
TriggerPrimitive.hpp:39
dunedaq::trgdataformats::TriggerPrimitive::samples_to_peak
uint64_t samples_to_peak
Definition
TriggerPrimitive.hpp:40
dunedaq::trgdataformats::TriggerPrimitive::adc_integral
uint64_t adc_integral
Definition
TriggerPrimitive.hpp:42
triggeralgs::TriggerActivity
Definition
TriggerActivity.hpp:20
triggeralgs::TriggerActivity::inputs
std::vector< TriggerPrimitive > inputs
Definition
TriggerActivity.hpp:33
Generated on
for DUNE-DAQ by
1.17.0