File indexing completed on 2026-10-15 08:25:58
0001 #include "PHGarfieldRossegger.h"
0002
0003 #include <fun4all/Fun4AllReturnCodes.h>
0004 #include <phool/PHCompositeNode.h>
0005 #include <phool/phool.h>
0006
0007 #include <TFile.h>
0008 #include <TH2.h>
0009 #include <TH2D.h>
0010 #include <TH3D.h>
0011 #include <TNamed.h>
0012 #include <TObject.h>
0013
0014 #include <algorithm>
0015 #include <array>
0016 #include <cmath>
0017 #include <cstddef>
0018 #include <format>
0019 #include <fstream>
0020 #include <iostream>
0021 #include <limits>
0022 #include <memory>
0023 #include <numbers>
0024 #include <ranges>
0025 #include <regex>
0026 #include <sstream>
0027 #include <stdexcept>
0028 #include <utility>
0029
0030 namespace
0031 {
0032 constexpr double epsilon0 = 8.8541878128e-12;
0033 constexpr double cmToM = 1.0e-2;
0034 constexpr double mToCm = 100.0;
0035 constexpr double elementaryCharge = 1.602176634e-19;
0036 constexpr unsigned int nTpcLayers = 48;
0037 constexpr unsigned int nTpcSectors = 12;
0038 constexpr unsigned int nTpcModules = 3;
0039 constexpr unsigned int nTpcSides = 2;
0040 constexpr unsigned int nLayersPerModule = 16;
0041
0042 constexpr std::array<double, nTpcModules> firstLayerRadiusMm{{314.9835971194212, 416.5920253621078, 589.1096334932404}};
0043 constexpr std::array<double, nTpcModules> layerHeightMm{{5.657908935192669, 10.206891263554285, 10.970474549405283}};
0044 constexpr double r1GemInnerCm = 22.8;
0045
0046 struct FrameDimensions
0047 {
0048 double inner_r_min_cm;
0049 double inner_r_max_cm;
0050 double outer_r_min_cm;
0051 double outer_r_max_cm;
0052 double left_width_cm;
0053 double right_width_cm;
0054 };
0055
0056 constexpr unsigned int frameReferenceSector = 2;
0057 constexpr std::array<FrameDimensions, nTpcModules> frameDimensions{{{21.78, 22.20, 40.26, 40.61, 0.35, 0.52},
0058 {40.79, 41.14, 57.49, 57.84, 0.35, 0.52},
0059 {58.00, 58.35, 75.84, 76.28, 0.54, 0.54}}};
0060 constexpr double frameHalfAngle = 15.0 * std::numbers::pi / 180.0;
0061
0062 unsigned int src_index(unsigned int ir, unsigned int ip, unsigned int iz, unsigned int np, unsigned int nz)
0063 {
0064 return (ir * np + ip) * nz + iz;
0065 }
0066
0067 unsigned int fld_index(unsigned int ir, unsigned int ip, unsigned int iz, unsigned int np, unsigned int nz)
0068 {
0069 return (ir * np + ip) * nz + iz;
0070 }
0071
0072 unsigned int mode_index(unsigned int in, unsigned int il, unsigned int nl)
0073 {
0074 return in * nl + il;
0075 }
0076
0077 std::vector<double> edges(double lo, double hi, unsigned int n)
0078 {
0079 std::vector<double> out(n + 1);
0080 const double step = (hi - lo) / static_cast<double>(n);
0081 for (unsigned int i = 0; i <= n; ++i)
0082 {
0083 out[i] = lo + step * static_cast<double>(i);
0084 }
0085 return out;
0086 }
0087
0088 std::vector<double> centers(const std::vector<double>& e)
0089 {
0090 std::vector<double> out(e.size() - 1);
0091 for (unsigned int i = 0; i + 1 < e.size(); ++i)
0092 {
0093 out[i] = 0.5 * (e[i] + e[i + 1]);
0094 }
0095 return out;
0096 }
0097
0098 void append_edge_cm(std::vector<double>& edges_m, double value_cm)
0099 {
0100 edges_m.push_back(value_cm * cmToM);
0101 }
0102
0103 void append_uniform_edges_cm(std::vector<double>& edges_m, double lo_cm, double hi_cm, double max_step_cm)
0104 {
0105 if (hi_cm <= lo_cm)
0106 {
0107 return;
0108 }
0109 if (edges_m.empty())
0110 {
0111 append_edge_cm(edges_m, lo_cm);
0112 }
0113 const unsigned int n = std::max(1U, static_cast<unsigned int>(std::ceil((hi_cm - lo_cm) / max_step_cm)));
0114 for (unsigned int i = 1; i <= n; ++i)
0115 {
0116 append_edge_cm(edges_m, lo_cm + (hi_cm - lo_cm) * static_cast<double>(i) / static_cast<double>(n));
0117 }
0118 }
0119
0120 void sort_unique_edges(std::vector<double>& edges_m)
0121 {
0122 std::sort(edges_m.begin(), edges_m.end());
0123 edges_m.erase(std::unique(edges_m.begin(), edges_m.end(), [](double lhs, double rhs)
0124 { return std::fabs(lhs - rhs) < 1.0e-12; }),
0125 edges_m.end());
0126 }
0127
0128 std::vector<double> frame_observation_r_edges(double inner_cm, double outer_cm)
0129 {
0130 constexpr double fine_padding_cm = 1.20;
0131 std::vector<double> breakpoints{inner_cm, outer_cm};
0132 for (const FrameDimensions& f : frameDimensions)
0133 {
0134 for (const double boundary_cm : {f.inner_r_min_cm, f.inner_r_max_cm, f.outer_r_min_cm, f.outer_r_max_cm})
0135 {
0136 breakpoints.push_back(std::clamp(boundary_cm - fine_padding_cm, inner_cm, outer_cm));
0137 breakpoints.push_back(std::clamp(boundary_cm, inner_cm, outer_cm));
0138 breakpoints.push_back(std::clamp(boundary_cm + fine_padding_cm, inner_cm, outer_cm));
0139 }
0140 }
0141 std::sort(breakpoints.begin(), breakpoints.end());
0142 breakpoints.erase(std::unique(breakpoints.begin(), breakpoints.end(), [](double lhs, double rhs)
0143 { return std::fabs(lhs - rhs) < 1.0e-9; }),
0144 breakpoints.end());
0145
0146 std::vector<double> out;
0147 for (unsigned int i = 0; i + 1 < breakpoints.size(); ++i)
0148 {
0149 const double lo_cm = breakpoints[i];
0150 const double hi_cm = breakpoints[i + 1];
0151 const double mid_cm = 0.5 * (lo_cm + hi_cm);
0152 bool fine = false;
0153 for (const FrameDimensions& f : frameDimensions)
0154 {
0155 fine = fine || (mid_cm >= f.inner_r_min_cm - fine_padding_cm && mid_cm <= f.inner_r_max_cm + fine_padding_cm) ||
0156 (mid_cm >= f.outer_r_min_cm - fine_padding_cm && mid_cm <= f.outer_r_max_cm + fine_padding_cm);
0157 }
0158 append_uniform_edges_cm(out, lo_cm, hi_cm, fine ? 0.15 : 0.75);
0159 }
0160 sort_unique_edges(out);
0161 return out;
0162 }
0163
0164 std::vector<double> frame_observation_z_edges(double half_length_cm)
0165 {
0166 const double near_padplane_cm = std::max(0.0, half_length_cm - 2.0);
0167 const double transition_cm = std::max(0.0, half_length_cm - 10.0);
0168 std::vector<double> out;
0169 append_uniform_edges_cm(out, 0.0, transition_cm, 5.0);
0170 append_uniform_edges_cm(out, transition_cm, near_padplane_cm, 0.50);
0171 append_uniform_edges_cm(out, near_padplane_cm, half_length_cm, 0.08);
0172 sort_unique_edges(out);
0173 return out;
0174 }
0175
0176 double frame_sector_center(unsigned int side, unsigned int sector, double frame_reference_phi)
0177 {
0178 constexpr double sector_width = std::numbers::pi / 6.0;
0179 const int dsector = static_cast<int>(sector) - static_cast<int>(frameReferenceSector);
0180 double phi = side == 1U ? frame_reference_phi + dsector * sector_width : -frame_reference_phi - dsector * sector_width;
0181 const double twopi = 2.0 * std::numbers::pi;
0182 phi = std::fmod(phi, twopi);
0183 return phi < 0.0 ? phi + twopi : phi;
0184 }
0185
0186 double local_frame_phi(unsigned int side, unsigned int sector, double global_phi, double frame_reference_phi)
0187 {
0188 const double center = frame_sector_center(side, sector, frame_reference_phi);
0189 const double delta = std::atan2(std::sin(global_phi - center), std::cos(global_phi - center));
0190 return side == 1U ? delta : -delta;
0191 }
0192
0193 std::vector<double> frame_phi_edges(unsigned int , unsigned int side, double frame_reference_phi, double lo)
0194 {
0195 double fine_half_width = 0.0;
0196 for (const FrameDimensions& f : frameDimensions)
0197 {
0198 fine_half_width = std::max(fine_half_width, std::asin(f.left_width_cm / f.inner_r_max_cm));
0199 fine_half_width = std::max(fine_half_width, std::asin(f.right_width_cm / f.inner_r_max_cm));
0200 }
0201 constexpr double fine_step = 0.0015;
0202 constexpr double coarse_step = 0.05;
0203 const double hi = lo + 2.0 * std::numbers::pi;
0204 const double twopi = 2.0 * std::numbers::pi;
0205
0206 auto shift_into_range = [lo, twopi](double phi)
0207 {
0208 while (phi < lo)
0209 {
0210 phi += twopi;
0211 }
0212 while (phi >= lo + twopi)
0213 {
0214 phi -= twopi;
0215 }
0216 return phi;
0217 };
0218
0219 std::vector<double> breakpoints{lo, hi};
0220 auto add_edge = [&breakpoints, shift_into_range, lo, hi, fine_half_width](double phi)
0221 {
0222 const double folded = shift_into_range(phi);
0223 for (const double edge : {folded - fine_half_width, folded, folded + fine_half_width})
0224 {
0225 const double shifted = shift_into_range(edge);
0226 breakpoints.push_back(shifted);
0227 if (shifted < lo + fine_half_width)
0228 {
0229 breakpoints.push_back(shifted + 2.0 * std::numbers::pi);
0230 }
0231 if (shifted > hi - fine_half_width)
0232 {
0233 breakpoints.push_back(shifted - 2.0 * std::numbers::pi);
0234 }
0235 }
0236 };
0237
0238 for (unsigned int sector = 0; sector < nTpcSectors; ++sector)
0239 {
0240 const double center = frame_sector_center(side, sector, frame_reference_phi);
0241 add_edge(center - frameHalfAngle);
0242 add_edge(center + frameHalfAngle);
0243 }
0244
0245 std::sort(breakpoints.begin(), breakpoints.end());
0246 breakpoints.erase(std::unique(breakpoints.begin(), breakpoints.end(), [](double lhs, double rhs)
0247 { return std::fabs(lhs - rhs) < 1.0e-12; }),
0248 breakpoints.end());
0249
0250 std::vector<double> out;
0251 for (unsigned int i = 0; i + 1 < breakpoints.size(); ++i)
0252 {
0253 const double plo = std::clamp(breakpoints[i], lo, hi);
0254 const double phi_hi = std::clamp(breakpoints[i + 1], lo, hi);
0255 if (phi_hi <= plo)
0256 {
0257 continue;
0258 }
0259 const double mid = 0.5 * (plo + phi_hi);
0260 bool fine = false;
0261 for (unsigned int sector = 0; sector < nTpcSectors; ++sector)
0262 {
0263 const double center = frame_sector_center(side, sector, frame_reference_phi);
0264 for (const double boundary : {center - frameHalfAngle, center + frameHalfAngle})
0265 {
0266 const double folded = shift_into_range(boundary);
0267 const double delta = std::atan2(std::sin(mid - folded), std::cos(mid - folded));
0268 fine = fine || std::fabs(delta) <= fine_half_width;
0269 }
0270 }
0271 if (out.empty())
0272 {
0273 out.push_back(plo);
0274 }
0275 const unsigned int n = std::max(1U, static_cast<unsigned int>(std::ceil((phi_hi - plo) / (fine ? fine_step : coarse_step))));
0276 for (unsigned int j = 1; j <= n; ++j)
0277 {
0278 out.push_back(plo + (phi_hi - plo) * static_cast<double>(j) / static_cast<double>(n));
0279 }
0280 }
0281 sort_unique_edges(out);
0282 if (!out.empty())
0283 {
0284 out.front() = lo;
0285 out.back() = hi;
0286 }
0287 return out;
0288 }
0289
0290 std::vector<double> phi_edges(unsigned int n, double lo = 0.0)
0291 {
0292 std::vector<double> out(n + 1);
0293 const double step = 2.0 * std::numbers::pi / static_cast<double>(n);
0294 for (unsigned int i = 0; i <= n; ++i)
0295 {
0296 out[i] = lo + static_cast<double>(i) * step;
0297 }
0298 return out;
0299 }
0300
0301 std::vector<double> phi_centers(unsigned int n, double lo = 0.0)
0302 {
0303 std::vector<double> out(n);
0304 const double step = 2.0 * std::numbers::pi / static_cast<double>(n);
0305 for (unsigned int i = 0; i < n; ++i)
0306 {
0307 out[i] = lo + (static_cast<double>(i) + 0.5) * step;
0308 }
0309 return out;
0310 }
0311
0312 double wrap_phi(double phi)
0313 {
0314 const double twopi = 2.0 * std::numbers::pi;
0315 phi = std::fmod(phi, twopi);
0316 return phi < 0.0 ? phi + twopi : phi;
0317 }
0318
0319 double wrapped_positive_delta(double phi_to, double phi_from)
0320 {
0321 return wrap_phi(phi_to - phi_from);
0322 }
0323
0324 double wrapped_delta_phi(double phi_observation, double phi_source)
0325 {
0326 return std::atan2(std::sin(phi_observation - phi_source), std::cos(phi_observation - phi_source));
0327 }
0328
0329 std::vector<std::pair<double, double>> periodic_segments(double start, double width)
0330 {
0331 const double twopi = 2.0 * std::numbers::pi;
0332 start = wrap_phi(start);
0333 const double end = start + width;
0334 if (end <= twopi + 1.0e-14)
0335 {
0336 return {{start, std::min(end, twopi)}};
0337 }
0338 return {{start, twopi}, {0.0, end - twopi}};
0339 }
0340
0341 double segment_overlap_length(const std::vector<std::pair<double, double>>& a, const std::vector<std::pair<double, double>>& b)
0342 {
0343 double total = 0.0;
0344 for (const auto& [alo, ahi] : a)
0345 {
0346 for (const auto& [blo, bhi] : b)
0347 {
0348 total += std::max(0.0, std::min(ahi, bhi) - std::max(alo, blo));
0349 }
0350 }
0351 return total;
0352 }
0353
0354 double jprime(unsigned int m, double x)
0355 {
0356 return m == 0 ? -std::cyl_bessel_j(1, x) : 0.5 * (std::cyl_bessel_j(m - 1, x) - std::cyl_bessel_j(m + 1, x));
0357 }
0358
0359 double yprime(unsigned int m, double x)
0360 {
0361 return m == 0 ? -std::cyl_neumann(1, x) : 0.5 * (std::cyl_neumann(m - 1, x) - std::cyl_neumann(m + 1, x));
0362 }
0363
0364 double sinh_ratio(double k, double z, double len)
0365 {
0366 const double klen = k * len;
0367 if (klen < 50.0)
0368 {
0369 return std::sinh(k * z) / std::sinh(klen);
0370 }
0371 return std::exp(k * (z - len)) * (1.0 - std::exp(-2.0 * k * z)) / (1.0 - std::exp(-2.0 * klen));
0372 }
0373
0374 double dz_sinh_ratio(double k, double z, double len)
0375 {
0376 const double klen = k * len;
0377 if (klen < 50.0)
0378 {
0379 return k * std::cosh(k * z) / std::sinh(klen);
0380 }
0381 return k * std::exp(k * (z - len)) * (1.0 + std::exp(-2.0 * k * z)) / (1.0 - std::exp(-2.0 * klen));
0382 }
0383
0384 void scale_to_cm(std::vector<double>& values)
0385 {
0386 for (double& value : values)
0387 {
0388 value *= mToCm;
0389 }
0390 }
0391 }
0392
0393 PHGarfieldRossegger::PHGarfieldRossegger(const std::string& name)
0394 : SubsysReco(name)
0395 {
0396 }
0397
0398 void PHGarfieldRossegger::setGeometryCm(double inner_radius, double outer_radius, double half_length)
0399 {
0400 m_aCm = inner_radius;
0401 m_bCm = outer_radius;
0402 m_lCm = half_length;
0403 }
0404
0405 void PHGarfieldRossegger::setSourceRadiusCm(double min_radius, double max_radius)
0406 {
0407 m_sourceRMinCm = min_radius;
0408 m_sourceRMaxCm = max_radius;
0409 }
0410
0411 void PHGarfieldRossegger::setDensity(double reference_density_nC_per_m3, double k_eff, double radial_power_alpha)
0412 {
0413 m_rhoReferenceNCPerM3 = reference_density_nC_per_m3;
0414 m_kEff = k_eff;
0415 m_radialPowerAlpha = radial_power_alpha;
0416 }
0417
0418 void PHGarfieldRossegger::setPhiModulation(double m1_amplitude, double m1_phase, double m12_amplitude, double m12_phase)
0419 {
0420 m_m1Amplitude = m1_amplitude;
0421 m_m1Phase = m1_phase;
0422 m_m12Amplitude = m12_amplitude;
0423 m_m12Phase = m12_phase;
0424 }
0425
0426 void PHGarfieldRossegger::setSourceGrid(unsigned int nr, unsigned int nphi, unsigned int nz)
0427 {
0428 std::cout << "setSourceGrid nr nphi nz" << nr << " " << nphi << " " << nz << std::endl;
0429 m_nrSource = nr;
0430 m_nphiSource = nphi;
0431 m_nzSource = nz;
0432 }
0433
0434 void PHGarfieldRossegger::setObservationGrid(unsigned int nr, unsigned int nphi, unsigned int nz)
0435 {
0436 m_nrObs = nr;
0437 m_nphiObs = nphi;
0438 m_nzObs = nz;
0439 }
0440
0441 void PHGarfieldRossegger::setModeTruncation(unsigned int m_phi_max, unsigned int n_radial_modes, unsigned int n_longitudinal_modes)
0442 {
0443 m_mPhiMax = m_phi_max;
0444 m_nRadialModes = n_radial_modes;
0445 m_nLongitudinalModes = n_longitudinal_modes;
0446 }
0447
0448 void PHGarfieldRossegger::setRadialJob(unsigned int job_index, unsigned int n_jobs)
0449 {
0450 m_jobIndex = job_index;
0451 m_nJobs = std::max(1U, n_jobs);
0452 }
0453
0454 void PHGarfieldRossegger::setGainHistograms(const std::string& side0_histogram, const std::string& side1_histogram)
0455 {
0456 m_gainHistograms = {{side0_histogram, side1_histogram}};
0457 }
0458
0459 void PHGarfieldRossegger::setDensityMapFile(const std::string& filename, const std::string& side0_histogram, const std::string& side1_histogram)
0460 {
0461 m_densityMapFile = filename;
0462 m_densityMapHistograms = {{side0_histogram, side1_histogram}};
0463 }
0464
0465 void PHGarfieldRossegger::setPHGarfieldField3DOutputFiles(const std::string& side0_filename, const std::string& side1_filename)
0466 {
0467 m_phGarfieldField3DOutputFiles = {{side0_filename, side1_filename}};
0468 }
0469
0470 int PHGarfieldRossegger::Init(PHCompositeNode* )
0471 {
0472 return InitRun(nullptr);
0473 }
0474
0475 int PHGarfieldRossegger::InitRun(PHCompositeNode* )
0476 {
0477 if (m_done)
0478 {
0479 return Fun4AllReturnCodes::EVENT_OK;
0480 }
0481 const int status = calculate();
0482 m_done = status == Fun4AllReturnCodes::EVENT_OK;
0483 return status;
0484 }
0485
0486 int PHGarfieldRossegger::process_event(PHCompositeNode* )
0487 {
0488 return Fun4AllReturnCodes::EVENT_OK;
0489 }
0490
0491 bool PHGarfieldRossegger::validateConfig() const
0492 {
0493 const double a = m_aCm * cmToM;
0494 const double b = m_bCm * cmToM;
0495 const double smin = m_sourceRMinCm * cmToM;
0496 const double smax = m_sourceRMaxCm * cmToM;
0497 const bool valid_source_radii = m_useDensityMap ? (0.0 < a && a <= smin && smin < smax && smax <= b) : (0.0 < a && a < smin && smin < smax && smax <= b);
0498 if (!m_useFrameChargeModel && !valid_source_radii)
0499 {
0500 std::cout << PHWHERE << " invalid Rossegger geometry/source radii" << std::endl;
0501 return false;
0502 }
0503 if (m_useDensityMap && (m_useFrameChargeModel || m_useRealTpcSourceGeometry || m_densityMapFile.empty()))
0504 {
0505 std::cout << PHWHERE << " density-map mode is incompatible with frame/real-TPC source modes or has no input file" << std::endl;
0506 return false;
0507 }
0508 if (m_lCm <= 0.0 || m_nrSource == 0 || m_nphiSource == 0 || m_nzSource == 0 ||
0509 m_nrObs == 0 || m_nphiObs == 0 || m_nzObs == 0 || m_nRadialModes == 0 || m_nLongitudinalModes == 0)
0510 {
0511 std::cout << PHWHERE << " invalid Rossegger grid or mode configuration" << std::endl;
0512 return false;
0513 }
0514 if (m_jobIndex >= m_nJobs)
0515 {
0516 std::cout << PHWHERE << " job index outside number of jobs" << std::endl;
0517 return false;
0518 }
0519 if (m_tpcSide >= nTpcSides)
0520 {
0521 std::cout << PHWHERE << " TPC side must be 0 or 1" << std::endl;
0522 return false;
0523 }
0524 return true;
0525 }
0526
0527 unsigned int PHGarfieldRossegger::effectivePhiMax() const
0528 {
0529 if (m_useDensityMap || m_useRealTpcSourceGeometry || m_useFrameChargeModel)
0530 {
0531 return m_mPhiMax;
0532 }
0533 const bool uniform = std::fabs(m_m1Amplitude) < 1.0e-15 && std::fabs(m_m12Amplitude) < 1.0e-15;
0534 return (m_autoAxisymmetric && uniform) ? 0 : m_mPhiMax;
0535 }
0536
0537 std::pair<unsigned int, unsigned int> PHGarfieldRossegger::radialRange(unsigned int nr_obs) const
0538 {
0539 return {(nr_obs * m_jobIndex) / m_nJobs, (nr_obs * (m_jobIndex + 1)) / m_nJobs};
0540 }
0541
0542 double PHGarfieldRossegger::radialBoundaryFunction(double kval, unsigned int mode_m) const
0543 {
0544 const double a = m_aCm * cmToM;
0545 const double b = m_bCm * cmToM;
0546 return std::cyl_bessel_j(mode_m, kval * b) * std::cyl_neumann(mode_m, kval * a) -
0547 std::cyl_neumann(mode_m, kval * b) * std::cyl_bessel_j(mode_m, kval * a);
0548 }
0549
0550 double PHGarfieldRossegger::radialBasis(double kval, unsigned int mode_m, double radius_m) const
0551 {
0552 const double a = m_aCm * cmToM;
0553 return std::cyl_bessel_j(mode_m, kval * radius_m) * std::cyl_neumann(mode_m, kval * a) -
0554 std::cyl_neumann(mode_m, kval * radius_m) * std::cyl_bessel_j(mode_m, kval * a);
0555 }
0556
0557 double PHGarfieldRossegger::radialBasisDerivative(double kval, unsigned int mode_m, double radius_m) const
0558 {
0559 const double a = m_aCm * cmToM;
0560 return kval * (jprime(mode_m, kval * radius_m) * std::cyl_neumann(mode_m, kval * a) -
0561 yprime(mode_m, kval * radius_m) * std::cyl_bessel_j(mode_m, kval * a));
0562 }
0563
0564 std::vector<double> PHGarfieldRossegger::findRadialRoots(unsigned int mode_m, unsigned int n_roots) const
0565 {
0566 const double width = (m_bCm - m_aCm) * cmToM;
0567 const double step = 0.20 * std::numbers::pi / width;
0568 double left_k = std::max(1.0e-6, 0.10 * std::numbers::pi / width);
0569 double left_val = radialBoundaryFunction(left_k, mode_m);
0570 const double max_k = static_cast<double>(n_roots + mode_m + 20) * std::numbers::pi / width;
0571 std::vector<double> roots;
0572
0573 for (double right_k = left_k + step; right_k <= max_k && roots.size() < n_roots; right_k += step)
0574 {
0575 const double right_val = radialBoundaryFunction(right_k, mode_m);
0576 if (std::isfinite(left_val) && std::isfinite(right_val) && left_val * right_val < 0.0)
0577 {
0578 double lo = right_k - step;
0579 double hi = right_k;
0580 double flo = radialBoundaryFunction(lo, mode_m);
0581 for (unsigned int iter = 0; iter < 120; ++iter)
0582 {
0583 const double mid = 0.5 * (lo + hi);
0584 const double fmid = radialBoundaryFunction(mid, mode_m);
0585 if (std::fabs(fmid) < 1.0e-14 || std::fabs(hi - lo) < 1.0e-12)
0586 {
0587 lo = mid;
0588 hi = mid;
0589 break;
0590 }
0591 if (flo * fmid <= 0.0)
0592 {
0593 hi = mid;
0594 }
0595 else
0596 {
0597 lo = mid;
0598 flo = fmid;
0599 }
0600 }
0601 const double root = 0.5 * (lo + hi);
0602 if (roots.empty() || std::fabs(root - roots.back()) > 1.0e-8)
0603 {
0604 roots.push_back(root);
0605 }
0606 }
0607 left_k = right_k;
0608 left_val = right_val;
0609 }
0610 if (roots.size() != n_roots)
0611 {
0612 throw std::runtime_error(std::format("Found {} roots for m={}, requested {}", roots.size(), mode_m, n_roots));
0613 }
0614 return roots;
0615 }
0616
0617 std::vector<double> PHGarfieldRossegger::legendreRootsAndWeights(unsigned int n_points, std::vector<double>& weights) const
0618 {
0619 std::vector<double> roots(n_points);
0620 weights.assign(n_points, 0.0);
0621 const unsigned int half = (n_points + 1) / 2;
0622 for (unsigned int i = 0; i < half; ++i)
0623 {
0624 double z = std::cos(std::numbers::pi * (static_cast<double>(i) + 0.75) / (static_cast<double>(n_points) + 0.5));
0625 double old_z = 0.0;
0626 double pp = 0.0;
0627 while (std::fabs(z - old_z) > 1.0e-15)
0628 {
0629 double p1 = 1.0;
0630 double p2 = 0.0;
0631 for (unsigned int j = 1; j <= n_points; ++j)
0632 {
0633 const double p3 = p2;
0634 p2 = p1;
0635 p1 = ((2.0 * static_cast<double>(j) - 1.0) * z * p2 - (static_cast<double>(j) - 1.0) * p3) / static_cast<double>(j);
0636 }
0637 pp = static_cast<double>(n_points) * (z * p1 - p2) / (z * z - 1.0);
0638 old_z = z;
0639 z = old_z - p1 / pp;
0640 }
0641 roots[i] = -z;
0642 roots[n_points - 1 - i] = z;
0643 const double weight = 2.0 / ((1.0 - z * z) * pp * pp);
0644 weights[i] = weight;
0645 weights[n_points - 1 - i] = weight;
0646 }
0647 return roots;
0648 }
0649
0650 PHGarfieldRossegger::SourceGrid PHGarfieldRossegger::buildSourceGrid() const
0651 {
0652 if (m_useFrameChargeModel)
0653 {
0654 SourceGrid source_grid;
0655 std::vector<double>& redges = source_grid.r_edges_m;
0656 constexpr double max_step_cm = 0.25;
0657
0658 auto add_interval = [&redges](double rmin_cm, double rmax_cm)
0659 {
0660 redges.push_back(rmin_cm * cmToM);
0661
0662 const double width_cm = rmax_cm - rmin_cm;
0663 const unsigned int n = std::max(1U, static_cast<unsigned int>(std::ceil(width_cm / max_step_cm)));
0664 for (unsigned int i = 1; i < n; ++i)
0665 {
0666 const double r_cm = rmin_cm + width_cm * static_cast<double>(i) / static_cast<double>(n);
0667 redges.push_back(r_cm * cmToM);
0668 }
0669
0670 redges.push_back(rmax_cm * cmToM);
0671 };
0672
0673 for (const FrameDimensions& f : frameDimensions)
0674 {
0675 add_interval(f.inner_r_min_cm, f.inner_r_max_cm);
0676 add_interval(f.inner_r_max_cm, f.outer_r_min_cm);
0677 add_interval(f.outer_r_min_cm, f.outer_r_max_cm);
0678 }
0679 std::sort(redges.begin(), redges.end());
0680 redges.erase(std::unique(redges.begin(), redges.end(), [](double lhs, double rhs)
0681 { return std::fabs(lhs - rhs) < 1.0e-12; }),
0682 redges.end());
0683 if (!(m_aCm * cmToM <= redges.front() && redges.back() <= m_bCm * cmToM))
0684 {
0685 throw std::runtime_error("Frame source radial grid is outside Rossegger field cage geometry");
0686 }
0687 source_grid.r_centers_m = centers(redges);
0688 source_grid.module_index.assign(source_grid.r_centers_m.size(), -1);
0689 source_grid.layer_index.assign(source_grid.r_centers_m.size(), -1);
0690 source_grid.is_antenna.assign(source_grid.r_centers_m.size(), false);
0691 for (unsigned int ir = 0; ir < source_grid.r_centers_m.size(); ++ir)
0692 {
0693 const double rcm = source_grid.r_centers_m[ir] * mToCm;
0694 for (unsigned int module = 0; module < frameDimensions.size(); ++module)
0695 {
0696 const FrameDimensions& f = frameDimensions[module];
0697 if (rcm >= f.inner_r_min_cm && rcm <= f.outer_r_max_cm)
0698 {
0699 source_grid.module_index[ir] = module;
0700 source_grid.layer_index[ir] = module;
0701 break;
0702 }
0703 }
0704 }
0705 return source_grid;
0706 }
0707
0708 if (!m_useDensityMap && !m_useRealTpcSourceGeometry)
0709 {
0710 SourceGrid source_grid;
0711 source_grid.r_edges_m = edges(m_sourceRMinCm * cmToM, m_sourceRMaxCm * cmToM, m_nrSource);
0712 source_grid.r_centers_m = centers(source_grid.r_edges_m);
0713 source_grid.module_index.assign(source_grid.r_centers_m.size(), 0);
0714 source_grid.layer_index.assign(source_grid.r_centers_m.size(), 0);
0715 source_grid.is_antenna.assign(source_grid.r_centers_m.size(), false);
0716 return source_grid;
0717 }
0718
0719 std::array<double, nTpcModules> first_cm{};
0720 std::array<double, nTpcModules> pitch_cm{};
0721 std::array<double, nTpcModules> first_low{};
0722 for (unsigned int module = 0; module < nTpcModules; ++module)
0723 {
0724 first_cm[module] = firstLayerRadiusMm[module] / 10.0;
0725 pitch_cm[module] = layerHeightMm[module] / 10.0;
0726 first_low[module] = first_cm[module] - 0.5 * pitch_cm[module];
0727 }
0728
0729 SourceGrid source_grid;
0730 std::vector<double>& redges = source_grid.r_edges_m;
0731
0732
0733 redges.push_back(r1GemInnerCm * cmToM);
0734 std::vector<double> inner_r1_edges_cm;
0735 for (double edge_cm = first_low[0] - pitch_cm[0]; edge_cm > r1GemInnerCm + 1.0e-12; edge_cm -= pitch_cm[0])
0736 {
0737 inner_r1_edges_cm.push_back(edge_cm);
0738 }
0739 for (double& iter : std::ranges::reverse_view(inner_r1_edges_cm))
0740 {
0741 redges.push_back(iter * cmToM);
0742 }
0743 redges.push_back(first_low[0] * cmToM);
0744
0745 for (unsigned int ilocal = 0; ilocal < nLayersPerModule; ++ilocal)
0746 {
0747 redges.push_back((first_cm[0] + (static_cast<double>(ilocal) + 0.5) * pitch_cm[0]) * cmToM);
0748 }
0749
0750 for (unsigned int module = 1; module < nTpcModules; ++module)
0751 {
0752 redges.push_back(first_low[module] * cmToM);
0753 for (unsigned int ilocal = 0; ilocal < nLayersPerModule; ++ilocal)
0754 {
0755 redges.push_back((first_cm[module] + (static_cast<double>(ilocal) + 0.5) * pitch_cm[module]) * cmToM);
0756 }
0757 }
0758
0759 for (unsigned int i = 1; i < redges.size(); ++i)
0760 {
0761 if (!(redges[i - 1] < redges[i]))
0762 {
0763 throw std::runtime_error("TPC source radial edges are not strictly increasing");
0764 }
0765 }
0766 if (!(m_aCm * cmToM < redges.front() && redges.back() <= m_bCm * cmToM))
0767 {
0768 throw std::runtime_error("TPC source radial grid is outside Rossegger field cage geometry");
0769 }
0770
0771 double min_width_cm = (redges[1] - redges[0]) * mToCm;
0772 double max_width_cm = min_width_cm;
0773 for (unsigned int i = 1; i < redges.size(); ++i)
0774 {
0775 const double width_cm = (redges[i] - redges[i - 1]) * mToCm;
0776 min_width_cm = std::min(min_width_cm, width_cm);
0777 max_width_cm = std::max(max_width_cm, width_cm);
0778 }
0779 std::cout << " real-TPC source radial bins = " << (redges.size() - 1)
0780 << ", bin width min/max = " << min_width_cm << "/" << max_width_cm << " cm" << std::endl;
0781
0782 source_grid.r_centers_m = centers(redges);
0783 const unsigned int nr = source_grid.r_centers_m.size();
0784 source_grid.module_index.assign(nr, -1);
0785 source_grid.layer_index.assign(nr, -1);
0786 source_grid.is_antenna.assign(nr, false);
0787
0788 for (unsigned int ir = 0; ir < nr; ++ir)
0789 {
0790 const double rcm = source_grid.r_centers_m[ir] * mToCm;
0791 if (rcm >= r1GemInnerCm && rcm < first_low[0])
0792 {
0793 source_grid.module_index[ir] = 0;
0794 source_grid.layer_index[ir] = 0;
0795 source_grid.is_antenna[ir] = true;
0796 continue;
0797 }
0798
0799 for (unsigned int module = 0; module < nTpcModules; ++module)
0800 {
0801 for (unsigned int ilocal = 0; ilocal < nLayersPerModule; ++ilocal)
0802 {
0803 const double lo = first_cm[module] + (static_cast<double>(ilocal) - 0.5) * pitch_cm[module];
0804 const double hi = first_cm[module] + (static_cast<double>(ilocal) + 0.5) * pitch_cm[module];
0805 if (rcm >= lo - 1.0e-12 && rcm < hi - 1.0e-12)
0806 {
0807 source_grid.module_index[ir] = module;
0808 source_grid.layer_index[ir] = module * nLayersPerModule + ilocal;
0809 }
0810 }
0811 }
0812 }
0813
0814 return source_grid;
0815 }
0816
0817 PHGarfieldRossegger::PadGeometry PHGarfieldRossegger::parsePadPlacementFile() const
0818 {
0819 PadGeometry geometry{};
0820 std::array<std::array<std::array<bool, nTpcSectors>, nTpcSides>, nTpcModules> seen{};
0821
0822 std::ifstream input(m_padPlacementFile);
0823 if (!input.is_open())
0824 {
0825 throw std::runtime_error("Could not open pad placement file " + m_padPlacementFile);
0826 }
0827
0828 const std::regex region_pattern(R"(region\s+(\d+)\s+first_layer\s+(\d+)\s+pads_per_sector\s+(\d+)\s+total_phibins\s+(\d+))");
0829 const std::regex entry_pattern(R"(side\s+([01])\s+sector\s+(\d+)\s+first_pad\s+(\d+)\s+first_phi\s+([-+0-9.eE]+)\s+last_pad\s+(\d+)\s+last_phi\s+([-+0-9.eE]+))");
0830
0831 int current_region = -1;
0832 std::string line;
0833 std::smatch match;
0834 while (std::getline(input, line))
0835 {
0836 if (std::regex_search(line, match, region_pattern))
0837 {
0838 current_region = std::stoi(match[1].str());
0839 if (current_region < 0 || current_region >= static_cast<int>(nTpcModules))
0840 {
0841 throw std::runtime_error("Pad placement file has invalid region " + std::to_string(current_region));
0842 }
0843 continue;
0844 }
0845
0846 if (current_region >= 0 && std::regex_search(line, match, entry_pattern))
0847 {
0848 const unsigned int side = std::stoul(match[1].str());
0849 const unsigned int sector = std::stoul(match[2].str());
0850 const int first_pad = std::stoi(match[3].str());
0851 const double first_phi = std::stod(match[4].str());
0852 const int last_pad = std::stoi(match[5].str());
0853 const double last_phi = std::stod(match[6].str());
0854 if (side >= nTpcSides || sector >= nTpcSectors)
0855 {
0856 throw std::runtime_error("Pad placement file has invalid side/sector");
0857 }
0858 const int n_steps = last_pad - first_pad;
0859 if (n_steps <= 0)
0860 {
0861 throw std::runtime_error("Pad placement file has invalid pad indices");
0862 }
0863
0864 const double center_delta = wrapped_delta_phi(last_phi, first_phi);
0865 const double pad_pitch = center_delta / static_cast<double>(n_steps);
0866 const double start_edge = wrap_phi(first_phi - 0.5 * pad_pitch);
0867 const double end_edge = wrap_phi(last_phi + 0.5 * pad_pitch);
0868 const int direction = pad_pitch > 0.0 ? 1 : -1;
0869 const double angular_width = direction > 0 ? wrapped_positive_delta(end_edge, start_edge) : wrapped_positive_delta(start_edge, end_edge);
0870
0871 geometry[current_region][side][sector] = {start_edge, angular_width, direction};
0872 seen[current_region][side][sector] = true;
0873 }
0874 }
0875
0876 unsigned int nseen = 0;
0877 for (const auto& by_side : seen)
0878 {
0879 for (const auto& by_sector : by_side)
0880 {
0881 for (const bool value : by_sector)
0882 {
0883 if (value)
0884 {
0885 ++nseen;
0886 }
0887 }
0888 }
0889 }
0890 if (nseen != nTpcModules * nTpcSides * nTpcSectors)
0891 {
0892 throw std::runtime_error(std::format("Expected {} pad geometry entries, found {}", nTpcModules * nTpcSides * nTpcSectors, nseen));
0893 }
0894
0895 return geometry;
0896 }
0897
0898 PHGarfieldRossegger::GainMap PHGarfieldRossegger::readGainMap() const
0899 {
0900 GainMap gain{};
0901 std::unique_ptr<TFile> input(TFile::Open(m_gainMapFile.c_str(), "READ"));
0902 if (!input || input->IsZombie())
0903 {
0904 throw std::runtime_error("Could not open gain ROOT file " + m_gainMapFile);
0905 }
0906
0907 for (unsigned int side = 0; side < nTpcSides; ++side)
0908 {
0909 auto* hist = dynamic_cast<TH2*>(input->Get(m_gainHistograms[side].c_str()));
0910 if (!hist)
0911 {
0912 throw std::runtime_error("Missing gain histogram " + m_gainHistograms[side] + " in " + m_gainMapFile);
0913 }
0914 if (hist->GetNbinsX() != static_cast<int>(nTpcSectors) || hist->GetNbinsY() != static_cast<int>(nTpcLayers))
0915 {
0916 throw std::runtime_error(std::format("{} has ({},{}) bins; expected ({},{})", m_gainHistograms[side], hist->GetNbinsX(), hist->GetNbinsY(), nTpcSectors, nTpcLayers));
0917 }
0918 for (unsigned int sector = 0; sector < nTpcSectors; ++sector)
0919 {
0920 for (unsigned int layer = 0; layer < nTpcLayers; ++layer)
0921 {
0922 const double value = hist->GetBinContent(sector + 1, layer + 1);
0923 if (!std::isfinite(value) || value <= 0.0)
0924 {
0925 throw std::runtime_error(std::format("Invalid gain value for side {}, sector {}, layer {}", side, sector, layer));
0926 }
0927 gain[side][sector][layer] = value;
0928 }
0929 }
0930 }
0931 return gain;
0932 }
0933
0934 std::vector<double> PHGarfieldRossegger::makeChargeDensity(const SourceGrid& source_grid,
0935 const std::vector<double>& phi_source_edges,
0936 const std::vector<double>& z_source_edges_m,
0937 unsigned int side) const
0938 {
0939 const unsigned int nr = source_grid.r_centers_m.size();
0940 const unsigned int nphi = phi_source_edges.size() - 1;
0941 const unsigned int nz = z_source_edges_m.size() - 1;
0942 const double target_rho = m_kEff * m_rhoReferenceNCPerM3 * 1.0e-9;
0943
0944 std::vector<double> volumes(nr * nphi * nz, 0.0);
0945 std::vector<double> raw(nr * nphi * nz, 0.0);
0946 double volume_sum = 0.0;
0947
0948 if (m_useDensityMap)
0949 {
0950 std::unique_ptr<TFile> file(TFile::Open(m_densityMapFile.c_str(), "READ"));
0951 if (!file || file->IsZombie())
0952 {
0953 throw std::runtime_error("Cannot open density-map file " + m_densityMapFile);
0954 }
0955 auto* hist = dynamic_cast<TH2*>(file->Get(m_densityMapHistograms[side].c_str()));
0956 if (!hist)
0957 {
0958 throw std::runtime_error("Missing density histogram " + m_densityMapHistograms[side]);
0959 }
0960
0961 const auto ps = centers(phi_source_edges);
0962 const double xmin = hist->GetXaxis()->GetXmin();
0963 const double xmax = hist->GetXaxis()->GetXmax();
0964 const double period = xmax - xmin;
0965
0966 for (unsigned int ir = 0; ir < nr; ++ir)
0967 {
0968 const double rvol = 0.5 * (source_grid.r_edges_m[ir + 1] * source_grid.r_edges_m[ir + 1] - source_grid.r_edges_m[ir] * source_grid.r_edges_m[ir]);
0969 const double rcm = source_grid.r_centers_m[ir] * mToCm;
0970 for (unsigned int ip = 0; ip < nphi; ++ip)
0971 {
0972 double phi = ps[ip];
0973 while (phi < xmin)
0974 {
0975 phi += period;
0976 }
0977 while (phi >= xmax)
0978 {
0979 phi -= period;
0980 }
0981 const double value = std::max(0.0, hist->Interpolate(phi, rcm));
0982 for (unsigned int iz = 0; iz < nz; ++iz)
0983 {
0984 const unsigned int idx = src_index(ir, ip, iz, nphi, nz);
0985 volumes[idx] = rvol * (phi_source_edges[ip + 1] - phi_source_edges[ip]) * (z_source_edges_m[iz + 1] - z_source_edges_m[iz]);
0986 raw[idx] = value;
0987 volume_sum += volumes[idx];
0988 }
0989 }
0990 }
0991 }
0992 else if (!m_useRealTpcSourceGeometry)
0993 {
0994 const auto ps = phi_centers(nphi);
0995 for (unsigned int ir = 0; ir < nr; ++ir)
0996 {
0997 const double rvol = 0.5 * (source_grid.r_edges_m[ir + 1] * source_grid.r_edges_m[ir + 1] - source_grid.r_edges_m[ir] * source_grid.r_edges_m[ir]);
0998 const double rshape = std::pow(source_grid.r_edges_m.front() / source_grid.r_centers_m[ir], m_radialPowerAlpha);
0999 for (unsigned int ip = 0; ip < nphi; ++ip)
1000 {
1001 const double pshape = 1.0 + m_m1Amplitude * std::cos(ps[ip] - m_m1Phase) + m_m12Amplitude * std::cos(12.0 * (ps[ip] - m_m12Phase));
1002 if (pshape < 0.0)
1003 {
1004 throw std::runtime_error("Phi modulation makes rho negative");
1005 }
1006 for (unsigned int iz = 0; iz < nz; ++iz)
1007 {
1008 const unsigned int idx = src_index(ir, ip, iz, nphi, nz);
1009 volumes[idx] = rvol * (phi_source_edges[ip + 1] - phi_source_edges[ip]) * (z_source_edges_m[iz + 1] - z_source_edges_m[iz]);
1010 raw[idx] = rshape * pshape;
1011 volume_sum += volumes[idx];
1012 }
1013 }
1014 }
1015 }
1016 else
1017 {
1018 const PadGeometry pad_geometry = parsePadPlacementFile();
1019 const GainMap gain = readGainMap();
1020 const auto ps = phi_centers(nphi);
1021 std::vector<double> geometry_fraction(nr * nphi, 0.0);
1022 std::vector<double> inverse_gain_weight(nr * nphi, 0.0);
1023
1024 for (unsigned int ir = 0; ir < nr; ++ir)
1025 {
1026 const int module = source_grid.module_index[ir];
1027 const int layer = source_grid.layer_index[ir];
1028 if (module < 0 || layer < 0)
1029 {
1030 continue;
1031 }
1032
1033 for (unsigned int ip = 0; ip < nphi; ++ip)
1034 {
1035 const double bin_width = phi_source_edges[ip + 1] - phi_source_edges[ip];
1036 const auto bin_segments = periodic_segments(phi_source_edges[ip], bin_width);
1037 double fraction = 0.0;
1038 double weighted = 0.0;
1039 for (unsigned int sector = 0; sector < nTpcSectors; ++sector)
1040 {
1041 const PadInterval& interval = pad_geometry[module][side][sector];
1042 const double positive_start = interval.direction > 0 ? interval.start_edge : interval.start_edge - interval.angular_width;
1043 const auto active_segments = periodic_segments(positive_start, interval.angular_width);
1044 const double overlap = segment_overlap_length(bin_segments, active_segments) / bin_width;
1045 fraction += overlap;
1046 weighted += overlap * gain[side][sector][layer];
1047 }
1048 if (fraction > 1.0 + 1.0e-8)
1049 {
1050 throw std::runtime_error(std::format("Overlapping sector geometry for module {}, side {}, phi bin {}", module, side, ip));
1051 }
1052 geometry_fraction[ir * nphi + ip] = fraction;
1053 inverse_gain_weight[ir * nphi + ip] = weighted;
1054 }
1055 }
1056
1057 double base_integral = 0.0;
1058 double corrected_integral = 0.0;
1059 for (unsigned int ir = 0; ir < nr; ++ir)
1060 {
1061 const double rvol = 0.5 * (source_grid.r_edges_m[ir + 1] * source_grid.r_edges_m[ir + 1] - source_grid.r_edges_m[ir] * source_grid.r_edges_m[ir]);
1062 const double rshape = std::pow(source_grid.r_edges_m.front() / source_grid.r_centers_m[ir], m_radialPowerAlpha);
1063 for (unsigned int ip = 0; ip < nphi; ++ip)
1064 {
1065 for (unsigned int iz = 0; iz < nz; ++iz)
1066 {
1067 const unsigned int idx = src_index(ir, ip, iz, nphi, nz);
1068 volumes[idx] = rvol * (phi_source_edges[ip + 1] - phi_source_edges[ip]) * (z_source_edges_m[iz + 1] - z_source_edges_m[iz]);
1069
1070
1071 const double pshape = 1.0 + m_m1Amplitude * std::cos(ps[ip] - m_m1Phase) + m_m12Amplitude * std::cos(12.0 * (ps[ip] - m_m12Phase));
1072
1073 if (pshape < 0.0)
1074 {
1075 throw std::runtime_error("Phi modulation makes rho negative");
1076 }
1077
1078 const double base_raw = rshape * pshape * geometry_fraction[ir * nphi + ip];
1079 const double gain_raw = m_divideChargeByGain ? rshape * pshape * inverse_gain_weight[ir * nphi + ip] : base_raw;
1080
1081 raw[idx] = gain_raw;
1082 base_integral += base_raw * volumes[idx];
1083 corrected_integral += gain_raw * volumes[idx];
1084 volume_sum += volumes[idx];
1085 }
1086 }
1087 }
1088
1089 if (m_divideChargeByGain && m_normalizeGainWeightedTotal)
1090 {
1091 if (base_integral <= 0.0 || corrected_integral <= 0.0)
1092 {
1093 throw std::runtime_error("Invalid gain-weighted source normalization");
1094 }
1095 const double gain_normalization = corrected_integral / base_integral;
1096 for (double& value : raw)
1097 {
1098 value /= gain_normalization;
1099 }
1100 std::cout << " gain normalization side " << side << " = " << gain_normalization << std::endl;
1101 }
1102 }
1103
1104 double shape_sum = 0.0;
1105 for (unsigned int idx = 0; idx < raw.size(); ++idx)
1106 {
1107 shape_sum += raw[idx] * volumes[idx];
1108 }
1109 const double shape_mean = shape_sum / volume_sum;
1110 if (shape_mean <= 0.0)
1111 {
1112 throw std::runtime_error("Charge-density shape has zero volume average");
1113 }
1114
1115 const double scale = (m_useDensityMap && !m_normalizeDensityMap) ? 1.0e-9 : target_rho / shape_mean;
1116 std::vector<double> rho(raw.size(), 0.0);
1117 double total_charge = 0.0;
1118 for (unsigned int idx = 0; idx < rho.size(); ++idx)
1119 {
1120 rho[idx] = raw[idx] * scale;
1121 total_charge += rho[idx] * volumes[idx];
1122 }
1123
1124 if (m_useDensityMap)
1125 {
1126 std::cout << " density map side " << side << ": " << m_densityMapHistograms[side]
1127 << ", input mean = " << shape_mean
1128 << (m_normalizeDensityMap ? " arb., scale = " : " nC/m^3, scale = ") << scale << std::endl;
1129 }
1130 std::cout << " mean rho side " << side << " = " << total_charge / volume_sum
1131 << " C/m^3, charge = " << total_charge << " C, ions = " << total_charge / elementaryCharge << std::endl;
1132 return rho;
1133 }
1134
1135 bool PHGarfieldRossegger::pointInPolygon(const std::vector<Point2D>& polygon, double x_cm, double y_cm)
1136 {
1137 bool inside = false;
1138 const std::size_t n = polygon.size();
1139 if (n < 3)
1140 {
1141 return false;
1142 }
1143
1144 for (std::size_t i = 0, j = n - 1; i < n; j = i++)
1145 {
1146 const double xi = polygon[i].x_cm;
1147 const double yi = polygon[i].y_cm;
1148 const double xj = polygon[j].x_cm;
1149 const double yj = polygon[j].y_cm;
1150 const bool intersects = ((yi > y_cm) != (yj > y_cm)) &&
1151 (x_cm < (xj - xi) * (y_cm - yi) / (yj - yi) + xi);
1152 if (intersects)
1153 {
1154 inside = !inside;
1155 }
1156 }
1157 return inside;
1158 }
1159
1160 double PHGarfieldRossegger::polygonAreaCm2(const std::vector<Point2D>& polygon)
1161 {
1162 const std::size_t n = polygon.size();
1163 if (n < 3)
1164 {
1165 return 0.0;
1166 }
1167
1168 double area = 0.0;
1169 for (std::size_t i = 0, j = n - 1; i < n; j = i++)
1170 {
1171 area += polygon[j].x_cm * polygon[i].y_cm - polygon[i].x_cm * polygon[j].y_cm;
1172 }
1173 return 0.5 * std::fabs(area);
1174 }
1175
1176 void PHGarfieldRossegger::loadFramePolygons() const
1177 {
1178 if (m_framePolygonsLoaded)
1179 {
1180 return;
1181 }
1182
1183 FramePolygons polygons{};
1184 std::ifstream input(m_frameGeometryFile);
1185 if (!input.is_open())
1186 {
1187 throw std::runtime_error("Could not open frame geometry CSV " + m_frameGeometryFile);
1188 }
1189
1190 std::string line;
1191 std::getline(input, line);
1192 while (std::getline(input, line))
1193 {
1194 if (line.empty())
1195 {
1196 continue;
1197 }
1198
1199 std::stringstream ss(line);
1200 std::array<std::string, 5> fields{};
1201 for (std::string& field : fields)
1202 {
1203 if (!std::getline(ss, field, ','))
1204 {
1205 throw std::runtime_error("Malformed frame geometry CSV row: " + line);
1206 }
1207 }
1208
1209 const unsigned int module = std::stoul(fields[0]);
1210 if (module >= nTpcModules)
1211 {
1212 throw std::runtime_error("Frame geometry CSV has invalid module " + fields[0]);
1213 }
1214
1215 std::vector<Point2D>* polygon = nullptr;
1216 if (fields[1] == "inner")
1217 {
1218 polygon = &polygons[module].inner;
1219 }
1220 else if (fields[1] == "outer")
1221 {
1222 polygon = &polygons[module].outer;
1223 }
1224 else
1225 {
1226 throw std::runtime_error("Frame geometry CSV has invalid boundary " + fields[1]);
1227 }
1228
1229 polygon->push_back({std::stod(fields[3]) / 10.0, std::stod(fields[4]) / 10.0});
1230 }
1231
1232 for (unsigned int module = 0; module < nTpcModules; ++module)
1233 {
1234 if (polygons[module].inner.size() < 3 || polygons[module].outer.size() < 3)
1235 {
1236 throw std::runtime_error(std::format("Frame geometry CSV missing inner/outer polygon for module {}", module));
1237 }
1238 }
1239
1240 m_framePolygons = std::move(polygons);
1241 m_framePolygonsLoaded = true;
1242 }
1243
1244 bool PHGarfieldRossegger::pointInFrameGeometry(const FramePolygon& frame, double r_cm, double phi_rel) const
1245 {
1246 const double x_cm = r_cm * std::sin(phi_rel);
1247 const double y_cm = r_cm * std::cos(phi_rel);
1248 return pointInPolygon(frame.outer, x_cm, y_cm) && !pointInPolygon(frame.inner, x_cm, y_cm);
1249 }
1250
1251 double PHGarfieldRossegger::frameAreaM2(const FramePolygon& frame) const
1252 {
1253 return (polygonAreaCm2(frame.outer) - polygonAreaCm2(frame.inner)) * 1.0e-4;
1254 }
1255
1256 PHGarfieldRossegger::FrameBoundaryPattern PHGarfieldRossegger::makeFrameBoundaryPattern(const SourceGrid& source_grid,
1257 const std::vector<double>& phi_source_edges,
1258 unsigned int side) const
1259 {
1260 const unsigned int nr = source_grid.r_centers_m.size();
1261 const unsigned int nphi = phi_source_edges.size() - 1;
1262 FrameBoundaryPattern pattern;
1263 pattern.geometry_fraction.assign(nr * nphi, 0.0);
1264 pattern.weight.assign(nr * nphi, 0.0);
1265 pattern.boundary_potential.assign(nr * nphi, 0.0);
1266
1267 loadFramePolygons();
1268 std::array<double, nTpcModules> frame_area_by_module_m2{};
1269 double reference_piece_area = 0.0;
1270 for (unsigned int module = 0; module < nTpcModules; ++module)
1271 {
1272 const double inner_area_m2 = polygonAreaCm2(m_framePolygons[module].inner) * 1.0e-4;
1273 const double outer_area_m2 = polygonAreaCm2(m_framePolygons[module].outer) * 1.0e-4;
1274 frame_area_by_module_m2[module] = outer_area_m2 - inner_area_m2;
1275 if (frame_area_by_module_m2[module] <= 0.0)
1276 {
1277 throw std::runtime_error(std::format("Frame geometry CSV gives non-positive area for module {}", module));
1278 }
1279 std::cout << " frame geometry module " << module
1280 << ": outer area = " << outer_area_m2
1281 << " m^2, inner opening area = " << inner_area_m2
1282 << " m^2, frame area = " << frame_area_by_module_m2[module]
1283 << " m^2" << std::endl;
1284 reference_piece_area += frame_area_by_module_m2[module];
1285 }
1286 reference_piece_area /= static_cast<double>(nTpcModules);
1287
1288 constexpr unsigned int n_r_samples = 5;
1289 constexpr unsigned int n_phi_samples = 5;
1290 constexpr double inv_samples = 1.0 / static_cast<double>(n_r_samples * n_phi_samples);
1291 double weighted_area = 0.0;
1292 for (unsigned int ir = 0; ir < nr; ++ir)
1293 {
1294 const int module = source_grid.module_index[ir];
1295 if (module < 0)
1296 {
1297 continue;
1298 }
1299 const double r1_m = source_grid.r_edges_m[ir];
1300 const double r2_m = source_grid.r_edges_m[ir + 1];
1301 const double r1_cm = r1_m * mToCm;
1302 const double r2_cm = r2_m * mToCm;
1303 const double r2_span_cm2 = r2_cm * r2_cm - r1_cm * r1_cm;
1304 const double area_r = 0.5 * (r2_m * r2_m - r1_m * r1_m);
1305
1306 for (unsigned int ip = 0; ip < nphi; ++ip)
1307 {
1308 const double bin_width = phi_source_edges[ip + 1] - phi_source_edges[ip];
1309 double geometry_fraction = 0.0;
1310 double weight = 0.0;
1311
1312 for (unsigned int irs = 0; irs < n_r_samples; ++irs)
1313 {
1314 const double radial_fraction = (static_cast<double>(irs) + 0.5) / static_cast<double>(n_r_samples);
1315 const double r_cm = std::sqrt(r1_cm * r1_cm + radial_fraction * r2_span_cm2);
1316 for (unsigned int ips = 0; ips < n_phi_samples; ++ips)
1317 {
1318 const double phi = phi_source_edges[ip] + (static_cast<double>(ips) + 0.5) * bin_width / static_cast<double>(n_phi_samples);
1319 for (unsigned int sector = 0; sector < nTpcSectors; ++sector)
1320 {
1321 const double phi_rel = local_frame_phi(side, sector, phi, m_frameReferencePhi);
1322 if (!pointInFrameGeometry(m_framePolygons[static_cast<unsigned int>(module)], r_cm, phi_rel))
1323 {
1324 continue;
1325 }
1326 geometry_fraction += inv_samples;
1327 if (m_frameChargeWeighting == FrameChargeWeighting::EqualChargePerPiece)
1328 {
1329 weight += inv_samples * reference_piece_area / frame_area_by_module_m2[module];
1330 }
1331 else
1332 {
1333 weight += inv_samples;
1334 }
1335 }
1336 }
1337 }
1338
1339 const unsigned int idx = ir * nphi + ip;
1340 pattern.geometry_fraction[idx] = std::clamp(geometry_fraction, 0.0, 1.0);
1341 pattern.weight[idx] = weight;
1342 pattern.boundary_potential[idx] = m_frameBoundaryPotential * weight;
1343 weighted_area += area_r * bin_width * weight;
1344 }
1345 }
1346
1347 std::cout << " frame boundary potential side " << side << " = " << m_frameBoundaryPotential
1348 << " V, weighting = "
1349 << (m_frameChargeWeighting == FrameChargeWeighting::EqualChargePerPiece ? "EqualChargePerPiece" : "ProportionalToArea")
1350 << ", frame geometry = " << m_frameGeometryFile
1351 << ", frame reference sector = " << frameReferenceSector
1352 << ", weighted transverse area = " << weighted_area << " m^2" << std::endl;
1353 return pattern;
1354 }
1355
1356 int PHGarfieldRossegger::calculate()
1357 {
1358 if (!validateConfig())
1359 {
1360 return Fun4AllReturnCodes::ABORTRUN;
1361 }
1362 try
1363 {
1364 const double a = m_aCm * cmToM;
1365 const double b = m_bCm * cmToM;
1366 const double len = m_lCm * cmToM;
1367 const unsigned int mmax = effectivePhiMax();
1368
1369 std::cout << Name() << " Rossegger field calculation" << std::endl;
1370 if (m_useFrameChargeModel)
1371 {
1372 std::cout << " using frame boundary-potential source geometry" << std::endl;
1373 }
1374 else if (m_useDensityMap)
1375 {
1376 std::cout << " using ROOT r-phi density map: " << m_densityMapFile
1377 << (m_normalizeDensityMap ? " (normalized to setDensity reference)" : " (input values in nC/m^3)") << std::endl;
1378 }
1379 else if (m_useRealTpcSourceGeometry)
1380 {
1381 std::cout << " using real TPC radial source geometry and layer/sector gain maps" << std::endl;
1382 }
1383
1384 const SourceGrid source_grid = buildSourceGrid();
1385 const unsigned int nr_source = source_grid.r_centers_m.size();
1386 std::vector<double> pse;
1387 if (m_useFrameChargeModel)
1388 {
1389 pse = frame_phi_edges(m_nphiSource, m_tpcSide, m_frameReferencePhi, -std::numbers::pi);
1390 }
1391 else if (m_useDensityMap || m_useRealTpcSourceGeometry)
1392 {
1393 pse = phi_edges(m_nphiSource, -std::numbers::pi);
1394 }
1395 else
1396 {
1397 pse = phi_edges(m_nphiSource);
1398 }
1399 auto zse = edges(0.0, len, m_nzSource);
1400 auto roe = m_useFrameChargeModel ? frame_observation_r_edges(m_aCm, m_bCm) : edges(a, b, m_nrObs);
1401 std::vector<double> poe;
1402 if (m_useFrameChargeModel)
1403 {
1404 poe = frame_phi_edges(m_nphiObs, m_tpcSide, m_frameReferencePhi, -std::numbers::pi);
1405 }
1406 else if (m_useDensityMap || m_writePHGarfieldField3D)
1407 {
1408 poe = phi_edges(m_nphiObs, -std::numbers::pi);
1409 }
1410 else
1411 {
1412 poe = phi_edges(m_nphiObs);
1413 }
1414 auto zoe = m_useFrameChargeModel ? frame_observation_z_edges(m_lCm) : edges(0.0, len, m_nzObs);
1415 m_nphiSource = static_cast<unsigned int>(pse.size() - 1);
1416 m_nphiObs = static_cast<unsigned int>(poe.size() - 1);
1417 m_nzSource = static_cast<unsigned int>(zse.size() - 1);
1418 m_nrObs = static_cast<unsigned int>(roe.size() - 1);
1419 m_nzObs = static_cast<unsigned int>(zoe.size() - 1);
1420 const auto [r_begin, r_end] = radialRange(m_nrObs);
1421 std::cout << " radial job " << m_jobIndex << "/" << m_nJobs << " bins [" << r_begin << ", " << r_end << ")" << std::endl;
1422 const auto ps = centers(pse);
1423 const auto zs = centers(zse);
1424 const auto ro = centers(roe);
1425 const auto po = centers(poe);
1426 const auto zo = centers(zoe);
1427
1428 std::vector<std::vector<double>> km(mmax + 1);
1429 std::vector<std::vector<double>> norms(mmax + 1);
1430 std::vector<double> wg;
1431 const auto xg = legendreRootsAndWeights(192, wg);
1432 std::vector<double> rg(xg.size(), 0.0);
1433 const double jac = 0.5 * (b - a);
1434 for (unsigned int ig = 0; ig < xg.size(); ++ig)
1435 {
1436 rg[ig] = jac * xg[ig] + 0.5 * (b + a);
1437 }
1438 for (unsigned int im = 0; im <= mmax; ++im)
1439 {
1440 km[im] = findRadialRoots(im, m_nRadialModes);
1441 norms[im].assign(m_nRadialModes, 0.0);
1442 for (unsigned int in = 0; in < m_nRadialModes; ++in)
1443 {
1444 double sum = 0.0;
1445 for (unsigned int ig = 0; ig < xg.size(); ++ig)
1446 {
1447 const double rb = radialBasis(km[im][in], im, rg[ig]);
1448 sum += wg[ig] * rg[ig] * rb * rb;
1449 }
1450 norms[im][in] = jac * sum;
1451 }
1452 }
1453
1454 std::array<bool, nTpcSides> run_side{{false, false}};
1455 run_side[m_tpcSide] = true;
1456 if (m_writePHGarfieldField3D)
1457 {
1458 run_side[0] = true;
1459 run_side[1] = true;
1460 }
1461
1462 if (m_useFrameChargeModel)
1463 {
1464 std::vector<double> wg_source;
1465 const auto xg_source = legendreRootsAndWeights(8, wg_source);
1466 for (unsigned int side = 0; side < nTpcSides; ++side)
1467 {
1468 if (!run_side[side])
1469 {
1470 continue;
1471 }
1472 const FrameBoundaryPattern frame_pattern = makeFrameBoundaryPattern(source_grid, pse, side);
1473
1474 std::vector<std::vector<double>> mc(mmax + 1);
1475 std::vector<std::vector<double>> ms(mmax + 1);
1476 for (unsigned int im = 0; im <= mmax; ++im)
1477 {
1478 const double anorm = im == 0 ? 2.0 * std::numbers::pi : std::numbers::pi;
1479 mc[im].assign(m_nRadialModes, 0.0);
1480 ms[im].assign(m_nRadialModes, 0.0);
1481 for (unsigned int in = 0; in < m_nRadialModes; ++in)
1482 {
1483 double cproj = 0.0;
1484 double sproj = 0.0;
1485 for (unsigned int irs = 0; irs < nr_source; ++irs)
1486 {
1487 const double r1 = source_grid.r_edges_m[irs];
1488 const double r2 = source_grid.r_edges_m[irs + 1];
1489 const double radial_jac = 0.5 * (r2 - r1);
1490 const double radial_center = 0.5 * (r2 + r1);
1491 double radial_integral = 0.0;
1492 for (unsigned int ig = 0; ig < xg_source.size(); ++ig)
1493 {
1494 const double r = radial_jac * xg_source[ig] + radial_center;
1495 const double rb = radialBasis(km[im][in], im, r);
1496 radial_integral += wg_source[ig] * r * rb;
1497 }
1498 radial_integral *= radial_jac;
1499 for (unsigned int ips = 0; ips < m_nphiSource; ++ips)
1500 {
1501 const unsigned int idx = irs * m_nphiSource + ips;
1502 const double boundary_potential = frame_pattern.boundary_potential[idx];
1503 if (boundary_potential == 0.0)
1504 {
1505 continue;
1506 }
1507 const double phi1 = pse[ips];
1508 const double phi2 = pse[ips + 1];
1509 double int_cos = 0.0;
1510 double int_sin = 0.0;
1511 if (im == 0)
1512 {
1513 int_cos = phi2 - phi1;
1514 }
1515 else
1516 {
1517 const double m = static_cast<double>(im);
1518 int_cos = (std::sin(m * phi2) - std::sin(m * phi1)) / m;
1519 int_sin = (std::cos(m * phi1) - std::cos(m * phi2)) / m;
1520 }
1521 cproj += boundary_potential * int_cos * radial_integral;
1522 if (im > 0)
1523 {
1524 sproj += boundary_potential * int_sin * radial_integral;
1525 }
1526 }
1527 }
1528 mc[im][in] = cproj / (norms[im][in] * anorm);
1529 ms[im][in] = im == 0 ? 0.0 : sproj / (norms[im][in] * anorm);
1530 }
1531 }
1532 for (unsigned int in = 0; in < std::min(3U, m_nRadialModes); ++in)
1533 {
1534 mc[0][in] = 0.0;
1535 }
1536
1537 const unsigned int fsize = m_nrObs * m_nphiObs * m_nzObs;
1538 std::vector<double> phi(fsize, 0.0);
1539 std::vector<double> er(fsize, 0.0);
1540 std::vector<double> ep(fsize, 0.0);
1541 std::vector<double> ez(fsize, 0.0);
1542 for (unsigned int iro = r_begin; iro < r_end; ++iro)
1543 {
1544 for (unsigned int im = 0; im <= mmax; ++im)
1545 {
1546 std::vector<double> rb(m_nRadialModes, 0.0);
1547 std::vector<double> drb(m_nRadialModes, 0.0);
1548 for (unsigned int in = 0; in < m_nRadialModes; ++in)
1549 {
1550 rb[in] = radialBasis(km[im][in], im, ro[iro]);
1551 drb[in] = radialBasisDerivative(km[im][in], im, ro[iro]);
1552 }
1553 for (unsigned int ipo = 0; ipo < m_nphiObs; ++ipo)
1554 {
1555 const double cp = std::cos(static_cast<double>(im) * po[ipo]);
1556 const double sp = std::sin(static_cast<double>(im) * po[ipo]);
1557 for (unsigned int izo = 0; izo < m_nzObs; ++izo)
1558 {
1559 const unsigned int idx = fld_index(iro, ipo, izo, m_nphiObs, m_nzObs);
1560 for (unsigned int in = 0; in < m_nRadialModes; ++in)
1561 {
1562 const double kval = km[im][in];
1563 const double ac = mc[im][in];
1564 const double as = ms[im][in];
1565 const double amp = ac * cp + as * sp;
1566 const double zfac = sinh_ratio(kval, zo[izo], len);
1567 const double dzfac = dz_sinh_ratio(kval, zo[izo], len);
1568 phi[idx] += rb[in] * zfac * amp;
1569 er[idx] -= drb[in] * zfac * amp;
1570 ez[idx] -= m_frameEzScale * rb[in] * dzfac * amp;
1571 if (im > 0)
1572 {
1573 ep[idx] += (static_cast<double>(im) / ro[iro]) * rb[in] * zfac * (ac * sp - as * cp);
1574 }
1575 }
1576 }
1577 }
1578 }
1579 }
1580
1581 if (side == m_tpcSide)
1582 {
1583 writeGarfieldRootFile(roe, poe, zoe, er, ep, ez, r_begin, r_end);
1584 if (m_writeField3D)
1585 {
1586 writeFrameBoundaryField3DRootFile(source_grid.r_edges_m, roe, pse, poe, zoe, frame_pattern, phi, er, ep, ez, r_begin, r_end);
1587 }
1588 }
1589 if (m_writePHGarfieldField3D)
1590 {
1591 writePHGarfieldField3DRootFile(roe, poe, zoe, phi, er, ep, ez, side, r_begin, r_end);
1592 }
1593 }
1594 }
1595 else
1596 {
1597 std::vector<double> q(m_nLongitudinalModes, 0.0);
1598 for (unsigned int il = 0; il < m_nLongitudinalModes; ++il)
1599 {
1600 q[il] = static_cast<double>(il + 1) * std::numbers::pi / len;
1601 }
1602
1603 for (unsigned int side = 0; side < nTpcSides; ++side)
1604 {
1605 if (!run_side[side])
1606 {
1607 continue;
1608 }
1609 const std::vector<double> rho = makeChargeDensity(source_grid, pse, zse, side);
1610
1611 std::vector<std::vector<double>> mc(mmax + 1);
1612 std::vector<std::vector<double>> ms(mmax + 1);
1613 for (unsigned int im = 0; im <= mmax; ++im)
1614 {
1615 mc[im].assign(m_nRadialModes * m_nLongitudinalModes, 0.0);
1616 ms[im].assign(m_nRadialModes * m_nLongitudinalModes, 0.0);
1617 for (unsigned int in = 0; in < m_nRadialModes; ++in)
1618 {
1619 for (unsigned int il = 0; il < m_nLongitudinalModes; ++il)
1620 {
1621 double cproj = 0.0;
1622 double sproj = 0.0;
1623 for (unsigned int irs = 0; irs < nr_source; ++irs)
1624 {
1625 const double rb = radialBasis(km[im][in], im, source_grid.r_centers_m[irs]);
1626 const double rvol = 0.5 * (source_grid.r_edges_m[irs + 1] * source_grid.r_edges_m[irs + 1] - source_grid.r_edges_m[irs] * source_grid.r_edges_m[irs]);
1627 for (unsigned int ips = 0; ips < m_nphiSource; ++ips)
1628 {
1629 const double cp = std::cos(static_cast<double>(im) * ps[ips]);
1630 const double sp = std::sin(static_cast<double>(im) * ps[ips]);
1631 const double dphi = pse[ips + 1] - pse[ips];
1632 for (unsigned int izs = 0; izs < m_nzSource; ++izs)
1633 {
1634 const unsigned int idx = src_index(irs, ips, izs, m_nphiSource, m_nzSource);
1635 const double dz = zse[izs + 1] - zse[izs];
1636 const double charge = rho[idx] * rvol * dphi * dz;
1637 const double zfactor = std::sin(zs[izs] * q[il]);
1638 cproj += charge * rb * cp * zfactor;
1639 sproj += charge * rb * sp * zfactor;
1640 }
1641 }
1642 }
1643 mc[im][mode_index(in, il, m_nLongitudinalModes)] = cproj;
1644 ms[im][mode_index(in, il, m_nLongitudinalModes)] = im == 0 ? 0.0 : sproj;
1645 }
1646 }
1647 }
1648
1649 const unsigned int fsize = m_nrObs * m_nphiObs * m_nzObs;
1650 std::vector<double> phi(fsize, 0.0);
1651 std::vector<double> er(fsize, 0.0);
1652 std::vector<double> ep(fsize, 0.0);
1653 std::vector<double> ez(fsize, 0.0);
1654 for (unsigned int iro = r_begin; iro < r_end; ++iro)
1655 {
1656 for (unsigned int im = 0; im <= mmax; ++im)
1657 {
1658 const double anorm = im == 0 ? 2.0 * std::numbers::pi : std::numbers::pi;
1659 std::vector<double> rb(m_nRadialModes, 0.0);
1660 std::vector<double> drb(m_nRadialModes, 0.0);
1661 for (unsigned int in = 0; in < m_nRadialModes; ++in)
1662 {
1663 rb[in] = radialBasis(km[im][in], im, ro[iro]);
1664 drb[in] = radialBasisDerivative(km[im][in], im, ro[iro]);
1665 }
1666 for (unsigned int ipo = 0; ipo < m_nphiObs; ++ipo)
1667 {
1668 const double cp = std::cos(static_cast<double>(im) * po[ipo]);
1669 const double sp = std::sin(static_cast<double>(im) * po[ipo]);
1670 for (unsigned int izo = 0; izo < m_nzObs; ++izo)
1671 {
1672 const unsigned int idx = fld_index(iro, ipo, izo, m_nphiObs, m_nzObs);
1673 for (unsigned int in = 0; in < m_nRadialModes; ++in)
1674 {
1675 for (unsigned int il = 0; il < m_nLongitudinalModes; ++il)
1676 {
1677 const double kval = km[im][in];
1678 const double qval = q[il];
1679 const unsigned int midx = mode_index(in, il, m_nLongitudinalModes);
1680 const double denom = epsilon0 * norms[im][in] * (len / 2.0) * anorm * (kval * kval + qval * qval);
1681 const double ac = mc[im][midx] / denom;
1682 const double as = ms[im][midx] / denom;
1683 const double amp = ac * cp + as * sp;
1684 const double sz = std::sin(zo[izo] * qval);
1685 const double qcz = qval * std::cos(zo[izo] * qval);
1686 phi[idx] += rb[in] * sz * amp;
1687 er[idx] -= drb[in] * sz * amp;
1688 ez[idx] -= rb[in] * qcz * amp;
1689 if (im > 0)
1690 {
1691 ep[idx] += (static_cast<double>(im) / ro[iro]) * rb[in] * sz * (ac * sp - as * cp);
1692 }
1693 }
1694 }
1695 }
1696 }
1697 }
1698 }
1699
1700 if (side == m_tpcSide)
1701 {
1702 writeGarfieldRootFile(roe, poe, zoe, er, ep, ez, r_begin, r_end);
1703 if (m_writeField3D)
1704 {
1705 writeField3DRootFile(source_grid.r_edges_m, roe, pse, poe, zse, zoe, rho, phi, er, ep, ez, r_begin, r_end);
1706 }
1707 }
1708 if (m_writePHGarfieldField3D)
1709 {
1710 writePHGarfieldField3DRootFile(roe, poe, zoe, phi, er, ep, ez, side, r_begin, r_end);
1711 }
1712 }
1713 }
1714
1715 if (m_verifyOutput && !verifyOutput())
1716 {
1717 return Fun4AllReturnCodes::ABORTRUN;
1718 }
1719 }
1720 catch (const std::exception& e)
1721 {
1722 std::cout << PHWHERE << " " << Name() << " failed: " << e.what() << std::endl;
1723 return Fun4AllReturnCodes::ABORTRUN;
1724 }
1725 return Fun4AllReturnCodes::EVENT_OK;
1726 }
1727
1728 void PHGarfieldRossegger::writeGarfieldRootFile(const std::vector<double>& r_edges_m,
1729 const std::vector<double>& phi_edges,
1730 const std::vector<double>& z_edges_m,
1731 const std::vector<double>& er,
1732 const std::vector<double>& ephi,
1733 const std::vector<double>& ez,
1734 unsigned int r_begin,
1735 unsigned int r_end) const
1736 {
1737 auto rcm = r_edges_m;
1738 auto zcm = z_edges_m;
1739 scale_to_cm(rcm);
1740 scale_to_cm(zcm);
1741
1742 TFile file(m_garfieldOutputFile.c_str(), "RECREATE");
1743 if (!file.IsOpen() || file.IsZombie())
1744 {
1745 throw std::runtime_error("Could not create " + m_garfieldOutputFile);
1746 }
1747 file.mkdir("QA");
1748 file.cd("QA");
1749 TH2D hEr("hErDefault", "Axisymmetric radial field for PHGarfield", m_nrObs, rcm.data(), m_nzObs, zcm.data());
1750 TH2D hEphi("hEphiDefault", "Axisymmetric azimuthal field for PHGarfield", m_nrObs, rcm.data(), m_nzObs, zcm.data());
1751 TH2D hEz("hEzDefault", "Axisymmetric local longitudinal field for PHGarfield", m_nrObs, rcm.data(), m_nzObs, zcm.data());
1752 TH2D hErRPhi("hErRPhi", "Radial field for PHGarfield averaged over |z|;r [cm];#phi [rad]", m_nrObs, rcm.data(), m_nphiObs, phi_edges.data());
1753 TH2D hEphiRPhi("hEphiRPhi", "Azimuthal field for PHGarfield averaged over |z|;r [cm];#phi [rad]", m_nrObs, rcm.data(), m_nphiObs, phi_edges.data());
1754 TH2D hEzRPhi("hEzRPhi", "Local longitudinal field for PHGarfield averaged over |z|;r [cm];#phi [rad]", m_nrObs, rcm.data(), m_nphiObs, phi_edges.data());
1755 TH2D hErZPhi("hErZPhi", "Radial field for PHGarfield averaged over r;|z| [cm];#phi [rad]", m_nzObs, zcm.data(), m_nphiObs, phi_edges.data());
1756 TH2D hEphiZPhi("hEphiZPhi", "Azimuthal field for PHGarfield averaged over r;|z| [cm];#phi [rad]", m_nzObs, zcm.data(), m_nphiObs, phi_edges.data());
1757 TH2D hEzZPhi("hEzZPhi", "Local longitudinal field for PHGarfield averaged over r;|z| [cm];#phi [rad]", m_nzObs, zcm.data(), m_nphiObs, phi_edges.data());
1758 hEr.GetXaxis()->SetTitle("r [cm]");
1759 hEr.GetYaxis()->SetTitle("|z| [cm]");
1760 hEr.GetZaxis()->SetTitle("E_{r} [V/m]");
1761 hEphi.GetXaxis()->SetTitle("r [cm]");
1762 hEphi.GetYaxis()->SetTitle("|z| [cm]");
1763 hEphi.GetZaxis()->SetTitle("E_{#phi} [V/m]");
1764 hEz.GetXaxis()->SetTitle("r [cm]");
1765 hEz.GetYaxis()->SetTitle("|z| [cm]");
1766 hEz.GetZaxis()->SetTitle("E_{z}^{local} [V/m]");
1767 hErRPhi.GetXaxis()->SetTitle("r [cm]");
1768 hErRPhi.GetYaxis()->SetTitle("#phi [rad]");
1769 hErRPhi.GetZaxis()->SetTitle("E_{r} [V/m]");
1770 hEphiRPhi.GetXaxis()->SetTitle("r [cm]");
1771 hEphiRPhi.GetYaxis()->SetTitle("#phi [rad]");
1772 hEphiRPhi.GetZaxis()->SetTitle("E_{#phi} [V/m]");
1773 hEzRPhi.GetXaxis()->SetTitle("r [cm]");
1774 hEzRPhi.GetYaxis()->SetTitle("#phi [rad]");
1775 hEzRPhi.GetZaxis()->SetTitle("E_{z}^{local} [V/m]");
1776 hErZPhi.GetXaxis()->SetTitle("|z| [cm]");
1777 hErZPhi.GetYaxis()->SetTitle("#phi [rad]");
1778 hErZPhi.GetZaxis()->SetTitle("E_{r} [V/m]");
1779 hEphiZPhi.GetXaxis()->SetTitle("|z| [cm]");
1780 hEphiZPhi.GetYaxis()->SetTitle("#phi [rad]");
1781 hEphiZPhi.GetZaxis()->SetTitle("E_{#phi} [V/m]");
1782 hEzZPhi.GetXaxis()->SetTitle("|z| [cm]");
1783 hEzZPhi.GetYaxis()->SetTitle("#phi [rad]");
1784 hEzZPhi.GetZaxis()->SetTitle("E_{z}^{local} [V/m]");
1785
1786 for (unsigned int ir = r_begin; ir < r_end; ++ir)
1787 {
1788 for (unsigned int iz = 0; iz < m_nzObs; ++iz)
1789 {
1790 double ersum = 0.0;
1791 double ephisum = 0.0;
1792 double ezsum = 0.0;
1793 for (unsigned int ip = 0; ip < m_nphiObs; ++ip)
1794 {
1795 const unsigned int idx = fld_index(ir, ip, iz, m_nphiObs, m_nzObs);
1796 ersum += er[idx];
1797 ephisum += ephi[idx];
1798 ezsum += ez[idx];
1799 }
1800 hEr.SetBinContent(ir + 1, iz + 1, ersum / static_cast<double>(m_nphiObs));
1801 hEphi.SetBinContent(ir + 1, iz + 1, ephisum / static_cast<double>(m_nphiObs));
1802 hEz.SetBinContent(ir + 1, iz + 1, ezsum / static_cast<double>(m_nphiObs));
1803 }
1804
1805 for (unsigned int ip = 0; ip < m_nphiObs; ++ip)
1806 {
1807 double ersum = 0.0;
1808 double ephisum = 0.0;
1809 double ezsum = 0.0;
1810 for (unsigned int iz = 0; iz < m_nzObs; ++iz)
1811 {
1812 const unsigned int idx = fld_index(ir, ip, iz, m_nphiObs, m_nzObs);
1813 ersum += er[idx];
1814 ephisum += ephi[idx];
1815 ezsum += ez[idx];
1816 }
1817 hErRPhi.SetBinContent(ir + 1, ip + 1, ersum / static_cast<double>(m_nzObs));
1818 hEphiRPhi.SetBinContent(ir + 1, ip + 1, ephisum / static_cast<double>(m_nzObs));
1819 hEzRPhi.SetBinContent(ir + 1, ip + 1, ezsum / static_cast<double>(m_nzObs));
1820 }
1821 }
1822
1823 const unsigned int nr_filled = r_end - r_begin;
1824 if (nr_filled > 0)
1825 {
1826 for (unsigned int iz = 0; iz < m_nzObs; ++iz)
1827 {
1828 for (unsigned int ip = 0; ip < m_nphiObs; ++ip)
1829 {
1830 double ersum = 0.0;
1831 double ephisum = 0.0;
1832 double ezsum = 0.0;
1833 for (unsigned int ir = r_begin; ir < r_end; ++ir)
1834 {
1835 const unsigned int idx = fld_index(ir, ip, iz, m_nphiObs, m_nzObs);
1836 ersum += er[idx];
1837 ephisum += ephi[idx];
1838 ezsum += ez[idx];
1839 }
1840 hErZPhi.SetBinContent(iz + 1, ip + 1, ersum / static_cast<double>(nr_filled));
1841 hEphiZPhi.SetBinContent(iz + 1, ip + 1, ephisum / static_cast<double>(nr_filled));
1842 hEzZPhi.SetBinContent(iz + 1, ip + 1, ezsum / static_cast<double>(nr_filled));
1843 }
1844 }
1845 }
1846
1847 hEr.Write();
1848 hEphi.Write();
1849 hEz.Write();
1850 hErRPhi.Write();
1851 hEphiRPhi.Write();
1852 hEzRPhi.Write();
1853 hErZPhi.Write();
1854 hEphiZPhi.Write();
1855 hEzZPhi.Write();
1856 file.cd();
1857 const std::string meta = std::format("{{\"format\":\"PHGarfield axisymmetric correction map\",\"histograms\":{{\"Er\":\"QA/hErDefault\",\"Ephi\":\"QA/hEphiDefault\",\"Ez\":\"QA/hEzDefault\",\"ErRPhi\":\"QA/hErRPhi\",\"EphiRPhi\":\"QA/hEphiRPhi\",\"EzRPhi\":\"QA/hEzRPhi\",\"ErZPhi\":\"QA/hErZPhi\",\"EphiZPhi\":\"QA/hEphiZPhi\",\"EzZPhi\":\"QA/hEzZPhi\"}},\"field_units\":\"V/m\",\"job_index\":{},\"n_jobs\":{},\"radial_bin_begin\":{},\"radial_bin_end\":{}}}", m_jobIndex, m_nJobs, r_begin, r_end);
1858 TNamed("metadata_json", meta.c_str()).Write();
1859 file.Close();
1860 std::cout << Name() << " wrote " << m_garfieldOutputFile << std::endl;
1861 }
1862
1863 void PHGarfieldRossegger::writeField3DRootFile(const std::vector<double>& r_source_edges_m, const std::vector<double>& r_obs_edges_m, const std::vector<double>& phi_source_edges, const std::vector<double>& phi_obs_edges, const std::vector<double>& z_source_edges_m, const std::vector<double>& z_obs_edges_m, const std::vector<double>& rho, const std::vector<double>& potential, const std::vector<double>& er, const std::vector<double>& ephi, const std::vector<double>& ez, unsigned int r_begin, unsigned int r_end) const
1864 {
1865 auto rscm = r_source_edges_m;
1866 auto rocm = r_obs_edges_m;
1867 auto zscm = z_source_edges_m;
1868 auto zocm = z_obs_edges_m;
1869 scale_to_cm(rscm);
1870 scale_to_cm(rocm);
1871 scale_to_cm(zscm);
1872 scale_to_cm(zocm);
1873 TFile file(m_field3DOutputFile.c_str(), "RECREATE");
1874 if (!file.IsOpen() || file.IsZombie())
1875 {
1876 throw std::runtime_error("Could not create " + m_field3DOutputFile);
1877 }
1878 const unsigned int nr_source = r_source_edges_m.size() - 1;
1879 TH3D hRho("hRho", "Positive-ion density", nr_source, rscm.data(), m_nphiSource, phi_source_edges.data(), m_nzSource, zscm.data());
1880 TH3D hPhi("hPhi", "Space-charge potential", m_nrObs, rocm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zocm.data());
1881 TH3D hEr("hEr", "Radial electric field", m_nrObs, rocm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zocm.data());
1882 TH3D hEphi("hEphi", "Azimuthal electric field", m_nrObs, rocm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zocm.data());
1883 TH3D hEz("hEz", "Longitudinal electric field", m_nrObs, rocm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zocm.data());
1884 for (unsigned int ir = 0; ir < nr_source; ++ir)
1885 {
1886 for (unsigned int ip = 0; ip < m_nphiSource; ++ip)
1887 {
1888 for (unsigned int iz = 0; iz < m_nzSource; ++iz)
1889 {
1890 const unsigned int idx = src_index(ir, ip, iz, m_nphiSource, m_nzSource);
1891 hRho.SetBinContent(ir + 1, ip + 1, iz + 1, rho[idx]);
1892 }
1893 }
1894 }
1895 for (unsigned int ir = r_begin; ir < r_end; ++ir)
1896 {
1897 for (unsigned int ip = 0; ip < m_nphiObs; ++ip)
1898 {
1899 for (unsigned int iz = 0; iz < m_nzObs; ++iz)
1900 {
1901 const unsigned int idx = fld_index(ir, ip, iz, m_nphiObs, m_nzObs);
1902 hPhi.SetBinContent(ir + 1, ip + 1, iz + 1, potential[idx]);
1903 hEr.SetBinContent(ir + 1, ip + 1, iz + 1, er[idx]);
1904 hEphi.SetBinContent(ir + 1, ip + 1, iz + 1, ephi[idx]);
1905 hEz.SetBinContent(ir + 1, ip + 1, iz + 1, ez[idx]);
1906 }
1907 }
1908 }
1909 hRho.Write();
1910 hPhi.Write();
1911 hEr.Write();
1912 hEphi.Write();
1913 hEz.Write();
1914 file.cd();
1915 TNamed("metadata_json", "Rossegger modal solution for sPHENIX TPC").Write();
1916 file.Close();
1917 std::cout << Name() << " wrote " << m_field3DOutputFile << std::endl;
1918 }
1919
1920 void PHGarfieldRossegger::writeFrameBoundaryField3DRootFile(const std::vector<double>& r_source_edges_m,
1921 const std::vector<double>& r_obs_edges_m,
1922 const std::vector<double>& phi_source_edges,
1923 const std::vector<double>& phi_obs_edges,
1924 const std::vector<double>& z_obs_edges_m,
1925 const FrameBoundaryPattern& frame_pattern,
1926 const std::vector<double>& potential,
1927 const std::vector<double>& er,
1928 const std::vector<double>& ephi,
1929 const std::vector<double>& ez,
1930 unsigned int r_begin,
1931 unsigned int r_end) const
1932 {
1933 auto rscm = r_source_edges_m;
1934 auto rocm = r_obs_edges_m;
1935 auto zocm = z_obs_edges_m;
1936 scale_to_cm(rscm);
1937 scale_to_cm(rocm);
1938 scale_to_cm(zocm);
1939 TFile file(m_field3DOutputFile.c_str(), "RECREATE");
1940 if (!file.IsOpen() || file.IsZombie())
1941 {
1942 throw std::runtime_error("Could not create " + m_field3DOutputFile);
1943 }
1944
1945 const unsigned int nr_source = r_source_edges_m.size() - 1;
1946 TH2D hFrameGeometry("hFrameGeometry", "Frame geometry fraction at GEM;r [cm];#phi [rad]", nr_source, rscm.data(), m_nphiSource, phi_source_edges.data());
1947 TH2D hFrameWeight("hFrameWeight", "Frame boundary weighting at GEM;r [cm];#phi [rad]", nr_source, rscm.data(), m_nphiSource, phi_source_edges.data());
1948 TH2D hFrameBoundaryPotential("hFrameBoundaryPotential", "Effective frame boundary potential at GEM;r [cm];#phi [rad]", nr_source, rscm.data(), m_nphiSource, phi_source_edges.data());
1949 TH3D hPhi("hPhi", "Frame boundary potential solution;r [cm];#phi [rad];|z| [cm]", m_nrObs, rocm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zocm.data());
1950 TH3D hEr("hEr", "Radial electric field from frame boundary;r [cm];#phi [rad];|z| [cm]", m_nrObs, rocm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zocm.data());
1951 TH3D hEphi("hEphi", "Azimuthal electric field from frame boundary;r [cm];#phi [rad];|z| [cm]", m_nrObs, rocm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zocm.data());
1952 TH3D hEz("hEz", "Longitudinal electric field from frame boundary;r [cm];#phi [rad];|z| [cm]", m_nrObs, rocm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zocm.data());
1953
1954 for (unsigned int ir = 0; ir < nr_source; ++ir)
1955 {
1956 for (unsigned int ip = 0; ip < m_nphiSource; ++ip)
1957 {
1958 const unsigned int idx = ir * m_nphiSource + ip;
1959 hFrameGeometry.SetBinContent(ir + 1, ip + 1, frame_pattern.geometry_fraction[idx]);
1960 hFrameWeight.SetBinContent(ir + 1, ip + 1, frame_pattern.weight[idx]);
1961 hFrameBoundaryPotential.SetBinContent(ir + 1, ip + 1, frame_pattern.boundary_potential[idx]);
1962 }
1963 }
1964
1965 for (unsigned int ir = r_begin; ir < r_end; ++ir)
1966 {
1967 for (unsigned int ip = 0; ip < m_nphiObs; ++ip)
1968 {
1969 for (unsigned int iz = 0; iz < m_nzObs; ++iz)
1970 {
1971 const unsigned int idx = fld_index(ir, ip, iz, m_nphiObs, m_nzObs);
1972 hPhi.SetBinContent(ir + 1, ip + 1, iz + 1, potential[idx]);
1973 hEr.SetBinContent(ir + 1, ip + 1, iz + 1, er[idx]);
1974 hEphi.SetBinContent(ir + 1, ip + 1, iz + 1, ephi[idx]);
1975 hEz.SetBinContent(ir + 1, ip + 1, iz + 1, ez[idx]);
1976 }
1977 }
1978 }
1979
1980 hFrameGeometry.Write();
1981 hFrameWeight.Write();
1982 hFrameBoundaryPotential.Write();
1983 hPhi.Write();
1984 hEr.Write();
1985 hEphi.Write();
1986 hEz.Write();
1987 TNamed("metadata_json", std::format("{{\"format\":\"Rossegger frame boundary solution\",\"frame_boundary_potential_V\":{},\"frame_charge_weighting\":\"{}\"}}", m_frameBoundaryPotential, m_frameChargeWeighting == FrameChargeWeighting::EqualChargePerPiece ? "EqualChargePerPiece" : "ProportionalToArea").c_str()).Write();
1988 file.Close();
1989 std::cout << Name() << " wrote " << m_field3DOutputFile << std::endl;
1990 }
1991
1992 void PHGarfieldRossegger::writePHGarfieldField3DRootFile(const std::vector<double>& r_obs_edges_m,
1993 const std::vector<double>& phi_obs_edges,
1994 const std::vector<double>& z_obs_edges_m,
1995 const std::vector<double>& potential,
1996 const std::vector<double>& er,
1997 const std::vector<double>& ephi,
1998 const std::vector<double>& ez,
1999 unsigned int side,
2000 unsigned int r_begin,
2001 unsigned int r_end) const
2002 {
2003 auto rcm = r_obs_edges_m;
2004 auto zcm = z_obs_edges_m;
2005 scale_to_cm(rcm);
2006 scale_to_cm(zcm);
2007
2008 const std::string& filename = m_phGarfieldField3DOutputFiles[side];
2009 TFile file(filename.c_str(), "RECREATE");
2010 if (!file.IsOpen() || file.IsZombie())
2011 {
2012 throw std::runtime_error("Could not create " + filename);
2013 }
2014 auto* dir = file.mkdir("Field3D");
2015 if (!dir)
2016 {
2017 throw std::runtime_error("Could not create Field3D in " + filename);
2018 }
2019 dir->cd();
2020
2021 TH3D hPhi("hPhi", std::format("TPC electric potential, side {};r [cm];#phi [rad];|z| [cm]", side).c_str(), m_nrObs, rcm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zcm.data());
2022 TH3D hEr("hEr", std::format("TPC radial electric-field correction, side {};r [cm];#phi [rad];|z| [cm]", side).c_str(), m_nrObs, rcm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zcm.data());
2023 TH3D hEphi("hEphi", std::format("TPC azimuthal electric-field correction, side {};r [cm];#phi [rad];|z| [cm]", side).c_str(), m_nrObs, rcm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zcm.data());
2024 TH3D hEx("hEx", std::format("Local TPC E_{{x}} correction, side {};r [cm];#phi [rad];|z| [cm]", side).c_str(), m_nrObs, rcm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zcm.data());
2025 TH3D hEy("hEy", std::format("Local TPC E_{{y}} correction, side {};r [cm];#phi [rad];|z| [cm]", side).c_str(), m_nrObs, rcm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zcm.data());
2026 TH3D hEz("hEz", std::format("TPC E_{{z}} correction in +|z| solver coordinate, side {};r [cm];#phi [rad];|z| [cm]", side).c_str(), m_nrObs, rcm.data(), m_nphiObs, phi_obs_edges.data(), m_nzObs, zcm.data());
2027 hEx.GetXaxis()->SetTitle("r [cm]");
2028 hEx.GetYaxis()->SetTitle("#phi [rad]");
2029 hEx.GetZaxis()->SetTitle("|z| [cm]");
2030 hEy.GetXaxis()->SetTitle("r [cm]");
2031 hEy.GetYaxis()->SetTitle("#phi [rad]");
2032 hEy.GetZaxis()->SetTitle("|z| [cm]");
2033 hEz.GetXaxis()->SetTitle("r [cm]");
2034 hEz.GetYaxis()->SetTitle("#phi [rad]");
2035 hEz.GetZaxis()->SetTitle("|z| [cm]");
2036
2037 const auto phi_cent = centers(phi_obs_edges);
2038 for (unsigned int ir = r_begin; ir < r_end; ++ir)
2039 {
2040 for (unsigned int ip = 0; ip < m_nphiObs; ++ip)
2041 {
2042 const double cphi = std::cos(phi_cent[ip]);
2043 const double sphi = std::sin(phi_cent[ip]);
2044 for (unsigned int iz = 0; iz < m_nzObs; ++iz)
2045 {
2046 const unsigned int idx = fld_index(ir, ip, iz, m_nphiObs, m_nzObs);
2047 hPhi.SetBinContent(ir + 1, ip + 1, iz + 1, potential[idx]);
2048 hEr.SetBinContent(ir + 1, ip + 1, iz + 1, er[idx]);
2049 hEphi.SetBinContent(ir + 1, ip + 1, iz + 1, ephi[idx]);
2050 hEx.SetBinContent(ir + 1, ip + 1, iz + 1, er[idx] * cphi - ephi[idx] * sphi);
2051 hEy.SetBinContent(ir + 1, ip + 1, iz + 1, er[idx] * sphi + ephi[idx] * cphi);
2052 hEz.SetBinContent(ir + 1, ip + 1, iz + 1, ez[idx]);
2053 }
2054 }
2055 }
2056
2057 hPhi.SetEntries(static_cast<double>(m_nrObs) * static_cast<double>(m_nphiObs) * static_cast<double>(m_nzObs));
2058 hEr.SetEntries(hPhi.GetEntries());
2059 hEphi.SetEntries(hPhi.GetEntries());
2060 hEx.SetEntries(static_cast<double>(m_nrObs) * static_cast<double>(m_nphiObs) * static_cast<double>(m_nzObs));
2061 hEy.SetEntries(hEx.GetEntries());
2062 hEz.SetEntries(hEx.GetEntries());
2063 hPhi.Write("hPhi", TObject::kOverwrite);
2064 hEr.Write("hEr", TObject::kOverwrite);
2065 hEphi.Write("hEphi", TObject::kOverwrite);
2066 hEx.Write("hEx", TObject::kOverwrite);
2067 hEy.Write("hEy", TObject::kOverwrite);
2068 hEz.Write("hEz", TObject::kOverwrite);
2069
2070 file.cd();
2071 TNamed("field_map_format", "PHGarfield Field3D v1").Write();
2072 TNamed("coordinate_convention", std::format("local TPC Cartesian Ex,Ey,Ez for side {}; axes=(r_cm,phi_rad,abs_z_cm); phi in [-pi,pi], periodic; histogram contents V/m; Ez is derivative along the +|z| solver coordinate", side).c_str()).Write();
2073 const std::string meta = std::format("{{\"format\":\"PHGarfield Field3D v1\",\"coordinate_system\":\"local TPC\",\"side\":{},\"gain_file\":\"{}\",\"gain_histogram\":\"{}\",\"gain_application\":\"rho_corrected = rho_base / gain\",\"field_components\":[\"Ex\",\"Ey\",\"Ez\"],\"field_units\":\"V/m\",\"r_axis_units\":\"cm\",\"phi_axis_units\":\"rad\",\"z_axis_units\":\"cm\",\"z_axis_definition\":\"abs(z); Ez stored in +|z| solver coordinate\",\"phi_periodic\":true,\"phi_range\":[{},{}],\"shape\":[{},{},{}],\"source_phi_cells\":{},\"source_z_cells\":{},\"N_RADIAL_MODES\":{},\"N_LONGITUDINAL_MODES\":{},\"M_PHI_MAX\":{},\"K_EFF\":{}}}", side, m_gainMapFile, m_gainHistograms[side], phi_obs_edges.front(), phi_obs_edges.back(), m_nrObs, m_nphiObs, m_nzObs, m_nphiSource, m_nzSource, m_nRadialModes, m_nLongitudinalModes, m_mPhiMax, m_kEff);
2074 TNamed("metadata_json", meta.c_str()).Write();
2075 file.Close();
2076 std::cout << Name() << " wrote " << filename << std::endl;
2077 }
2078
2079 bool PHGarfieldRossegger::verifyOutput() const
2080 {
2081 TFile file(m_garfieldOutputFile.c_str(), "READ");
2082 if (!file.IsOpen() || file.IsZombie())
2083 {
2084 return false;
2085 }
2086 return file.Get("QA/hErDefault") && file.Get("QA/hEphiDefault") && file.Get("QA/hEzDefault") &&
2087 file.Get("QA/hErRPhi") && file.Get("QA/hEphiRPhi") && file.Get("QA/hEzRPhi") &&
2088 file.Get("QA/hErZPhi") && file.Get("QA/hEphiZPhi") && file.Get("QA/hEzZPhi") &&
2089 file.Get("metadata_json");
2090 }