R3BROOT
R3B analysis software
Loading...
Searching...
No Matches
R3BNeulandMillepede.cxx
Go to the documentation of this file.
1/******************************************************************************
2 * Copyright (C) 2019 GSI Helmholtzzentrum für Schwerionenforschung GmbH *
3 * Copyright (C) 2019-2023 Members of R3B Collaboration *
4 * *
5 * This software is distributed under the terms of the *
6 * GNU General Public Licence (GPL) version 3, *
7 * copied verbatim in the file "LICENSE". *
8 * *
9 * In applying this license GSI does not waive the privileges and immunities *
10 * granted to it by virtue of its status as an Intergovernmental Organization *
11 * or submit itself to any jurisdiction. *
12 ******************************************************************************/
13
14#include "R3BNeulandMillepede.h"
15#include "Mille.h"
16#include "ParResultReader.h"
17#include "R3BDataMonitor.h"
18#include "R3BNeulandCalData2.h"
21#include <R3BException.h>
23#include <R3BNeulandCommon.h>
24#include <SteerWriter.h>
25
26#include <TGraphErrors.h>
27#include <TH1.h>
28#include <array>
29#include <cstdlib>
30#include <fairlogger/Logger.h>
31#include <filesystem>
32#include <fmt/core.h>
33#include <fmt/format.h>
34#include <iterator>
35#include <memory>
36#include <numeric>
37#include <optional>
38#include <range/v3/algorithm/all_of.hpp>
39#include <range/v3/algorithm/copy.hpp>
40#include <range/v3/algorithm/min_element.hpp>
41#include <range/v3/iterator/operations.hpp>
42#include <range/v3/numeric/accumulate.hpp>
43#include <range/v3/view/all.hpp>
44#include <range/v3/view/filter.hpp>
45#include <range/v3/view/sliding.hpp>
46#include <range/v3/view/transform.hpp>
47#include <stdexcept>
48#include <string>
49#include <string_view>
50#include <utility>
51#include <vector>
52
53namespace rng = ranges;
54
55constexpr auto DEFAULT_RES_FILENAME = "millepede.res";
56constexpr auto SCALE_FACTOR = 10.F;
57// constexpr auto REFERENCE_BAR_NUM = 25;
58constexpr auto MILLE_BUFFER_SIZE = std::size_t{ 100000 };
59constexpr auto DEFAULT_T_ERROR = 2; // ns
60
61namespace
62{
63 void calculate_time_offset(R3B::Neuland::Cal2HitPar& cal_to_hit_par)
64 {
65 auto& module_pars = cal_to_hit_par.GetListOfModuleParRef();
66 for (auto& [module_num, module_par] : module_pars)
67 {
68 if (module_par.effective_speed.value != 0)
69 {
70 module_par.t_diff = module_par.t_diff / module_par.effective_speed;
71 }
72 }
73 }
74
75 void change_time_offset(R3B::Neuland::Cal2HitPar& cal_to_hit_par)
76 {
77 auto& module_pars = cal_to_hit_par.GetListOfModuleParRef();
78 for (auto& [module_num, module_par] : module_pars)
79 {
80 module_par.t_diff = module_par.t_diff * module_par.effective_speed;
81 }
82 }
83} // namespace
84
86{
88 {
90 fs::create_directories(fs::path(working_dir_));
91 cal_to_hit_par_ = GetTask()->GetCal2HitPar();
92
93 par_result_.set_filename((fs::path(working_dir_) / DEFAULT_RES_FILENAME).string());
94 pede_launcher_.set_steer_filename(pede_steer_filename_);
95 pede_launcher_.set_parameter_filename(parameter_filename_);
96 if (const auto* r3b_dir = std::getenv("R3BROOTPATH"); r3b_dir != nullptr)
97 {
98 pede_launcher_.set_binary_dir(fmt::format("{}/bin", r3b_dir));
99 // TODO: make subdir configurable
100 pede_launcher_.set_working_dir(fs::absolute(fs::path{ working_dir_ }).string());
101 }
102 else
103 {
104 throw R3B::runtime_error(
105 "Environment variable R3BROOTPATH is not defined! Did you forget to source the \"config.sh\" file?");
106 }
107 binary_data_writer_ = std::make_unique<Mille>((fs::path{ working_dir_ } / input_data_filename_).string());
108 binary_data_writer_->set_buffer_size(MILLE_BUFFER_SIZE);
109 data_preprocessor_ = std::make_unique<MilleDataProcessor>(GetModuleSize());
110 data_preprocessor_->set_p_value_cut(p_value_cut_);
111
114 }
115
117 {
118 auto filename = FairRun::Instance()->GetSink()->GetFileName();
119 auto output_dir_str = filename.View();
120 auto output_path = fs::path{ output_dir_str };
121 auto output_dir = output_path.parent_path();
122 auto output_filename = output_path.filename();
123 working_dir_ = (output_dir / DEFAULT_SUB_DIR / output_filename);
124 LOGP(debug, "Working dir has set to be: {}", working_dir_);
125 }
126
127 // output: module_num & global label
128 inline auto MillepedeEngine::to_module_num_label(int par_num) -> std::pair<int, GlobalLabel>
129 {
130 const auto num_of_module = GetModuleSize();
131 auto res = std::pair<int, GlobalLabel>{};
132 const auto factor = (par_num - 1) / num_of_module;
133 res.first = (par_num - 1) % num_of_module + 1;
134
135 switch (factor)
136 {
137 // case 0:
138 // res.second = GlobalLabel::tsync;
139 // break;
140 // case 1:
141 // res.second = GlobalLabel::offset_effective_c;
142 // break;
143 // case 2:
144 // res.second = GlobalLabel::effective_c;
145 // break;
146 case 0:
148 break;
149 case 1:
150 res.second = GlobalLabel::effective_c;
151 break;
152 default:
153 throw R3B::logic_error(fmt::format("An error occured with unrecognized global par id: {}", par_num));
154 }
155
156 return res;
157 }
158
159 inline auto MillepedeEngine::get_global_label_id(int module_num, GlobalLabel label) -> int
160 {
161 const auto num_of_module = GetModuleSize();
162 switch (label)
163 {
164 // case GlobalLabel::tsync:
165 // return module_num;
166 // case GlobalLabel::offset_effective_c:
167 // return module_num + num_of_module;
168 // case GlobalLabel::effective_c:
169 // return module_num + (2 * num_of_module);
171 return module_num;
173 return module_num + num_of_module;
174 default:
175 throw R3B::logic_error("An error occured with unrecognized global tag");
176 }
177 }
178
180 Neuland::Cal2HitPar& cal_to_hit_par)
181 {
182 change_time_offset(cal_to_hit_par);
183 const auto& pars = result.get_pars();
184 for (const auto& [par_id, par] : pars)
185 {
186 const auto [module_num, global_label] = to_module_num_label(par_id);
187 auto& module_pars = cal_to_hit_par.GetListOfModuleParRef();
188
189 auto& par_ref = module_pars.emplace(module_num, HitModulePar{}).first->second;
190 switch (global_label)
191 {
193 par_ref.t_sync.value = par.value * SCALE_FACTOR;
194 par_ref.t_sync.error = par.error * SCALE_FACTOR;
195 break;
197 // The value here is the product of tDiff and effectiveSped. Real tDiff will be calculated later
198 par_ref.t_diff.value += par.value * SCALE_FACTOR;
199 par_ref.t_diff.error += par.error * SCALE_FACTOR;
200 break;
202 par_ref.effective_speed.value += par.value;
203 par_ref.effective_speed.error += par.error;
204 break;
205 default:
206 throw std::runtime_error("An error occured with unrecognized global tag");
207 }
208 }
209
210 calculate_time_offset(cal_to_hit_par);
211 }
212
213 auto MillepedeEngine::set_minimum_values(const std::vector<R3B::Neuland::BarCalData>& signals) -> bool
214 {
215 // make sure only one hit exists in one bar
216 auto filtered_signals = rng::filter_view(
217 signals | rng::views::all,
218 [](const auto& bar_signal) { return bar_signal.left.size() == 1 and bar_signal.right.size() == 1; });
219 if (filtered_signals.empty())
220 {
221 return false;
222 }
223
224 if (not average_t_sum_.has_value())
225 {
226 auto t_sum_view = filtered_signals | rng::views::transform(
227 [](const auto& bar_signal)
228 {
229 const auto& left_signal = bar_signal.left.front();
230 const auto& right_signal = bar_signal.right.front();
231 return (left_signal.leading_time - left_signal.trigger_time +
232 right_signal.leading_time - right_signal.trigger_time)
233 .value;
234 });
235 auto sum = rng::accumulate(t_sum_view, 0.F);
236 average_t_sum_ = sum / static_cast<float>(rng::distance(t_sum_view.begin(), t_sum_view.end()));
237 LOGP(info, "Average t_sum is calculated to be {}", average_t_sum_.value());
238 }
239 return true;
240 }
241
242 auto MillepedeEngine::SignalFilter(const std::vector<BarCalData>& signals) -> bool
243 {
244 // select out rays with few hits
245 if (signals.size() < minimum_hit_)
246 {
247 return false;
248 }
249
250 // select out vertical cosmic rays
251 if (rng::all_of(signals |
252 rng::views::transform([](const auto& bar_signal)
253 { return ModuleID2PlaneID(bar_signal.module_num - 1); }) |
254 rng::views::sliding(2),
255 [](const auto& pair) { return pair.front() == pair.back(); }))
256 {
257 return false;
258 }
259
260 if (not set_minimum_values(signals))
261 {
262 return false;
263 }
264
265 return true;
266 }
267
269 {
270 buffer_clear();
271 const auto module_num = static_cast<int>(signal.module_num);
272 const auto pos_z = ModuleNum2ZPos<float>(static_cast<int>(module_num));
273
274 auto init_effective_c = cal_to_hit_par_->GetModuleParAt(module_num).effective_speed.value;
275
276 const auto& left_signal = signal.left;
277 const auto& right_signal = signal.right;
278 const auto t_sum = (left_signal.leading_time - left_signal.trigger_time) +
279 (right_signal.leading_time - right_signal.trigger_time) - average_t_sum_.value_or(0.F);
280
281 input_data_buffer_.measurement =
282 static_cast<float>((t_sum.value / SCALE_FACTOR / 2.F) - (BarLength / SCALE_FACTOR / init_effective_c));
283 input_data_buffer_.sigma = static_cast<float>(t_sum.error / SCALE_FACTOR / 2. * error_scale_factor_);
284 // input_data_buffer_.sigma = static_cast<float>(DEFAULT_MEAS_ERROR);
285 const auto local_derivs_t = std::array{ 0.F, 0.F, pos_z / SCALE_FACTOR, 0.F, 0.F, 1.F };
286#ifdef HAS_CXX_17
287 std::copy(local_derivs_t.begin(), local_derivs_t.end(), std::back_inserter(input_data_buffer_.locals));
288#else
289 ranges::copy(local_derivs_t, std::back_inserter(input_data_buffer_.locals));
290#endif
291 input_data_buffer_.globals.emplace_back(get_global_label_id(module_num, GlobalLabel::tsync), 1.F);
292 input_data_buffer_.globals.emplace_back(get_global_label_id(module_num, GlobalLabel::effective_c),
293 -BarLength / SCALE_FACTOR / 2.F / init_effective_c / init_effective_c);
294
296 }
297
298 void MillepedeEngine::add_spacial_local_constraint(int plane_id, const std::vector<MilleCalData>& plane_signals)
299 {
300 buffer_clear();
301 const auto pos_z = PlaneID2ZPos<float>(plane_id);
302 const auto is_horizontal = IsPlaneIDHorizontal(plane_id);
303 // const auto pos_bar_vert_disp = GetBarVerticalDisplacement(module_num);
304 const auto pos_bar_vert_disp = std::accumulate(plane_signals.begin(),
305 plane_signals.end(),
306 0.,
307 [](double sum, const auto& signal) {
308 return sum + GetBarVerticalDisplacement(signal.module_num);
309 }) /
310 static_cast<double>(plane_signals.size());
311 const auto local_derivs = is_horizontal ? std::array{ 0.F, pos_z / SCALE_FACTOR, 0.F, 1.F }
312 : std::array{ pos_z / SCALE_FACTOR, 0.F, 1.F, 0.F };
313 // const auto local_derivs = is_horizontal ? std::array{ 0.F, pos_z / SCALE_FACTOR, 0.F, 0.F, 1.F, 0.F }
314 // : std::array{ pos_z / SCALE_FACTOR, 0.F, 0.F, 1.F, 0.F, 0.F };
315
316 input_data_buffer_.measurement = static_cast<float>(pos_bar_vert_disp / SCALE_FACTOR);
317 input_data_buffer_.sigma = static_cast<float>(BarSize_XY / SQRT_12 / SCALE_FACTOR * error_scale_factor_);
318
319 // if (not is_horizontal)
320 // {
321 // fmt::println("c_value_1.append({})\na_value_1.append({})\nb_value_1.append({})",
322 // pos_bar_vert_disp / SCALE_FACTOR,
323 // pos_z / SCALE_FACTOR,
324 // 1.);
325 // }
326#ifdef HAS_CXX_17
327 std::copy(local_derivs.begin(), local_derivs.end(), std::back_inserter(input_data_buffer_.locals));
328#else
329 ranges::copy(local_derivs, std::back_inserter(input_data_buffer_.locals));
330#endif
332 }
333
335 {
336 buffer_clear();
337 const auto module_num = static_cast<int>(signal.module_num);
338 const auto plane_id = ModuleID2PlaneID(static_cast<int>(module_num) - 1);
339 const auto is_horizontal = IsPlaneIDHorizontal(plane_id);
340 const auto& module_par = cal_to_hit_par_->GetModuleParAt(module_num);
341 auto init_effective_c = module_par.effective_speed.value;
342 auto init_t_offset = module_par.t_diff.value;
343 const auto pos_z = static_cast<float>(PlaneID2ZPos(plane_id));
344
345 const auto& left_signal = signal.left;
346 const auto& right_signal = signal.right;
347 const auto t_diff = (right_signal.leading_time - right_signal.trigger_time) -
348 (left_signal.leading_time - left_signal.trigger_time);
349
350 const auto t_error = t_diff.error == 0 ? DEFAULT_T_ERROR : t_diff.error;
351 input_data_buffer_.measurement = static_cast<float>((init_effective_c * init_t_offset / 2. / SCALE_FACTOR) -
352 (init_effective_c * t_diff.value / SCALE_FACTOR / 2.));
353 input_data_buffer_.sigma =
354 static_cast<float>(t_error / SCALE_FACTOR / 2. * std::abs(init_effective_c) * error_scale_factor_);
355 const auto local_derivs = is_horizontal ? std::array{ pos_z / SCALE_FACTOR, 0.F, 1.F, 0.F }
356 : std::array{ 0.F, pos_z / SCALE_FACTOR, 0.F, 1.F };
357 // const auto local_derivs = is_horizontal ? std::array{ pos_z / SCALE_FACTOR, 0.F, 0.F, 1.F, 0.F, 0.F }
358 // : std::array{ 0.F, pos_z / SCALE_FACTOR, 0.F, 0.F, 1.F, 0.F };
359#ifdef HAS_CXX_17
360 std::copy(local_derivs.begin(), local_derivs.end(), std::back_inserter(input_data_buffer_.locals));
361#else
362 ranges::copy(local_derivs, std::back_inserter(input_data_buffer_.locals));
363#endif
364 // fmt::println("Adding global: {}", get_global_label_id(module_num, GlobalLabel::offset_effective_c));
366 -0.5F);
367 input_data_buffer_.globals.emplace_back(get_global_label_id(module_num, GlobalLabel::effective_c),
368 static_cast<float>(t_diff.value / SCALE_FACTOR / 2.));
369
370 // if (is_horizontal)
371 // {
372 // const auto c_val = (0.5 * (module_par.t_diff * module_par.effective_speed).value / SCALE_FACTOR) -
373 // (module_par.effective_speed.value * t_diff.value / SCALE_FACTOR / 2.);
374 // fmt::println(
375 // "c_value_2.append({})\na_value_2.append({})\nb_value_2.append({})", c_val, pos_z / SCALE_FACTOR, 1.);
376 // }
378 LOGP(debug,
379 "Writting Mille data to binary file with meas = {} and z = {}",
380 input_data_buffer_.measurement,
381 pos_z);
382 }
383
384 auto MillepedeEngine::select_t_diff_signal(const std::vector<MilleCalData>& plane_data)
385 {
386 if (plane_data.empty())
387 {
388 return plane_data.end();
389 }
390 auto calculate_residual = [this](const MilleCalData& signal) -> double
391 {
392 const auto t_diff = (signal.right.leading_time - signal.right.trigger_time) -
393 (signal.left.leading_time - signal.left.trigger_time);
394 const auto& module_par = cal_to_hit_par_->GetModuleParAt(signal.module_num);
395 const auto position = (-t_diff + module_par.t_diff) / 2 * module_par.effective_speed;
396 const auto res =
397 data_preprocessor_->calculate_residual(position.value, static_cast<int>(signal.module_num));
398 return res;
399 };
400 auto iter = ranges::min_element(plane_data,
401 [calculate_residual](const auto& first, const auto& second)
402 { return calculate_residual(first) < calculate_residual(second); });
403 if (iter != plane_data.end())
404 {
405 auto residual = calculate_residual(*iter);
406 hist_t_offset_residual_->Fill(residual);
407 if (residual > t_diff_residual_cut_)
408 {
409 return plane_data.end();
410 }
411 // fmt::println("selected signal residual: {}", residual);
412 }
413 return iter;
414 }
415
416 void MillepedeEngine::AddSignals(const std::vector<BarCalData>& signals)
417 {
418
419 // fmt::println("==================new event===============\n");
420 if (not data_preprocessor_->filter(signals))
421 {
422 return;
423 }
424 const auto& processed_data = data_preprocessor_->get_data();
425 for (const auto& [plane_id, plane_signals] :
426 processed_data |
427 rng::views::filter([](const auto& planeid_signals) { return not planeid_signals.second.empty(); }))
428 {
429 // fmt::println("\n--------------------\n");
430 auto iter = select_t_diff_signal(plane_signals);
431 if (iter == plane_signals.end())
432 {
433 continue;
434 }
435 // fmt::println("selected signal: {}", *iter);
436 // for (const auto& signal : plane_signals)
437 // {
438 // // add_signal_t_sum(signal);
439 add_signal_t_diff(*iter);
440 add_spacial_local_constraint(plane_id, plane_signals);
441 // }
442 }
443 }
444
446 {
447 binary_data_writer_->close();
448 LOGP(info, "Launching pede algorithm..");
449 pede_launcher_.launch();
450 pede_launcher_.end();
451
452 par_result_.read();
454 fill_data_to_figure(hit_par);
455 }
456
458 {
459 const auto& pars = hit_par.GetListOfModulePar();
460 for (const auto& [module_num, par] : pars)
461 {
462 graph_time_offset_->SetPoint(static_cast<int>(module_num), module_num, par.t_diff.value);
463 graph_time_offset_->SetPointError(static_cast<int>(module_num), 0., par.t_diff.error);
464 graph_time_sync_->SetPoint(static_cast<int>(module_num), module_num, par.t_sync.value);
465 graph_time_sync_->SetPointError(static_cast<int>(module_num), 0., par.t_sync.error);
466 graph_effective_c_->SetPoint(static_cast<int>(module_num), module_num, par.effective_speed.value);
467 graph_effective_c_->SetPointError(static_cast<int>(module_num), 0., par.effective_speed.error);
468 }
469 }
470
471 void MillepedeEngine::EndOfEvent(unsigned int /*event_num*/)
472 {
473 // TODO: could be an empty event
474 binary_data_writer_->end();
475 data_preprocessor_->reset();
476 }
477
479
481 {
482 const auto module_size = GetModuleSize();
483
484 graph_time_offset_ = histograms.add_graph("time_offset", std::make_unique<TGraphErrors>(module_size));
485 graph_time_offset_->SetTitle("Time offset vs BarNum");
486
487 graph_time_sync_ = histograms.add_graph("time_sync", std::make_unique<TGraphErrors>(module_size));
488 graph_time_sync_->SetTitle("Time sync vs BarNum");
489
490 graph_effective_c_ = histograms.add_graph("effective_c", std::make_unique<TGraphErrors>(module_size));
491 graph_effective_c_->SetTitle("Effective c vs BarNum");
492
493 static constexpr auto RESIDUAL_BIN_NUM = 500;
494 hist_t_offset_residual_ = histograms.add_hist<TH1D>(
495 "t_diff_residual", "Residual values of the positios calculated from t_dff", RESIDUAL_BIN_NUM, 0., 1000.);
496 }
497
499 {
500 input_data_buffer_.locals.clear();
501 input_data_buffer_.globals.clear();
502 input_data_buffer_.measurement = 0.F;
503 input_data_buffer_.sigma = 0.F;
504 }
505
507
509 {
510 average_t_sum_.reset();
511 buffer_clear();
512 }
513
515 {
516 if (cal_to_hit_par_ == nullptr)
517 {
518 throw R3B::runtime_error("Pointer to cal_to_hit_par is nullptr!");
519 }
520 }
521
523 {
524 auto steer_writer = SteerWriter{};
525 steer_writer.set_working_dir(working_dir_);
526 steer_writer.set_filepath(pede_steer_filename_);
527 steer_writer.set_parameter_file(parameter_filename_);
528 steer_writer.set_data_filepath(input_data_filename_);
529 static constexpr auto NUMBER_OF_ITERARTION = 3.F;
530 static constexpr auto CONVERGENCE_RECOGNITION = 0.001F;
531 steer_writer.add_method(SteerWriter::Method::inversion,
532 std::make_pair(NUMBER_OF_ITERARTION, CONVERGENCE_RECOGNITION));
533 steer_writer.add_other_options(std::vector<std::string>{ "hugecut", "50000" });
534 steer_writer.add_other_options(std::vector<std::string>{ "outlierdownweighting", "4" });
535
536 // const auto module_size = GetModuleSize();
537 // for (int module_num{ 1 }; module_num <= module_size; ++module_num)
538 // {
539 // const auto& module_par = cal_to_hit_par_->GetModuleParAt(module_num);
540 // steer_writer.add_parameter_default(
541 // get_global_label_id(module_num, GlobalLabel::effective_c),
542 // // std::make_pair(module_par.effective_speed.value, module_par.effective_speed.error));
543 // std::make_pair(DEFAULT_EFFECTIVE_C, module_par.effective_speed.error));
544
545 // const auto offset_effective_c = module_par.t_diff * module_par.effective_speed;
546 // steer_writer.add_parameter_default(
547 // get_global_label_id(module_num, GlobalLabel::offset_effective_c),
548 // std::make_pair(offset_effective_c.value / SCALE_FACTOR, offset_effective_c.error / SCALE_FACTOR));
549 // }
550 // steer_writer.add_parameter_default(get_global_label_id(REFERENCE_BAR_NUM, GlobalLabel::tsync),
551 // std::make_pair(0.F, -1.F));
552 steer_writer.write();
553 }
554} // namespace R3B::Neuland::Calibration
constexpr auto SCALE_FACTOR
constexpr auto DEFAULT_T_ERROR
constexpr auto DEFAULT_RES_FILENAME
constexpr auto MILLE_BUFFER_SIZE
auto add_hist(std::unique_ptr< TH1 > hist) -> TH1 *
auto add_graph(std::string_view graph_name, std::unique_ptr< GraphType > graph) -> GraphType *
auto get_pars() const -> const auto &
auto GetListOfModulePar() const -> const std::unordered_map< int, ::R3B::Neuland::HitModulePar > &
auto to_module_num_label(int par_num) -> std::pair< int, GlobalLabel >
void Calibrate(Cal2HitPar &hit_par) override
void add_spacial_local_constraint(int plane_id, const std::vector< MilleCalData > &plane_signals)
auto set_minimum_values(const std::vector< R3B::Neuland::BarCalData > &signals) -> bool
void EndOfEvent(unsigned int event_num=0) override
auto select_t_diff_signal(const std::vector< MilleCalData > &plane_data)
static constexpr std::string_view DEFAULT_SUB_DIR
void fill_module_parameters(const Millepede::ResultReader &result, Neuland::Cal2HitPar &cal_to_hit_par)
std::unique_ptr< MilleDataProcessor > data_preprocessor_
void add_signal_t_sum(const MilleCalData &signal)
void add_signal_t_diff(const MilleCalData &signal)
auto SignalFilter(const std::vector< BarCalData > &signals) -> bool override
void AddSignals(const std::vector< BarCalData > &signals) override
auto get_global_label_id(int module_num, GlobalLabel label) -> int
void HistInit(DataMonitor &histograms) override
void set_working_dir(std::string_view dir)
Definition SteerWriter.h:51
STL class.
constexpr auto ModuleID2PlaneID(int moduleID) -> int
constexpr auto SQRT_12
constexpr auto BarLength
constexpr auto BarSize_XY
constexpr auto ModuleNum2ZPos(int module_num) -> T
constexpr auto IsPlaneIDHorizontal(int plane_id) -> bool
constexpr auto PlaneID2ZPos(int plane_id) -> T