Back to home page

sPhenix code displayed by LXR

 
 

    


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 /*unused*/, 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 }  // namespace
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* /*unused*/)
0471 {
0472   return InitRun(nullptr);
0473 }
0474 
0475 int PHGarfieldRossegger::InitRun(PHCompositeNode* /*unused*/)
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* /*unused*/)
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   // In real-TPC and density-map modes m_nrSource is intentionally unused; radial bins come from detector geometry.
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           // const double base_raw = rshape * geometry_fraction[ir * nphi + ip];
1070           // const double gain_raw = m_divideChargeByGain ? rshape * inverse_gain_weight[ir * nphi + ip] : base_raw;
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 }