Back to home page

sPhenix code displayed by LXR

 
 

    


File indexing completed on 2025-12-18 09:17:52

0001 #include "AnnularFieldSim.h"
0002 
0003 #include "AnalyticFieldModel.h"
0004 #include "ChargeMapReader.h"
0005 #include "MultiArray.h"  //for TH3 alternative
0006 #include "Rossegger.h"
0007 
0008 #include <TCanvas.h>
0009 #include <TFile.h>
0010 #include <TH1.h>
0011 #include <TH2.h>
0012 #include <TH3.h>
0013 #include <TLatex.h>
0014 #include <TPad.h>
0015 #include <TStyle.h>
0016 #include <TTree.h>
0017 #include <TVector3.h>
0018 
0019 #include <algorithm>
0020 #include <cassert>  // for assert
0021 #include <cmath>
0022 #include <cstdint>
0023 #include <cstdlib>
0024 #include <format>
0025 #include <iostream>
0026 
0027 #define ALMOST_ZERO 0.00001
0028 
0029 AnnularFieldSim::AnnularFieldSim(float in_innerRadius, float in_outerRadius, float in_outerZ,
0030                                  int r, int roi_r0, int roi_r1, int /*in_rLowSpacing*/, int /*in_rHighSize*/,
0031                                  int phi, int roi_phi0, int roi_phi1, int /*in_phiLowSpacing*/, int /*in_phiHighSize*/,
0032                                  int z, int roi_z0, int roi_z1, int /*in_zLowSpacing*/, int /*in_zHighSize*/,
0033                                  float vdr, LookupCase in_lookupCase, ChargeCase in_chargeCase)
0034 {
0035   // check well-ordering:
0036   if (roi_r0 >= r || roi_r1 > r || roi_r0 >= roi_r1)
0037   {
0038     exit(1);
0039   }
0040   if (roi_phi0 >= phi || roi_phi1 > phi || roi_phi0 >= roi_phi1)
0041   {
0042     std::cout << "phi roi is out of range or spans the wrap-around.  Please spare me that math." << std::endl;
0043     exit(1);
0044   }
0045   if (roi_z0 >= z || roi_z1 > z || roi_z0 >= roi_z1)
0046   {
0047     exit(1);
0048   }
0049 
0050   hasTwin = false;  // we never have a twin to start with.
0051   green_shift = 0;  // and so we don't shift our greens functions unless told to.
0052 
0053   std::cout << std::format("AnnularFieldSim::AnnularFieldSim with ({}x{}x{}) grid", r, phi, z) << std::endl;
0054   std::cout << std::format("units m={:.2E}, cm={:.2E}, mm={:.2E}, um={:.2E},", m, cm, mm, um) << std::endl;
0055   std::cout << std::format("units s={:.2E}, us={:.2E}, ns={:.2E},", s, us, ns) << std::endl;
0056   std::cout << std::format("units C={:.2E}, nC={:.2E}, fC={:.2E}, ", C, nC, fC) << std::endl;
0057   std::cout << std::format("units Tesla={:.2E}, kGauss={:.2E}", Tesla, kGauss) << std::endl;
0058 
0059   // debug defaults:
0060   //
0061   debug_printActionEveryN = -1;
0062   debug_printCounter = 0;
0063   debug_distortionScale.SetXYZ(0, 0, 0);
0064   debug_npercent = 5;
0065 
0066   // internal states used for the debug flag
0067   // goes with: if(debugFlag()) print_need_cout("%d: blah\n",__LINE__);
0068 
0069   // load constants of motion, dimensions, etc:
0070   Enominal = 400 * V / cm;  // v/cm
0071   Bnominal = 1.4 * Tesla;   // Tesla
0072   vdrift = vdr * cm / s;    // cm/s
0073   langevin_T1 = 1.0;
0074   langevin_T2 = 1.0;
0075   omegatau_nominal = -10 * (Bnominal / kGauss) * (vdrift / (cm / us)) / (Enominal / (V / cm));  // to match to the familiar formula
0076 
0077   rmin = in_innerRadius * cm;
0078   rmax = in_outerRadius * cm;
0079   zmin = 0;
0080   zmax = in_outerZ * cm;
0081   if (zmin > zmax)
0082   {
0083     zmin = zmax;
0084     zmax = 0;  // for now, we continue to assume one end of the TPC is at z=0.
0085   }
0086   zero_vector.SetXYZ(0, 0, 0);
0087 
0088   // define the size of the volume:
0089   dim.SetXYZ(1, 0, 0);
0090   dim.SetPerp(rmax - rmin);
0091   dim.SetPhi(0);
0092   phispan = 2 * M_PI;
0093   dim.SetZ(zmax - zmin);
0094 
0095   // set the green's functions model:
0096   // UseFreeSpaceGreens();
0097   // blah
0098   green = nullptr;
0099 
0100   // load parameters of the whole-volume tiling
0101   nr = r;
0102   nphi = phi;
0103   nz = z;  // number of fundamental bins (f-bins) in each direction
0104   std::cout << std::format("AnnularFieldSim::AnnularFieldSim set variables nr={}, nphi={}, nz={}", nr, nphi, nz) << std::endl;
0105   // and set the default truncation distance:
0106   truncation_length = -1;  // anything <1 and it won't truncate.
0107 
0108   // calculate the size of an f-bin:
0109   // note that you have to set a non-zero value to start or perp won't update.
0110   step.SetXYZ(1, 0, 0);
0111   step.SetPerp(dim.Perp() / nr);
0112   step.SetPhi(phispan / nphi);
0113   step.SetZ(dim.Z() / nz);
0114   std::cout << std::format("f-bin size:  r={:.6f}, phi={:.6f}, z={:.6f}, wanted {:.6f},{:.6f}", step.Perp(), step.Phi(), (rmax - rmin) / nr, phispan / nphi, (zmax - zmin) / nz) << std::endl;
0115 
0116   // create an array to store the charge in each f-bin
0117   //  q = new MultiArray<double>(nr, nphi, nz);
0118   // q->SetAll(0);
0119   q = new ChargeMapReader(nr, rmin, rmax, nphi, 0, phispan, nz, zmin, zmax);
0120   chargestring = "No spacecharge present.";
0121 
0122   // load parameters of our region of interest
0123   rmin_roi = roi_r0;
0124   phimin_roi = roi_phi0;
0125   zmin_roi = roi_z0;  // lower edge of our region of interest, measured in f-bins
0126   rmax_roi = roi_r1;
0127   phimax_roi = roi_phi1;
0128   zmax_roi = roi_z1;  // exlcuded upper edge of our region of interest, measured in f-bins
0129   std::cout << std::format("AnnularFieldSim::AnnularFieldSim set roi variables rmin={} phimin={} zmin={} rmax={} phimax={} zmax={}",
0130                            rmin_roi, phimin_roi, zmin_roi, rmax_roi, phimax_roi, zmax_roi)
0131             << std::endl;
0132   // calculate the dimensions, in f-bins in our region of interest
0133   nr_roi = rmax_roi - rmin_roi;
0134   nphi_roi = phimax_roi - phimin_roi;
0135   nz_roi = zmax_roi - zmin_roi;
0136   std::cout << std::format("AnnularFieldSim::AnnularFieldSim calc'd roi variables nr={} nphi={} nz={}",
0137                            nr_roi, nphi_roi, nz_roi)
0138             << std::endl;
0139 
0140   // create an array to hold the SC-induced electric field in the roi with the specified dimensions
0141   Efield = new MultiArray<TVector3>(nr_roi, nphi_roi, nz_roi);
0142   for (int i = 0; i < Efield->Length(); i++)
0143   {
0144     Efield->GetFlat(i)->SetXYZ(0, 0, 0);
0145   }
0146 
0147   // and to hold the external electric fieldmap over the region of interest
0148   Eexternal = new MultiArray<TVector3>(nr_roi, nphi_roi, nz_roi);
0149   for (int i = 0; i < Eexternal->Length(); i++)
0150   {
0151     Eexternal->GetFlat(i)->SetXYZ(0, 0, 0);
0152   }
0153 
0154   // ditto the external magnetic fieldmap
0155   Bfield = new MultiArray<TVector3>(nr_roi, nphi_roi, nz_roi);
0156   for (int i = 0; i < Bfield->Length(); i++)
0157   {
0158     Bfield->GetFlat(i)->SetXYZ(0, 0, 0);
0159   }
0160 
0161   // handle the lookup table construction:
0162   lookupCase = in_lookupCase;
0163   chargeCase = in_chargeCase;
0164   if (chargeCase == ChargeCase::NoSpacecharge)
0165   {
0166     lookupCase = LookupCase::NoLookup;  // don't build a lookup model if there's no charge.  It just wastes time.
0167   }
0168 
0169   if (lookupCase == Full3D)
0170   {
0171     std::cout << std::format("AnnularFieldSim::AnnularFieldSim building Epartial (full3D) with nr_roi={} nphi_roi={} nz_roi={}  =~{:.2f}M TVector3 objects",
0172                              nr_roi, nphi_roi, nz_roi, nr_roi * nphi_roi * nz_roi * nr * nphi * nz / 1.0e6)
0173               << std::endl;
0174 
0175     Epartial = new MultiArray<TVector3>(nr_roi, nphi_roi, nz_roi, nr, nphi, nz);
0176     for (int i = 0; i < Epartial->Length(); i++)
0177     {
0178       Epartial->GetFlat(i)->SetXYZ(0, 0, 0);
0179     }
0180     // and kill the arrays we shouldn't be using:
0181     Epartial_highres = new MultiArray<TVector3>(1);
0182     Epartial_highres->GetFlat(0)->SetXYZ(0, 0, 0);
0183 
0184     Epartial_lowres = new MultiArray<TVector3>(1);
0185     Epartial_lowres->GetFlat(0)->SetXYZ(0, 0, 0);
0186 
0187     Epartial_phislice = new MultiArray<TVector3>(1);
0188     Epartial_phislice->GetFlat(0)->SetXYZ(0, 0, 0);
0189     q_lowres = new MultiArray<double>(1);
0190     *(q_lowres->GetFlat(0)) = 0;
0191     q_local = new MultiArray<double>(1);
0192     *(q_local->GetFlat(0)) = 0;
0193   }
0194   else if (lookupCase == HybridRes)
0195   {
0196     std::cout << "lookupCase==HybridRes" << std::endl;
0197     // zero out the other two:
0198     Epartial = new MultiArray<TVector3>(1);
0199     Epartial->GetFlat(0)->SetXYZ(0, 0, 0);
0200 
0201     Epartial_phislice = new MultiArray<TVector3>(1);
0202     Epartial_phislice->GetFlat(0)->SetXYZ(0, 0, 0);
0203   }
0204   else if (lookupCase == PhiSlice)
0205   {
0206     std::cout << "lookupCase==PhiSlice" << std::endl;
0207 
0208     Epartial_phislice = new MultiArray<TVector3>(nr_roi, 1, nz_roi, nr, nphi, nz);
0209     for (int i = 0; i < Epartial_phislice->Length(); i++)
0210     {
0211       Epartial_phislice->GetFlat(i)->SetXYZ(0, 0, 0);
0212     }
0213     // zero out the other two:
0214     Epartial = new MultiArray<TVector3>(1);
0215     Epartial->GetFlat(0)->SetXYZ(0, 0, 0);
0216     Epartial_highres = new MultiArray<TVector3>(1);
0217     Epartial_highres->GetFlat(0)->SetXYZ(0, 0, 0);
0218 
0219     Epartial_lowres = new MultiArray<TVector3>(1);
0220     Epartial_lowres->GetFlat(0)->SetXYZ(0, 0, 0);
0221 
0222     q_lowres = new MultiArray<double>(1);
0223     *(q_lowres->GetFlat(0)) = 0;
0224     q_local = new MultiArray<double>(1);
0225     *(q_local->GetFlat(0)) = 0;
0226   }
0227   else if (lookupCase == Analytic || lookupCase == NoLookup)
0228   {
0229     std::cout << "lookupCase==Analytic (or NoLookup)" << std::endl;
0230 
0231     // zero them all out:
0232     Epartial_phislice = new MultiArray<TVector3>(1);
0233     Epartial_phislice->GetFlat(0)->SetXYZ(0, 0, 0);
0234 
0235     Epartial = new MultiArray<TVector3>(1);
0236     Epartial->GetFlat(0)->SetXYZ(0, 0, 0);
0237 
0238     Epartial_highres = new MultiArray<TVector3>(1);
0239     Epartial_highres->GetFlat(0)->SetXYZ(0, 0, 0);
0240 
0241     Epartial_lowres = new MultiArray<TVector3>(1);
0242     Epartial_lowres->GetFlat(0)->SetXYZ(0, 0, 0);
0243 
0244     q_lowres = new MultiArray<double>(1);
0245     *(q_lowres->GetFlat(0)) = 0;
0246     q_local = new MultiArray<double>(1);
0247     *(q_local->GetFlat(0)) = 0;
0248   }
0249   else
0250   {
0251     std::cout << "Ran into wrong lookupCase logic in constructor." << std::endl;
0252     exit(1);
0253   }
0254 
0255   return;
0256 }
0257 AnnularFieldSim::AnnularFieldSim(float in_innerRadius, float in_outerRadius, float in_outerZ,
0258                                  int r, int roi_r0, int roi_r1, int in_rLowSpacing, int in_rHighSize,
0259                                  int phi, int roi_phi0, int roi_phi1, int in_phiLowSpacing, int in_phiHighSize,
0260                                  int z, int roi_z0, int roi_z1, int in_zLowSpacing, int in_zHighSize,
0261                                  float vdr, LookupCase in_lookupCase)
0262   : AnnularFieldSim(in_innerRadius, in_outerRadius, in_outerZ,
0263                     r, roi_r0, roi_r1, in_rLowSpacing, in_rHighSize,
0264                     phi, roi_phi0, roi_phi1, in_phiLowSpacing, in_phiHighSize,
0265                     z, roi_z0, roi_z1, in_zLowSpacing, in_zHighSize,
0266                     vdr, in_lookupCase, ChargeCase::FromFile)
0267 {
0268   std::cout << "AnnularFieldSim::OldConstructor building AnnularFieldSim with ChargeCase::FromFile" << std::endl;
0269   return;
0270 }
0271 AnnularFieldSim::AnnularFieldSim(float in_innerRadius, float in_outerRadius, float in_outerZ,
0272                                  int r, int roi_r0, int roi_r1,
0273                                  int phi, int roi_phi0, int roi_phi1,
0274                                  int z, int roi_z0, int roi_z1,
0275                                  float vdr, LookupCase in_lookupCase)
0276   : AnnularFieldSim(in_innerRadius, in_outerRadius, in_outerZ,
0277                     r, roi_r0, roi_r1, 1, 3,
0278                     phi, roi_phi0, roi_phi1, 1, 3,
0279                     z, roi_z0, roi_z1, 1, 3,
0280                     vdr, in_lookupCase, ChargeCase::FromFile)
0281 {
0282   // just passing through for the old version again
0283   // creates a region with high-res size of 3 (enough to definitely cover the eight local l-bins) and low-res spacing of 1, which ought to match the behavior (with a little more overhead) from when there was no highres-lowres distinction
0284   std::cout << "AnnularFieldSim::OldConstructor building AnnularFieldSim with local_size=1 in all dimensions, lowres_spacing=1 in all dimensions" << std::endl;
0285   return;
0286 }
0287 AnnularFieldSim::AnnularFieldSim(float rin, float rout, float dz, int r, int phi, int z, float vdr)
0288   : AnnularFieldSim(rin, rout, dz,
0289                     r, 0, r,
0290                     phi, 0, phi,
0291                     z, 0, z,
0292                     vdr, LookupCase::PhiSlice)
0293 {
0294   // just a pass-through to go from the old style to the more detailed version.
0295   std::cout << "AnnularFieldSim::OldConstructor building AnnularFieldSim with roi=full in all dimensions" << std::endl;
0296   return;
0297 }
0298 
0299 TVector3 AnnularFieldSim::calc_unit_field(TVector3 at, TVector3 from)
0300 {
0301   // if(debugFlag()) print_need_cout("%d: AnnularFieldSim::calc_unit_field(at=(r=%f,phi=%f,z=%f))\n",__LINE__,at.Perp(),at.Phi(),at.Z());
0302   int r_position;
0303   if (GetRindexAndCheckBounds(at.Perp(), &r_position) != InBounds)
0304   {
0305     std::cout << std::format("something's asking for 'at' with r={:.6f}, which is index={}",
0306                              at.Perp(), r_position)
0307               << std::endl;
0308     exit(1);
0309   }
0310   // note this is the field due to a fixed point charge in free space.
0311   // if doing cylindrical calcs with different boundary conditions, this needs to change.
0312 
0313   // this could check roi bounds before returning, if things start acting funny.
0314 
0315   TVector3 field(0, 0, 0);
0316 
0317   // if we're more than truncation_length away, don't bother? rcc food for thought.  right now _length is in bins, so tricky.
0318 
0319   // if the green's function class is not present use free space:
0320   if (green == nullptr)
0321   {
0322     TVector3 delr = at - from;
0323     field = delr;  // to set the direction.
0324     if (delr.Mag() < ALMOST_ZERO * ALMOST_ZERO)
0325     {  // note that this has blurred units -- it should scale with all three dimensions of stepsize.  For lots of phi bins, especially, this might start to read as small before it's really small.
0326        // do nothing.  the vector is already zero, which will be our approximation.
0327        // field.SetMag(0);//no contribution if we're in the same cell. -- but root warns if trying to resize something of magnitude zero.
0328     }
0329     else
0330     {
0331       field.SetMag(k_perm * 1 / (delr * delr));  // scalar product on the bottom. unitful, since we defined k_perm and delr with their correct units. (native units V=1 C=1 cm=1)
0332     }
0333     // print_need_cout("calc_unit_field at (%2.2f,%2.2f,%2.2f) from  (%2.2f,%2.2f,%2.2f).  Mag=%2.4fe-9\n",at.x(),at.Y(),at.Z(),from.X(),from.Y(),from.Z(),field.Mag()*1e9);
0334   }
0335   else
0336   {
0337     double atphi = FilterPhiPos(at.Phi());
0338     double fromphi = FilterPhiPos(from.Phi());
0339     double delphi = std::abs(at.DeltaPhi(from));
0340     // to allow us to use the same greens function set for both sides of the tpc, we shift into the valid greens region if needed:
0341     at.SetZ(at.Z() + green_shift);
0342     from.SetZ(from.Z() + green_shift);
0343     double Er = green->Er(at.Perp(), atphi, at.Z(), from.Perp(), fromphi, from.Z());
0344     // don't try to compute Ephi if the delta phi is too small, since it will be zero (and will have a div0 somewhere):
0345     double Ephi = 0;
0346     if (delphi > ALMOST_ZERO)
0347     {
0348       Ephi = green->Ephi(at.Perp(), atphi, at.Z(), from.Perp(), fromphi, from.Z());
0349     }
0350     double Ez = green->Ez(at.Perp(), atphi, at.Z(), from.Perp(), fromphi, from.Z());
0351     field.SetXYZ(-Er, -Ephi, -Ez);  // now these are the correct components if our test point is at y=0 (hence phi=0);
0352     field = field * epsinv;         // scale field strength, since the greens functions as of Apr 1 2020 do not build-in this factor.
0353     field.RotateZ(at.Phi());        // rotate to the coordinates of our 'at' point, which is a small rotation for the phislice case.
0354     // but this does mean we need to be careful! If we are rotating so that this is pointing not in the R direction, then with azimuthally symmetric charge we will have some cancellation we don't want, maybe?  Make sure this matches how we rotate to sum_field!
0355   }
0356   return field;
0357 }
0358 
0359 TVector3 AnnularFieldSim::GetLocalFieldComponents(const TVector3 &field, const TVector3 &pos, const TVector3 &origin)
0360 {
0361   // this function returns the components of the field in the local coordinate system, which is in cylindrical coords
0362   // and has a specified origin.
0363   // it assumes that the field and all coordinates are given in the global, cartesian coordinate system.
0364   // it returns a TVector3 with the radial, azimuthal, and z components of the field in the local coordinate system.
0365 
0366   TVector3 local_pos = pos - origin;  // shift the position to the local coordinate system
0367   // get the cylindrical radial vector pointing from the origin to the position:
0368   TVector3 radial_vector = local_pos;
0369   radial_vector.SetZ(0);  // zero out the z component, so we have a radial vector in the xy plane.
0370   if (radial_vector.Mag() == 0)
0371   {
0372     // if it's zero, make it arbitrary direction.
0373     radial_vector.SetXYZ(1, 0, 0);  // set it to point in the x direction.
0374   }
0375   else
0376   {
0377     radial_vector.SetMag(1);  // normalize it to unit length, so we can use it to rotate the field vector.
0378   }
0379   // get the cylindrical azimuthal vector, which is perpendicular to the radial vector:
0380   TVector3 azimuthal_vector = radial_vector;
0381   azimuthal_vector.SetXYZ(-radial_vector.Y(), radial_vector.X(), 0);  // rotate by 90 degrees to get the azimuthal vector in the xy plane.
0382   // now we can project the field vector onto the radial and azimuthal vectors to get the components in the local coordinate system, and return a TVector3 with those components:
0383   TVector3 local_field;
0384   local_field.SetXYZ(field.Dot(radial_vector), field.Dot(azimuthal_vector), field.Z());  // radial, azimuthal, and z components
0385   return local_field;
0386 }
0387 
0388 double AnnularFieldSim::FilterPhiPos(double phi)
0389 {
0390   // this primarily takes the region [-pi,0] and maps it to [pi,2pi] by adding 2pi to it.
0391   // if math has pushed us past 2pi, it also subtracts to try to get us in range.
0392   double p = phi;
0393   if (p >= 2 * M_PI)  // phispan)
0394   {
0395     p -= 2 * M_PI;
0396   }
0397   if (p < 0)
0398   {
0399     p += 2 * M_PI;
0400   }
0401   if (p >= 2 * M_PI || p < 0)
0402   {
0403     std::cout << std::format("AnnularFieldSim::FilterPhiPos asked to filter {:.6f}, which is more than range={:.6f} out of bounds. Check what called this.",
0404                              phi, 2 * M_PI)
0405               << std::endl;
0406     exit(1);
0407   }
0408   return p;
0409 }
0410 int AnnularFieldSim::FilterPhiIndex(int phi, int range = -1) const
0411 {
0412   if (range < 0)
0413   {
0414     range = nphi;  // default input is range=-1.  in that case, use the intrinsic resolution of the q grid.
0415   }
0416 
0417   // shifts phi up or down by nphi (=2pi in phi index space), and complains if this doesn't put it in range.
0418   int p = phi;
0419   if (p >= range)
0420   {
0421     p -= range;
0422   }
0423   if (p < 0)
0424   {
0425     p += range;
0426   }
0427   if (p >= range || p < 0)
0428   {
0429     std::cout << std::format("AnnularFieldSim::FilterPhiIndex asked to filter {}, which is more than range={} out of bounds. Check what called this.", phi, range) << std::endl;
0430     exit(1);
0431   }
0432   return p;
0433 }
0434 
0435 int AnnularFieldSim::GetRindex(float pos)
0436 {
0437   float r0f = (pos - rmin) / step.Perp();  // the position in r, in units of step, starting from the low edge of the 0th bin.
0438   int r0 = std::floor(r0f);
0439   return r0;
0440 }
0441 
0442 int AnnularFieldSim::GetPhiIndex(float pos)
0443 {
0444   float p0f = (pos) / step.Phi();  // the position in phi, in units of step, starting from the low edge of the 0th bin.
0445   int phitemp = std::floor(p0f);
0446   int p0 = FilterPhiIndex(phitemp);
0447   return p0;
0448 }
0449 
0450 int AnnularFieldSim::GetZindex(float pos)
0451 {
0452   float z0f = (pos - zmin) / step.Z();  // the position in z, in units of step, starting from the low edge of the 0th bin.
0453   int z0 = std::floor(z0f);
0454   return z0;
0455 }
0456 
0457 AnnularFieldSim::BoundsCase AnnularFieldSim::GetRindexAndCheckBounds(float pos, int *r)
0458 {
0459   // if(debugFlag()) print_need_cout("%d: AnnularFieldSim::GetRindexAndCheckBounds(r=%f)\n",__LINE__,pos);
0460 
0461   float r0f = (pos - rmin) / step.Perp();  // the position in r, in units of step, starting from the low edge of the 0th bin.
0462   int r0 = std::floor(r0f);
0463   *r = r0;
0464 
0465   int r0lowered_slightly = std::floor(r0f - ALMOST_ZERO);
0466   int r0raised_slightly = std::floor(r0f + ALMOST_ZERO);
0467   if (r0lowered_slightly >= rmax_roi || r0raised_slightly < rmin_roi)
0468   {
0469     return OutOfBounds;
0470   }
0471 
0472   // now if we are out of bounds, it is because we are on an edge, within range of ALMOST_ZERO of being in fair territory.
0473   if (r0 >= rmax_roi)
0474   {
0475     return OnHighEdge;
0476   }
0477   if (r0 < rmin_roi)
0478   {
0479     return OnLowEdge;
0480   }
0481   // if we're still here, we're in bounds.
0482   return InBounds;
0483 }
0484 AnnularFieldSim::BoundsCase AnnularFieldSim::GetPhiIndexAndCheckBounds(float pos, int *phi)
0485 {
0486   // if(debugFlag()) print_need_cout("%d: AnnularFieldSim::GetPhiIndexAndCheckBounds(phi=%f)\n\n",__LINE__,pos);
0487   float p0f = (pos) / step.Phi();  // the position in phi, in units of step, starting from the low edge of the 0th bin.
0488   int phitemp = std::floor(p0f);
0489   int p0 = FilterPhiIndex(phitemp);
0490   *phi = p0;
0491 
0492   phitemp = std::floor(p0f - ALMOST_ZERO);
0493   int p0lowered_slightly = FilterPhiIndex(phitemp);
0494   phitemp = std::floor(p0f + ALMOST_ZERO);
0495   int p0raised_slightly = FilterPhiIndex(phitemp);
0496   // annoying detail:  if we are at index 0, we might go above pmax by going down.
0497   //  and if we are at nphi-1, we might go below pmin by going up.
0498   //  if we are not at p0=0 or nphi-1, the slight wiggles won't move us.
0499   //  if we are at p0=0, we are not at or above the max, and lowering slightly won't change that,
0500   //  and is we are at p0=nphi-1, we are not below the min, and raising slightly won't change that
0501   if ((p0lowered_slightly >= phimax_roi && p0 != 0) || (p0raised_slightly < phimin_roi && p0 != (nphi - 1)))
0502   {
0503     return OutOfBounds;
0504   }
0505   // now if we are out of bounds, it is because we are on an edge, within range of ALMOST_ZERO of being in fair territory.
0506   if (p0 >= phimax_roi)
0507   {
0508     return OnHighEdge;
0509   }
0510   if (p0 < phimin_roi)
0511   {
0512     return OnLowEdge;
0513   }
0514   // if we're still here, we're in bounds.
0515   return InBounds;
0516 }
0517 
0518 AnnularFieldSim::BoundsCase AnnularFieldSim::GetZindexAndCheckBounds(float pos, int *z)
0519 {
0520   // if(debugFlag()) print_need_cout("%d: AnnularFieldSim::GetZindexAndCheckBounds(z=%f)\n\n",__LINE__,pos);
0521   float z0f = (pos - zmin) / step.Z();  // the position in z, in units of step, starting from the low edge of the 0th bin.
0522   int z0 = std::floor(z0f);
0523   *z = z0;
0524 
0525   int z0lowered_slightly = std::floor(z0f - ALMOST_ZERO);
0526   int z0raised_slightly = std::floor(z0f + ALMOST_ZERO);
0527 
0528   if (z0lowered_slightly >= zmax_roi || z0raised_slightly < zmin_roi)
0529   {
0530     return OutOfBounds;
0531   }
0532   // now if we are out of bounds, it is because we are on an edge, within range of ALMOST_ZERO of being in fair territory.
0533   if (z0 >= zmax_roi)
0534   {
0535     return OnHighEdge;
0536   }
0537   if (z0 < zmin_roi)
0538   {
0539     return OnLowEdge;
0540   }
0541   // if we're still here, we're in bounds.
0542   return InBounds;
0543 }
0544 
0545 TVector3 AnnularFieldSim::analyticFieldIntegral(float zdest, TVector3 start, MultiArray<TVector3> *field)
0546 {
0547   // integrates E dz, from the starting point to the selected z position.  The path is assumed to be along z for each step, with adjustments to x and y accumulated after each step.
0548   // if(debugFlag()) print_need_cout("%d: AnnularFieldSim::fieldIntegral(x=%f,y=%f, z=%f) to z=%f\n\n",__LINE__,start.X(),start.Y(),start.Z(),zdest);
0549   //   print_need_cout("AnnularFieldSim::analyticFieldIntegral calculating from (%f,%f,%f) (rphiz)=(%f,%f,%f) to z=%f.\n",start.X(),start.Y(),start.Z(),start.Perp(),start.Phi(),start.Z(),zdest);
0550 
0551   // coordinates are assumed to be in native units (cm=1);
0552 
0553   int r;
0554   int phi;
0555   bool rOkay = (GetRindexAndCheckBounds(start.Perp(), &r) == InBounds);
0556   bool phiOkay = (GetPhiIndexAndCheckBounds(FilterPhiPos(start.Phi()), &phi) == InBounds);
0557 
0558   // bool isE=(field==Efield);
0559   // bool isB=(field==Bfield);
0560 
0561   // print_need_cout("anaFieldInt:  isE=%d, isB=%d, start=(%f,%f,%f)\n",isE,isB,start.X(),start.Y(),start.Z());
0562 
0563   if (!rOkay || !phiOkay)
0564   {
0565     std::cout << std::format("AnnularFieldSim::analyticFieldIntegral asked to integrate along (r={:.6f}, phi={:.6f}), index=({}, {}), which is out of bounds. returning starting position.",
0566                              start.Perp(), start.Phi(), r, phi)
0567               << std::endl;
0568     return start;
0569   }
0570 
0571   int dir = (start.Z() > zdest ? -1 : 1);  //+1 if going to larger z, -1 if going to smaller;  if they're the same, the sense doesn't matter.
0572 
0573   int zi;
0574   int zf;
0575   double startz;
0576   double endz;
0577   BoundsCase startBound;
0578   BoundsCase endBound;
0579 
0580   // make sure 'zi' is always the smaller of the two numbers, for handling the partial-steps.
0581   if (dir > 0)
0582   {
0583     startBound = GetZindexAndCheckBounds(start.Z(), &zi);  // highest cell with lower bound less than lower bound of integral
0584     endBound = GetZindexAndCheckBounds(zdest, &zf);        // highest cell with lower bound less than upper lower bound of integral
0585     startz = start.Z();
0586     endz = zdest;
0587   }
0588   else
0589   {
0590     endBound = GetZindexAndCheckBounds(start.Z(), &zf);  // highest cell with lower bound less than lower bound of integral
0591     startBound = GetZindexAndCheckBounds(zdest, &zi);    // highest cell with lower bound less than upper lower bound of integral
0592     startz = zdest;
0593     endz = start.Z();
0594   }
0595   bool startOkay = (startBound == InBounds);
0596   bool endOkay = (endBound == InBounds || endBound == OnHighEdge);  // if we just barely touch out-of-bounds on the high end, we can skip that piece of the integral
0597 
0598   if (!startOkay || !endOkay)
0599   {
0600     std::cout << std::format("AnnularFieldSim::analyticFieldIntegral asked to integrate from z={:.6f} to {:.6f}, index={} to {}, which is out of bounds. returning starting position.",
0601                              startz, endz, zi, zf)
0602               << std::endl;
0603     return start;
0604   }
0605 
0606   start.SetZ(startz);
0607   TVector3 integral;
0608   if (field == Efield)
0609   {
0610     integral = aliceModel->Eint(endz, start) + Eexternal->Get(r - rmin_roi, phi - phimin_roi, zi - zmin_roi) * (endz - startz);
0611     return dir * integral;
0612   }
0613   if (field == Bfield)
0614   {
0615     return interpolatedFieldIntegral(zdest, start, Bfield);
0616   }
0617   return integral;
0618 }
0619 
0620 TVector3 AnnularFieldSim::fieldIntegral(float zdest, const TVector3 &start, MultiArray<TVector3> *field)
0621 {
0622   // integrates E dz, from the starting point to the selected z position.  The path is assumed to be along z for each step, with adjustments to x and y accumulated after each step.
0623   // if(debugFlag()) print_need_cout("%d: AnnularFieldSim::fieldIntegral(x=%f,y=%f, z=%f) to z=%f\n\n",__LINE__,start.X(),start.Y(),start.Z(),zdest);
0624 
0625   int r;
0626   int phi;
0627   bool rOkay = (GetRindexAndCheckBounds(start.Perp(), &r) == InBounds);
0628   bool phiOkay = (GetPhiIndexAndCheckBounds(FilterPhiPos(start.Phi()), &phi) == InBounds);
0629 
0630   if (!rOkay || !phiOkay)
0631   {
0632     std::cout << std::format("AnnularFieldSim::fieldIntegral asked to integrate along (r={:.6f}, phi={:.6f}), index=({}, {}), which is out of bounds. returning starting position.",
0633                              start.Perp(), start.Phi(), r, phi)
0634               << std::endl;
0635     return start;
0636   }
0637 
0638   int dir = (start.Z() > zdest ? -1 : 1);  //+1 if going to larger z, -1 if going to smaller;  if they're the same, the sense doesn't matter.
0639 
0640   int zi;
0641   int zf;
0642   double startz;
0643   double endz;
0644   BoundsCase startBound;
0645   BoundsCase endBound;
0646 
0647   // make sure 'zi' is always the smaller of the two numbers, for handling the partial-steps.
0648   if (dir > 0)
0649   {
0650     startBound = GetZindexAndCheckBounds(start.Z(), &zi);  // highest cell with lower bound less than lower bound of integral
0651     endBound = GetZindexAndCheckBounds(zdest, &zf);        // highest cell with lower bound less than upper lower bound of integral
0652     startz = start.Z();
0653     endz = zdest;
0654   }
0655   else
0656   {
0657     endBound = GetZindexAndCheckBounds(start.Z(), &zf);  // highest cell with lower bound less than lower bound of integral
0658     startBound = GetZindexAndCheckBounds(zdest, &zi);    // highest cell with lower bound less than upper lower bound of integral
0659     startz = zdest;
0660     endz = start.Z();
0661   }
0662   bool startOkay = (startBound == InBounds);
0663   bool endOkay = (endBound == InBounds || endBound == OnHighEdge);  // if we just barely touch out-of-bounds on the high end, we can skip that piece of the integral
0664 
0665   if (!startOkay || !endOkay)
0666   {
0667     std::cout << std::format("AnnularFieldSim::fieldIntegral asked to integrate from z={:.6f} to {:.6f}, index={} to {}, which is out of bounds. returning starting position.",
0668                              startz, endz, zi, zf)
0669               << std::endl;
0670     return start;
0671   }
0672 
0673   TVector3 fieldInt(0, 0, 0);
0674   // print_need_cout("AnnularFieldSim::fieldIntegral requesting (%d,%d,%d)-(%d,%d,%d) (inclusive) cells\n",r,phi,zi,r,phi,zf-1);
0675   TVector3 tf;
0676   for (int i = zi; i < zf; i++)
0677   {  // count the whole cell of the lower end, and skip the whole cell of the high end.
0678     tf = field->Get(r - rmin_roi, phi - phimin_roi, i - zmin_roi);
0679     // print_need_cout("fieldAt (%d,%d,%d)=(%f,%f,%f) step=%f\n",r,phi,i,tf.X(),tf.Y(),tf.Z(),step.Z());
0680     fieldInt += tf * step.Z();
0681   }
0682 
0683   // since bins contain their lower bound, but not their upper, I can safely remove the unused portion of the lower cell:
0684   fieldInt -= field->Get(r - rmin_roi, phi - phimin_roi, zi - zmin_roi) * (startz - zi * step.Z());  // remove the part of the low end cell we didn't travel through
0685 
0686   // but only need to add the used portion of the upper cell if we go past the edge of it meaningfully:
0687   if (endz / step.Z() - zf > ALMOST_ZERO)
0688   {
0689     // print_need_cout("endz/step.Z()=%f, zf=%f\n",endz/step.Z(),zf*1.0);
0690     // if our final step is actually in the next step.
0691     fieldInt += field->Get(r - rmin_roi, phi - phimin_roi, zf - zmin_roi) * (endz - zf * step.Z());  // add the part of the high end cell we did travel through
0692   }
0693 
0694   return dir * fieldInt;
0695 }
0696 
0697 TVector3 AnnularFieldSim::GetCellCenter(int r, int phi, int z)
0698 {
0699   // returns the midpoint of the cell (halfway between each edge, not weighted center)
0700 
0701   TVector3 c(1, 0, 0);
0702   c.SetPerp((r + 0.5) * step.Perp() + rmin);
0703   c.SetPhi((phi + 0.5) * step.Phi());
0704   c.SetZ((z + 0.5) * step.Z() + zmin);
0705 
0706   return c;
0707 }
0708 
0709 TVector3 AnnularFieldSim::GetRoiCellCenter(int r, int phi, int z)
0710 {
0711   // returns the midpoint of the cell relative to the roi (halfway between each edge, not weighted center)
0712 
0713   // return zero if it's out of bounds:
0714   if (r > nr_roi || r < 0 || phi > nphi_roi || phi < 0 || z > nz_roi || z < 0)
0715   {
0716     return zero_vector;
0717   }
0718 
0719   TVector3 c(1, 0, 0);
0720   c.SetPerp((r + rmin_roi + 0.5) * step.Perp() + rmin);
0721   c.SetPhi((phi + phimin_roi + 0.5) * step.Phi());
0722   c.SetZ((z + zmin_roi + 0.5) * step.Z() + zmin);
0723 
0724   return c;
0725 }
0726 
0727 TVector3 AnnularFieldSim::GetGroupCellCenter(int r0, int r1, int phi0, int phi1, int z0, int z1)
0728 {
0729   // returns the midpoint of the cell (halfway between each edge, not weighted center)
0730   float ravg = (r0 + r1) / 2.0 + 0.5;
0731   if (phi0 > phi1)
0732   {
0733     phi1 += nphi;
0734   }
0735   if (phi0 > phi1)
0736   {
0737     std::cout << std::format("phi1({})<=phi0({}) even after boosting phi1. check what called this!", phi1, phi0) << std::endl;
0738     exit(1);
0739   }
0740   float phiavg = (r0 + r1) / 2.0 + 0.5;
0741   if (phiavg >= nphi)
0742   {
0743     phiavg -= nphi;
0744   }
0745 
0746   float zavg = (z0 + z1) / 2.0 + 0.5;
0747 
0748   TVector3 c(1, 0, 0);
0749   c.SetPerp((ravg) *step.Perp() + rmin);
0750   c.SetPhi((phiavg) *step.Phi());
0751   c.SetZ((zavg) *step.Z() + zmin);
0752 
0753   return c;
0754 }
0755 
0756 TVector3 AnnularFieldSim::GetWeightedCellCenter(int r, int phi, int z)
0757 {
0758   // returns the weighted center of the cell by volume.
0759   // todo:  this is vaguely hefty, and might be worth storing the result of, if speed is needed
0760   TVector3 c(1, 0, 0);
0761 
0762   float rin = r * step.Perp() + rmin;
0763   float rout = rin + step.Perp();
0764 
0765   float rMid = (4 * sin(step.Phi() / 2) * (pow(rout, 3) - pow(rin, 3)) / (3 * step.Phi() * (pow(rout, 2) - pow(rin, 2))));
0766   c.SetPerp(rMid);
0767   c.SetPhi((phi + 0.5) * step.Phi());
0768   c.SetZ((z + 0.5) * step.Z());
0769 
0770   return c;
0771 }
0772 
0773 TVector3 AnnularFieldSim::interpolatedFieldIntegral(float zdest, const TVector3 &start, MultiArray<TVector3> *field)
0774 {
0775   // print_need_cout("AnnularFieldSim::interpolatedFieldIntegral(x=%f,y=%f, z=%f)\n",start.X(),start.Y(),start.Z());
0776 
0777   float r0 = (start.Perp() - rmin) / step.Perp() - 0.5;  // the position in r, in units of step, starting from the center of the 0th bin.
0778   int r0i = std::floor(r0);                              // the integer portion of the position. -- what center is below our position?
0779   float r0d = r0 - r0i;                                  // the decimal portion of the position. -- how far past center are we?
0780   int ri[4];                                             // the r position of the four cell centers to consider
0781   ri[0] = ri[1] = r0i;
0782   ri[2] = ri[3] = r0i + 1;
0783   float rw[4];              // the weight of that cell, linear in distance from the center of it
0784   rw[0] = rw[1] = 1 - r0d;  // 1 when we're on it, 0 when we're at the other one.
0785   rw[2] = rw[3] = r0d;      // 1 when we're on it, 0 when we're at the other one
0786 
0787   bool skip[] = {false, false, false, false};
0788   if (ri[0] < rmin_roi)
0789   {
0790     skip[0] = true;  // don't bother handling 0 and 1 in the coordinates.
0791     skip[1] = true;
0792     rw[2] = rw[3] = 1;  // and weight like we're dead-center on the outer cells.
0793   }
0794   if (ri[2] >= rmax_roi)
0795   {
0796     skip[2] = true;  // don't bother handling 2 and 3 in the coordinates.
0797     skip[3] = true;
0798     rw[0] = rw[1] = 1;  // and weight like we're dead-center on the inner cells.
0799   }
0800 
0801   // now repeat that structure for phi:
0802   float p0 = (start.Phi()) / step.Phi() - 0.5;  // the position in phi, in units of step, starting from the center of the 0th bin.
0803   int p0i = std::floor(p0);                     // the integer portion of the position. -- what center is below our position?
0804   float p0d = p0 - p0i;                         // the decimal portion of the position. -- how far past center are we?
0805   int pi[4];                                    // the phi position of the four cell centers to consider
0806   pi[0] = pi[2] = FilterPhiIndex(p0i);
0807   pi[1] = pi[3] = FilterPhiIndex(p0i + 1);
0808   float pw[4];              // the weight of that cell, linear in distance from the center of it
0809   pw[0] = pw[2] = 1 - p0d;  // 1 when we're on it, 0 when we're at the other one.
0810   pw[1] = pw[3] = p0d;      // 1 when we're on it, 0 when we're at the other one
0811 
0812   // to handle wrap-around
0813   if (pi[0] < phimin_roi || pi[0] >= phimax_roi)
0814   {
0815     skip[0] = true;  // don't bother handling 0 and 1 in the coordinates.
0816     skip[2] = true;
0817     pw[1] = pw[3] = 1;  // and weight like we're dead-center on the outer cells.
0818   }
0819   if (pi[1] >= phimax_roi || pi[1] < phimin_roi)
0820   {
0821     skip[1] = true;  // don't bother handling 2 and 3 in the coordinates.
0822     skip[3] = true;
0823     pw[0] = pw[2] = 1;  // and weight like we're dead-center on the inner cells.
0824   }
0825 
0826   int dir = (start.Z() < zdest ? 1 : -1);  //+1 if going to larger z, -1 if going to smaller;  if they're the same, the sense doesn't matter.
0827 
0828   int zi;
0829   int zf;
0830   double startz;
0831   double endz;
0832   BoundsCase startBound;
0833   BoundsCase endBound;
0834 
0835   // make sure 'zi' is always the smaller of the two numbers, for handling the partial-steps.
0836   if (dir > 0)
0837   {
0838     startBound = GetZindexAndCheckBounds(start.Z(), &zi);  // highest cell with lower bound less than lower bound of integral
0839     endBound = GetZindexAndCheckBounds(zdest, &zf);        // highest cell with lower bound less than upper bound of integral
0840     startz = start.Z();
0841     endz = zdest;
0842   }
0843   else
0844   {
0845     endBound = GetZindexAndCheckBounds(start.Z(), &zf);  // highest cell with lower bound less than lower bound of integral
0846     startBound = GetZindexAndCheckBounds(zdest, &zi);    // highest cell with lower bound less than upper bound of integral
0847     startz = zdest;
0848     endz = start.Z();
0849   }
0850   bool startOkay = (startBound == InBounds || startBound == OnLowEdge);  // maybe todo: add handling for being just below the low edge.
0851   bool endOkay = (endBound == InBounds || endBound == OnHighEdge);       // if we just barely touch out-of-bounds on the high end, we can skip that piece of the integral
0852 
0853   if (!startOkay || !endOkay)
0854   {
0855     std::cout << std::format("AnnularFieldSim::InterpolatedFieldIntegral asked to integrate from z={} to {}, index={} to {}, which is out of bounds. Returning zero.", startz, endz, zi, zf) << std::endl;
0856     ;
0857     return zero_vector;
0858   }
0859 
0860   if (startBound == OnLowEdge)
0861   {
0862     // we were just below the low edge, so we will be asked to sample a bin in z we're not actually using
0863     zi++;  // avoid it.  We weren't integrating anything in it anyway.
0864   }
0865 
0866   if (false)
0867   {  // debugging options
0868     bool isE = (field == Efield);
0869     bool isB = (field == Bfield);
0870     std::cout << std::format("interpFieldInt:  isE={}, isB={}, start=({:.4f},{:.4f},{:.4f}) dest={:.4f}", isE, isB, start.X(), start.Y(), start.Z(), zdest) << std::endl;
0871 
0872     std::cout << std::format("interpolating fieldInt for {} at  r={}, phi={}", (field == Efield) ? "Efield" : "Bfield", r0, p0) << std::endl;
0873 
0874     std::cout << std::format("direction={}, startz={:.4f}, endz={:.4f}, zi={}, zf={}", dir, startz, endz, zi, zf) << std::endl;
0875     for (int i = 0; i < 4; i++)
0876     {
0877       std::cout << std::format("skip[{}]={}\tw[i]={:.2f}, (pw={:.2f}, rw={:.2f})", i, skip[i], rw[i] * pw[i], pw[i], rw[i]) << std::endl;
0878     }
0879   }
0880 
0881   TVector3 fieldInt(0, 0, 0);
0882   TVector3 partialInt;  // where we'll store integrals as we generate them.
0883 
0884   for (int i = 0; i < 4; i++)
0885   {
0886     if (skip[i])
0887     {
0888       // print_need_cout("skipping element r=%d,phi=%d\n",ri[i],pi[i]);
0889       continue;  // we invalidated this one for some reason.
0890     }
0891     partialInt.SetXYZ(0, 0, 0);
0892     for (int j = zi; j < zf; j++)
0893     {  // count the whole cell of the lower end, and skip the whole cell of the high end.
0894 
0895       partialInt += field->Get(ri[i] - rmin_roi, pi[i] - phimin_roi, j - zmin_roi) * step.Z();
0896     }
0897     if (startBound != OnLowEdge)
0898     {
0899       partialInt -= field->Get(ri[i] - rmin_roi, pi[i] - phimin_roi, zi - zmin_roi) * (startz - (zi * step.Z() + zmin));  // remove the part of the low end cell we didn't travel through
0900       // print_need_cout("removing low end of cell we didn't travel through (zi-zmin_roi=%d, length=%f)\n",zi-zmin_roi, startz-(zi*step.Z()+zmin));
0901     }
0902     if ((endz - zmin) / step.Z() - zf > ALMOST_ZERO)
0903     {
0904       partialInt += field->Get(ri[i] - rmin_roi, pi[i] - phimin_roi, zf - zmin_roi) * (endz - (zf * step.Z() + zmin));  // add the part of the high end cell we did travel through
0905       // print_need_cout("adding low end of cell we did travel through (zf-zmin_roi=%d, length=%f)\n",zf-zmin_roi, endz-(zf*step.Z()+zmin));
0906     }
0907     // print_need_cout("element r=%d,phi=%d, w=%f partialInt=(%2.2E,%2.2E,%2.2E)\n",ri[i],pi[i],rw[i]*pw[i],partialInt.X(),partialInt.Y(),partialInt.Z());
0908 
0909     fieldInt += rw[i] * pw[i] * partialInt;
0910   }
0911 
0912   return dir * fieldInt;
0913 }
0914 
0915 void AnnularFieldSim::load_analytic_spacecharge(float scalefactor = 1)
0916 {
0917   // scalefactor should be chosen so Rho(pos) returns C/cm^3
0918 
0919   // sphenix:
0920   // double ifc_radius=20;
0921   // double ofc_radius=78;
0922   //   double tpc_halfz=
0923 
0924   double ifc_radius = 83.5 * cm;
0925   double ofc_radius = 254.5 * cm;
0926   double tpc_halfz = 250 * cm;
0927 
0928   aliceModel = new AnalyticFieldModel(ifc_radius / cm, ofc_radius / cm, tpc_halfz / cm, scalefactor);
0929   double totalcharge = 0;
0930   double localcharge = 0;
0931 
0932   TVector3 pos;
0933   for (int ifr = 0; ifr < nr; ifr++)
0934   {
0935     for (int ifphi = 0; ifphi < nphi; ifphi++)
0936     {
0937       for (int ifz = 0; ifz < nz; ifz++)
0938       {
0939         pos = GetCellCenter(ifr, ifphi, ifz);
0940         pos = pos * (1.0 / cm);  // divide by cm so we're in units of cm when we query the charge model.
0941         double vol = step.Z() * step.Phi() * (2 * (ifr * step.Perp() + rmin) + step.Perp()) * step.Perp();
0942         // if(debugFlag()) print_need_cout("%d: AnnularFieldSim::load_analytic_spacecharge adding Q=%f into cell (%d,%d,%d)\n",__LINE__,qbin,i,j,k,localr,localphi,localz);
0943         localcharge = vol * aliceModel->Rho(pos);  // TODO:  figure out what units this is in.
0944         totalcharge += localcharge;
0945         // q->Add(ifr, ifphi, ifz, localcharge);  //scalefactor must be applied to charge _and_ field, and so is handled in the aliceModel code.
0946         q->AddChargeInBin(ifr, ifphi, ifz, localcharge);  // scalefactor must be applied to charge _and_ field, and so is handled in the aliceModel code.
0947       }
0948     }
0949   }
0950   std::cout << std::format("AnnularFieldSim::load_analytic_spacecharge:  Total charge Q={:E} Coulombs", totalcharge) << std::endl;
0951 
0952   if (lookupCase == HybridRes)
0953   {
0954     // go through the q array and build q_lowres.
0955     for (int i = 0; i < q_lowres->Length(); i++)
0956     {
0957       *(q_lowres->GetFlat(i)) = 0;
0958     }
0959 
0960     // fill our low-res
0961     // note that this assumes the last bin is short or normal length, not long.
0962     for (int ifr = 0; ifr < nr; ifr++)
0963     {
0964       int r_low = ifr / r_spacing;  // index of our l-bin is just the integer division of the index of our f-bin
0965       for (int ifphi = 0; ifphi < nphi; ifphi++)
0966       {
0967         int phi_low = ifphi / phi_spacing;
0968         for (int ifz = 0; ifz < nz; ifz++)
0969         {
0970           int z_low = ifz / z_spacing;
0971           q_lowres->Add(r_low, phi_low, z_low, q->GetChargeInBin(ifr, ifphi, ifz));
0972         }
0973       }
0974     }
0975   }
0976   return;
0977 }
0978 
0979 void AnnularFieldSim::loadEfield(const std::string &filename, const std::string &treename, int zsign)
0980 {
0981   // prep variables so that loadField can just iterate over the tree entries and fill our selected tree agnostically
0982   // assumes file stores fields as V/cm.
0983   std::cout << std::format("AnnularFieldSim::loadEfield:  loading E field from file {}, tree {} (sign={})", filename, treename, zsign) << std::endl;
0984   TFile fieldFile(filename.c_str(), "READ");
0985   TTree *fTree;
0986   fieldFile.GetObject(treename.c_str(), fTree);
0987   Efieldname = "E:" + filename + ":" + treename;
0988   float r;
0989   float z;  // coordinates
0990   float fr;
0991   float fz;
0992   float fphi;  // field components at that coordinate
0993   fTree->SetBranchAddress("r", &r);
0994   fTree->SetBranchAddress("er", &fr);
0995   fTree->SetBranchAddress("z", &z);
0996   fTree->SetBranchAddress("ez", &fz);
0997   // phi would go here if we had it.
0998   fphi = 0;  // no phi components yet.
0999   // float phi = 0.;
1000   // phi += 1;
1001   // phi = 0;  //satisfy picky racf compiler
1002   loadField(&Eexternal, fTree, &r, nullptr, &z, &fr, &fphi, &fz, V / cm, zsign);
1003   std::cout << std::format("AnnularFieldSim::loadEfield:  finished loading E field from file {}, tree {} (sign={})", filename, treename, zsign) << std::endl;
1004   fieldFile.Close();
1005   return;
1006 }
1007 void AnnularFieldSim::loadBfield(const std::string &filename, const std::string &treename)
1008 {
1009   // prep variables so that loadField can just iterate over the tree entries and fill our selected tree agnostically
1010   // assumes file stores field as Tesla.
1011   TFile fieldFile(filename.c_str(), "READ");
1012   TTree *fTree;
1013   fieldFile.GetObject(treename.c_str(), fTree);
1014   Bfieldname = "B:" + filename + ":" + treename;
1015   float r;
1016   float z;  // coordinates
1017   float fr;
1018   float fz;
1019   float fphi;  // field components at that coordinate
1020   fTree->SetBranchAddress("r", &r);
1021   fTree->SetBranchAddress("br", &fr);
1022   fTree->SetBranchAddress("z", &z);
1023   fTree->SetBranchAddress("bz", &fz);
1024   // phi would go here if we had it.
1025   fphi = 0;  // no phi components yet.
1026   // float phi = 0.;
1027   // phi += 1;
1028   // phi = 0;  //satisfy picky racf compiler
1029   loadField(&Bfield, fTree, &r, nullptr, &z, &fr, &fphi, &fz, Tesla, 1);
1030   std::cout << std::format("AnnularFieldSim::loadBfield:  finished loading B field from file {}, tree {}", filename, treename) << std::endl;
1031   ;
1032   fieldFile.Close();
1033 
1034   return;
1035 }
1036 
1037 void AnnularFieldSim::load3dBfield(const std::string &filename, const std::string &treename, int zsign, float scale, float xshift, float yshift, float zshift)
1038 {
1039   // prep variables so that loadField can just iterate over the tree entries and fill our selected tree agnostically
1040   // assumes file stores field as Tesla.
1041   std::cout << std::format("AnnularFieldSim::load3dBfield:  loading B field from file {}, tree {} (sign={})", filename, treename, zsign) << std::endl;
1042 
1043   TFile fieldFile(filename.c_str(), "READ");
1044   TTree *fTree;
1045   fieldFile.GetObject(treename.c_str(), fTree);
1046   Bfieldname = "B(3D):" + filename + ":" + treename + " Scale =" + std::format("{:.2f}", scale);
1047   float r;
1048   float z;
1049   float phi;  // coordinates
1050   float fr;
1051   float fz;
1052   float fphi;  // field components at that coordinate
1053   fTree->SetBranchAddress("rho", &r);
1054   fTree->SetBranchAddress("brho", &fr);
1055   fTree->SetBranchAddress("z", &z);
1056   fTree->SetBranchAddress("bz", &fz);
1057   fTree->SetBranchAddress("phi", &phi);
1058   fTree->SetBranchAddress("bphi", &fphi);
1059   loadField(&Bfield, fTree, &r, &phi, &z, &fr, &fphi, &fz, Tesla * scale, zsign, xshift, yshift, zshift);
1060   std::cout << std::format("AnnularFieldSim::load3dBfield:  finished loading B field from file {}, tree {} (sign={}) with scale {}\n", filename, treename, zsign, scale) << std::endl;
1061   fieldFile.Close();
1062   return;
1063 }
1064 
1065 void AnnularFieldSim::loadField(MultiArray<TVector3> **field, TTree *source, const float *rptr, const float *phiptr, const float *zptr, const float *frptr, const float *fphiptr, const float *fzptr, float fieldunit, int zsign, float xshift, float yshift, float zshift)
1066 {
1067   // we're loading a tree of unknown size and spacing -- and possibly uneven spacing -- into our local data.
1068   // formally, we might want to interpolate or otherwise weight, but for now, carve this into our usual bins, and average, similar to the way we load spacecharge.
1069   // the x,y,z shift moves the detector in the field (hence for a +1 shift in each, the field value at (0,0,0) is recorded at detector coordinate (-1,-1,-1)
1070   TVector3 origin = TVector3(xshift, yshift, zshift);  // the origin of the detector in the field coordinate system.
1071   bool phiSymmetry = (phiptr == nullptr);              // if the phi pointer is zero, assume phi symmetry.
1072 
1073   std::cout << std::format("AnnularFieldSim::loadField:  loading field from {}, detector origin at ({},{},{}) in the field map, field unit {}, zsign {}, handling={}", source->GetName(), origin.X(), origin.Y(), origin.Z(), fieldunit, zsign, (phiSymmetry ? "phi-symmetric" : "not phi-symmetric")) << std::endl;
1074 
1075   int lowres_factor = 3;  // to fill in gaps, we group together loweres^3 cells into one block and use that average.
1076 
1077   // float rbinsize=(rmax-rmin)/nr;  // the size of the bins in r
1078   // needed, because the interpolation doesn't know that phi should wrap:
1079   // float phibinsize=(2.0*M_PI)/nphi;  // the size of the bins in phi
1080   // float zbinsize=(zmax-zmin)/nz;  // the size of the bins in z
1081 
1082   // since our lowres_factor may not divide evenly into the number of bins, we need to adjust the number of bins in each dimension:
1083   int nlowbins_phi = nphi / lowres_factor + 1;
1084   int nlowbins_r = nr / lowres_factor + 1;
1085   int nlowbins_z = nz / lowres_factor + 1;
1086   float phi_lowres_step = M_PI * 2.0 / nlowbins_phi;  // the step size in phi for the low-res histogram
1087   float r_lowres_step = (rmax - rmin) / nlowbins_r;   // the step size in r for the low-res histogram
1088   float z_lowres_step = (zmax - zmin) / nlowbins_z;   // the step size in z for the low-res histogram
1089 
1090   std::cout << std::format("loading field from {}<z<{}", zmin, zmax) << std::endl;
1091   TH3F *htEntries = new TH3F("htentries", "num of entries in the field loading", nphi, 0, M_PI * 2.0, nr, rmin, rmax, nz, zmin, zmax);
1092   TH3F *htSum[3];
1093   TH3F *htEntriesLow = new TH3F("htentrieslow", "num of lowres entries in the field loading", nlowbins_phi + 2, -phi_lowres_step, M_PI * 2.0 + phi_lowres_step, nlowbins_r + 2, rmin - r_lowres_step, rmax + r_lowres_step, nlowbins_z + 2, zmin - z_lowres_step, zmax + z_lowres_step);
1094   TH3F *htSumLow[3];
1095   std::cout << std::format("AnnularFieldSim::loadField:  hires has {} phi bins from {} to {}, {} r bins from {} to {}, and {} z bins from {} to {}",
1096                            htEntries->GetNbinsX(), htEntries->GetXaxis()->GetXmin(), htEntries->GetXaxis()->GetXmax(),
1097                            htEntries->GetNbinsY(), htEntries->GetYaxis()->GetXmin(), htEntries->GetYaxis()->GetXmax(),
1098                            htEntries->GetNbinsZ(), htEntries->GetZaxis()->GetXmin(), htEntries->GetZaxis()->GetXmax())
1099             << std::endl;
1100   std::cout << std::format("AnnularFieldSim::loadField:  lowres has {} phi bins from {} to {}, {} r bins from {} to {}, and {} z bins from {} to {}",
1101                            htEntriesLow->GetNbinsX(), htEntriesLow->GetXaxis()->GetXmin(), htEntriesLow->GetXaxis()->GetXmax(),
1102                            htEntriesLow->GetNbinsY(), htEntriesLow->GetYaxis()->GetXmin(), htEntriesLow->GetYaxis()->GetXmax(),
1103                            htEntriesLow->GetNbinsZ(), htEntriesLow->GetZaxis()->GetXmin(), htEntriesLow->GetZaxis()->GetXmax())
1104             << std::endl;
1105 
1106   std::string axis[]{"r", "p", "z"};
1107   for (int i = 0; i < 3; i++)
1108   {
1109     // make each htSum inherit its bounds from htEntries:
1110     htSum[i] = new TH3F(std::string("htsum" + std::to_string(i)).c_str(), std::string("sum of " + axis[i] + "-axis entries in the field loading").c_str(), htEntries->GetNbinsX(), htEntries->GetXaxis()->GetXmin(), htEntries->GetXaxis()->GetXmax(), htEntries->GetNbinsY(), htEntries->GetYaxis()->GetXmin(), htEntries->GetYaxis()->GetXmax(), htEntries->GetNbinsZ(), htEntries->GetZaxis()->GetXmin(), htEntries->GetZaxis()->GetXmax());
1111     // make each htSumLow inherit its bounds from htEntriesLow:
1112     htSumLow[i] = new TH3F(std::string("htsumlow" + std::to_string(i)).c_str(), std::string("sum of low " + axis[i] + "-axis entries in the field loading").c_str(), htEntriesLow->GetNbinsX(), htEntriesLow->GetXaxis()->GetXmin(), htEntriesLow->GetXaxis()->GetXmax(), htEntriesLow->GetNbinsY(), htEntriesLow->GetYaxis()->GetXmin(), htEntriesLow->GetYaxis()->GetXmax(), htEntriesLow->GetNbinsZ(), htEntriesLow->GetZaxis()->GetXmin(), htEntriesLow->GetZaxis()->GetXmax());
1113   }
1114   // define the lowres stepsizes for sanity:
1115 
1116   std::vector<float> phiCoords(nphi);  // the phi coordinates of the bins in our internal rep.
1117   int nPhiCoords = 1;
1118   if (phiSymmetry)
1119   {
1120     //  if we do have phi symmetry, enumerate the phi coordinates we will use:
1121     // std::cout << std::format("AnnularFieldSim::loadField:  phi symmetry, using {} phi coordinates:", nphi) << std::endl;
1122     for (int j = 0; j < nphi; j++)
1123     {
1124       float phi0 = (j + 0.5) * step.Phi();  // stand-in for our phi pointer that doesn't exist.
1125       phiCoords[j] = phi0;
1126       std::cout << std::format("{:.2f} ", phi0) << std::endl;
1127     }
1128     std::cout << std::endl;
1129     nPhiCoords = nphi;
1130   }
1131 
1132   // traverse the field tree and fill the histograms.
1133   int nEntries = source->GetEntries();
1134   for (int i = 0; i < nEntries; i++)
1135   {  // could probably do this with an iterator
1136 
1137     // keep track of progress:
1138     int rem = i % (source->GetEntries() / 10);
1139     int quo = i / (source->GetEntries() / 10);
1140     if (rem == 0 && quo > 0)
1141     {
1142       std::cout << std::format("loadField:  {}0%", quo) << std::endl;
1143     }
1144 
1145     source->GetEntry(i);
1146     float zval = *zptr * zsign;  // since the field map may assume symmetry wrt z, we  need the ability to flip the sign of the z coordinate.
1147     // note that the z sign also needs to affect the field sign in that direction, which is handled outside in the z components of the fills
1148     float rval = *rptr;
1149     float phival;
1150     // we have to also carefully transform the field itself:
1151     float fzval = *fzptr * fieldunit * zsign;      // z component of the field (altered in rotations of the cylinder, but not translations), note that we flip the sign if requested (ie the Efield loader).
1152     float fphival = *fphiptr * fieldunit * zsign;  // phi component of the field
1153     float frval = *frptr * fieldunit;              // radial component of the field
1154     TVector3 rphizField(frval, fphival, fzval);
1155 
1156     // if we aren't asking for phi symmetry, build just the one phi strip
1157 
1158     if (!phiSymmetry)
1159     {
1160       assert(phiptr);
1161       phiCoords[0] = *phiptr;
1162     }
1163     for (int j = 0; j < nPhiCoords; j++)
1164     {                         // this is a loop over n=1 if we have no symmetry, or nphi if we do.
1165       phival = phiCoords[j];  // the input phitr if no symmetry, or precompupted values if symmetry.
1166 
1167       // find the vector coordinate of this field position, in the field map coords.
1168       TVector3 fieldMapPos(rval, 0, zval);  // the position in the field map, in cylindrical coordinates.
1169       fieldMapPos.SetPhi(phival);           // set the phi coordinate in the field map.
1170 
1171       // convert the components of the field into the global cartesian system:
1172       TVector3 globalField = rphizField;  // the field vector in the global coordinate system, in cylindrical coordinates.
1173       globalField.RotateZ(phival);        // the rphiz frame is rotated from the global by the phi coordinate, so to make these match the cartesian system, we must rotate the field to the local rphiz frame
1174 
1175       // if our origin is not zero, apply the translation of field and coords:
1176       TVector3 tpcPos = fieldMapPos - origin;  // the position of this field datapoint in the tpc coordinate system
1177       // I think this could just be a rotation by -tpcPos.Phi()...
1178       TVector3 tpcField = GetLocalFieldComponents(globalField, fieldMapPos, origin);  // note that this returns the rphiz components at that point, in the tpc coordinate system. // NOLINT(readability-suspicious-call-argument)
1179 
1180       // get the cylindrical coordinates of our position in the TPC coordinate system:
1181       rval = tpcPos.Perp();                 // the radial coordinate in the tpc coordinate system
1182       phival = FilterPhiPos(tpcPos.Phi());  // the phi coordinate in the tpc coordinate system, wrapped into the expected range.
1183       zval = tpcPos.Z();                    // the z coordinate in the tpc coordinate system
1184 
1185       // and the field components in the tpc coordinate system:
1186       frval = tpcField.X();    // the radial component of the field in the tpc coordinate system
1187       fphival = tpcField.Y();  // the azimuthal component of the field in the tpc coordinate system
1188       fzval = tpcField.Z();    // the z component of the field in the tpc coordinate system
1189 
1190       htEntries->Fill(phival, rval, zval);  // for legacy reasons this histogram, like others, goes phi-r-z.
1191       htSum[0]->Fill(phival, rval, zval, frval);
1192       htSum[1]->Fill(phival, rval, zval, fphival);
1193       htSum[2]->Fill(phival, rval, zval, fzval);
1194       htEntriesLow->Fill(phival, rval, zval);  // for legacy reasons this histogram, like others, goes phi-r-z.
1195       htSumLow[0]->Fill(phival, rval, zval, frval);
1196       htSumLow[1]->Fill(phival, rval, zval, fphival);
1197       htSumLow[2]->Fill(phival, rval, zval, fzval);
1198     }
1199   }
1200   // now we just divide and fill our local plots (which should eventually be stored as histograms, probably) with the values from each hist cell:
1201   int nemptybins = 0;
1202   for (int i = 0; i < nphi; i++)
1203   {
1204     for (int j = 0; j < nr; j++)
1205     {
1206       for (int k = 0; k < nz; k++)
1207       {
1208         TVector3 cellcenter = GetCellCenter(j, i, k);
1209         int bin = htEntries->FindBin(FilterPhiPos(cellcenter.Phi()), cellcenter.Perp(), cellcenter.Z());
1210         TVector3 fieldvec(htSum[0]->GetBinContent(bin), htSum[1]->GetBinContent(bin), htSum[2]->GetBinContent(bin));  // r, phi, z components in that order
1211         if (htEntries->GetBinContent(bin) < 0.99)
1212         {
1213           // no entries here!
1214           nemptybins++;
1215         }
1216         else
1217         {
1218           fieldvec = fieldvec * (1.0 / htEntries->GetBinContent(bin));
1219         }
1220         // now we have the rphiz field at this position.  We want to store it, for our own sanity, in cartesian componnets (it makes the intergrals easier in the drift stage)
1221         //  so we have to rotate this to the proper angle
1222         //  (if it helps, remember that the x component of the fieldvec is the radial direction, and we need that to point from the origin to the cell center in order for it to be in cartesian coordinates)
1223         fieldvec.RotateZ(FilterPhiPos(cellcenter.Phi()));
1224         (*field)->Set(j, i, k, fieldvec);
1225       }
1226     }
1227   }
1228   if (nemptybins > 0)
1229   {
1230     std::cout << std::format("found {} empty bins when constructing {}.  Filling with lower resolution.", nemptybins, ((*field == Bfield) ? "Bfield" : "Eexternal")) << std::endl;
1231 
1232     // first, we need to stitch the lowest phibin together with the highest phibin, by setting the lowest phi bin contents and nentries to be the same as the highest-1, and the highest to the lowest+1:
1233     for (int j = 0; j < htEntriesLow->GetNbinsY(); j++)
1234     {
1235       for (int k = 0; k < htEntriesLow->GetNbinsZ(); k++)
1236       {
1237         int lowbin = htEntriesLow->GetBin(1, j, k);
1238         int highbin = htEntriesLow->GetBin(htEntriesLow->GetNbinsX() - 1, j, k);
1239         htEntriesLow->SetBinContent(lowbin,
1240                                     htEntriesLow->GetBinContent(htEntriesLow->GetBin(htEntriesLow->GetNbinsX() - 2, j, k)));
1241         // and the same for the triplet of htSumLow:
1242         for (auto &i : htSumLow)
1243         {
1244           i->SetBinContent(lowbin,
1245                            i->GetBinContent(i->GetBin(htEntriesLow->GetNbinsX() - 2, j, k)));
1246         }
1247         // and then we repeat for the highend stitch:
1248         htEntriesLow->SetBinContent(highbin,
1249                                     htEntriesLow->GetBinContent(htEntriesLow->GetBin(2, j, k)));
1250         for (auto &i : htSumLow)
1251         {
1252           i->SetBinContent(highbin,
1253                            i->GetBinContent(i->GetBin(2, j, k)));
1254         }
1255       }
1256     }
1257 
1258     // now that it is stitched, we can interpolate correctly.
1259     for (int i = 0; i < nphi; i++)
1260     {
1261       for (int j = 0; j < nr; j++)
1262       {
1263         for (int k = 0; k < nz; k++)
1264         {
1265           TVector3 cellcenter = GetCellCenter(j, i, k);
1266           int bin = htEntries->FindBin(FilterPhiPos(cellcenter.Phi()), cellcenter.Perp(), cellcenter.Z());
1267           if (htEntries->GetBinContent(bin) == 0)
1268           {
1269             if (false)
1270             {  // long debug check.
1271               std::cout << std::format("Filling coordinates p{},r{},z{}, (cell p{} r{} z{}) with lowres field", FilterPhiPos(cellcenter.Phi()), cellcenter.Perp(), cellcenter.Z(), i, j, k) << std::endl;
1272               std::cout << std::format(" sanity: htEntries->FindBins(p{:.2f},r{:.2f},z{:.2f})=(p{},r{},z{})={}, content={}", FilterPhiPos(cellcenter.Phi()), cellcenter.Perp(), cellcenter.Z(),
1273                                        htEntries->GetXaxis()->FindBin(FilterPhiPos(cellcenter.Phi())),
1274                                        htEntries->GetYaxis()->FindBin(cellcenter.Perp()),
1275                                        htEntries->GetZaxis()->FindBin(cellcenter.Z()), bin, htEntries->GetBinContent(bin))
1276                         << std::endl;
1277 
1278               TVector3 globalPos = cellcenter + origin;  // the position of this field datapoint in the input system
1279               // bounds of the bin of this cell in htEntries, so I can understand why this has no entries:
1280               float tr_low = htEntries->GetYaxis()->GetBinLowEdge(htEntries->GetYaxis()->FindBin(cellcenter.Perp()));
1281               float tr_high = htEntries->GetYaxis()->GetBinUpEdge(htEntries->GetYaxis()->FindBin(cellcenter.Perp()));
1282               float tz_low = htEntries->GetZaxis()->GetBinLowEdge(htEntries->GetZaxis()->FindBin(cellcenter.Z()));
1283               float tz_high = htEntries->GetZaxis()->GetBinUpEdge(htEntries->GetZaxis()->FindBin(cellcenter.Z()));
1284               float tphi_low = htEntries->GetXaxis()->GetBinLowEdge(htEntries->GetXaxis()->FindBin(FilterPhiPos(cellcenter.Phi())));
1285               float tphi_high = htEntries->GetXaxis()->GetBinUpEdge(htEntries->GetXaxis()->FindBin(FilterPhiPos(cellcenter.Phi())));
1286               TVector3 tlow(tr_low, 0, tz_low);     // the low edge of the bin in the input system
1287               tlow.RotateZ(tphi_low);               // rotate to the correct phi
1288               TVector3 thigh(tr_high, 0, tz_high);  // the high edge of the bin in the input system
1289               thigh.RotateZ(tphi_high);             // rotate to the correct phi
1290               tlow = tlow + origin;                 // translate to the input coordinate system
1291               thigh = thigh + origin;               // translate to the input coordinate system
1292               if (phiSymmetry)
1293               {
1294                 std::cout << std::format("this corresponds to (r,phi,z)=({},{},{}) in the input map coordinates, which has bounds roughly (r>{:.1f} && r<{:.1f} && z> {:.1f} && z<{:.1f})\n", globalPos.Perp(), globalPos.Phi(), globalPos.Z(), tlow.Perp(), thigh.Perp(), tlow.Z(), thigh.Z()) << std::endl;
1295               }
1296               else
1297               {
1298                 std::cout << std::format("this corresponds to (r,phi,z)=({},{},{}) in the input map coordinates, which has bounds roughly (r>{:.1f} && r<{:.1f} && phi>{:.1f} && phi<{:.1f} && z> {:.1f} && z<{:.1f})\n", globalPos.Perp(), globalPos.Phi(), globalPos.Z(), tlow.Perp(), thigh.Perp(), tlow.Phi(), thigh.Phi(), tlow.Z(), thigh.Z()) << std::endl;
1299               }
1300             }
1301 
1302             TVector3 fieldvec(htSumLow[0]->Interpolate(FilterPhiPos(cellcenter.Phi()), cellcenter.Perp(), cellcenter.Z()),
1303                               htSumLow[1]->Interpolate(FilterPhiPos(cellcenter.Phi()), cellcenter.Perp(), cellcenter.Z()),
1304                               htSumLow[2]->Interpolate(FilterPhiPos(cellcenter.Phi()), cellcenter.Perp(), cellcenter.Z()));
1305             int lowbin = htEntriesLow->FindBin(FilterPhiPos(cellcenter.Phi()), cellcenter.Perp(), cellcenter.Z());
1306             // TVector3 fieldvec(htSumLow[0]->GetBinContent(lowbin), htSumLow[1]->GetBinContent(lowbin), htSumLow[2]->GetBinContent(lowbin));
1307 
1308             if (htEntriesLow->GetBinContent(lowbin) < 0.99)
1309             {
1310               std::cout << std::format("not enough entries in source to fill fieldmap, even using the lower res fall-back. Value near r={:.2f}, phi={:.2f}, z={:.2f} is {}, (with range of {:.3f},{:.3f},{:.3f}) Pick lower granularity!",
1311                                        cellcenter.Perp(), FilterPhiPos(cellcenter.Phi()), cellcenter.Z(),
1312                                        htEntriesLow->GetBinContent(lowbin), r_lowres_step, phi_lowres_step, z_lowres_step)
1313                         << std::endl;
1314               std::cout << "Saving fieldmaps to debug.hist.root" << std::endl;
1315               TFile *debugfile = new TFile("debug.hist.root", "RECREATE");
1316               htEntries->Write();
1317               htEntriesLow->Write();
1318               for (int ii = 0; ii < 3; ii++)
1319               {
1320                 htSum[ii]->Write();
1321                 htSumLow[ii]->Write();
1322               }
1323               debugfile->Close();
1324               exit(1);
1325             }
1326             else
1327             {
1328               fieldvec = fieldvec * (1.0 / htEntriesLow->GetBinContent(lowbin));
1329             }
1330             // have to rotate this to the proper direction.
1331             fieldvec.RotateZ(FilterPhiPos(cellcenter.Phi()));  // rcc caution.  Does this rotation shift the sense of 'up'?
1332             (*field)->Set(j, i, k, fieldvec);
1333           }
1334         }
1335       }
1336     }
1337   }
1338   // std::cout << "Field loaded." << std::endl;
1339   return;
1340 }
1341 
1342 void AnnularFieldSim::load_spacecharge(const std::string &filename, const std::string &histname, float zoffset, float chargescale, float cmscale, bool isChargeDensity)
1343 {
1344   TFile *f = TFile::Open(filename.c_str());
1345   TH3 *scmap;
1346   f->GetObject(histname.c_str(), scmap);
1347   std::cout << "Loading spacecharge from '" << filename
1348             << "'.  Seeking histname '" << histname << "'" << std::endl;
1349   chargesourcename = filename + ":" + histname;
1350   load_spacecharge(scmap, zoffset, chargescale, cmscale, isChargeDensity, chargesourcename);
1351   f->Close();
1352   return;
1353 }
1354 
1355 void AnnularFieldSim::load_and_resample_spacecharge(int new_nphi, int new_nr, int new_nz, const std::string &filename, const std::string &histname, float zoffset, float chargescale, float cmscale, bool isChargeDensity)
1356 {
1357   TFile *f = TFile::Open(filename.c_str());
1358   TH3 *scmap;
1359   f->GetObject(histname.c_str(), scmap);
1360   std::cout << "Resampling spacecharge from '" << filename
1361             << "'.  Seeking histname '" << histname << "'" << std::endl;
1362   chargesourcename = filename + ":" + histname;
1363   load_and_resample_spacecharge(new_nphi, new_nr, new_nz, scmap, zoffset, chargescale, cmscale, isChargeDensity);
1364   f->Close();
1365   return;
1366 }
1367 
1368 void AnnularFieldSim::load_and_resample_spacecharge(int new_nphi, int new_nr, int new_nz, TH3 *hist, float zoffset, float chargescale, float cmscale, bool isChargeDensity)
1369 {
1370   // load spacecharge densities from a histogram, where scalefactor translates into local units of C/cm^3
1371   // and cmscale translate (hist coord) --> (hist position in cm)
1372   // noting that the histogram limits may differ from the simulation size, and have different granularity
1373   // hist is assumed/required to be x=phi, y=r, z=z
1374   // z offset 'drifts' the charge by that distance toward z=0.
1375 
1376   // Get dimensions of input
1377   float hrmin = hist->GetYaxis()->GetXmin();
1378   float hrmax = hist->GetYaxis()->GetXmax();
1379   float hphimin = hist->GetXaxis()->GetXmin();
1380   float hphimax = hist->GetXaxis()->GetXmax();
1381   float hzmin = hist->GetZaxis()->GetXmin();
1382   float hzmax = hist->GetZaxis()->GetXmax();
1383 
1384   // Get number of bins in each dimension
1385   int hrn = hist->GetNbinsY();
1386   int hphin = hist->GetNbinsX();
1387   int hzn = hist->GetNbinsZ();
1388   std::cout << std::format("From histogram, native r dimensions: {}<r<{}, hrn (Y axis)={}", hrmin, hrmax, hrn) << std::endl;
1389 
1390   std::cout << std::format("From histogram, native phi dimensions: {}<phi<{}, hrn (X axis)={}", hphimin, hphimax, hphin) << std::endl;
1391 
1392   std::cout << std::format("From histogram, native z dimensions: {}<z<{}, hrn (Z axis)={}", hzmin, hzmax, hzn) << std::endl;
1393 
1394   // do some computation of steps:
1395   float hrstep = (hrmax - hrmin) / hrn;
1396   float hphistep = (hphimax - hphimin) / hphin;
1397   float hzstep = (hzmax - hzmin) / hzn;
1398   float halfrstep = hrstep * 0.5;
1399   float halfphistep = hphistep * 0.5;
1400   float halfzstep = hzstep * 0.5;
1401 
1402   // All we have to do here is resample it so it fits into our expected grid, then pass it to the loader.
1403 
1404   // 1) convert the existing histogram to density if it isn't already:
1405   if (!isChargeDensity)
1406   {
1407     for (int i = 0; i < hphin; i++)
1408     {
1409       float phi = hphimin + hphistep * i;
1410       for (int j = 0; j < hrn; j++)
1411       {
1412         float r = hrmin + hrstep * j;
1413         for (int k = 0; k < hzn; k++)
1414         {
1415           float z = hzmin + hzstep * k;
1416           int bin = hist->FindBin(phi + halfphistep, r + halfrstep, z + halfzstep);
1417           double vol = hzstep * hphistep * (r + halfrstep) * hrstep;
1418           hist->SetBinContent(bin, hist->GetBinContent(bin) / vol);
1419         }
1420       }
1421     }
1422   }
1423   // I ought to consider a subtler approach that better preserves the overall integral, rather than just skipping the interpolation of the outer edges:
1424   TH3F *resampled = new TH3F("resampled", "resampled charge", new_nphi, hphimin, hphimax, new_nr, hrmin, hrmax, new_nz, hzmin, hzmax);
1425   float new_phistep = (hphimax - hphimin) / new_nphi;
1426   float new_rstep = (hrmax - hrmin) / new_nr;
1427   float new_zstep = (hzmax - hzmin) / new_nz;
1428 
1429   // 2 resample with interpolation on the interior, and with nearest-bin for the edge bins
1430   for (int i = 0; i < new_nphi; i++)
1431   {
1432     float phi = hphimin + new_phistep * (i + 0.5);                  // bin center of resampled
1433     float hphi = (phi - hphimin) / hphistep;                        // coord  in source hist
1434     bool phisafe = ((hphi - 0.75) > 0) && ((hphi + 0.75) < hphin);  // we're past the halfway point of the lowest bin, and short of the halfway point of the highest bin.
1435     for (int j = 0; j < new_nr; j++)
1436     {
1437       float r = hrmin + new_rstep * (j + 0.5);              // bin center of resampled
1438       float hr = (r - hrmin) / hrstep;                      // coord in source hist
1439       bool rsafe = ((hr - 0.5) > 0) && ((hr + 0.5) < hrn);  // we're past the halfway point of the lowest bin, and short of the halfway point of the highest bin.
1440       for (int k = 0; k < new_nz; k++)
1441       {
1442         float z = hzmin + new_zstep * (k + 0.5);              // bin center of resampled
1443         float hz = (z - hzmin) / hzstep;                      // coord in source hist
1444         bool zsafe = ((hz - 0.5) > 0) && ((hz + 0.5) < hzn);  // we're past the halfway point of the lowest bin, and short of the halfway point of the highest bin.
1445         // check if we need to do a bin lookup:
1446         if (phisafe && rsafe && zsafe)
1447         {
1448           // print_need_cout("resampling (%d,%d,%d) from (%2.2f/%d,%2.2f/%d,%2.2f/%d) p:(%1.1f<%1.1f<%1.1f) r:(%1.1f<%1.1f<%1.1f) z:(%1.1f<%1.1f<%1.1f)\n",i,j,k,hphi,hphin,hr,hrn,hz,hzn,hphimin,phi,hphimax,hrmin,r,hrmax,hzmin,z,hzmax);
1449           resampled->Fill(phi, r, z, hist->Interpolate(phi, r, z));  // leave as a density
1450         }
1451         else
1452         {
1453           int bin = hist->FindBin(phi, r, z);
1454           resampled->Fill(phi, r, z, hist->GetBinContent(bin));
1455         }
1456       }  // k (z loop)
1457     }  // j (r loop)
1458   }  // i (phi loop)
1459   std::cout << "here in load_and_resample_spacecharge, I am going to call load_spacecharge." << std::endl;
1460   load_spacecharge(resampled, zoffset, chargescale, cmscale, true);
1461 }
1462 
1463 void AnnularFieldSim::load_spacecharge(TH3 *hist, float zoffset, float chargescale, float cmscale, bool isChargeDensity, const std::string &inputchargestring)
1464 {
1465   // new plan:  use ChargeMapReader:
1466   if (std::abs(zoffset) > 0.001)
1467   {
1468     std::cout << std::format("nonzero zoffset given ({:E}) but new spacecharge loader can't deal with that. Failing.", zoffset) << std::endl;
1469 
1470     assert(false);
1471   }
1472   if (isChargeDensity)
1473   {
1474     std::cout << "Input dataset is flagged as recording density not total charge, but new loader can't deal with that  Failing.." << std::endl;
1475     assert(false);
1476   }
1477   q->ReadSourceCharge(hist, cmscale, chargescale);
1478 
1479   chargestring = "SC loaded externally: " + inputchargestring + ".";
1480   return;
1481 }
1482 
1483 void AnnularFieldSim::load_digital_current(TH3 *hist, TH2 *gainHist, float chargescale, float cmscale, const std::string &inputchargestring)
1484 {
1485   q->ReadSourceAdc(hist, gainHist, cmscale, chargescale);
1486 
1487   chargestring = "SC loaded externally: " + inputchargestring + ".";
1488   return;
1489 }
1490 
1491 void AnnularFieldSim::save_spacecharge(const std::string &filename)
1492 {
1493   // save a histogram giving the spacecharge in our local coordinates, as we'll be using them..
1494   TH3F *hsc = new TH3F("hInternalSpaceCharge", "Internal Charge Histogram;phi(rad);r(cm);z(cm)", nphi, 0, phispan, nr, rmin, rmax, nz, zmin, zmax);
1495   for (int i = 0; i < nphi; i++)
1496   {
1497     float phi = 0 + step.Phi() * (i + 0.5);
1498     for (int j = 0; j < nr; j++)
1499     {
1500       float r = rmin + step.Perp() * (j + 0.5);
1501       for (int k = 0; k < nz; k++)
1502       {
1503         float z = zmin + step.Z() * (k + 0.5);
1504         hsc->Fill(phi, r, z, q->GetChargeAtPosition(r, phi, z));
1505         // old version: hsc->Fill(phi,r,z,q->Get(j,i,k));
1506       }
1507     }
1508   }
1509   hsc->SaveAs(filename.c_str());
1510   return;
1511 }
1512 
1513 void AnnularFieldSim::add_testcharge(float r, float phi, float z, float coulombs)
1514 {
1515   q->AddChargeAtPosition(r, phi, z, coulombs * C);
1516   return;
1517   /*
1518   int rcell, phicell, zcell;
1519 
1520   //translate to which cell we're in:
1521   BoundsCase rb = GetRindexAndCheckBounds(r, &rcell);
1522   BoundsCase pb = GetPhiIndexAndCheckBounds(phi, &phicell);
1523   BoundsCase zb = GetZindexAndCheckBounds(z, &zcell);
1524   //really, we would like to confirm that the cell we find is in the charge map region, not the field map region.
1525   if (rb == BoundsCase::OutOfBounds || pb == BoundsCase::OutOfBounds || zb == BoundsCase::OutOfBounds)
1526   {
1527     print_need_cout("placing %f Coulombs at (r,p,z)=(%f,%f,%f).  Caution that that is out of the roi.\n", coulombs, r, phi, z);
1528   }
1529   if (rcell < 0 || phicell < 0 || zcell < 0)
1530   {
1531     print_need_cout("Tried placing %f Coulombs at (r,p,z)=(%f,%f,%f).  That is outside of the charge map region.\n", coulombs, r, phi, z);
1532     return;
1533   }
1534 
1535   q->Add(rcell, phicell, zcell, coulombs * C);
1536   if (lookupCase == HybridRes)
1537   {
1538     print_need_cout("write the code you didn't write earlier about reloading the lowres map.\n");
1539     exit(1);
1540   }
1541 
1542   return;
1543   */
1544 }
1545 
1546 /*
1547 void AnnularFieldSim::populate_analytic_fieldmap(){
1548   //sum the E field at every point in the region of interest
1549   // remember that Efield uses relative indices
1550   print_need_cout("in pop_analytic_fieldmap, n=(%d,%d,%d)\n",nr,nphi,nz);
1551 
1552   TVector3 localF;//holder for the summed field at the current position.
1553   for (int ir=rmin_roi;ir<rmax_roi;ir++){
1554     for (int iphi=phimin_roi;iphi<phimax_roi;iphi++){
1555       for (int iz=zmin_roi;iz<zmax_roi;iz++){
1556         localF=aliceModel->E(GetCellCenter(ir, iphi, iz))+Eexternal->Get(ir-rmin_roi,iphi-phimin_roi,iz-zmin_roi);
1557         Efield->Set(ir-rmin_roi,iphi-phimin_roi,iz-zmin_roi,localF);
1558         //if (localF.Mag()>1e-9)
1559         if(debugFlag()) print_need_cout("%d: AnnularFieldSim::populate_analytic_fieldmap fieldmap@ (%d,%d,%d) mag=%f\n",__LINE__,ir,iphi,iz,localF.Mag());
1560       }
1561     }
1562   }
1563   return;
1564 }
1565 */
1566 
1567 void AnnularFieldSim::populate_fieldmap()
1568 {
1569   // sum the E field at every point in the region of interest
1570   //  remember that Efield uses relative indices
1571   std::cout << std::format("in pop_fieldmap, n=({},{},{})\n", nr, nphi, nz) << std::endl;
1572 
1573   std::cout << std::format("populating fieldmap for ({}x{}x{}) grid with ({}x{}x{}) source ", nr_roi, nphi_roi, nz_roi, nr, nphi, nz) << std::endl;
1574 
1575   if (truncation_length > 0)
1576   {
1577     std::cout << std::format(" ==> truncating anything more than {} cells away", truncation_length) << std::endl;
1578   }
1579   unsigned long long totalelements = nr_roi;
1580   totalelements *= nphi_roi;
1581   totalelements *= nz_roi;  // breaking up this multiplication prevents a 32bit math overflow
1582   unsigned long long percent = totalelements / 100 * debug_npercent;
1583   std::cout << std::format("total elements = {}", totalelements * nr * nphi * nz) << std::endl;
1584 
1585   int el = 0;
1586 
1587   TVector3 localF;  // holder for the summed field at the current position.
1588   for (int ir = rmin_roi; ir < rmax_roi; ir++)
1589   {
1590     for (int iphi = phimin_roi; iphi < phimax_roi; iphi++)
1591     {
1592       for (int iz = zmin_roi; iz < zmax_roi; iz++)
1593       {
1594         localF = sum_field_at(ir, iphi, iz);  // asks in global coordinates
1595         if (!(el % percent))
1596         {
1597           std::cout << std::format("populate_fieldmap {}%:  ", static_cast<uint64_t>(debug_npercent) * el / percent);
1598 
1599           std::cout << std::format("sum_field_at (ir={}, iphi={}, iz={}) gives ({:E},{:E},{:E})", ir, iphi, iz, localF.X(), localF.Y(), localF.Z()) << std::endl;
1600         }
1601         el++;
1602 
1603         Efield->Set(ir - rmin_roi, iphi - phimin_roi, iz - zmin_roi, localF);  // sets in roi coordinates.
1604                                                                                // if (localF.Mag()>1e-9)
1605                                                                                // if(debugFlag()) print_need_cout("%d: AnnularFieldSim::populate_fieldmap fieldmap@ (%d,%d,%d) mag=%f\n",__LINE__,ir,iphi,iz,localF.Mag());
1606       }
1607     }
1608   }
1609   return;
1610 }
1611 
1612 void AnnularFieldSim::populate_lookup()
1613 {
1614   // with 'f' being the position the field is being measured at, and 'o' being the position of the charge generating the field.
1615   // remember the 'f' part of Epartial uses relative indices.
1616   //   TVector3 (*f)[fx][fy][fz][ox][oy][oz]=field_;
1617   // print_need_cout("populating lookup for (%dx%dx%d)x(%dx%dx%d) grid\n",fx,fy,fz,ox,oy,oz);
1618 
1619   if (lookupCase == Full3D)
1620   {
1621     std::cout << "lookupCase==Full3D" << std::endl;
1622 
1623     populate_full3d_lookup();
1624   }
1625   else if (lookupCase == HybridRes)
1626   {
1627     std::cout << "lookupCase==HybridRes" << std::endl;
1628     populate_highres_lookup();
1629     populate_lowres_lookup();
1630   }
1631   else if (lookupCase == PhiSlice)
1632   {
1633     std::cout << "Populating lookup:  lookupCase==PhiSlice" << std::endl;
1634     populate_phislice_lookup();
1635   }
1636   else if (lookupCase == Analytic)
1637   {
1638     std::cout << "Populating lookup:  lookupCase==Analytic ===> skipping!" << std::endl;
1639   }
1640   else if (lookupCase == NoLookup)
1641   {
1642     std::cout << "Populating lookup:  lookupCase==NoLookup ===> skipping!" << std::endl;
1643   }
1644   else
1645   {
1646     exit(1);
1647   }
1648   return;
1649 }
1650 
1651 void AnnularFieldSim::populate_full3d_lookup()
1652 {
1653   // with 'f' being the position the field is being measured at, and 'o' being the position of the charge generating the field.
1654   // remember the 'f' part of Epartial uses relative indices.
1655   //   TVector3 (*f)[fx][fy][fz][ox][oy][oz]=field_;
1656   std::cout << std::format("populating full lookup table for ({}x{}x{})x({}x{}x{}) grid",
1657                            rmax_roi - rmin_roi, phimax_roi - phimin_roi, zmax_roi - zmin_roi, nr, nphi, nz)
1658             << std::endl;
1659   unsigned long long totalelements = (rmax_roi - rmin_roi);
1660   totalelements *= (phimax_roi - phimin_roi);
1661   totalelements *= (zmax_roi - zmin_roi);
1662   totalelements *= nr;
1663   totalelements *= nphi;
1664   totalelements *= nz;  // breaking up this multiplication prevents a 32bit math overflow
1665   unsigned long long percent = totalelements / 100 * debug_npercent;
1666   std::cout << std::format("total elements = {}", totalelements) << std::endl;
1667   TVector3 at(1, 0, 0);
1668   TVector3 from(1, 0, 0);
1669   TVector3 zero(0, 0, 0);
1670 
1671   int el = 0;
1672   for (int ifr = rmin_roi; ifr < rmax_roi; ifr++)
1673   {
1674     for (int ifphi = phimin_roi; ifphi < phimax_roi; ifphi++)
1675     {
1676       for (int ifz = zmin_roi; ifz < zmax_roi; ifz++)
1677       {
1678         at = GetCellCenter(ifr, ifphi, ifz);
1679         for (int ior = 0; ior < nr; ior++)
1680         {
1681           for (int iophi = 0; iophi < nphi; iophi++)
1682           {
1683             for (int ioz = 0; ioz < nz; ioz++)
1684             {
1685               el++;
1686               if (!(el % percent))
1687               {
1688                 std::cout << std::format("populate_full3d_lookup {}%", static_cast<uint64_t>(debug_npercent) * el / percent) << std::endl;
1689               }
1690               from = GetCellCenter(ior, iophi, ioz);
1691 
1692               //*f[ifx][ify][ifz][iox][ioy][ioz]=cacl_unit_field(at,from);
1693               // print_need_cout("calc_unit_field...\n");
1694               if (ifr == ior && ifphi == iophi && ifz == ioz)
1695               {
1696                 Epartial->Set(ifr - rmin_roi, ifphi - phimin_roi, ifz - zmin_roi, ior, iophi, ioz, zero);
1697               }
1698               else
1699               {
1700                 Epartial->Set(ifr - rmin_roi, ifphi - phimin_roi, ifz - zmin_roi, ior, iophi, ioz, calc_unit_field(at, from));
1701               }
1702             }
1703           }
1704         }
1705       }
1706     }
1707   }
1708   return;
1709 }
1710 
1711 void AnnularFieldSim::populate_highres_lookup()
1712 {
1713   TVector3 at(1, 0, 0);
1714   TVector3 from(1, 0, 0);
1715   TVector3 zero(0, 0, 0);
1716 
1717   // populate_highres_lookup();
1718   int r_highres_dist = (nr_high - 1) / 2;
1719   int phi_highres_dist = (nphi_high - 1) / 2;
1720   int z_highres_dist = (nz_high - 1) / 2;
1721 
1722   // number of fbins contained in the 26 weirdly-shaped edge regions (and one center region which we won't use)
1723   static int nfbinsin[3][3][3];
1724   for (auto &i : nfbinsin)
1725   {
1726     for (auto &j : i)
1727     {
1728       for (int &k : j)
1729       {
1730         k = 0;  // we could count total volume, but without knowing the charge prior, it's not clear that'd be /better/
1731       }
1732     }
1733   }
1734 
1735   // todo: if this runs too slowly, I can do geometry instead of looping over all the cells that are possibly in range
1736 
1737   // loop over all the f-bins in the roi:
1738   TVector3 currentf;
1739   TVector3 newf;  // the averaged field vector without, and then with the new contribution from the f-bin being considered.
1740   for (int ifr = rmin_roi; ifr < rmax_roi; ifr++)
1741   {
1742     int r_parentlow = std::floor((ifr - r_highres_dist) / (r_spacing * 1.0));       // l-bin partly enclosed in our high-res region
1743     int r_parenthigh = std::floor((ifr + r_highres_dist) / (r_spacing * 1.0)) + 1;  // definitely not enclosed in our high-res region
1744     int r_startpoint = r_parentlow * r_spacing;                                     // the first f-bin of the lowest-r f-bin that our h-region touches.  COuld be less than zero.
1745     int r_endpoint = r_parenthigh * r_spacing;                                      // the first f-bin of the lowest-r l-bin after that that our h-region does not touch.  could be larger than max.
1746 
1747     for (int ifphi = phimin_roi; ifphi < phimax_roi; ifphi++)
1748     {
1749       int phi_parentlow = std::floor(FilterPhiIndex(ifphi - phi_highres_dist) / (phi_spacing * 1.0));  // note this may have wrapped around
1750       bool phi_parentlow_wrapped = (ifphi - phi_highres_dist < 0);
1751       int phi_startpoint = phi_parentlow * phi_spacing;  // the first f-bin of the lowest-z f-bin that our h-region touches.
1752       if (phi_parentlow_wrapped)
1753       {
1754         phi_startpoint -= nphi;  // if we wrapped, re-wrap us so we're negative again
1755       }
1756 
1757       int phi_parenthigh = std::floor(FilterPhiIndex(ifphi + phi_highres_dist) / (phi_spacing * 1.0)) + 1;  // note that this may have wrapped around
1758       bool phi_parenthigh_wrapped = (ifphi + phi_highres_dist >= nphi);
1759       int phi_endpoint = phi_parenthigh * phi_spacing;
1760       if (phi_parenthigh_wrapped)
1761       {
1762         phi_endpoint += nphi;  // if we wrapped, re-wrap us so we're larger than nphi again.  We use these relative coords to determine the position relative to the center of our h-region.
1763       }
1764 
1765       for (int ifz = zmin_roi; ifz < zmax_roi; ifz++)
1766       {
1767         // if(debugFlag()) print_need_cout("%d: AnnularFieldSim::populate_highres_lookup icell=(%d,%d,%d)\n",__LINE__,ifr,ifphi,ifz);
1768 
1769         int z_parentlow = std::floor((ifz - z_highres_dist) / (z_spacing * 1.0));
1770         int z_parenthigh = std::floor((ifz + z_highres_dist) / (z_spacing * 1.0)) + 1;
1771         int z_startpoint = z_parentlow * z_spacing;  // the first f-bin of the lowest-z f-bin that our h-region touches.
1772         int z_endpoint = z_parenthigh * z_spacing;   // the first f-bin of the lowest-z l-bin after that that our h-region does not touch.
1773 
1774         // our 'at' position, in global coords:
1775         at = GetCellCenter(ifr, ifphi, ifz);
1776         // define the farthest-away parent l-bin cells we're dealing with here:
1777         // note we're still in absolute coordinates
1778 
1779         // for every f-bin in the l-bins we're dealing with, figure out which relative highres bin it's in, and average the field vector into that bin's vector
1780         // note most of these relative bins have exactly one f-bin in them.  It's only the edges that can get more.
1781         // note this is a running average:  Anew=(Aold*Nold+V)/(Nold+1) and so on.
1782         // note also that we automatically skip f-bins that would've been out of the valid overall volume.
1783         for (int ir = r_startpoint; ir < r_endpoint; ir++)
1784         {
1785           // skip parts that are out of range:
1786           // could speed this up by moving this into the definition of start and endpoint.
1787           ir = std::max(ir, 0);
1788           if (ir >= nr)
1789           {
1790             break;
1791           }
1792 
1793           int rbin = (ir - ifr) + r_highres_dist;  // zeroth bin when we're at max distance below, etc.
1794           int rcell = 1;
1795           if (rbin <= 0)
1796           {
1797             rbin = 0;
1798             rcell = 0;
1799           }
1800           if (rbin >= nr_high)
1801           {
1802             rbin = nr_high - 1;
1803             rcell = 2;
1804           }
1805 
1806           for (int iphi = phi_startpoint; iphi < phi_endpoint; iphi++)
1807           {
1808             // no phi out-of-range checks since it's circular, but we provide ourselves a filtered version:
1809             int phiFilt = FilterPhiIndex(iphi);
1810             int phibin = (iphi - ifphi) + phi_highres_dist;
1811             int phicell = 1;
1812             if (phibin <= 0)
1813             {
1814               phibin = 0;
1815               phicell = 0;
1816             }
1817             if (phibin >= nphi_high)
1818             {
1819               phibin = nphi_high - 1;
1820               phicell = 2;
1821             }
1822             for (int iz = z_startpoint; iz < z_endpoint; iz++)
1823             {
1824               iz = std::max(iz, 0);
1825               if (iz >= nz)
1826               {
1827                 break;
1828               }
1829               int zbin = (iz - ifz) + z_highres_dist;
1830               int zcell = 1;
1831               if (zbin <= 0)
1832               {
1833                 zbin = 0;
1834                 zcell = 0;
1835               }
1836               if (zbin >= nz_high)
1837               {
1838                 zbin = nz_high - 1;
1839                 zcell = 2;
1840               }
1841               //'from' is in absolute coordinates
1842               from = GetCellCenter(ir, phiFilt, iz);
1843 
1844               nfbinsin[rcell][phicell][zcell]++;
1845               int nf = nfbinsin[rcell][phicell][zcell];
1846               // coordinates relative to the region of interest:
1847               int ir_rel = ifr - rmin_roi;
1848               int iphi_rel = ifphi - phimin_roi;
1849               int iz_rel = ifz - zmin_roi;
1850               if (zcell != 1 || rcell != 1 || phicell != 1)
1851               {
1852                 // we're not in the center, so deal with our weird shapes by averaging:
1853                 // but Epartial is in coordinates relative to the roi
1854                 if (iphi_rel < 0)
1855                 {
1856                   std::cout << std::format("{}: Getting with phi={}", __LINE__, iphi_rel) << std::endl;
1857                 }
1858                 currentf = Epartial_highres->Get(ir_rel, iphi_rel, iz_rel, rbin, phibin, zbin);
1859                 // to keep this as the average, we multiply what's there back to its initial summed-but-not-divided value
1860                 // then add our new value, and the divide the new sum by the total number of cells
1861                 newf = (currentf * (nf - 1) + calc_unit_field(at, from)) * (1 / (nf * 1.0));
1862                 Epartial_highres->Set(ir_rel, iphi_rel, iz_rel, rbin, phibin, zbin, newf);
1863               }
1864               else
1865               {
1866                 // we're in the center cell, which means any f-bin that's not on the outer edge of our region:
1867                 // calc_unit_field will return zero when at=from, so the center will be automatically zero here.
1868                 if (ifr == rbin && ifphi == phibin && ifz == zbin)
1869                 {
1870                   Epartial_highres->Set(ir_rel, iphi_rel, iz_rel, rbin, phibin, zbin, zero);
1871                 }
1872                 else
1873                 {  // for extra carefulness, only calc the field if it's not self-to-self.
1874                   newf = calc_unit_field(at, from);
1875                   Epartial_highres->Set(ir_rel, iphi_rel, iz_rel, rbin, phibin, zbin, newf);
1876                 }
1877               }
1878             }
1879           }
1880         }
1881       }
1882     }
1883   }
1884   return;
1885 }
1886 
1887 void AnnularFieldSim::populate_lowres_lookup()
1888 {
1889   TVector3 at(1, 0, 0);
1890   TVector3 from(1, 0, 0);
1891   TVector3 zero(0, 0, 0);
1892   int fphi_low;
1893   int fphi_high;
1894   int fz_low;
1895   int fz_high;  // edges of the outer l-bin
1896   int r_low;
1897   int r_high;
1898   int phi_low;
1899   int phi_high;
1900   int z_low;
1901   int z_high;  // edges of the inner l-bin
1902 
1903   // todo:  add in handling if roi_low is wrap-around in phi
1904   for (int ifr = rmin_roi_low; ifr < rmax_roi_low; ifr++)
1905   {
1906     int fr_low = ifr * r_spacing;
1907     int fr_high = fr_low + r_spacing - 1;
1908     if (fr_high >= nr)
1909     {
1910       fr_high = nr - 1;
1911     }
1912     for (int ifphi = phimin_roi_low; ifphi < phimax_roi_low; ifphi++)
1913     {
1914       fphi_low = ifphi * phi_spacing;
1915       fphi_high = fphi_low + phi_spacing - 1;
1916       if (fphi_high >= nphi)
1917       {
1918         fphi_high = nphi - 1;  // if our phi l-bins aren't evenly spaced, we need to catch that here.
1919       }
1920       for (int ifz = zmin_roi_low; ifz < zmax_roi_low; ifz++)
1921       {
1922         fz_low = ifz * z_spacing;
1923         fz_high = fz_low + z_spacing - 1;
1924         if (fz_high >= nz)
1925         {
1926           fz_high = nz - 1;
1927         }
1928         at = GetGroupCellCenter(fr_low, fr_high, fphi_low, fphi_high, fz_low, fz_high);
1929         // print_need_cout("ifr=%d, rlow=%d,rhigh=%d,r_spacing=%d\n",ifr,r_low,r_high,r_spacing);
1930         // if(debugFlag())    print_need_cout("%d: AnnularFieldSim::populate_lowres_lookup icell=(%d,%d,%d)\n",__LINE__,ifr,ifphi,ifz);
1931 
1932         for (int ior = 0; ior < nr_low; ior++)
1933         {
1934           r_low = ior * r_spacing;
1935           r_high = r_low + r_spacing - 1;
1936           int ir_rel = ifr - rmin_roi_low;
1937 
1938           if (r_high >= nr)
1939           {
1940             r_high = nr - 1;
1941           }
1942           for (int iophi = 0; iophi < nphi_low; iophi++)
1943           {
1944             phi_low = iophi * phi_spacing;
1945             phi_high = phi_low + phi_spacing - 1;
1946             if (phi_high >= nphi)
1947             {
1948               phi_high = nphi - 1;
1949             }
1950             int iphi_rel = ifphi - phimin_roi_low;
1951             for (int ioz = 0; ioz < nz_low; ioz++)
1952             {
1953               z_low = ioz * z_spacing;
1954               z_high = z_low + z_spacing - 1;
1955               if (z_high >= nz)
1956               {
1957                 z_high = nz - 1;
1958               }
1959               int iz_rel = ifz - zmin_roi_low;
1960               from = GetGroupCellCenter(r_low, r_high, phi_low, phi_high, z_low, z_high);
1961 
1962               if (ifr == ior && ifphi == iophi && ifz == ioz)
1963               {
1964                 Epartial_lowres->Set(ir_rel, iphi_rel, iz_rel, ior, iophi, ioz, zero);
1965               }
1966               else
1967               {  // for extra carefulness, only calc the field if it's not self-to-self.
1968                 Epartial_lowres->Set(ir_rel, iphi_rel, iz_rel, ior, iophi, ioz, calc_unit_field(at, from));
1969               }
1970 
1971               //*f[ifx][ify][ifz][iox][ioy][ioz]=cacl_unit_field(at,from);
1972               // print_need_cout("calc_unit_field...\n");
1973               // this calc's okay.
1974             }
1975           }
1976         }
1977       }
1978     }
1979   }
1980   return;
1981 }
1982 
1983 void AnnularFieldSim::populate_phislice_lookup()
1984 {
1985   // with 'f' being the position the field is being measured at, and 'o' being the position of the charge generating the field.
1986   // remember the 'f' part of Epartial uses relative indices.
1987   //   TVector3 (*f)[fx][fy][fz][ox][oy][oz]=field_;
1988   std::cout << std::format("populating phislice lookup for ({}x{}x{})x({}x{}x{}) grid", nr_roi, 1, nz_roi, nr, nphi, nz) << std::endl;
1989   unsigned long long totalelements = nr;  // nr*nphi*nz*nr_roi*nz_roi
1990   totalelements *= nphi;
1991   totalelements *= nz;
1992   totalelements *= nr_roi;
1993   totalelements *= nz_roi;  // breaking up this multiplication prevents a 32bit math overflow
1994   unsigned long long percent = totalelements / 100 * debug_npercent;
1995   std::cout << std::format("total elements = {}", totalelements) << std::endl;
1996   TVector3 at(1, 0, 0);
1997   TVector3 from(1, 0, 0);
1998   TVector3 zero(0, 0, 0);
1999 
2000   int el = 0;
2001   for (int ifr = rmin_roi; ifr < rmax_roi; ifr++)
2002   {
2003     for (int ifz = zmin_roi; ifz < zmax_roi; ifz++)
2004     {
2005       at = GetCellCenter(ifr, 0, ifz);
2006       for (int ior = 0; ior < nr; ior++)
2007       {
2008         for (int iophi = 0; iophi < nphi; iophi++)
2009         {
2010           for (int ioz = 0; ioz < nz; ioz++)
2011           {
2012             el++;
2013             from = GetCellCenter(ior, iophi, ioz);
2014             //*f[ifx][ify][ifz][iox][ioy][ioz]=cacl_unit_field(at,from);
2015             // print_need_cout("calc_unit_field...\n");
2016             if (ifr == ior && 0 == iophi && ifz == ioz)
2017             {
2018               if (!(el % percent))
2019               {
2020                 std::cout << std::format("populate_phislice_lookup {}%:  ", static_cast<uint64_t>(debug_npercent) * el / percent);
2021 
2022                 std::cout << std::format("self-to-self is zero (ir={}, iphi={}, iz={}) to (or={}, ophi=0, oz={}) gives ({:E},{:E},{:E})",
2023                                          ior, iophi, ioz, ifr, ifz, zero.X(), zero.Y(), zero.Z())
2024                           << std::endl;
2025               }
2026               Epartial_phislice->Set(ifr - rmin_roi, 0, ifz - zmin_roi, ior, iophi, ioz, zero);
2027             }
2028             else
2029             {
2030               TVector3 unitf = calc_unit_field(at, from);
2031               if (true)
2032               {
2033                 if (!(el % percent))
2034                 {
2035                   std::cout << std::format("populate_phislice_lookup {}%:  ", static_cast<uint64_t>(debug_npercent) * el / percent);
2036 
2037                   std::cout << std::format("calc_unit_field (ir={}, iphi={}, iz={}) to (or={}, ophi=0, oz={}) gives ({:E},{:E},{:E})",
2038                                            ior, iophi, ioz, ifr, ifz, unitf.X(), unitf.Y(), unitf.Z())
2039                             << std::endl;
2040                 }
2041               }
2042 
2043               Epartial_phislice->Set(ifr - rmin_roi, 0, ifz - zmin_roi, ior, iophi, ioz, unitf);  // the origin phi is relative to zero anyway.
2044             }
2045           }
2046         }
2047       }
2048     }
2049   }
2050   return;
2051 }
2052 
2053 void AnnularFieldSim::load_phislice_lookup(const std::string &sourcefile)
2054 {
2055   std::cout << std::format("loading phislice lookup for ({}x{}x{})x({}x{}x{}) grid from {}",
2056                            nr_roi, 1, nz_roi, nr, nphi, nz, sourcefile)
2057             << std::endl;
2058   unsigned long long totalelements = nr;  // nr*nphi*nz*nr_roi*nz_roi
2059   totalelements *= nphi;
2060   totalelements *= nz;
2061   totalelements *= nr_roi;
2062   totalelements *= nz_roi;  // breaking up this multiplication prevents a 32bit math overflow
2063   unsigned long long percent = totalelements / 100 * debug_npercent;
2064   std::cout << std::format("total elements = {}", totalelements) << std::endl;
2065 
2066   TFile *input = TFile::Open(sourcefile.c_str(), "READ");
2067   TTree *tInfo;
2068   input->GetObject("info", tInfo);
2069   assert(tInfo);
2070 
2071   float file_rmin;
2072   float file_rmax;
2073   float file_zmin;
2074   float file_zmax;
2075   int file_rmin_roi;
2076   int file_rmax_roi;
2077   int file_zmin_roi;
2078   int file_zmax_roi;
2079   int file_nr;
2080   int file_np;
2081   int file_nz;
2082   tInfo->SetBranchAddress("rmin", &file_rmin);
2083   tInfo->SetBranchAddress("rmax", &file_rmax);
2084   tInfo->SetBranchAddress("zmin", &file_zmin);
2085   tInfo->SetBranchAddress("zmax", &file_zmax);
2086   tInfo->SetBranchAddress("rmin_roi_index", &file_rmin_roi);
2087   tInfo->SetBranchAddress("rmax_roi_index", &file_rmax_roi);
2088   tInfo->SetBranchAddress("zmin_roi_index", &file_zmin_roi);
2089   tInfo->SetBranchAddress("zmax_roi_index", &file_zmax_roi);
2090   tInfo->SetBranchAddress("nr", &file_nr);
2091   tInfo->SetBranchAddress("nphi", &file_np);
2092   tInfo->SetBranchAddress("nz", &file_nz);
2093   tInfo->GetEntry(0);
2094 
2095   std::cout << "param\tobj\tfile" << std::endl;
2096 
2097   std::cout << std::format("nr\t{}\t{}", nr, file_nr) << std::endl;
2098   std::cout << std::format("np\t{}\t{}", nphi, file_np) << std::endl;
2099   std::cout << std::format("nz\t{}\t{}", nz, file_nz) << std::endl;
2100   std::cout << std::format("rmin\t{:.2f}\t{:.2f}", rmin, file_rmin) << std::endl;
2101   std::cout << std::format("rmax\t{:.2f}\t{:.2f}", rmax, file_rmax) << std::endl;
2102   std::cout << std::format("zmin\t{:.2f}\t{:.2f}", zmin, file_zmin) << std::endl;
2103   std::cout << std::format("zmax\t{:.2f}\t{:.2f}", zmax, file_zmax) << std::endl;
2104   std::cout << std::format("rmin_roi\t{}\t{}", rmin_roi, file_rmin_roi) << std::endl;
2105   std::cout << std::format("rmax_roi\t{}\t{}", rmax_roi, file_rmax_roi) << std::endl;
2106   std::cout << std::format("zmin_roi\t{}\t{}", zmin_roi, file_zmin_roi) << std::endl;
2107   std::cout << std::format("zmax_roi\t{}\t{}", zmax_roi, file_zmax_roi) << std::endl;
2108 
2109   if (file_rmin != rmin || file_rmax != rmax ||
2110       file_zmin != zmin || file_zmax != zmax ||
2111       file_rmin_roi != rmin_roi || file_rmax_roi != rmax_roi ||
2112       file_zmin_roi != zmin_roi || file_zmax_roi != zmax_roi ||
2113       file_nr != nr || file_np != nphi || file_nz != nz)
2114   {
2115     std::cout << "file parameters do not match fieldsim parameters:" << std::endl;
2116 
2117     exit(1);
2118   }
2119 
2120   TTree *tLookup;
2121   input->GetObject("phislice", tLookup);
2122   assert(tLookup);
2123   int ior;
2124   int ifr;
2125   int iophi;
2126   int ioz;
2127   int ifz;
2128   TVector3 *unitf = nullptr;
2129   tLookup->SetBranchAddress("ir_source", &ior);
2130   tLookup->SetBranchAddress("ir_target", &ifr);
2131   tLookup->SetBranchAddress("ip_source", &iophi);
2132   // always zero: tLookup->SetBranchAddress("ip_target",&ifphi);
2133   tLookup->SetBranchAddress("iz_source", &ioz);
2134   tLookup->SetBranchAddress("iz_target", &ifz);
2135   tLookup->SetBranchAddress("Evec", &unitf);  // assume field has units V/(C*cm)
2136 
2137   int el = 0;
2138   std::cout << std::format("{} has {} entries", sourcefile, tLookup->GetEntries());
2139   for (int i = 0; i < (int) totalelements; i++)
2140   {
2141     el++;
2142     tLookup->GetEntry(i);
2143     // print_need_cout("loading i=%d\n",i);
2144     Epartial_phislice->Set(ifr - rmin_roi, 0, ifz - zmin_roi, ior, iophi, ioz, (*unitf) * (-1.0) * (V / (cm * C)));  // load assuming field has units V/(C*cm), which is how we save it.
2145     // note that we save the gradient terms, not the field, hence we need to multiply by (-1.0)
2146     if (!(el % percent))
2147     {
2148       std::cout << std::format("load_phislice_lookup {}%:  ", static_cast<uint64_t>(debug_npercent) * el / percent);
2149 
2150       std::cout << std::format("field from (ir={}, iphi={}, iz={}) to (or={}, ophi=0, oz={}) is ({:E},{:E},{:E})",
2151                                ior, iophi, ioz, ifr, ifz, unitf->X(), unitf->Y(), unitf->Z())
2152                 << std::endl;
2153     }
2154   }
2155 
2156   input->Close();
2157   return;
2158 }
2159 
2160 void AnnularFieldSim::save_phislice_lookup(const std::string &destfile)
2161 {
2162   std::cout << std::format("saving phislice  lookup for ({}x{}x{})x({}x{}x{}) grid to {}", nr_roi, 1, nz_roi, nr, nphi, nz, destfile) << std::endl;
2163   unsigned long long totalelements = nr;  // nr*nphi*nz*nr_roi*nz_roi
2164   totalelements *= nphi;
2165   totalelements *= nz;
2166   totalelements *= nr_roi;
2167   totalelements *= nz_roi;  // breaking up this multiplication prevents a 32bit math overflow
2168   unsigned long long percent = totalelements / 100 * debug_npercent;
2169   std::cout << std::format("total elements = {}", totalelements) << std::endl;
2170 
2171   TFile *output = TFile::Open(destfile.c_str(), "RECREATE");
2172   output->cd();
2173 
2174   TTree *tInfo = new TTree("info", "Information about Lookup Table");
2175   tInfo->Branch("rmin", &rmin);
2176   tInfo->Branch("rmax", &rmax);
2177   tInfo->Branch("zmin", &zmin);
2178   tInfo->Branch("zmax", &zmax);
2179   tInfo->Branch("rmin_roi_index", &rmin_roi);
2180   tInfo->Branch("rmax_roi_index", &rmax_roi);
2181   tInfo->Branch("zmin_roi_index", &zmin_roi);
2182   tInfo->Branch("zmax_roi_index", &zmax_roi);
2183   tInfo->Branch("nr", &nr);
2184   tInfo->Branch("nphi", &nphi);
2185   tInfo->Branch("nz", &nz);
2186   tInfo->Fill();
2187   std::cout << "info tree built." << std::endl;
2188   ;
2189 
2190   TTree *tLookup = new TTree("phislice", "Phislice Lookup Table");
2191   int ior;
2192   int ifr;
2193   int iophi;
2194   int ioz;
2195   int ifz;
2196   TVector3 unitf;
2197   tLookup->Branch("ir_source", &ior);
2198   tLookup->Branch("ir_target", &ifr);
2199   tLookup->Branch("ip_source", &iophi);
2200   // always zero: tLookup->Branch("ip_target",&ifphi);
2201   tLookup->Branch("iz_source", &ioz);
2202   tLookup->Branch("iz_target", &ifz);
2203   tLookup->Branch("Evec", &unitf);
2204   std::cout << "lookup tree built." << std::endl;
2205   ;
2206 
2207   int el = 0;
2208   for (ifr = rmin_roi; ifr < rmax_roi; ifr++)
2209   {
2210     for (ifz = zmin_roi; ifz < zmax_roi; ifz++)
2211     {
2212       for (ior = 0; ior < nr; ior++)
2213       {
2214         for (iophi = 0; iophi < nphi; iophi++)
2215         {
2216           for (ioz = 0; ioz < nz; ioz++)
2217           {
2218             el++;
2219             unitf = Epartial_phislice->Get(ifr - rmin_roi, 0, ifz - zmin_roi, ior, iophi, ioz) * (-1 / (V / (C * cm)));  // save in units of V/(C*cm) note that we introduce a -1 here for legcy reasons.
2220             if (true)
2221             {
2222               if (!(el % percent))
2223               {
2224                 std::cout << std::format("save_phislice_lookup {}%:  ", static_cast<uint64_t>(debug_npercent) * el / percent);
2225 
2226                 std::cout << std::format("field from (ir={}, iphi={}, iz={}) to (or={}, ophi=0, oz={}) is ({:E},{:E},{:E})",
2227                                          ior, iophi, ioz, ifr, ifz, unitf.X(), unitf.Y(), unitf.Z())
2228                           << std::endl;
2229               }
2230             }
2231 
2232             tLookup->Fill();
2233           }
2234         }
2235       }
2236     }
2237   }
2238   output->cd();
2239   tInfo->Write();
2240   tLookup->Write();
2241   // output->Write();
2242   output->Close();
2243   return;
2244 }
2245 
2246 void AnnularFieldSim::setFlatFields(float B, float E)
2247 {
2248   // these only cover the roi, but since we address them flat, we don't need to know that here.
2249   std::cout << std::format("AnnularFieldSim::setFlatFields(B={} Tesla, E={} V/cm)", B, E) << std::endl;
2250   std::cout << std::format("lengths:  Eext={}, Bfie={}", Eexternal->Length(), Bfield->Length()) << std::endl;
2251   Efieldname = "E:Flat:" + std::to_string(E);
2252   Bfieldname = "B:Flat:" + std::to_string(B);
2253 
2254   Enominal = E * (V / cm);
2255   Bnominal = B * Tesla;
2256   for (int i = 0; i < Eexternal->Length(); i++)
2257   {
2258     Eexternal->GetFlat(i)->SetXYZ(0, 0, Enominal);
2259   }
2260   for (int i = 0; i < Bfield->Length(); i++)
2261   {
2262     Bfield->GetFlat(i)->SetXYZ(0, 0, Bnominal);
2263   }
2264   UpdateOmegaTau();
2265   return;
2266 }
2267 
2268 TVector3 AnnularFieldSim::sum_field_at(int r, int phi, int z)
2269 {
2270   // sum the E field over all nr by ny by nz cells of sources, at the global coordinate position r,phi,z.
2271   // note the specific position in Epartial is in relative coordinates.
2272   //  if(debugFlag()) print_need_cout("%d: AnnularFieldSim::sum_field_at(r=%d,phi=%d, z=%d)\n",__LINE__,r,phi,z);
2273 
2274   /*
2275     for the near future:
2276   TVector3 sum=(sum_local_field_at(r, phi, z)
2277                 + sum_nonlocal_field_at(r,phi,z)
2278                 + Escale*Eexternal->Get(r-rmin_roi,phi-phimin_roi,z-zmin_roi));
2279   return sum;
2280   */
2281 
2282   TVector3 sum(0, 0, 0);
2283   if (lookupCase == Full3D)
2284   {
2285     sum += sum_full3d_field_at(r, phi, z);
2286   }
2287   else if (lookupCase == HybridRes)
2288   {
2289     sum += sum_local_field_at(r, phi, z);
2290     sum += sum_nonlocal_field_at(r, phi, z);
2291   }
2292   else if (lookupCase == PhiSlice)
2293   {
2294     sum += sum_phislice_field_at(r, phi, z);
2295   }
2296   else if (lookupCase == Analytic)
2297   {
2298     sum += aliceModel->E(GetCellCenter(r, phi, z));
2299   }
2300   else if (lookupCase == NoLookup)
2301   {
2302     // do nothing.  We are forcibly assuming E from spacecharge is zero everywhere.
2303   }
2304   sum += Eexternal->Get(r - rmin_roi, phi - phimin_roi, z - zmin_roi);
2305   if (debugFlag())
2306   {
2307     std::cout << std::format("summed field at ({},{},{})=({},{},{})", r, phi, z, sum.X(), sum.Y(), sum.Z()) << std::endl;
2308   }
2309 
2310   return sum;
2311 }
2312 
2313 TVector3 AnnularFieldSim::sum_full3d_field_at(int r, int phi, int z)
2314 {
2315   // sum the E field over all nr by ny by nz cells of sources, at the specific position r,phi,z.
2316   // note the specific position in Epartial is in relative coordinates.
2317   // print_need_cout("AnnularFieldSim::sum_field_at(r=%d,phi=%d, z=%d)\n",r,phi,z);
2318   TVector3 sum(0, 0, 0);
2319   float rdist;
2320   float phidist;
2321   float zdist;
2322   float remdist;
2323   for (int ir = 0; ir < nr; ir++)
2324   {
2325     if (truncation_length > 0)
2326     {
2327       rdist = std::abs(ir - r);
2328       remdist = std::sqrt(truncation_length * truncation_length - rdist * rdist);
2329       if (remdist < 0)
2330       {
2331         continue;  // skip if we're too far away
2332       }
2333     }
2334     for (int iphi = 0; iphi < nphi; iphi++)
2335     {
2336       if (truncation_length > 0)
2337       {
2338         phidist = std::min(std::abs(iphi - phi), std::abs(std::abs(iphi - phi) - nphi));  // think about this in phi... rcc food for thought.
2339         remdist = std::sqrt(truncation_length * truncation_length - phidist * phidist);
2340         if (remdist < 0)
2341         {
2342           continue;  // skip if we're too far away
2343         }
2344       }
2345       for (int iz = 0; iz < nz; iz++)
2346       {
2347         if (truncation_length > 0)
2348         {
2349           zdist = std::abs(iz - z);
2350           remdist = std::sqrt(truncation_length * truncation_length - zdist * zdist);
2351           if (remdist < 0)
2352           {
2353             continue;  // skip if we're too far away
2354           }
2355         }
2356         // sum+=*partial[x][phi][z][ix][iphi][iz] * *q[ix][iphi][iz];
2357         if (r == ir && phi == iphi && z == iz)
2358         {
2359           continue;  // dont' compute self-to-self field.
2360         }
2361         sum += Epartial->Get(r - rmin_roi, phi - phimin_roi, z - zmin_roi, ir, iphi, iz) * q->GetChargeInBin(ir, iphi, iz);
2362       }
2363     }
2364   }
2365   // print_need_cout("summed field at (%d,%d,%d)=(%f,%f,%f)\n",x,y,z,sum.X(),sum.Y(),sum.Z());
2366   return sum;
2367 }
2368 
2369 TVector3 AnnularFieldSim::sum_local_field_at(int r, int phi, int z)
2370 {
2371   // do the summation of the inner high-resolution region charges:
2372   //
2373   //  bin 0  1 2 ...  n-2 n-1
2374   //   . . .|.|.|.|.|.|.|. . .
2375   //        | | | | | | |
2376   //   . . .|.|.|.|.|.|.|. . .
2377   //   -----+-+-+-+-+-+-+-----
2378   //   . . .|.|.|.|.|.|.|. . .
2379   //   -----+-+-+-+-+-+-+-----
2380   //   . . .|.|.|.|.|.|.|. . .
2381   //   -----+-+-+-+-+-+-+-----
2382   //   . . .|.|.|.|.|.|.|. . .
2383   //   -----+-+-+-+-+-+-+-----
2384   //   . . .|.|.|.|.|.|.|. . .
2385   //        | | | | | | |
2386   //   . . .|.|.|.|.|.|.|. . .
2387   //
2388   //
2389 
2390   int r_highres_dist = (nr_high - 1) / 2;
2391   int phi_highres_dist = (nphi_high - 1) / 2;
2392   int z_highres_dist = (nz_high - 1) / 2;
2393 
2394   int r_parentlow = std::floor((r - r_highres_dist) / (r_spacing * 1.0));       // partly enclosed in our high-res region
2395   int r_parenthigh = std::floor((r + r_highres_dist) / (r_spacing * 1.0)) + 1;  // definitely not enclosed in our high-res region
2396   int phi_parentlow = std::floor((phi - phi_highres_dist) / (phi_spacing * 1.0));
2397   int phi_parenthigh = std::floor((phi + phi_highres_dist) / (phi_spacing * 1.0)) + 1;  // note that this can be bigger than nphi!  We keep track of that.
2398   int z_parentlow = std::floor((z - z_highres_dist) / (z_spacing * 1.0));
2399   int z_parenthigh = std::floor((z + z_highres_dist) / (z_spacing * 1.0)) + 1;
2400   std::cout << std::format("AnnularFieldSim::sum_local_field_at parents: rlow={}, philow={}, zlow={}, rhigh={}, phihigh={}, zhigh={}",
2401                            r_parentlow, phi_parentlow, z_parentlow, r_parenthigh, phi_parenthigh, z_parenthigh)
2402             << std::endl;
2403 
2404   // zero our current qlocal holder:
2405   for (int i = 0; i < q_local->Length(); i++)
2406   {
2407     *(q_local->GetFlat(i)) = 0;
2408   }
2409 
2410   // get the charge involved in the local highres block:
2411   for (int ir = r_parentlow * r_spacing; ir < r_parenthigh * r_spacing; ir++)
2412   {
2413     ir = std::max(ir, 0);
2414     if (ir >= nr)
2415     {
2416       break;
2417     }
2418     int rbin = (ir - r) + r_highres_dist;  // index in our highres locale.  zeroth bin when we're at max distance below, etc.
2419     rbin = std::max(rbin, 0);
2420     if (rbin >= nr_high)
2421     {
2422       rbin = nr_high - 1;
2423     }
2424     for (int iphi = phi_parentlow * phi_spacing; iphi < phi_parenthigh * phi_spacing; iphi++)
2425     {
2426       // no phi range checks since it's circular.
2427       int phiFilt = FilterPhiIndex(iphi);
2428       int phibin = (iphi - phi) + phi_highres_dist;
2429       phibin = std::max(phibin, 0);
2430       if (phibin >= nphi_high)
2431       {
2432         phibin = nphi_high - 1;
2433       }
2434       for (int iz = z_parentlow * z_spacing; iz < z_parenthigh * z_spacing; iz++)
2435       {
2436         iz = std::max(iz, 0);
2437         if (iz >= nz)
2438         {
2439           break;
2440         }
2441         // if(debugFlag()) print_need_cout("%d: AnnularFieldSim::sum_field_at, reading q in f-bin at(r=%d,phi=%d, z=%d)\n",__LINE__,ir,iphi,iz);
2442 
2443         int zbin = (iz - z) + z_highres_dist;
2444         zbin = std::max(zbin, 0);
2445         if (zbin >= nz_high)
2446         {
2447           zbin = nz_high - 1;
2448         }
2449         // print_need_cout("filtering in local highres block\n");
2450         q_local->Add(rbin, phibin, zbin, q->GetChargeInBin(ir, phiFilt, iz));
2451         // print_need_cout("done filtering in local highres block\n");
2452       }
2453     }
2454   }
2455 
2456   // now q_highres is up to date for our current cell of interest.
2457   // start building our full sum by scaling the local lookup table by q.
2458   // note that the lookup table needs to have already accounted for cell centers.
2459 
2460   TVector3 sum(0, 0, 0);
2461 
2462   // note that Epartial_highres returns zero if we're outside of the global region.  q_local will also be zero there.
2463   // these are loops over the position in the epartial highres grid, so relative to the point in question:
2464   // reminder: variables r, phi, and z are global f-bin indices.
2465 
2466   // assuming highres correctly gives a zero when asked for the middle element in each of the last three indices,
2467   // and assuming q_local has no contribution from regions outside the global bounds, this is correct:
2468   for (int ir = 0; ir < nr_high; ir++)
2469   {
2470     for (int iphi = 0; iphi < nphi_high; iphi++)
2471     {
2472       for (int iz = 0; iz < nz_high; iz++)
2473       {
2474         // first three are relative to the roi, last three are relative to the point in the first three.  ooph.
2475         if (phi - phimin_roi < 0)
2476         {
2477           std::cout << std::format("{}: Getting with phi={}", __LINE__, (phi - phimin_roi)) << std::endl;
2478         }
2479         sum += Epartial_highres->Get(r - rmin_roi, phi - phimin_roi, z - zmin_roi, ir, iphi, iz) * q_local->Get(ir, iphi, iz);
2480       }
2481     }
2482   }
2483 
2484   return sum;
2485 }
2486 
2487 TVector3 AnnularFieldSim::sum_nonlocal_field_at(int r, int phi, int z)
2488 {
2489   // now we look for our low_res contribution, which will be the interpolated summed field from the eight blocks closest to our f-bin of interest:
2490 
2491   // find our interpolated position between the eight nearby lowres cells:
2492   // this is similar to the interpolated integral stuff, except we're at integer steps inside integer blocks
2493   // and we have z as well, now.
2494   bool skip[] = {false, false, false, false, false, false, false, false};
2495 
2496   float r0 = r / (1.0 * r_spacing) - 0.5 - rmin_roi_low;  // the position in r, in units of r_spacing, starting from the center of the 0th l-bin in the roi.
2497   int r0i = std::floor(r0);                               // the integer portion of the position. -- what center is below our position?
2498   float r0d = r0 - r0i;                                   // the decimal portion of the position. -- how far past center are we?
2499   // instead of listing all eight, I'll address these as i%2, (i/2)%2 and (i/4)%2 to avoid typos
2500   int ri[2];  // the r position of the eight cell centers to consider.
2501   ri[0] = r0i;
2502   ri[1] = r0i + 1;
2503   float rw[2];      // the weight of that cell, linear in distance from the center of it
2504   rw[0] = 1 - r0d;  // 1 when we're on it, 0 when we're at the other one.
2505   rw[1] = r0d;      // 1 when we're on it, 0 when we're at the other one
2506 
2507   // zero out if the requested l-bin is out of the roi (happens if we're close to the outer than the inner edge of the outermost l-bin)
2508   if (ri[0] < 0)
2509   {
2510     for (int i = 0; i < 8; i++)
2511     {
2512       if ((i / 4) % 2 == 0)
2513       {
2514         skip[i] = true;  // don't handle contributions from ri[0].
2515       }
2516     }
2517     rw[1] = 1;  // and weight like we're dead-center on the outer cells.
2518   }
2519   if (ri[1] >= nr_roi_low)
2520   {
2521     for (int i = 0; i < 8; i++)
2522     {
2523       if ((i / 4) % 2 == 1)
2524       {
2525         skip[i] = true;  // don't bother handling ri[1].
2526       }
2527     }
2528     rw[0] = 1;  // and weight like we're dead-center on the inner cells.
2529   }
2530 
2531   // now repeat that structure for phi:
2532 
2533   float p0 = phi / (1.0 * phi_spacing) - 0.5 - phimin_roi_low;  // the position 'phi' in  units of phi_spacing, starting from the center of the 0th bin.
2534   // print_need_cout("prepping to filter low, p0=%f, phi=%d, phi_spacing=%d, phimin_roi_low=%d\n",p0,phi,phi_spacing,phimin_roi_low);
2535 
2536   int p0i = std::floor(p0);  // the integer portion of the position. -- what center is below our position?
2537   float p0d = p0 - p0i;      // the decimal portion of the position. -- how far past center are we?
2538   int pi[4];                 // the local phi position of the four l-bin centers to consider
2539   // print_need_cout("filtering low, p0i=%d, nphi_low=%d\n",p0i,nphi_low);
2540   // Q: why do I need to filter here?
2541   // A: Worst case scenario, this produces -1<p0<0.  If that wraps around and is still in the roi, I should include it
2542   // A: Likewise, if I get p0>nphi_low, then that means it ought to wrap around and be considered against the other limit.
2543   pi[0] = FilterPhiIndex(p0i, nphi_low);
2544   pi[1] = FilterPhiIndex(p0i + 1, nphi_low);
2545   // having filtered, we will always have a positive number.
2546 
2547   // print_need_cout("done filtering low\n");
2548   float pw[2];      // the weight of that cell, linear in distance from the center of it
2549   pw[0] = 1 - p0d;  // 1 when we're on it, 0 when we're at the other one.
2550   pw[1] = p0d;      // 1 when we're on it, 0 when we're at the other one
2551 
2552   // note that since phi wraps around itself, we have the possibility of being outside by being above/below the opposite end of where we thought we were
2553   // remember these are coordinates wrt the beginning of the roi, and are positive because we filtered.  If that rotation didn't put them back in the roi, then they weren't before, either.
2554   if (pi[0] >= nphi_roi_low)
2555   {
2556     for (int i = 0; i < 8; i++)
2557     {
2558       if ((i / 2) % 2 == 0)
2559       {
2560         skip[i] = true;  // don't bother handling pi[0].
2561       }
2562     }
2563     pw[1] = 1;  // and weight like we're dead-center on the high cells.
2564   }
2565   if (pi[1] >= nphi_roi_low)
2566   {
2567     for (int i = 0; i < 8; i++)
2568     {
2569       if ((i / 2) % 2 == 1)
2570       {
2571         skip[i] = true;  // don't bother handling pi[1].
2572       }
2573     }
2574     pw[0] = 1;  // and weight like we're dead-center on the high cells.
2575   }
2576 
2577   // and once more for z.  ooph.
2578 
2579   float z0 = z / (1.0 * z_spacing) - 0.5 - zmin_roi_low;  // the position in r, in units of r_spacing, starting from the center of the 0th bin.
2580   int z0i = std::floor(z0);                               // the integer portion of the position. -- what center is below our position?
2581   float z0d = z0 - z0i;                                   // the decimal portion of the position. -- how far past center are we?
2582   // instead of listing all eight, I'll address these as i%2, (i/2)%2 and (i/4)%2 to avoid typos
2583   int zi[2];  // the r position of the eight cell centers to consider.
2584   zi[0] = z0i;
2585   zi[1] = z0i + 1;
2586   float zw[2];      // the weight of that cell, linear in distance from the center of it
2587   zw[0] = 1 - z0d;  // 1 when we're on it, 0 when we're at the other one.
2588   zw[1] = z0d;      // 1 when we're on it, 0 when we're at the other one
2589 
2590   if (zi[0] < 0)
2591   {
2592     for (int i = 0; i < 8; i++)
2593     {
2594       if ((i) % 2 == 0)
2595       {
2596         skip[i] = true;  // don't bother handling zi[0].
2597       }
2598     }
2599     zw[1] = 1;  // and weight like we're dead-center on the higher cells.
2600   }
2601   if (zi[1] >= nz_roi_low)
2602   {
2603     for (int i = 0; i < 8; i++)
2604     {
2605       if ((i) % 2 == 1)
2606       {
2607         skip[i] = true;  // don't bother handling zi[1].
2608       }
2609     }
2610     zw[0] = 1;  // and weight like we're dead-center on the lower cells.
2611   }
2612 
2613   TVector3 sum(0, 0, 0);
2614   // at this point, we should be skipping all destination l-bins that are out-of-bounds.
2615   // note that if any out-of-bounds ones survive somehow, the call to Epartial_lowres will fail loudly.
2616   int lBinEdge[2];     // lower and upper (included) edges of the low-res bin, measured in f-bins, reused per-dimension
2617   int hRegionEdge[2];  // lower and upper edge of the high-res region, measured in f-bins, reused per-dimension.
2618   bool overlapsPhi;
2619   bool overlapsZ;  // whether we overlap in R, phi, and z.
2620 
2621   int r_highres_dist = (nr_high - 1) / 2;
2622   int phi_highres_dist = (nphi_high - 1) / 2;
2623   int z_highres_dist = (nz_high - 1) / 2;
2624 
2625   for (int ir = 0; ir < nr_low; ir++)
2626   {
2627     lBinEdge[0] = ir * r_spacing;
2628     lBinEdge[1] = (ir + 1) * r_spacing - 1;
2629     hRegionEdge[0] = r - r_highres_dist;
2630     hRegionEdge[1] = r + r_highres_dist;
2631     bool overlapsR = (lBinEdge[0] <= hRegionEdge[1] && hRegionEdge[0] <= lBinEdge[1]);
2632     for (int iphi = 0; iphi < nphi_low; iphi++)
2633     {
2634       lBinEdge[0] = iphi * phi_spacing;
2635       lBinEdge[1] = (iphi + 1) * phi_spacing - 1;
2636       hRegionEdge[0] = phi - phi_highres_dist;
2637       hRegionEdge[1] = phi + phi_highres_dist;
2638       overlapsPhi = (lBinEdge[0] <= hRegionEdge[1] && hRegionEdge[0] <= lBinEdge[1]);
2639       for (int iz = 0; iz < nz_low; iz++)
2640       {
2641         lBinEdge[0] = iz * z_spacing;
2642         lBinEdge[1] = (iz + 1) * z_spacing - 1;
2643         hRegionEdge[0] = z - z_highres_dist;
2644         hRegionEdge[1] = z + z_highres_dist;
2645         overlapsZ = (lBinEdge[0] <= hRegionEdge[1] && hRegionEdge[0] <= lBinEdge[1]);
2646         // conceptually: see if the l-bin overlaps with the high-res region:
2647         // the high-res region includes all indices from r-(nr_high-1)/2 to r+(nr_high-1)/2.
2648         // each low-res region includes all indices from ir*r_spacing to (ir+1)*r_spacing-1.
2649         if (overlapsR && overlapsPhi && overlapsZ)
2650         {
2651           // if their bounds are interleaved in all dimensions, there is overlap, and we've already summed this region.
2652           continue;
2653         }
2654 
2655         // if(debugFlag()) print_need_cout("%d: AnnularFieldSim::sum_field_at, considering l-bin at(r=%d,phi=%d, z=%d)\n",__LINE__,ir,iphi,iz);
2656 
2657         for (int i = 0; i < 8; i++)
2658         {
2659           if (skip[i])
2660           {
2661             continue;
2662           }
2663           if (ri[(i / 4) % 2] + rmin_roi_low == ir && pi[(i / 2) % 2] + phimin_roi_low == iphi && zi[(i) % 2] + zmin_roi_low == iz)
2664           {
2665             std::cout << std::format("considering an l-bins effect on itself, r={}, phi={}, z={} (matches i={}, not skipped), means we're not interpolating fairly", ir, iphi, iz, i) << std::endl;
2666             exit(1);
2667           }
2668           // the ri, pi, and zi elements are relative to the roi, as needed for Epartial.
2669           // the ir, iphi, and iz are all absolute, as needed for q_lowres
2670           if (pi[(i / 2) % 2] < 0)
2671           {
2672             std::cout << std::format("{}: Getting with phi={}", __LINE__, (pi[(i / 2) % 2])) << std::endl;
2673           }
2674           sum += (Epartial_lowres->Get(ri[(i / 4) % 2], pi[(i / 2) % 2], zi[(i) % 2], ir, iphi, iz) * q_lowres->Get(ir, iphi, iz)) * zw[(i) % 2] * pw[(i / 2) % 2] * rw[(i / 4) % 2];
2675         }
2676       }
2677     }
2678   }
2679 
2680   return sum;
2681 }
2682 
2683 TVector3 AnnularFieldSim::sum_phislice_field_at(int r, int phi, int z)
2684 {
2685   // sum the E field over all nr by ny by nz cells of sources, at the specific position r,phi,z.
2686   // note the specific position in Epartial is in relative coordinates.
2687   // print_need_cout("AnnularFieldSim::sum_field_at(r=%d,phi=%d, z=%d)\n",r,phi,z);
2688   TVector3 pos = GetRoiCellCenter(r - rmin_roi, phi - phimin_roi, z - zmin_roi);
2689   TVector3 slicepos = GetRoiCellCenter(r - rmin_roi, 0, z - zmin_roi);
2690   float rotphi = pos.Phi() - slicepos.Phi();  // probably this is phi*step.Phi();
2691 
2692   // caution:  if you print progress of each of these, it will print a great deal of data, since this is called per-bin
2693   // unsigned long long totalelements=nr*nphi*nz;
2694   //  unsigned long long percent=totalelements/100;
2695 
2696   // unsigned long long el=0;
2697 
2698   TVector3 sum(0, 0, 0);
2699   TVector3 unrotatedField(0, 0, 0);
2700   TVector3 unitField(0, 0, 0);
2701   int phirel;
2702   for (int ir = 0; ir < nr; ir++)
2703   {
2704     for (int iphi = 0; iphi < nphi; iphi++)
2705     {
2706       for (int iz = 0; iz < nz; iz++)
2707       {
2708         // sum+=*partial[x][phi][z][ix][iphi][iz] * *q[ix][iphi][iz];
2709         if (r == ir && phi == iphi && z == iz)
2710         {
2711           continue;  // dont' compute self-to-self field.
2712         }
2713         phirel = FilterPhiIndex(iphi - phi);
2714         unitField = Epartial_phislice->Get(r - rmin_roi, 0, z - zmin_roi, ir, phirel, iz);
2715         unitField.RotateZ(rotphi);  // previously was rotate by the step.Phi()*phi.    //annoying that I can't rename this to 'rotated field' here without unnecessary overhead.
2716 
2717         sum += unitField * q->GetChargeInBin(ir, iphi, iz);
2718         ;
2719 
2720         /*
2721         if(!(el%percent)) {print_need_cout("summing phislices %d%%:  ",(int)(el/percent));
2722           print_need_cout("unit field at (r=%d,p=%d,z=%d) from  (ir=%d,ip=%d,iz=%d) is (%E,%E,%E) (xyz), q=%E\n",
2723                  r,phi,z,ir,iphi,iz,unitField.X(),unitField.Y(),unitField.Z(),q->GetChargeInBin(ir,iphi,iz));
2724         }
2725         el++;
2726         */
2727       }
2728     }
2729   }
2730   // print_need_cout("summed field at (%d,%d,%d)=(%f,%f,%f)\n",x,y,z,sum.X(),sum.Y(),sum.Z());
2731   return sum;
2732 }
2733 
2734 TVector3 AnnularFieldSim::swimToInAnalyticSteps(float zdest, TVector3 start, int steps = 1, int *goodToStep = nullptr)
2735 {
2736   // assume coordinates are given in native units (cm=1 unless that changed!).
2737   double zdist = (zdest - start.Z()) * cm;
2738   double zstep = zdist / steps;
2739   start = start * cm;  // scale us to cm.
2740 
2741   TVector3 ret = start;
2742   TVector3 accumulated_distortion(0, 0, 0);
2743   TVector3 accumulated_drift(0, 0, 0);
2744   TVector3 drift_step(0, 0, zstep);
2745 
2746   int rt;
2747   int pt;
2748   int zt;  // just placeholders for the bounds-checking.
2749   BoundsCase zBound;
2750   for (int i = 0; i < steps; i++)
2751   {
2752     zBound = GetZindexAndCheckBounds(ret.Z(), &zt);
2753     if (zBound == OnLowEdge)
2754     {
2755       // print_need_cout("AnnularFieldSIm::swimToInAnalyticSteps requests z-nudge from z=%f to %f\n", ret.Z(), ret.Z()+ALMOST_ZERO);//nudge it in z:
2756       ret.SetZ(ret.Z() + ALMOST_ZERO);
2757     }
2758     if (GetRindexAndCheckBounds(ret.Perp(), &rt) != InBounds || GetPhiIndexAndCheckBounds(FilterPhiPos(ret.Phi()), &pt) != InBounds || (zBound == OutOfBounds))
2759     {
2760       std::cout << std::format("AnnularFieldSim::swimToInAnalyticSteps at step {} asked to swim particle from ({},{},{}) (rphiz)=({}cm,{}rad,{}cm) which is outside the ROI.",
2761                                i, ret.X() / cm, ret.Y() / cm, ret.Z() / cm, ret.Perp() / cm, ret.Phi(), ret.Z() / cm)
2762                 << std::endl;
2763 
2764       std::cout << std::format(" -- {} <= r < {} \t{} <= phi < {} \t{} <= z < {}",
2765                                rmin_roi * step.Perp() + rmin, rmax_roi * step.Perp() + rmin,
2766                                phimin_roi * step.Phi(), phimax_roi * step.Phi(),
2767                                zmin_roi * step.Z(), zmax_roi * step.Z())
2768                 << std::endl;
2769 
2770       if (!(goodToStep == nullptr))
2771       {
2772         *goodToStep = i - 1;
2773       }
2774       return ret * (1.0 / cm);
2775     }
2776     // print_need_cout("AnnularFieldSim::swimToInAnalyticSteps at step %d, asked to swim particle from (%f,%f,%f) (rphiz)=(%f,%f,%f).\n",i,ret.X(),ret.Y(),ret.Z(),ret.Perp(),ret.Phi(),ret.Z());
2777     // rcc note: once I put the z distoriton back in, I need to check that ret.Z+zstep is still in bounds:
2778     accumulated_distortion += GetStepDistortion(ret.Z() + zstep, ret, true, true);
2779 
2780     accumulated_drift += drift_step;
2781 
2782     // this seems redundant, but if the distortions are small they may lose precision and stop actually changing the position when step size is small.  This allows them to accumulate separately so they can grow properly:
2783     ret = start + accumulated_distortion + accumulated_drift;
2784   }
2785 
2786   return ret * (1.0 / cm);
2787 }
2788 
2789 TVector3 AnnularFieldSim::swimToInSteps(float zdest, const TVector3 &start, int steps = 1, bool interpolate = false, int *goodToStep = nullptr)
2790 {
2791   int *success = nullptr;
2792   TVector3 straightline(start.X(), start.Y(), zdest);
2793   TVector3 distortion = GetTotalDistortion(zdest, start, steps, interpolate, goodToStep, success);
2794   return straightline + distortion;
2795 }
2796 
2797 // NOLINTNEXTLINE(misc-no-recursion)
2798 TVector3 AnnularFieldSim::GetTotalDistortion(float zdest, const TVector3 &start, int steps, bool interpolate, int *goodToStep, int *success)
2799 {
2800   // work in native units is automatic.
2801   // double zdist=(zdest-start.Z())*cm;
2802   // start=start*cm;
2803 
2804   // check the z bounds:
2805   int rt;
2806   int pt;
2807   int zt;  // just placeholders for the bounds-checking.
2808   BoundsCase zBound;
2809   zBound = GetZindexAndCheckBounds(zdest, &zt);
2810   bool rdrswitch = RdeltaRswitch;
2811   if (success)
2812   {
2813     *success = 0;
2814   }
2815   if (zBound == OutOfBounds)
2816   {
2817     if (hasTwin)
2818     {  // check if this destination is valid for our twin, if we have one:
2819       zBound = GetZindexAndCheckBounds(-zdest, &zt);
2820       if (zBound == InBounds || zBound == OnLowEdge)
2821       {
2822         // destination is in the twin
2823         return twin->GetTotalDistortion(zdest, start, steps, interpolate, goodToStep, success);
2824       }
2825     }
2826     // otherwise, we're not in the twin, and default to our usual gripe:
2827     std::cout << std::format("AnnularFieldSim::GetTotalDistortion starting at ({},{},{})=(r{},p{},z{}) asked to drift to z={}, which is outside the ROI. hasTwin= {}. Returning zero_vector.",
2828                              start.X(), start.Y(), start.Z(), start.Perp(), FilterPhiPos(start.Phi()), start.Z(), zdest, static_cast<int>(hasTwin))
2829               << std::endl;
2830 
2831     std::cout << std::format(" -- {} <= r < {} \t{} <= phi < {} \t{} <= z < {}",
2832                              rmin_roi * step.Perp() + rmin, rmax_roi * step.Perp() + rmin,
2833                              phimin_roi * step.Phi(), phimax_roi * step.Phi(),
2834                              zmin_roi * step.Z(), zmax_roi * step.Z())
2835               << std::endl;
2836 
2837     *goodToStep = 0;
2838     *success = 0;
2839     return zero_vector;  // rcchere
2840   }
2841   if (zBound == OnLowEdge)
2842   {
2843     // nudge it in z:
2844     zdest += ALMOST_ZERO;
2845   }
2846   zBound = GetZindexAndCheckBounds(start.Z(), &zt);
2847   if (zBound == OutOfBounds)
2848   {
2849     std::cout << std::format("AnnularFieldSim::GetTotalDistortion starting at ({},{},{})=(r{},p{},z{}) asked to drift from z={}, which is outside the ROI. Returning zero_vector.",
2850                              start.X(), start.Y(), start.Z(), start.Perp(), FilterPhiPos(start.Phi()), start.Z(), start.Z())
2851               << std::endl;
2852 
2853     std::cout << std::format(" -- {} <= r < {} \t{} <= phi < {} \t{} <= z < {}",
2854                              rmin_roi * step.Perp() + rmin, rmax_roi * step.Perp() + rmin,
2855                              phimin_roi * step.Phi(), phimax_roi * step.Phi(),
2856                              zmin_roi * step.Z(), zmax_roi * step.Z())
2857               << std::endl;
2858     *goodToStep = 0;
2859     *success = 0;
2860     return zero_vector;
2861   }
2862   if (zBound == OnLowEdge)
2863   {
2864     // nudge it in z:
2865     zdest += ALMOST_ZERO;
2866   }
2867 
2868   // now we are guaranteed the z limits are in range, and don't need to check them again.
2869 
2870   double zstep = (zmax - zmin) / steps;  // always a positive number
2871   // print_need_cout("Lets do some handy math here, the value of the first zstep: %f \n",zstep);
2872   // print_need_cout("Lets do some handy math here, the value of zmax is: %f \n",zmax);
2873   // print_need_cout("Lets do some handy math here, the value of zmin is: %f \n",zmin);
2874   // print_need_cout("Lets do some handy math here, the value of steps is: %d \n",steps);
2875   if ((zdest - start.Z()) < 0)
2876   {
2877     zstep = -zstep;  // It goes two directions, so there will be positive and negative value
2878   }
2879   int integernumbersteps = (zdest - start.Z()) / zstep;
2880   // print_need_cout("Lets do some handy math here, the value of integernumbersteps is: %d \n",integernumbersteps);
2881   // print_need_cout("Lets do some handy math here, the value of zdest is: %f \n",zdest);
2882   // print_need_cout("Lets do some handy math here, the value of start.Z() is: %f \n",start.Z());
2883   // print_need_cout("Lets do some handy math here, the value of zstep is: %f \n",zstep);
2884 
2885   // print_need_cout("Hello, this is the zmax-zmin length: %f\n", (zmax-zmin));
2886   // print_need_cout("Hello, this is the zdest-start.Z(): %f\n", (zdest-start.Z()));
2887   // print_need_cout("Hello, this is the zstep number: %f\n", zstep);
2888   // print_need_cout("Hello, this is the integernumbersteps number: %d\n", integernumbersteps);
2889 
2890   TVector3 position = start;
2891   TVector3 accumulated_distortion(0, 0, 0);
2892   TVector3 accumulated_drift(0, 0, 0);
2893   TVector3 drift_step(0, 0, zstep);
2894 
2895   // the conceptual approach here is to get the vector distortion in each z step, and use the transverse component of that to update the position of the particle for the next step, while accumulating the total distortion separately from the position.  This allows a small residual to accumulate, rather than being lost.  We do not correct the z position, so that the stepping does not 'skip' parts of the trajectory.
2896 
2897   // describe what you're doing here:
2898   // print_need_cout("Hello again, this is the zdest: %f\n", (zdest));
2899   // print_need_cout("Hello again, this is the start.Z(): %f\n", (start.Z()));
2900 
2901   accumulated_distortion += GetStepDistortion(zdest - (integernumbersteps * zstep), position, true, false);
2902 
2903   // short-circuit if there's no travel length:
2904 
2905   position.SetX(start.X() + accumulated_distortion.X());
2906   position.SetY(start.Y() + accumulated_distortion.Y());
2907   position.SetZ(zdest - (integernumbersteps * zstep));
2908 
2909   float StartR;
2910   float FinalR;
2911   float DeltaR;
2912   for (int i = 0; i < integernumbersteps; i++)
2913   {
2914     if (GetRindexAndCheckBounds(position.Perp(), &rt) != InBounds || GetPhiIndexAndCheckBounds(FilterPhiPos(position.Phi()), &pt) != InBounds || (zBound == OutOfBounds))
2915     {
2916       // print_need_cout("AnnularFieldSim::GetTotalDistortion starting at (%f,%f,%f)=(r%f,p%f,z%f) with drift_step=%f, at step %d, asked to swim particle from (%f,%f,%f) (rphiz)=(%f,%f,%f)which is outside the ROI.\n", start.X(), start.Y(), start.Z(), start.Perp(), FilterPhiPos(start.Phi()), start.Z(), zstep, i, position.X(), position.Y(), position.Z(), position.Perp(), position.Phi(), position.Z());
2917       // print_need_cout(" -- %f <= r < %f \t%f <= phi < %f \t%f <= z < %f \n", rmin_roi * step.Perp() + rmin, rmax_roi * step.Perp() + rmin, phimin_roi * step.Phi(), phimax_roi * step.Phi(), zmin_roi * step.Z(), zmax_roi * step.Z());
2918       // print_need_cout("Returning last good position.\n");
2919       if (!(goodToStep == nullptr))
2920       {
2921         *goodToStep = i - 1, *success = 0;
2922       }
2923       // assert (1==2);
2924       return (accumulated_distortion);
2925     }
2926 
2927     StartR = position.Perp();
2928     // as we accumulate distortions, add these to the x and y positions, but do not change the z position, otherwise we will 'skip' parts of the drift, which is not the intended behavior.
2929     accumulated_distortion += GetStepDistortion(position.Z() + zstep, position, true, false);
2930     position.SetX(start.X() + accumulated_distortion.X());
2931     position.SetY(start.Y() + accumulated_distortion.Y());
2932     FinalR = position.Perp();
2933     // PositionXAfter=start.X() + accumulated_distortion.X();
2934     // PositionYAfter=start.Y() + accumulated_distortion.Y();//
2935 
2936     // StartR=sqrt(PositionXBefore*PositionXBefore+PositionYBefore*PositionYBefore);
2937     DeltaR = FinalR - StartR;  // sqrt(PositionXAfter*PositionXAfter+PositionYAfter*PositionYAfter)-StartR;
2938     if (rdrswitch)
2939     {
2940       hRdeltaRComponent->Fill(StartR, DeltaR);
2941     }
2942     position.SetZ(position.Z() + zstep);
2943   }
2944   if (GetRindexAndCheckBounds(position.Perp(), &rt) != InBounds || GetPhiIndexAndCheckBounds(FilterPhiPos(position.Phi()), &pt) != InBounds || (zBound == OutOfBounds))
2945   {
2946     *goodToStep = integernumbersteps - 1;
2947     *success = 0;
2948     return (accumulated_distortion);
2949   }
2950   *goodToStep = integernumbersteps;
2951   *success = 1;
2952   return accumulated_distortion;
2953 }
2954 
2955 void AnnularFieldSim::PlotFieldSlices(const std::string &filebase, const TVector3 &pos, char which)
2956 {
2957   bool mapEfield = true;
2958   if (which == 'B')
2959   {
2960     mapEfield = false;
2961   }
2962   which = mapEfield ? 'E' : 'B';
2963   std::string units;
2964   if (mapEfield)
2965   {
2966     units = "V/cm";
2967   }
2968   else
2969   {
2970     units = "T";
2971   }
2972 
2973   std::cout << std::format("plotting field slices for {} field, slicing at ({:.2f},{:.2f},{:.2f})...",
2974                            which, pos.Perp(), FilterPhiPos(pos.Phi()), pos.Z())
2975             << std::endl;
2976 
2977   std::cout << "file=" << filebase << std::endl;
2978 
2979   std::string plotfilename = filebase + "." + which + "field_slices.pdf";
2980   TVector3 inner = GetInnerEdge();
2981   TVector3 outer = GetOuterEdge();
2982   TVector3 steplocal = GetFieldStep();
2983 
2984   TCanvas *c;
2985 
2986   TH2F *hEfield[3][3];
2987   TH2F *hCharge[3];
2988   TH1F *hEfieldComp[3][3];
2989   std::string axis[] = {"r", "p", "z", "r", "p", "z"};
2990   float axisval[] = {(float) pos.Perp(), (float) FilterPhiPos(pos.Phi()), (float) pos.Z(), (float) pos.Perp(), (float) FilterPhiPos(pos.Phi()), (float) pos.Z()};
2991   int axn[] = {nr_roi, nphi_roi, nz_roi, nr_roi, nphi_roi, nz_roi};
2992   float axtop[] = {(float) outer.Perp(), 2 * M_PI, (float) outer.Z(), (float) outer.Perp(), 2 * M_PI, (float) outer.Z()};
2993   float axbot[] = {(float) inner.Perp(), 0, (float) inner.Z(), (float) inner.Perp(), 0, (float) inner.Z()};
2994 
2995   // if we are in charge of a twin, extend our axes:
2996   if (hasTwin)
2997   {
2998     // axtop[2]=axtop[5]=(float)(twin->GetOuterEdge().Z());
2999     axbot[2] = axbot[5] = (float) (twin->GetInnerEdge().Z());
3000   }
3001   std::cout << std::format("rpz bounds are {}<r{}\t {}<phi{}\t {}<z{}",
3002                            axbot[0], axtop[0], axbot[1], axtop[1], axbot[2], axtop[2])
3003             << std::endl;
3004 
3005   float axstep[6];
3006   for (int i = 0; i < 6; i++)
3007   {
3008     axstep[i] = (axtop[i] - axbot[i]) / (1.0 * axn[i]);
3009   }
3010   TVector3 field;
3011   TVector3 lpos;
3012 
3013   // it's a bit meta, but this loop over axes is a compact way to generate all the histogram titles.
3014   for (int ax = 0; ax < 3; ax++)
3015   {
3016     // loop over which axis slice to take
3017     hCharge[ax] = new TH2F(std::string("hCharge" + axis[ax]).c_str(),
3018                            std::format("Spacecharge Distribution in the {}{} plane at {}={:.3f} (C/cm^3);{};{}",
3019                                        axis[ax + 1], axis[ax + 2], axis[ax], axisval[ax], axis[ax + 1], axis[ax + 2])
3020                                .c_str(),
3021                            axn[ax + 1], axbot[ax + 1], axtop[ax + 1],
3022                            axn[ax + 2], axbot[ax + 2], axtop[ax + 2]);
3023     for (int i = 0; i < 3; i++)
3024     {
3025       // loop over which axis of the field to read
3026       hEfield[ax][i] = new TH2F(std::format("h{}field{}_{}{}", which, axis[i], axis[ax + 1], axis[ax + 2]).c_str(),
3027                                 std::format("{} component of {} Field in the {}{} plane at {}={:.3f} ({}) ;{};{}",
3028                                             axis[i], which, axis[ax + 1], axis[ax + 2], axis[ax], axisval[ax], units, axis[ax + 1], axis[ax + 2])
3029                                     .c_str(),
3030 
3031                                 axn[ax + 1], axbot[ax + 1], axtop[ax + 1],
3032                                 axn[ax + 2], axbot[ax + 2], axtop[ax + 2]);
3033       hEfieldComp[ax][i] = new TH1F(std::format("h{}fieldComp{}_{}{}", which, axis[i], axis[ax + 1], axis[ax + 2]).c_str(),
3034                                     std::format("Log Magnitude of {} component of {} Field in the {}{} plane at {}={:.3f};log10(mag)",
3035                                                 axis[i], which, axis[ax + 1], axis[ax + 2], axis[ax], axisval[ax])
3036                                         .c_str(),
3037                                     200, -5, 5);
3038     }
3039   }
3040 
3041   float rpz_coord[3];
3042   for (int ax = 0; ax < 3; ax++)
3043   {  // we have three sets of 'slices'.  the R slice is a 2d plot in phi-z, etc.
3044     rpz_coord[ax] = axisval[ax] + axstep[ax] / 2;
3045     for (int i = 0; i < axn[ax + 1]; i++)
3046     {  // for each slice, loop over the bins of the 2d plot:
3047       rpz_coord[(ax + 1) % 3] = axbot[ax + 1] + (i + 0.5) * axstep[ax + 1];
3048       for (int j = 0; j < axn[ax + 2]; j++)
3049       {
3050         rpz_coord[(ax + 2) % 3] = axbot[ax + 2] + (j + 0.5) * axstep[ax + 2];
3051         lpos.SetXYZ(rpz_coord[0], 0, rpz_coord[2]);
3052         lpos.SetPhi(rpz_coord[1]);
3053         if (false && ax == 0)
3054         {
3055           std::cout << std::format("sampling rpz=({},{},{})=({},{},{}) after conversion to xyz=({},{},{})",
3056                                    rpz_coord[0], rpz_coord[1], rpz_coord[2], lpos.Perp(), FilterPhiPos(lpos.Phi()), lpos.Z(),
3057                                    lpos.X(), lpos.Y(), lpos.Z())
3058                     << std::endl;
3059         }
3060         if (mapEfield)
3061         {
3062           // GetFieldAt automatically asks the twin if we are out of bounds here.
3063           field = GetFieldAt(lpos) * (1.0 * cm / V);  // get units so we're drawing in V/cm when we draw.
3064         }
3065         else
3066         {
3067           field = GetBFieldAt(lpos) * (1.0 / Tesla);  // get units so we're drawing in Tesla when we draw.
3068           // if (hasTwin && lpos.Z()<0) field=twin->GetBFieldAt(lpos)*1.0/Tesla;
3069         }
3070         field.RotateZ(-rpz_coord[1]);  // rotate us so we can read the y component as the phi component
3071         // if (field.Mag()>0) continue; //nothing has mag zero because of the drift field.
3072         hEfield[ax][0]->Fill(rpz_coord[(ax + 1) % 3], rpz_coord[(ax + 2) % 3], field.X());
3073         hEfield[ax][1]->Fill(rpz_coord[(ax + 1) % 3], rpz_coord[(ax + 2) % 3], field.Y());
3074         hEfield[ax][2]->Fill(rpz_coord[(ax + 1) % 3], rpz_coord[(ax + 2) % 3], field.Z());
3075         hCharge[ax]->Fill(rpz_coord[(ax + 1) % 3], rpz_coord[(ax + 2) % 3], GetChargeAt(lpos));
3076         hEfieldComp[ax][0]->Fill((std::abs(field.X())));
3077         hEfieldComp[ax][1]->Fill((std::abs(field.Y())));
3078         hEfieldComp[ax][2]->Fill((std::abs(field.Z())));
3079       }
3080     }
3081   }
3082 
3083   c = new TCanvas("cfieldslices", "electric field", 1200, 800);
3084   c->Divide(4, 3);
3085   gStyle->SetOptStat();
3086   for (int ax = 0; ax < 3; ax++)
3087   {
3088     for (int i = 0; i < 3; i++)
3089     {
3090       c->cd(ax * 4 + i + 1);
3091       gPad->SetRightMargin(0.15);
3092       hEfield[ax][i]->SetStats(false);
3093       hEfield[ax][i]->Draw("colz");
3094       // hEfield[ax][i]->GetListOfFunctions()->Print();
3095       gPad->Modified();
3096       // hEfieldComp[ax][i]->Draw();//"colz");
3097     }
3098     c->cd(ax * 4 + 4);
3099     gPad->SetRightMargin(0.15);
3100     hCharge[ax]->SetStats(false);
3101     hCharge[ax]->Draw("colz");
3102     // pal=dynamic_cast<TPaletteAxis*>(hCharge[ax]->GetListOfFunctions()->FindObject("palette"));
3103     if (false)
3104     {
3105       // pal->SetX1NDC(0.86);
3106       // pal->SetX2NDC(0.91);
3107       gPad->Modified();
3108     }
3109   }
3110   c->SaveAs(plotfilename.c_str());
3111   std::cout << "after plotting field slices..." << std::endl;
3112   std::cout << "file=" << filebase << std::endl;
3113 
3114   return;
3115 }
3116 
3117 void AnnularFieldSim::GenerateSeparateDistortionMaps(const std::string &filebase, int nSteps, int r_subsamples, int p_subsamples, int z_subsamples, int /*z_substeps*/, bool andCartesian)
3118 {
3119   // generates the distortion map for one or both sides of the detector, separating them so
3120   // they do not try to interpolate across the CM.
3121 
3122   // 1) pick a map spacing ('s')
3123   TVector3 steplocal(step.Perp() / r_subsamples, 0, step.Z() / z_subsamples);
3124   steplocal.SetPhi(step.Phi() / p_subsamples);
3125   float deltar = steplocal.Perp();  //(rf-ri)/nr;
3126   float deltap = steplocal.Phi();   //(pf-pi)/np;
3127   float deltaz = steplocal.Z();     //(zf-zi)/nz;
3128   bool rdrswitch = RdeltaRswitch;
3129   TVector3 stepzvec(0, 0, deltaz);
3130 
3131   //  int nSteps = 500;  //how many steps to take in the particle path.  hardcoded for now.  Think about this later.
3132 
3133   int nSides = 1;
3134   if (hasTwin)
3135   {
3136     nSides = 2;
3137   }
3138   // idea for a faster way to build a map:
3139 
3140   // 2) generate the distortions steplocal.Z() away from the readout
3141   // 3) generate the distortion from (i)*steplocal.Z() away to (i-1) away, then add the interpolated value from the (i-1) layer
3142 
3143   // for interpolation, Henry needs one extra buffer bin on each side.
3144 
3145   // so we define the histogram bounds (the 'h' suffix) to be the full range
3146   // plus an additional step in each direction so interpolation can work at the edges
3147   TVector3 lowerEdge = GetRoiCellCenter(rmin_roi, phimin_roi, zmin_roi);
3148   TVector3 upperEdge = GetRoiCellCenter(rmax_roi - 1, phimax_roi - 1, zmax_roi - 1);
3149   int nph = nphi * p_subsamples + 2;  // number of phibins in the histogram
3150   int nrh = nr * r_subsamples + 2;    // number of r bins in the histogram
3151   int nzh = nz * z_subsamples + 2;    // number of z you get the idea.
3152 
3153   float rih = lowerEdge.Perp() - 0.5 * step.Perp() - steplocal.Perp();             // lower bound of roi, minus one
3154   float rfh = upperEdge.Perp() + 0.5 * step.Perp() + steplocal.Perp();             // upper bound of roi, plus one
3155   float pih = FilterPhiPos(lowerEdge.Phi()) - 0.5 * step.Phi() - steplocal.Phi();  // can't automate this or we'll run afoul of phi looping.
3156   float pfh = FilterPhiPos(upperEdge.Phi()) + 0.5 * step.Phi() + steplocal.Phi();  // can't automate this or we'll run afoul of phi looping.
3157   float zih = lowerEdge.Z() - 0.5 * step.Z() - steplocal.Z();                      // lower bound of roi, minus one
3158   float zfh = upperEdge.Z() + 0.5 * step.Z() + steplocal.Z();                      // upper bound of roi, plus one
3159   float z_readout = upperEdge.Z() + 0.5 * step.Z();                                // readout plane.  Note we assume this is positive.
3160 
3161   std::cout << "generating distortion map..." << std::endl;
3162   std::cout << "file=" << filebase << std::endl;
3163   std::cout << std::format("Phi:  {} steps from {} to {} (field has {} steps)", nph, pih, pfh, GetFieldStepsPhi()) << std::endl;
3164   std::cout << std::format("R:  {} steps from {} to {} (field has {} steps)", nrh, rih, rfh, GetFieldStepsR()) << std::endl;
3165   std::cout << std::format("Z:  {} steps from {} to {} (field has {} steps)", nzh, zih, zfh, GetFieldStepsZ()) << std::endl;
3166   std::string distortionFilename = filebase + ".distortion_map.hist.root";
3167   std::string summaryFilename = filebase + "distortion_summary.pdf";
3168   std::string diffSummaryFilename = filebase + "differential_summary.pdf";
3169 
3170   TFile *outf = TFile::Open(distortionFilename.c_str(), "RECREATE");
3171   outf->cd();
3172 
3173   // actual output maps:
3174 
3175   TH3F *hDistortionR = new TH3F("hDistortionR", "Per-z-bin Distortion in the R direction as a function of (phi,r,z) (centered in r,phi, z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3176   TH3F *hDistortionP = new TH3F("hDistortionP", "Per-z-bin Distortion in the RPhi direction as a function of (phi,r,z)  (centered in r,phi, z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3177   TH3F *hDistortionZ = new TH3F("hDistortionZ", "Per-z-bin Distortion in the Z direction as a function of (phi,r,z)  (centered in r,phi, z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3178   TH3F *hIntDistortionR = new TH3F("hIntDistortionR", "Integrated R Distortion from (phi,r,z) to z=0 (centered in r,phi, and z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3179   TH3F *hIntDistortionP = new TH3F("hIntDistortionP", "Integrated R Distortion from (phi,r,z) to z=0 (centered in r,phi, and z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3180   TH3F *hIntDistortionZ = new TH3F("hIntDistortionZ", "Integrated R Distortion from (phi,r,z) to z=0  (centered in r,phi, and z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3181 
3182   TH3F *hIntDistortionX = new TH3F("hIntDistortionX", "Integrated X Distortion from (phi,r,z) to z=0 (centered in r,phi, and z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3183   TH3F *hIntDistortionY = new TH3F("hIntDistortionY", "Integrated Y Distortion from (phi,r,z) to z=0 (centered in r,phi, and z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3184 
3185   const int nMapComponents = 6;
3186   TH3F *hSeparatedMapComponent[2][6];  // side, then xyzrp
3187   TH3F *hValidToStepComponent[2];
3188   TH3F *hReachesReadout[2];
3189   std::string side[2];
3190   side[0] = "soloz";
3191   if (hasTwin)
3192   {
3193     side[0] = "posz";
3194     side[1] = "negz";
3195   }
3196   std::string sepAxis[] = {"X", "Y", "Z", "R", "P", "RPhi"};
3197   float zlower;
3198   float zupper;
3199   for (int i = 0; i < nSides; i++)
3200   {
3201     if (i == 0)
3202     {  // doing the positive side
3203       zlower = std::min(zih, zfh);
3204       zupper = std::max(zih, zfh);
3205     }
3206     else
3207     {
3208       zlower = -1 * std::max(zih, zfh);
3209       zupper = -1 * std::min(zih, zfh);
3210     }
3211     for (int j = 0; j < nMapComponents; j++)
3212     {
3213       hSeparatedMapComponent[i][j] = new TH3F(std::string("hIntDistortion" + sepAxis[j] + "_" + side[i]).c_str(),
3214                                               std::string("Integrated" + sepAxis[j] + " Deflection drifting from (phi,r,z) to z=endcap;phi;r;z (" + side[i] + " side)").c_str(),
3215                                               nph, pih, pfh, nrh, rih, rfh, nzh, zlower, zupper);
3216     }
3217     hValidToStepComponent[i] = new TH3F(std::string("hValidToStep_" + side[i]).c_str(),
3218                                         std::string("Step before out of bounds drifting from (phi,r,z) to z=endcap;phi;r;z (" + side[i] + " side)").c_str(),
3219                                         nph, pih, pfh, nrh, rih, rfh, nzh, zlower, zupper);
3220     hReachesReadout[i] = new TH3F(std::string("hReachesReadout_" + side[i]).c_str(),
3221                                   std::string("Bool value for checking if successfuly drifting from (phi,r,z) to z=endcap and reaches the readout;phi;r;z (" + side[i] + " side)").c_str(),
3222                                   nph, pih, pfh, nrh, rih, rfh, nzh, zlower, zupper);
3223   }
3224   if (rdrswitch)
3225   {
3226     hRdeltaRComponent = new TH2F("hRdeltaR", "Bool value of input for a 2D R_versus_deltaR plot from Zstart to Zend;R;deltaR", nrh, rih, rfh, 10 * nrh, rih - 70, rfh + 170);
3227     // hRdeltaRComponent = new TH2F("hRdeltaR","Bool value of input for a 2D R_versus_deltaR plot from Zstart to Zend;R;deltaR", nrh, rih, rfh, 10*nrh, rih-20, rfh-77);
3228     twin->hRdeltaRComponent = hRdeltaRComponent;
3229   }
3230   // monitor plots, and the position that that plot monitors at:
3231 
3232   // TVector3 pos((nrh/2+0.5)*s.Perp()+rih,0,(nzh/2+0.5)*s.Z()+zih);
3233   TVector3 pos(((nrh / 2) + 0.5) * steplocal.Perp() + rih, 0, zmin + ((nz / 2) + 0.5) * step.Z()); // NOLINT(bugprone-integer-division)
3234   float posphi = ((nph / 2) + 0.5) * steplocal.Phi() + pih; // NOLINT(bugprone-integer-division)
3235   pos.SetPhi(posphi);
3236   // int xi[3]={nrh/2,nph/2,nzh/2};
3237   int xi[3] = {(int) std::floor((pos.Perp() - rih) / steplocal.Perp()), (int) std::floor((posphi - pih) / steplocal.Phi()), (int) std::floor((pos.Z() - zih) / steplocal.Z())};
3238   if (!hasTwin)
3239   {
3240     std::cout << std::format("rpz slice indices= ({},{},{}) (no twin)", xi[0], xi[1], xi[2]) << std::endl;
3241   }
3242   int twinz = (-pos.Z() - zih) / steplocal.Z();
3243   if (hasTwin)
3244   {
3245     std::cout << std::format("rpz slice indices= ({},{},{}) twinz={}", xi[0], xi[1], xi[2], twinz) << std::endl;
3246   }
3247 
3248   const std::string axname[] = {"r", "p", "z", "r", "p", "z"};
3249   int axn[] = {nrh, nph, nzh, nrh, nph, nzh};
3250   float axval[] = {(float) pos.Perp(), (float) pos.Phi(), (float) pos.Z(), (float) pos.Perp(), (float) pos.Phi(), (float) pos.Z()};
3251   float axbot[] = {rih, pih, zih, rih, pih, zih};
3252   float axtop[] = {rfh, pfh, zfh, rfh, pfh, zfh};
3253   TH2F *hIntDist[3][3];
3254   TH1F *hRDist[2][3];  // now with a paired friend for the negative side
3255   TH2F *hDiffDist[3][3];
3256   TH1F *hRDiffDist[2][3];
3257   for (int i = 0; i < 3; i++)
3258   {
3259     // loop over which axis of the distortion to read
3260     for (int ax = 0; ax < 3; ax++)
3261     {
3262       // loop over which plane to work in
3263       hDiffDist[ax][i] = new TH2F(std::string("hDiffDist" + axname[i] + "_" + axname[ax + 1] + axname[ax + 2]).c_str(),
3264                                   std::format("{} component of diff. distortion in {}{} plane at {}={:.3f};{};{}",
3265                                               axname[i], axname[ax + 1], axname[ax + 2], axname[ax], axval[ax], axname[ax + 1], axname[ax + 2])
3266                                       .c_str(),
3267                                   axn[ax + 1], axbot[ax + 1], axtop[ax + 1],
3268                                   axn[ax + 2], axbot[ax + 2], axtop[ax + 2]);
3269       hIntDist[ax][i] = new TH2F(std::string("hIntDist" + axname[i] + "_" + axname[ax + 1] + axname[ax + 2]).c_str(),
3270                                  std::format("{} component of int. distortion in {}{} plane at {}={:.3f};{};{}",
3271                                              axname[i], axname[ax + 1], axname[ax + 2], axname[ax], axval[ax], axname[ax + 1], axname[ax + 2])
3272                                      .c_str(),
3273                                  axn[ax + 1], axbot[ax + 1], axtop[ax + 1],
3274                                  axn[ax + 2], axbot[ax + 2], axtop[ax + 2]);
3275     }
3276     hRDist[0][i] = new TH1F(std::string("hRDist" + axname[i]).c_str(),
3277                             std::format("{} component of int. distortion vs r with {}={:.3f} and {}={:.3f};r(cm);#delta (cm)", axname[i], axname[1], axval[1], axname[2], axval[2]).c_str(),
3278                             axn[0], axbot[0], axtop[0]);
3279     hRDist[1][i] = new TH1F(std::string("hRDist" + axname[i] + "Neg").c_str(),
3280                             std::format("{} component of int. distortion vs r with {}={:.3f} and {}={:.3f};r(cm);#delta (cm)", axname[i], axname[1], axval[1], axname[2], axval[2]).c_str(),
3281                             axn[0], axbot[0], axtop[0]);
3282     hRDiffDist[0][i] = new TH1F(std::string("hRDiffDist" + axname[i]).c_str(),
3283                                 std::format("{} component of int. distortion vs r with {}={:.3f} and {}={:.3f};r(cm);#delta (cm)", axname[i], axname[1], axval[1], axname[2], axval[2]).c_str(),
3284                                 axn[0], axbot[0], axtop[0]);
3285     hRDiffDist[1][i] = new TH1F(std::string("hRDiffDist" + axname[i] + "Neg").c_str(),
3286                                 std::format("{} component of int. distortion vs r with {}={:.3f} and {}={:.3f};r(cm);#delta (cm)", axname[i], axname[1], axval[1], axname[2], axval[2]).c_str(),
3287                                 axn[0], axbot[0], axtop[0]);
3288   }
3289 
3290   TVector3 inpart;
3291   TVector3 outpart;
3292   TVector3 diffdistort;
3293   TVector3 distort;
3294   int validToStep;
3295   int successCheck;
3296 
3297   // TTree version:
3298   float partR;
3299   float partP;
3300   float partZ;
3301   int ir;
3302   int ip;
3303   int iz;
3304   float distortR;
3305   float distortP;
3306   float distortZ;
3307   float distortX;
3308   float distortY;
3309   float diffdistR;
3310   float diffdistP;
3311   float diffdistZ;
3312   TTree *dTree = new TTree("dTree", "Distortion per step z");
3313   dTree->Branch("r", &partR);
3314   dTree->Branch("p", &partP);
3315   dTree->Branch("z", &partZ);
3316   dTree->Branch("ir", &ir);
3317   dTree->Branch("ip", &ip);
3318   dTree->Branch("iz", &iz);
3319   dTree->Branch("dr", &distortR);
3320   dTree->Branch("drp", &distortP);
3321   dTree->Branch("dz", &distortZ);
3322 
3323   std::cout << std::format("generating separated distortion map with ({}x{}x{}) grid ", nrh, nph, nzh) << std::endl;
3324   unsigned long long totalelements = nrh;
3325   totalelements *= nph;
3326   totalelements *= nzh;  // breaking up this multiplication prevents a 32bit math overflow
3327   if (hasTwin)
3328   {
3329     totalelements *= 2;  // if we have a twin, we have twice as many z bins as we thought.
3330   }
3331 
3332   unsigned long long percent = totalelements / 100;
3333   unsigned long long waypoint = percent * debug_npercent;
3334   std::cout << std::format("total elements = {}", totalelements) << std::endl;
3335 
3336   int el = 0;
3337 
3338   // we want to loop over the entire region to be mapped, but we also need to include
3339   // one additional bin at each edge, to allow the mc drift code to interpolate properly.
3340   // hence we count from -1 to n+1, and manually adjust the position in those edge cases
3341   // to avoid sampling nonphysical regions in r and z.  the phi case is free to wrap as
3342   //  normal.
3343 
3344   // note that we apply the adjustment to the particle position (inpart) and not the plotted position (partR etc)
3345   inpart.SetXYZ(1, 0, 0);
3346   for (ir = 0; ir < nrh; ir++)
3347   {
3348     partR = (ir + 0.5) * deltar + rih;
3349     if (ir == 0)
3350     {
3351       inpart.SetPerp(partR + deltar);
3352     }
3353     else if (ir == nrh - 1)
3354     {
3355       inpart.SetPerp(partR - deltar);
3356     }
3357     else
3358     {
3359       inpart.SetPerp(partR);
3360     }
3361     for (ip = 0; ip < nph; ip++)
3362     {
3363       partP = (ip + 0.5) * deltap + pih;
3364       inpart.SetPhi(partP);
3365       // since phi loops, there's no need to adjust phis that are out of bounds.
3366       for (iz = 0; iz < nzh; iz++)
3367       {
3368         partZ = (iz) *deltaz + zih;  // start us at the EDGE of the bin,
3369         if (iz == 0)
3370         {
3371           inpart.SetZ(partZ + deltaz);
3372         }
3373         else if (iz == nzh - 1)
3374         {
3375           inpart.SetZ(partZ - deltaz);
3376         }
3377         else
3378         {
3379           inpart.SetZ(partZ);
3380         }
3381         partZ += 0.5 * deltaz;  // move to center of histogram bin.
3382         for (int localside = 0; localside < nSides; localside++)
3383         {
3384           if (localside == 0)
3385           {
3386             diffdistort = zero_vector;  // GetTotalDistortion(inpart.Z() + deltaz, inpart, nSteps, true, &validToStep, &successCheck);
3387             distort = GetTotalDistortion(z_readout, inpart, nSteps, true, &validToStep, &successCheck);
3388           }
3389           else
3390           {
3391             // if we have more than one side,
3392             // flip z coords and do the twin instead:
3393             partZ *= -1;                   // position to place in histogram
3394             inpart.SetZ(-1 * inpart.Z());  // position to seek in sim
3395             diffdistort = zero_vector;     // twin->GetTotalDistortion(inpart.Z() - deltaz, inpart, nSteps, true, &validToStep, &successCheck);
3396             distort = twin->GetTotalDistortion(-z_readout, inpart, nSteps, true, &validToStep, &successCheck);
3397           }
3398 
3399           diffdistort.RotateZ(-inpart.Phi());  // rotate so that distortion components are wrt the x axis
3400           diffdistP = diffdistort.Y();         // the phi component is now the y component.
3401           diffdistR = diffdistort.X();         // and the r component is the x component
3402           diffdistZ = diffdistort.Z();
3403 
3404           distortX = distort.X();
3405           distortY = distort.Y();
3406           distort.RotateZ(-inpart.Phi());  // rotate so that distortion components are wrt the x axis
3407           distortP = distort.Y();          // the phi component is now the y component.
3408           distortR = distort.X();          // and the r component is the x component
3409           distortZ = distort.Z();
3410 
3411           float distComp[nMapComponents];  // by components
3412           distComp[0] = distortX;
3413           distComp[1] = distortY;
3414           distComp[2] = distortZ;
3415           distComp[3] = distortR;
3416           distComp[4] = distortP / partR;  // 'P' now refers to phi in radians, rather than unitful
3417           distComp[5] = distortP;          // 'RPhi' is the one that correponds to the []meter unitful phi-hat value
3418 
3419           for (int c = 0; c < nMapComponents; c++)
3420           {
3421             hSeparatedMapComponent[localside][c]->Fill(partP, partR, partZ, distComp[c]);
3422           }
3423 
3424           hValidToStepComponent[localside]->Fill(partP, partR, partZ, validToStep);
3425           hReachesReadout[localside]->Fill(partP, partR, partZ, successCheck);
3426 
3427           // recursive integral distortion:
3428           // get others working first!
3429 
3430           // print_need_cout("part=(rpz)(%f,%f,%f),distortP=%f\n",partP,partR,partZ,distortP);
3431           hIntDistortionR->Fill(partP, partR, partZ, distortR);
3432           hIntDistortionP->Fill(partP, partR, partZ, distortP);
3433           hIntDistortionZ->Fill(partP, partR, partZ, distortZ);
3434 
3435           if (andCartesian)
3436           {
3437             hIntDistortionX->Fill(partP, partR, partZ, distortX);
3438             hIntDistortionY->Fill(partP, partR, partZ, distortY);
3439           }
3440 
3441           // now we fill particular slices for integral visualizations:
3442           if (ir == xi[0] && localside == 0)
3443           {  // r slice
3444             // print_need_cout("ir=%d, r=%f (pz)=(%d,%d), distortR=%2.2f, distortP=%2.2f\n",ir,partR,ip,iz,distortR,distortP);
3445             hIntDist[0][0]->Fill(partP, partZ, distortR);
3446             hIntDist[0][1]->Fill(partP, partZ, distortP);
3447             hIntDist[0][2]->Fill(partP, partZ, distortZ);
3448             hDiffDist[0][0]->Fill(partP, partZ, diffdistR);
3449             hDiffDist[0][1]->Fill(partP, partZ, diffdistP);
3450             hDiffDist[0][2]->Fill(partP, partZ, diffdistZ);
3451           }
3452           if (ip == xi[1] && localside == 0)
3453           {  // phi slice
3454             // print_need_cout("ip=%d, p=%f (rz)=(%d,%d), distortR=%2.2f, distortP=%2.2f\n",ip,partP,ir,iz,distortR,distortP);
3455             hIntDist[1][0]->Fill(partZ, partR, distortR);
3456             hIntDist[1][1]->Fill(partZ, partR, distortP);
3457             hIntDist[1][2]->Fill(partZ, partR, distortZ);
3458             hDiffDist[1][0]->Fill(partZ, partR, diffdistR);
3459             hDiffDist[1][1]->Fill(partZ, partR, diffdistP);
3460             hDiffDist[1][2]->Fill(partZ, partR, diffdistZ);
3461 
3462             if (iz == xi[2] && localside == 0)
3463             {  // z slices of phi slices= r line at mid phi, mid z:
3464               hRDist[0][0]->Fill(partR, distortR);
3465               hRDist[0][1]->Fill(partR, distortP);
3466               hRDist[0][2]->Fill(partR, distortZ);
3467               hRDiffDist[0][0]->Fill(partR, diffdistR);
3468               hRDiffDist[0][1]->Fill(partR, diffdistP);
3469               hRDiffDist[0][2]->Fill(partR, diffdistZ);
3470             }
3471             if (hasTwin && iz == twinz && localside == 1)
3472             {  // z slices of phi slices= r line at mid phi, mid z:
3473               hRDist[1][0]->Fill(partR, distortR);
3474               hRDist[1][1]->Fill(partR, distortP);
3475               hRDist[1][2]->Fill(partR, distortZ);
3476               hRDiffDist[1][0]->Fill(partR, diffdistR);
3477               hRDiffDist[1][1]->Fill(partR, diffdistP);
3478               hRDiffDist[1][2]->Fill(partR, diffdistZ);
3479             }
3480           }
3481           if (iz == xi[2] && localside == 0)
3482           {  // z slice
3483             // print_need_cout("iz=%d, z=%f (rp)=(%d,%d), distortR=%2.2f, distortP=%2.2f\n",iz,partZ,ir,ip,distortR,distortP);
3484 
3485             hIntDist[2][0]->Fill(partR, partP, distortR);
3486             hIntDist[2][1]->Fill(partR, partP, distortP);
3487             hIntDist[2][2]->Fill(partR, partP, distortZ);
3488             hDiffDist[2][0]->Fill(partR, partP, diffdistR);
3489             hDiffDist[2][1]->Fill(partR, partP, diffdistP);
3490             hDiffDist[2][2]->Fill(partR, partP, diffdistZ);
3491           }
3492 
3493           if (!(el % waypoint))
3494           {
3495             std::cout << std::format("generating distortions {}%:  ", static_cast<int>(el / percent));
3496             std::cout << std::format("distortion at (ir={}, ip={}, iz={}) is ({:E},{:E},{:E})", ir, ip, iz, distortR, distortP, distortZ) << std::endl;
3497           }
3498           el++;
3499         }
3500       }
3501     }
3502   }
3503   std::cout << "Completed distortion generation.  Saving outputs..." << std::endl;
3504 
3505   TCanvas *canvas = new TCanvas("cdistort", "distortion integrals", 1200, 800);
3506   // take 10 of the bottom of this for data?
3507   std::cout << "was able to make a tcanvas" << std::endl;
3508   canvas->cd();
3509   TPad *c = new TPad("cplots", "distortion integral plots", 0, 0.2, 1, 1);
3510   canvas->cd();
3511   TPad *textpad = new TPad("ctext", "distortion integral plots", 0, 0.0, 1, 0.2);
3512   std::cout << "was able to make some tpads" << std::endl;
3513 
3514   c->Divide(4, 3);
3515   gStyle->SetOptStat();
3516   std::cout << "was able to interact with gStyle" << std::endl;
3517 
3518   for (int i = 0; i < 3; i++)
3519   {
3520     // component
3521     for (int ax = 0; ax < 3; ax++)
3522     {
3523       std::cout << std::format("looping over components i={} ax={}", i, ax) << std::endl;
3524 
3525       // plane
3526       c->cd(i * 4 + ax + 1);
3527       gPad->SetRightMargin(0.15);
3528       hIntDist[ax][i]->SetStats(false);
3529       hIntDist[ax][i]->Draw("colz");
3530     }
3531 
3532     std::cout << "drawing R profile " << i << std::endl;
3533 
3534     c->cd(i * 4 + 4);
3535     hRDist[0][i]->SetStats(false);
3536     hRDist[0][i]->SetFillColor(kRed);
3537     hRDist[0][i]->Draw("hist");
3538     if (hasTwin)
3539     {
3540       std::cout << "drawing R profile twin " << i << std::endl;
3541       hRDist[1][i]->SetStats(false);
3542       hRDist[1][i]->SetLineColor(kBlue);
3543       hRDist[1][i]->Draw("hist,same");
3544     }
3545   }
3546   std::cout << "switching to textpad" << std::endl;
3547 
3548   textpad->cd();
3549   float texpos = 0.9;
3550   float texshift = 0.12;
3551   TLatex *tex = new TLatex(0.0, texpos, "Fill Me In");
3552   std::cout << "built TLatex" << std::endl;
3553 
3554   tex->SetTextSize(texshift * 0.8);
3555   tex->DrawLatex(0.05, texpos, GetFieldString().c_str());
3556   texpos -= texshift;
3557   tex->DrawLatex(0.05, texpos, GetChargeString().c_str());
3558   texpos -= texshift;
3559   // tex->DrawLatex(0.05,texpos,Form("Drift Field = %2.2f V/cm",GetNominalE()));texpos-=texshift;
3560   tex->DrawLatex(0.05, texpos, std::format("Drifting grid of (rp)=({} x {}) electrons with {} steps", nrh, nph, nSteps).c_str());
3561   texpos -= texshift;
3562   tex->DrawLatex(0.05, texpos, GetLookupString().c_str());
3563   texpos -= texshift;
3564   tex->DrawLatex(0.05, texpos, GetGasString().c_str());
3565   texpos -= texshift;
3566   if (debug_distortionScale.Mag() > 0)
3567   {
3568     tex->DrawLatex(0.05, texpos, std::format("Distortion scaled by (r,p,z)=({:.2f},{:.2f},{:.2f})", debug_distortionScale.X(), debug_distortionScale.Y(), debug_distortionScale.Z()).c_str());
3569     texpos -= texshift;
3570   }
3571   texpos = 0.9;
3572   std::cout << "cd'ing to canvas:" << std::endl;
3573 
3574   canvas->cd();
3575   std::cout << "draw1" << std::endl;
3576 
3577   c->Draw();
3578   canvas->cd();
3579   std::cout << "draw2" << std::endl;
3580   textpad->Draw();
3581   std::cout << "was able to complete drawing on both pads" << std::endl;
3582 
3583   canvas->SaveAs(summaryFilename.c_str());
3584 
3585   // canvas->cd();
3586   // already done TPad *c=new TPad("cplots","distortion differential plots",0,0.2,1,1);
3587   // canvas->cd();
3588   // already done TPad *textpad=new TPad("ctext","distortion differential plots",0,0.0,1,0.2);
3589   // already done c->Divide(4,3);
3590   // gStyle->SetOptStat();
3591   for (int i = 0; i < 3; i++)
3592   {
3593     // component
3594     for (int ax = 0; ax < 3; ax++)
3595     {
3596       // plane
3597       c->cd(i * 4 + ax + 1);
3598       gPad->SetRightMargin(0.15);
3599       hDiffDist[ax][i]->SetStats(false);
3600       hDiffDist[ax][i]->Draw("colz");
3601     }
3602     c->cd(i * 4 + 4);
3603     hRDiffDist[0][i]->SetStats(false);
3604     hRDiffDist[0][i]->SetFillColor(kRed);
3605     hRDiffDist[0][i]->Draw("hist");
3606     if (hasTwin)
3607     {
3608       hRDiffDist[1][i]->SetStats(false);
3609       hRDiffDist[1][i]->SetLineColor(kBlue);
3610       hRDiffDist[1][i]->Draw("hist same");
3611     }
3612   }
3613   textpad->cd();
3614   texpos = 0.9;
3615   texshift = 0.12;
3616   tex->SetTextSize(texshift * 0.8);
3617   tex->DrawLatex(0.05, texpos, GetFieldString().c_str());
3618   texpos -= texshift;
3619   tex->DrawLatex(0.05, texpos, GetChargeString().c_str());
3620   texpos -= texshift;
3621   // tex->DrawLatex(0.05,texpos,Form("Drift Field = %2.2f V/cm",GetNominalE()));texpos-=texshift;
3622   tex->DrawLatex(0.05, texpos, std::format("Drifting grid of (rp)=({} x {}) electrons with {} steps", nrh, nph, nSteps).c_str());
3623   texpos -= texshift;
3624   tex->DrawLatex(0.05, texpos, GetLookupString().c_str());
3625   texpos -= texshift;
3626   tex->DrawLatex(0.05, texpos, GetGasString().c_str());
3627   texpos -= texshift;
3628   tex->DrawLatex(0.05, texpos, "Differential Plots");
3629   texpos -= texshift;
3630   if (debug_distortionScale.Mag() > 0)
3631   {
3632     tex->DrawLatex(0.05, texpos, std::format("Distortion scaled by (r,p,z)=({:.2f},{:.2f},{:.2f})", debug_distortionScale.X(), debug_distortionScale.Y(), debug_distortionScale.Z()).c_str());
3633     texpos -= texshift;
3634   }
3635   texpos = 0.9;
3636 
3637   canvas->cd();
3638   c->Draw();
3639   canvas->cd();
3640   textpad->Draw();
3641   canvas->SaveAs(diffSummaryFilename.c_str());
3642 
3643   std::cout << "saving map histograms to:" << distortionFilename << std::endl;
3644 
3645   outf->cd();
3646   for (int i = 0; i < nSides; i++)
3647   {
3648     std::cout << "Saving side " << side[i] << std::endl;
3649     for (int j = 0; j < nMapComponents; j++)
3650     {
3651       hSeparatedMapComponent[i][j]->GetSumw2()->Set(0);
3652       hSeparatedMapComponent[i][j]->Write();
3653     }
3654     hValidToStepComponent[i]->GetSumw2()->Set(0);
3655     hValidToStepComponent[i]->Write();
3656 
3657     hReachesReadout[i]->GetSumw2()->Set(0);
3658     hReachesReadout[i]->Write();
3659   }
3660   if (rdrswitch)
3661   {
3662     hRdeltaRComponent->GetSumw2()->Set(0);
3663     hRdeltaRComponent->Write();
3664   }
3665 
3666   hDistortionR->GetSumw2()->Set(0);
3667   hDistortionP->GetSumw2()->Set(0);
3668   hDistortionZ->GetSumw2()->Set(0);
3669   hIntDistortionR->GetSumw2()->Set(0);
3670   hIntDistortionP->GetSumw2()->Set(0);
3671   hIntDistortionZ->GetSumw2()->Set(0);
3672   if (andCartesian)
3673   {
3674     hIntDistortionX->GetSumw2()->Set(0);
3675     hIntDistortionY->GetSumw2()->Set(0);
3676   }
3677   hDistortionR->Write();
3678   hDistortionP->Write();
3679   hDistortionZ->Write();
3680   hIntDistortionR->Write();
3681   hIntDistortionP->Write();
3682   hIntDistortionZ->Write();
3683   if (false && andCartesian)
3684   {
3685     hIntDistortionX->Write();
3686     hIntDistortionY->Write();
3687   }
3688   std::cout << "finished writing histograms" << std::endl;
3689   // dTree->Write();
3690   //  print_need_cout("wrote dTree\n");
3691   outf->Close();
3692   // print_need_cout("map:%s.closed\n",distortionFilename.Data());
3693 
3694   std::cout << "wrote separated map and summary to " << filebase << std::endl;
3695   std::cout << "map: " << distortionFilename << std::endl;
3696   std::cout << "summary: " << summaryFilename << std::endl;
3697   // print_need_cout("wrote map and summary to %s and %s.\n",distortionFilename.Data(),summaryFilename.Data());
3698   return;
3699 }
3700 
3701 void AnnularFieldSim::GenerateDistortionMaps(const std::string &filebase, int r_subsamples, int p_subsamples, int z_subsamples, int /*z_substeps*/, bool andCartesian)
3702 {
3703   // generates the distortion map for the full detector instead of one map per side.
3704   // This produces wrong behavior when interpolating across the CM and should not be used
3705   // unless you're doing some debugging/backward compatibility checking.  Eventually this should be removed  (said in early 2022)
3706   std::cout << "WARNING:  You called the version of the distortion generator that generates a unified map.  Are you sure you meant to do this?" << std::endl;
3707 
3708   // 1) pick a map spacing ('s')
3709   bool makeUnifiedMap = true;  // if true, generate a single map across the whole TPC.  if false, save two maps, one for each side.
3710 
3711   TVector3 steplocal(step.Perp() / r_subsamples, 0, step.Z() / z_subsamples);
3712   steplocal.SetPhi(step.Phi() / p_subsamples);
3713   float deltar = steplocal.Perp();  //(rf-ri)/nr;
3714   float deltap = steplocal.Phi();   //(pf-pi)/np;
3715   float deltaz = steplocal.Z();     //(zf-zi)/nz;
3716   TVector3 stepzvec(0, 0, deltaz);
3717   int nSteps = 500;  // how many steps to take in the particle path.  hardcoded for now.  Think about this later.
3718 
3719   // idea for a faster way to build a map:
3720 
3721   // 2) generate the distortions steplocal.Z() away from the readout
3722   // 3) generate the distortion from (i)*steplocal.Z() away to (i-1) away, then add the interpolated value from the (i-1) layer
3723 
3724   // for interpolation, Henry needs one extra buffer bin on each side.
3725 
3726   // so we define the histogram bounds (the 'h' suffix) to be the full range
3727   // plus an additional step in each direction so interpolation can work at the edges
3728   TVector3 lowerEdge = GetRoiCellCenter(rmin_roi, phimin_roi, zmin_roi);
3729   TVector3 upperEdge = GetRoiCellCenter(rmax_roi - 1, phimax_roi - 1, zmax_roi - 1);
3730   int nph = nphi * p_subsamples + 2;  // nuber of phibins in the histogram
3731   int nrh = nr * r_subsamples + 2;    // number of r bins in the histogram
3732   int nzh = nz * z_subsamples + 2;    // number of z you get the idea.
3733   int successCheck;
3734 
3735   if (hasTwin && makeUnifiedMap)
3736   {  // double the z range if we have a twin.  r and phi are the same, unless we had a phi roi...
3737     lowerEdge.SetZ(-1 * upperEdge.Z());
3738     nzh += nz * z_subsamples;
3739   }
3740 
3741   float rih = lowerEdge.Perp() - 0.5 * step.Perp() - steplocal.Perp();             // lower bound of roi, minus one
3742   float rfh = upperEdge.Perp() + 0.5 * step.Perp() + steplocal.Perp();             // upper bound of roi, plus one
3743   float pih = FilterPhiPos(lowerEdge.Phi()) - 0.5 * step.Phi() - steplocal.Phi();  // can't automate this or we'll run afoul of phi looping.
3744   float pfh = FilterPhiPos(upperEdge.Phi()) + 0.5 * step.Phi() + steplocal.Phi();  // can't automate this or we'll run afoul of phi looping.
3745   float zih = lowerEdge.Z() - 0.5 * step.Z() - steplocal.Z();                      // lower bound of roi, minus one
3746   float zfh = upperEdge.Z() + 0.5 * step.Z() + steplocal.Z();                      // upper bound of roi, plus one
3747   float z_readout = upperEdge.Z() + 0.5 * step.Z();                                // readout plane.  Note we assume this is positive.
3748 
3749   std::cout << "generating distortion map..." << std::endl;
3750   std::cout << "file=" << filebase << std::endl;
3751   std::cout << std::format("Phi:  {} steps from {} to {} (field has {} steps)", nph, pih, pfh, GetFieldStepsPhi()) << std::endl;
3752   std::cout << std::format("R:  %d steps from %f to %f (field has %d steps)", nrh, rih, rfh, GetFieldStepsR()) << std::endl;
3753   std::cout << std::format("Z:  %d steps from %f to %f (field has %d steps)", nzh, zih, zfh, GetFieldStepsZ()) << std::endl;
3754   std::string distortionFilename = filebase + ".distortion_summary.pdf";
3755   std::string summaryFilename = filebase + ".distortion_summary.pdf";
3756   std::string diffSummaryFilename = filebase + ".differential_summary.pdf";
3757 
3758   TFile *outf = TFile::Open(distortionFilename.c_str(), "RECREATE");
3759   outf->cd();
3760 
3761   // actual output maps:
3762 
3763   TH3F *hDistortionR = new TH3F("hDistortionR", "Per-z-bin Distortion in the R direction as a function of (phi,r,z) (centered in r,phi, z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3764   TH3F *hDistortionP = new TH3F("hDistortionP", "Per-z-bin Distortion in the RPhi direction as a function of (phi,r,z)  (centered in r,phi, z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3765   TH3F *hDistortionZ = new TH3F("hDistortionZ", "Per-z-bin Distortion in the Z direction as a function of (phi,r,z)  (centered in r,phi, z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3766   TH3F *hIntDistortionR = new TH3F("hIntDistortionR", "Integrated R Distortion from (phi,r,z) to z=0 (centered in r,phi, and z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3767   TH3F *hIntDistortionP = new TH3F("hIntDistortionP", "Integrated R Distortion from (phi,r,z) to z=0 (centered in r,phi, and z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3768   TH3F *hIntDistortionZ = new TH3F("hIntDistortionZ", "Integrated R Distortion from (phi,r,z) to z=0  (centered in r,phi, and z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3769 
3770   TH3F *hIntDistortionX = new TH3F("hIntDistortionX", "Integrated X Distortion from (phi,r,z) to z=0 (centered in r,phi, and z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3771   TH3F *hIntDistortionY = new TH3F("hIntDistortionY", "Integrated Y Distortion from (phi,r,z) to z=0 (centered in r,phi, and z);phi;r;z", nph, pih, pfh, nrh, rih, rfh, nzh, zih, zfh);
3772 
3773   /*
3774   TH3F* hNewIntDistortionR=new TH3F("hNewIntDistortionR","Recursively Integrated R Distortion from (r,phi,z) to z=0 (centered in r,phi, and z);phi;r;z",nph,pih,pfh,nrh,rih,rfh,nzh,zih,zfh);
3775   TH3F* hNewIntDistortionP=new TH3F("hNewIntDistortionP","Recursively Integrated R Distortion from (r,phi,z) to z=0 (centered in r,phi, and z);phi;r;z",nph,pih,pfh,nrh,rih,rfh,nzh,zih,zfh);
3776   TH3F* hNewIntDistortionZ=new TH3F("hNewIntDistortionZ","Recursively Integrated R Distortion from (r,phi,z) to z=0  (centered in r,phi, and z);phi;r;z",nph,pih,pfh,nrh,rih,rfh,nzh,zih,zfh);
3777 //for the interchanging distortion maps.
3778     TH2F* hNewIntDistortionR=new TH3F("hNewIntDistortionR","Recursively Integrated R Distortion from (r,phi,z) to z=0 (centered in r,phi, and z);phi;r;z",nph,pih,pfh,nrh,rih,rfh,nzh,zih,zfh);
3779   TH3F* hNewIntDistortionP=new TH3F("hNewIntDistortionP","Recursively Integrated R Distortion from (r,phi,z) to z=0 (centered in r,phi, and z);phi;r;z",nph,pih,pfh,nrh,rih,rfh,nzh,zih,zfh);
3780   TH3F* hNewIntDistortionZ=new TH3F("hNewIntDistortionZ","Recursively Integrated R Distortion from (r,phi,z) to z=0  (centered in r,phi, and z);phi;r;z",nph,pih,pfh,nrh,rih,rfh,nzh,zih,zfh);
3781   */
3782 
3783   // monitor plots, and the position that plot monitors at:
3784 
3785   // TVector3 pos((nrh/2+0.5)*steplocal.Perp()+rih,0,(nzh/2+0.5)*steplocal.Z()+zih);
3786   TVector3 pos(((nrh / 2) + 0.5) * steplocal.Perp() + rih, 0, zmin + ((nz / 2) + 0.5) * step.Z()); // NOLINT(bugprone-integer-division)
3787   float posphi = ((nph / 2) + 0.5) * steplocal.Phi() + pih; // NOLINT(bugprone-integer-division)
3788   pos.SetPhi(posphi);
3789   // int xi[3]={nrh/2,nph/2,nzh/2};
3790   int xi[3] = {(int) std::floor((pos.Perp() - rih) / steplocal.Perp()), (int) std::floor((posphi - pih) / steplocal.Phi()), (int) std::floor((pos.Z() - zih) / steplocal.Z())};
3791   if (!hasTwin)
3792   {
3793     std::cout << std::format("rpz slice indices= ({},{},{}) (no twin)", xi[0], xi[1], xi[2]) << std::endl;
3794   }
3795   int twinz = 0;  // this is meant to be the matching position to xi[2] in the twin, hence generally -1*pos.  Better to just ask the twin rather than trying to calculate it ourselves...
3796   if (hasTwin)
3797   {
3798     twinz = twin->GetZindex(-1 * pos.Z());
3799     std::cout << std::format("rpz slice indices= ({},{},{}) twinz={}", xi[0], xi[1], xi[2], twinz) << std::endl;
3800   }
3801 
3802   const std::string axname[] = {"r", "p", "z", "r", "p", "z"};
3803   int axn[] = {nrh, nph, nzh, nrh, nph, nzh};
3804   float axval[] = {(float) pos.Perp(), (float) pos.Phi(), (float) pos.Z(), (float) pos.Perp(), (float) pos.Phi(), (float) pos.Z()};
3805   float axbot[] = {rih, pih, zih, rih, pih, zih};
3806   float axtop[] = {rfh, pfh, zfh, rfh, pfh, zfh};
3807   TH2F *hIntDist[3][3];
3808   TH1F *hRDist[2][3];  // now with a paired friend for the negative side
3809   TH2F *hDiffDist[3][3];
3810   TH1F *hRDiffDist[2][3];
3811   for (int i = 0; i < 3; i++)
3812   {
3813     // loop over which axis of the distortion to read
3814     for (int ax = 0; ax < 3; ax++)
3815     {
3816       // loop over which plane to work in
3817       hDiffDist[ax][i] = new TH2F(std::string("hDiffDist" + axname[i] + "_" + axname[ax + 1] + axname[ax + 2]).c_str(),
3818                                   std::format("{} component of diff. distortion in {}{} plane at {}={:.3f};{};{}", axname[i], axname[ax + 1], axname[ax + 2], axname[ax], axval[ax], axname[ax + 1], axname[ax + 2]).c_str(),
3819                                   axn[ax + 1], axbot[ax + 1], axtop[ax + 1],
3820                                   axn[ax + 2], axbot[ax + 2], axtop[ax + 2]);
3821       hIntDist[ax][i] = new TH2F(std::string("hDIntDist" + axname[i] + "_" + axname[ax + 1] + axname[ax + 2]).c_str(),
3822                                  std::format("{} component of int. distortion in {}{} plane at {}={:.3f};{};{}", axname[i], axname[ax + 1], axname[ax + 2], axname[ax], axval[ax], axname[ax + 1], axname[ax + 2]).c_str(),
3823                                  axn[ax + 1], axbot[ax + 1], axtop[ax + 1],
3824                                  axn[ax + 2], axbot[ax + 2], axtop[ax + 2]);
3825     }
3826     hRDist[0][i] = new TH1F(std::string("hRDist" + axname[i]).c_str(),
3827                             std::format("{} component of int. distortion vs r with {}={:.3f} and {}={:.3f};r(cm);#delta (cm)", axname[i], axname[1], axval[1], axname[2], axval[2]).c_str(),
3828                             axn[0], axbot[0], axtop[0]);
3829     hRDist[1][i] = new TH1F(std::string("hRDist" + axname[i] + "Neg").c_str(),
3830                             std::format("{} component of int. distortion vs r with {}={:.3f} and {}={:.3f};r(cm);#delta (cm)", axname[i], axname[1], axval[1], axname[2], axval[2]).c_str(),
3831                             axn[0], axbot[0], axtop[0]);
3832     hRDiffDist[0][i] = new TH1F(std::string("hRDist" + axname[i]).c_str(),
3833                                 std::format("{} component of int. distortion vs r with {}={:.3f} and {}={:.3f};r(cm);#delta (cm)", axname[i], axname[1], axval[1], axname[2], axval[2]).c_str(),
3834                                 axn[0], axbot[0], axtop[0]);
3835     hRDiffDist[1][i] = new TH1F(std::string("hRDist" + axname[i] + "Neg").c_str(),
3836                                 std::format("{} component of int. distortion vs r with {}={:.3f} and {}={:.3f};r(cm);#delta (cm)", axname[i], axname[1], axval[1], axname[2], axval[2]).c_str(),
3837                                 axn[0], axbot[0], axtop[0]);
3838   }
3839 
3840   TVector3 inpart;
3841   TVector3 outpart;
3842   TVector3 distort;
3843   int validToStep;
3844 
3845   // TTree version:
3846   float partR;
3847   float partP;
3848   float partZ;
3849   int ir;
3850   int ip;
3851   int iz;
3852   float distortR;
3853   float distortP;
3854   float distortZ;
3855   float distortX;
3856   float distortY;
3857   float diffdistR;
3858   float diffdistP;
3859   float diffdistZ;
3860   TTree *dTree = new TTree("dTree", "Distortion per step z");
3861   dTree->Branch("r", &partR);
3862   dTree->Branch("p", &partP);
3863   dTree->Branch("z", &partZ);
3864   dTree->Branch("ir", &ir);
3865   dTree->Branch("ip", &ip);
3866   dTree->Branch("iz", &iz);
3867   dTree->Branch("dr", &distortR);
3868   dTree->Branch("drp", &distortP);
3869   dTree->Branch("dz", &distortZ);
3870 
3871   std::cout << "generating distortion map with (" << nrh << "x" << nph << "x" << nzh << " grid" << std::endl;
3872   unsigned long long totalelements = nrh;
3873   totalelements *= nph;
3874   totalelements *= nzh;  // breaking up this multiplication prevents a 32bit math overflow
3875   unsigned long long percent = totalelements / 100 * debug_npercent;
3876   std::cout << std::format("total elements = {}", totalelements) << std::endl;
3877 
3878   int el = 0;
3879 
3880   // we want to loop over the entire region to be mapped, but we also need to include
3881   // one additional bin at each edge, to allow the mc drift code to interpolate properly.
3882   // hence we count from -1 to n+1, and manually adjust the position in those edge cases
3883   // to avoid sampling nonphysical regions in r and z.  the phi case is free to wrap as
3884   //  normal.
3885 
3886   // note that we apply the adjustment to the particle position (inpart) and not the plotted position (partR etc)
3887   inpart.SetXYZ(1, 0, 0);
3888   for (ir = 0; ir < nrh; ir++)
3889   {
3890     partR = (ir + 0.5) * deltar + rih;
3891     if (ir == 0)
3892     {
3893       inpart.SetPerp(partR + deltar);
3894     }
3895     else if (ir == nrh - 1)
3896     {
3897       inpart.SetPerp(partR - deltar);
3898     }
3899     else
3900     {
3901       inpart.SetPerp(partR);
3902     }
3903     for (ip = 0; ip < nph; ip++)
3904     {
3905       partP = (ip + 0.5) * deltap + pih;
3906       inpart.SetPhi(partP);
3907       // since phi loops, there's no need to adjust phis that are out of bounds.
3908       for (iz = 0; iz < nzh; iz++)
3909       {
3910         partZ = (iz) *deltaz + zih;  // start us at the EDGE of the bin, maybe has problems at the CM when twinned.
3911         if (iz == 0)
3912         {
3913           inpart.SetZ(partZ + deltaz);
3914         }
3915         else if (iz == nzh - 1)
3916         {
3917           inpart.SetZ(partZ - deltaz);
3918         }
3919         else
3920         {
3921           inpart.SetZ(partZ);
3922         }
3923         partZ += 0.5 * deltaz;  // move to center of histogram bin.
3924 
3925         // print_need_cout("iz=%d, zcoord=%2.2f, bin=%d\n",iz,partZ,  hIntDist[0][0]->GetYaxis()->FindBin(partZ));
3926 
3927         // differential distortion:
3928         // be careful with the math of a distortion.  The R distortion is NOT the perp() component of outpart-inpart -- that's the transverse magnitude of the distortion!
3929         if (hasTwin && inpart.Z() < 0)
3930         {
3931           distort = twin->GetTotalDistortion(inpart.Z(), inpart + stepzvec, nSteps, true, &validToStep, &successCheck);  // step across the cell in the opposite direction, starting at the high side and going to the low side..
3932         }
3933         else
3934         {
3935           distort = GetTotalDistortion(inpart.Z() + deltaz, inpart, nSteps, true, &validToStep, &successCheck);
3936         }
3937         distort.RotateZ(-inpart.Phi());  // rotate so that that is on the x axis
3938         diffdistP = distort.Y();         // the phi component is now the y component.
3939         diffdistR = distort.X();         // and the r component is the x component
3940         diffdistZ = distort.Z();
3941         hDistortionR->Fill(partP, partR, partZ, diffdistR);
3942         hDistortionP->Fill(partP, partR, partZ, diffdistP);
3943         hDistortionZ->Fill(partP, partR, partZ, diffdistZ);
3944         dTree->Fill();
3945 
3946         // integral distortion:
3947         if (hasTwin && makeUnifiedMap && inpart.Z() < 0)
3948         {
3949           distort = twin->GetTotalDistortion(-z_readout, inpart + stepzvec, nSteps, true, &validToStep, &successCheck);
3950         }
3951         else
3952         {
3953           distort = GetTotalDistortion(z_readout, inpart, nSteps, true, &validToStep, &successCheck);
3954         }
3955         distortX = distort.X();
3956         distortY = distort.Y();
3957         distort.RotateZ(-inpart.Phi());  // rotate so that that is on the x axis
3958         distortP = distort.Y();          // the phi component is now the y component.
3959         distortR = distort.X();          // and the r component is the x component
3960         distortZ = distort.Z();
3961 
3962         // recursive integral distortion:
3963         // get others working first!
3964 
3965         // print_need_cout("part=(rpz)(%f,%f,%f),distortP=%f\n",partP,partR,partZ,distortP);
3966         hIntDistortionR->Fill(partP, partR, partZ, distortR);
3967         hIntDistortionP->Fill(partP, partR, partZ, distortP);
3968         hIntDistortionZ->Fill(partP, partR, partZ, distortZ);
3969 
3970         if (andCartesian)
3971         {
3972           hIntDistortionX->Fill(partP, partR, partZ, distortX);
3973           hIntDistortionY->Fill(partP, partR, partZ, distortY);
3974         }
3975 
3976         // now we fill particular slices for integral visualizations:
3977         if (ir == xi[0])
3978         {  // r slice
3979           // print_need_cout("ir=%d, r=%f (pz)=(%d,%d), distortR=%2.2f, distortP=%2.2f\n",ir,partR,ip,iz,distortR,distortP);
3980           hIntDist[0][0]->Fill(partP, partZ, distortR);
3981           hIntDist[0][1]->Fill(partP, partZ, distortP);
3982           hIntDist[0][2]->Fill(partP, partZ, distortZ);
3983           hDiffDist[0][0]->Fill(partP, partZ, diffdistR);
3984           hDiffDist[0][1]->Fill(partP, partZ, diffdistP);
3985           hDiffDist[0][2]->Fill(partP, partZ, diffdistZ);
3986         }
3987         if (ip == xi[1])
3988         {  // phi slice
3989           // print_need_cout("ip=%d, p=%f (rz)=(%d,%d), distortR=%2.2f, distortP=%2.2f\n",ip,partP,ir,iz,distortR,distortP);
3990           hIntDist[1][0]->Fill(partZ, partR, distortR);
3991           hIntDist[1][1]->Fill(partZ, partR, distortP);
3992           hIntDist[1][2]->Fill(partZ, partR, distortZ);
3993           hDiffDist[1][0]->Fill(partZ, partR, diffdistR);
3994           hDiffDist[1][1]->Fill(partZ, partR, diffdistP);
3995           hDiffDist[1][2]->Fill(partZ, partR, diffdistZ);
3996 
3997           if (iz == xi[2])
3998           {  // z slices of phi slices= r line at mid phi, mid z:
3999             hRDist[0][0]->Fill(partR, distortR);
4000             hRDist[0][1]->Fill(partR, distortP);
4001             hRDist[0][2]->Fill(partR, distortZ);
4002             hRDiffDist[0][0]->Fill(partR, diffdistR);
4003             hRDiffDist[0][1]->Fill(partR, diffdistP);
4004             hRDiffDist[0][2]->Fill(partR, diffdistZ);
4005           }
4006           if (hasTwin && iz == twinz)
4007           {  // z slices of phi slices= r line at mid phi, mid z:
4008             hRDist[1][0]->Fill(partR, distortR);
4009             hRDist[1][1]->Fill(partR, distortP);
4010             hRDist[1][2]->Fill(partR, distortZ);
4011             hRDiffDist[1][0]->Fill(partR, diffdistR);
4012             hRDiffDist[1][1]->Fill(partR, diffdistP);
4013             hRDiffDist[1][2]->Fill(partR, diffdistZ);
4014           }
4015         }
4016         if (iz == xi[2])
4017         {  // z slice
4018           // print_need_cout("iz=%d, z=%f (rp)=(%d,%d), distortR=%2.2f, distortP=%2.2f\n",iz,partZ,ir,ip,distortR,distortP);
4019 
4020           hIntDist[2][0]->Fill(partR, partP, distortR);
4021           hIntDist[2][1]->Fill(partR, partP, distortP);
4022           hIntDist[2][2]->Fill(partR, partP, distortZ);
4023           hDiffDist[2][0]->Fill(partR, partP, diffdistR);
4024           hDiffDist[2][1]->Fill(partR, partP, diffdistP);
4025           hDiffDist[2][2]->Fill(partR, partP, diffdistZ);
4026         }
4027 
4028         if (!(el % percent))
4029         {
4030           std::cout << std::format("generating distortions {}%:  ", static_cast<int>(debug_npercent * (el / percent)));
4031           std::cout << std::format("distortion at (ir={}, ip={}, iz={}) is ({:E},{:E},{:E})", ir, ip, iz, distortR, distortP, distortZ) << std::endl;
4032         }
4033         el++;
4034       }
4035     }
4036   }
4037 
4038   TCanvas *canvas = new TCanvas("cdistort", "distortion integrals", 1200, 800);
4039   // take 10 of the bottom of this for data?
4040   canvas->cd();
4041   TPad *c = new TPad("cplots", "distortion integral plots", 0, 0.2, 1, 1);
4042   canvas->cd();
4043   TPad *textpad = new TPad("ctext", "distortion integral plots", 0, 0.0, 1, 0.2);
4044   c->Divide(4, 3);
4045   gStyle->SetOptStat();
4046   for (int i = 0; i < 3; i++)
4047   {
4048     // component
4049     for (int ax = 0; ax < 3; ax++)
4050     {
4051       // plane
4052       c->cd(i * 4 + ax + 1);
4053       gPad->SetRightMargin(0.15);
4054       hIntDist[ax][i]->SetStats(false);
4055       hIntDist[ax][i]->Draw("colz");
4056     }
4057     c->cd(i * 4 + 4);
4058     hRDist[0][i]->SetStats(false);
4059     hRDist[0][i]->SetFillColor(kRed);
4060     hRDist[0][i]->Draw("hist");
4061     if (hasTwin)
4062     {
4063       hRDist[1][i]->SetStats(false);
4064       hRDist[1][i]->SetLineColor(kBlue);
4065       hRDist[1][i]->Draw("hist,same");
4066     }
4067     /*
4068       double Cut = 40;
4069       h->SetFillColor(kRed);
4070       TH1F *hNeg = (TH1F*)hRDist[i]->Clone(Form("hNegRDist%d",i));
4071       hNeg->SetFillColor(kGreen);
4072       for (int n = 1; n <= hNeg->GetNbinsX(); n++) {
4073       hNeg->SetBinContent(n,Cut);
4074       }
4075       h3->Draw(); h.Draw("same");
4076       TH1F *h2 = (TH1F*)h->Clone("h2");
4077       h2->SetFillColor(kGray-4);
4078       h2->SetMaximum(Cut);
4079       h2->Draw("same");
4080     */
4081   }
4082   textpad->cd();
4083   float texpos = 0.9;
4084   float texshift = 0.12;
4085   TLatex *tex = new TLatex(0.0, texpos, "Fill Me In");
4086   tex->SetTextSize(texshift * 0.8);
4087   tex->DrawLatex(0.05, texpos, GetFieldString().c_str());
4088   texpos -= texshift;
4089   tex->DrawLatex(0.05, texpos, GetChargeString().c_str());
4090   texpos -= texshift;
4091   // tex->DrawLatex(0.05,texpos,Form("Drift Field = %2.2f V/cm",GetNominalE()));texpos-=texshift;
4092   tex->DrawLatex(0.05, texpos, std::format("Drifting grid of (rp)=({} x {}) electrons with {} steps", nrh, nph, nSteps).c_str());
4093   texpos -= texshift;
4094   tex->DrawLatex(0.05, texpos, GetLookupString().c_str());
4095   texpos -= texshift;
4096   tex->DrawLatex(0.05, texpos, GetGasString().c_str());
4097   texpos -= texshift;
4098   if (debug_distortionScale.Mag() > 0)
4099   {
4100     tex->DrawLatex(0.05, texpos, std::format("Distortion scaled by (r,p,z)=({:.2f},{:.2f},{:.2f})", debug_distortionScale.X(), debug_distortionScale.Y(), debug_distortionScale.Z()).c_str());
4101     texpos -= texshift;
4102   }
4103   texpos = 0.9;
4104 
4105   canvas->cd();
4106   c->Draw();
4107   canvas->cd();
4108   textpad->Draw();
4109   canvas->SaveAs(summaryFilename.c_str());
4110 
4111   // canvas->cd();
4112   // already done TPad *c=new TPad("cplots","distortion differential plots",0,0.2,1,1);
4113   // canvas->cd();
4114   // already done TPad *textpad=new TPad("ctext","distortion differential plots",0,0.0,1,0.2);
4115   // already done c->Divide(4,3);
4116   // gStyle->SetOptStat();
4117   for (int i = 0; i < 3; i++)
4118   {
4119     // component
4120     for (int ax = 0; ax < 3; ax++)
4121     {
4122       // plane
4123       c->cd(i * 4 + ax + 1);
4124       gPad->SetRightMargin(0.15);
4125       hDiffDist[ax][i]->SetStats(false);
4126       hDiffDist[ax][i]->Draw("colz");
4127     }
4128     c->cd(i * 4 + 4);
4129     hRDiffDist[0][i]->SetStats(false);
4130     hRDiffDist[0][i]->SetFillColor(kRed);
4131     hRDiffDist[0][i]->Draw("hist");
4132     if (hasTwin)
4133     {
4134       hRDiffDist[1][i]->SetStats(false);
4135       hRDiffDist[1][i]->SetLineColor(kBlue);
4136       hRDiffDist[1][i]->Draw("hist same");
4137     }
4138   }
4139   textpad->cd();
4140   texpos = 0.9;
4141   texshift = 0.12;
4142   tex->SetTextSize(texshift * 0.8);
4143   tex->DrawLatex(0.05, texpos, GetFieldString().c_str());
4144   texpos -= texshift;
4145   tex->DrawLatex(0.05, texpos, GetChargeString().c_str());
4146   texpos -= texshift;
4147   // tex->DrawLatex(0.05,texpos,Form("Drift Field = %2.2f V/cm",GetNominalE()));texpos-=texshift;
4148   tex->DrawLatex(0.05, texpos, std::format("Drifting grid of (rp)=({} x {}) electrons with {} steps", nrh, nph, nSteps).c_str());
4149   texpos -= texshift;
4150   tex->DrawLatex(0.05, texpos, GetLookupString().c_str());
4151   texpos -= texshift;
4152   tex->DrawLatex(0.05, texpos, GetGasString().c_str());
4153   texpos -= texshift;
4154   tex->DrawLatex(0.05, texpos, "Differential Plots");
4155   texpos -= texshift;
4156   if (debug_distortionScale.Mag() > 0)
4157   {
4158     tex->DrawLatex(0.05, texpos, std::format("Distortion scaled by (r,p,z)=({:.2f},{:.2f},{:.2f})", debug_distortionScale.X(), debug_distortionScale.Y(), debug_distortionScale.Z()).c_str());
4159     texpos -= texshift;
4160   }
4161   texpos = 0.9;
4162 
4163   canvas->cd();
4164   c->Draw();
4165   canvas->cd();
4166   textpad->Draw();
4167   canvas->SaveAs(diffSummaryFilename.c_str());
4168 
4169   //  print_need_cout("map:%s.\n",distortionFilename.c_str());
4170 
4171   outf->cd();
4172 
4173   hDistortionR->Write();
4174   hDistortionP->Write();
4175   hDistortionZ->Write();
4176   hIntDistortionR->Write();
4177   hIntDistortionP->Write();
4178   hIntDistortionZ->Write();
4179   if (andCartesian)
4180   {
4181     hIntDistortionX->Write();
4182     hIntDistortionY->Write();
4183   }
4184   dTree->Write();
4185   outf->Close();
4186   // print_need_cout("map:%s.closed\n",distortionFilename.c_str());
4187 
4188   /*
4189   //all histograms associated with this file should be deleted when we closed the file.
4190   //delete the histograms we made:
4191   TH3F *m;
4192   m = (TH3F*)gROOT­>FindObject("hDistortionR");  if(m) { print_need_cout("found in memory still.\n"); m­>Delete();}
4193   m = (TH3F*)gROOT­>FindObject("hDistortionP");  if(m) m­>Delete();
4194   m = (TH3F*)gROOT­>FindObject("hDistortionZ");  if(m) m­>Delete();
4195   m = (TH3F*)gROOT­>FindObject("hIntDistortionR");  if(m) m­>Delete();
4196   m = (TH3F*)gROOT­>FindObject("hIntDistortionP");  if(m) m­>Delete();
4197   m = (TH3F*)gROOT­>FindObject("hIntDistortionZ");  if(m) m­>Delete();
4198 
4199   print_need_cout("map:%s.deleted via convoluted call.  sigh.\n",distortionFilename.c_str());
4200   for (int i=0;i<3;i++){
4201     //loop over which axis of the distortion to read
4202     for (int ax=0;ax<3;ax++){
4203       //loop over which plane to work in
4204       hIntDist[ax][i]->Delete();
4205     }
4206     hRDist[i]->Delete();
4207   }
4208   */
4209   std::cout << "wrote map and summary to " << filebase << std::endl;
4210   std::cout << "map: " << distortionFilename << std::endl;
4211   std::cout << "summary: " << summaryFilename << std::endl;
4212   // print_need_cout("wrote map and summary to %s and %s.\n",distortionFilename.c_str(),summaryFilename.c_str());
4213   return;
4214 }
4215 
4216 TVector3 AnnularFieldSim::swimTo(float zdest, const TVector3 &start, bool interpolate, bool useAnalytic)
4217 {
4218   int defaultsteps = 100;
4219   int goodtostep = 0;
4220   if (useAnalytic)
4221   {
4222     return swimToInAnalyticSteps(zdest, start, defaultsteps, &goodtostep);
4223   }
4224   return swimToInSteps(zdest, start, defaultsteps, interpolate, &goodtostep);
4225 }
4226 
4227 TVector3 AnnularFieldSim::GetStepDistortion(float zdest, const TVector3 &start, bool interpolate, bool useAnalytic)
4228 {
4229   // getting the distortion instead of the post-step position allows us to accumulate small deviations from the original position that might be lost in the large number
4230 
4231   // using second order langevin expansion from http://skipper.physics.sunysb.edu/~prakhar/tpc/Papers/ALICE-INT-2010-016.pdf
4232   // TVector3 (*field)[nr][ny][nz]=field_;
4233   int rt;
4234   int pt;
4235   int zt;  // these are filled by the checkbounds that follow, but are not used.
4236   BoundsCase zBound = GetZindexAndCheckBounds(start.Z(), &zt);
4237   if (GetRindexAndCheckBounds(start.Perp(), &rt) != InBounds || GetPhiIndexAndCheckBounds(FilterPhiPos(start.Phi()), &pt) != InBounds || (zBound != InBounds && zBound != OnHighEdge))
4238   {
4239     // print_need_cout("AnnularFieldSim::swimTo asked to swim particle from (xyz)=(%f,%f,%f) which is outside the ROI:\n", start.X(), start.Y(), start.Z());
4240     // print_need_cout(" -- %f <= r < %f \t%f <= phi < %f \t%f <= z < %f \n", rmin_roi * step.Perp(), rmax_roi * step.Perp(), phimin_roi * step.Phi(), phimax_roi * step.Phi(), zmin_roi * step.Z(), zmax_roi * step.Z());
4241     // print_need_cout("Returning original position.\n");
4242     return start;
4243   }
4244 
4245   // set the direction of the external fields.
4246 
4247   double zdist = zdest - start.Z();
4248 
4249   // short-circuit if there's no travel length:
4250 
4251   if (std::abs(zdist) < ALMOST_ZERO * step.Z())
4252   {
4253     //   std::cout <<  std::format("Asked  particle from ({},{},{}) to z={}, which is a distance of {}cm.  Returning zero.", start.X(), start.Y(), start.Z(), zdest, zdist) << std::endl;
4254     return zero_vector;
4255   }
4256 
4257   TVector3 fieldInt;   // integral of E field along path
4258   TVector3 fieldIntB;  // integral of B field along path
4259 
4260   // note that using analytic takes priority over interpolating todo: clean this up to use a status rther than a pair of flags
4261   if (useAnalytic)
4262   {
4263     fieldInt = analyticFieldIntegral(zdest, start, Efield);
4264     fieldIntB = analyticFieldIntegral(zdest, start, Bfield);
4265   }
4266   else if (interpolate)
4267   {
4268     fieldInt = interpolatedFieldIntegral(zdest, start, Efield);
4269     fieldIntB = interpolatedFieldIntegral(zdest, start, Bfield);
4270   }
4271   else
4272   {
4273     fieldInt = fieldIntegral(zdest, start, Efield);
4274     fieldIntB = fieldIntegral(zdest, start, Bfield);
4275   }
4276 
4277   if (std::abs(fieldInt.Z() / zdist) < ALMOST_ZERO)
4278   {
4279     std::cout << "GetStepDistortion is attempting to swim with no drift field:" << std::endl;
4280     std::cout << std::format("GetStepDistortion: ({:.4f},{:.4f},{:.4f}) to z={:.4f}", start.X(), start.Y(), start.Z(), zdest) << std::endl;
4281     std::cout << std::format("GetStepDistortion: fieldInt=({:E},{:E},{:E})", fieldInt.X(), fieldInt.Y(), fieldInt.Z()) << std::endl;
4282     exit(1);
4283   }
4284   // float fieldz=field_[in3(x,y,0,fx,fy,fz)].Z()+E.Z();// *field[x][y][zi].Z();
4285   double EfieldZ = fieldInt.Z() / zdist;  // average field over the path.
4286   double BfieldZ = fieldIntB.Z() / zdist;
4287   // double fieldz=Enominal; // ideal field over path.
4288 
4289   // these values should be with real, not nominal field?
4290   // double mu=std::abs(vdrift/Enominal);//vdrift in [cm/s], field in [V/cm] hence mu in [cm^2/(V*s)];  should be a positive value.  drift velocity over field magnitude, not field direction.
4291   // double omegatau=-mu*Bnominal;//minus sign is for electron charge.
4292   double omegatau = omegatau_nominal;  // don't compute this every time!
4293   // or:  omegatau=-10*(10*B.Z()/Tesla)*(vdrift/(cm/us))/(fieldz/(V/cm)); //which is the same as my calculation up to a sign.
4294   // std::cout << "omegatau=" << omegatau << std::endl;
4295 
4296   double T1om = langevin_T1 * omegatau;
4297   double T2om2 = langevin_T2 * omegatau * langevin_T2 * omegatau;
4298   double c0 = 1 / (1 + T2om2);           //
4299   double c1 = T1om / (1 + T1om * T1om);  // Carlos gets this term wrong.  It should be linear on top, quadratic on bottom.
4300   double c2 = T2om2 / (1 + T2om2);
4301 
4302   TVector3 EintOverEz = 1 / EfieldZ * fieldInt;   // integral of E/E_z= integral of E / integral of E_z * delta_z
4303   TVector3 BintOverBz = 1 / BfieldZ * fieldIntB;  // should this (and the above?) be BfieldZ or Bnominal?
4304 
4305   // really this should be the integral of the ratio, not the ratio of the integrals.
4306   // and should be integrals over the B field, btu for now that's fixed and constant across the region, so not necessary
4307   // there's no reason to do this as r phi.  This is an equivalent result, since I handle everything in vectors.
4308   double deltaX = c0 * EintOverEz.X() + c1 * EintOverEz.Y() - c1 * BintOverBz.Y() + c2 * BintOverBz.X();
4309   double deltaY = c0 * EintOverEz.Y() - c1 * EintOverEz.X() + c1 * BintOverBz.X() + c2 * BintOverBz.Y();
4310   // strictly, for deltaZ we want to integrate v'(E)*(E-E0)dz and v''(E)*(E-E0)^2 dz, but over a short step the field is constant, and hence this can be a product of the integral and not an integral of the product:
4311 
4312   double vprime = (5000 * cm / s) / (100 * V / cm);  // hard-coded value for 50:50.  Eventually this needs to be part of the constructor, as do most of the repeated math terms here.
4313   double vdoubleprime = 0;                           // neglecting the v'' term for now.  Fair? It's pretty linear at our operating point, and it would require adding an additional term to the field integral.
4314 
4315   // note: as long as my step is very small, I am essentially just reading the field at a point and multiplying by the step size.
4316   // hence integral of P dz, where P is a function of the fields, int|P(E(x,y,z))dz=P(int|E(x,y,z)dz/deltaZ)*deltaZ
4317   // hence: , eg, int|E^2dz=(int|Edz)^2/deltaz
4318   double deltaZ = vprime / vdrift * (fieldInt.Z() - zdist * Enominal) + vdoubleprime / vdrift * (fieldInt.Z() - Enominal * zdist) * (fieldInt.Z() - Enominal * zdist) / (2 * zdist) - 0.5 / zdist * (EintOverEz.X() * EintOverEz.X() + EintOverEz.Y() * EintOverEz.Y()) + c1 / zdist * (EintOverEz.X() * BintOverBz.Y() - EintOverEz.Y() * BintOverBz.X()) + c2 / zdist * (EintOverEz.X() * BintOverBz.X() + EintOverEz.Y() * BintOverBz.Y()) + c2 / zdist * (BintOverBz.X() * BintOverBz.X() + BintOverBz.Y() * BintOverBz.Y());  // missing v'' term.
4319 
4320   if (false)
4321   {
4322     std::cout << std::format("GetStepDistortion:  (c0,c1,c2)=({:E},{:E},{:E})", c0, c1, c2) << std::endl;
4323     std::cout << std::format("GetStepDistortion:  EintOverEz==({:E},{:E},{:E})", EintOverEz.X(), EintOverEz.Y(), EintOverEz.Z()) << std::endl;
4324     std::cout << std::format("GetStepDistortion:  BintOverBz==({:E},{:E},{:E})", BintOverBz.X(), BintOverBz.Y(), BintOverBz.Z()) << std::endl;
4325     std::cout << std::format("GetStepDistortion: ({:.4f},{:.4f},{:.4f}) to z={:.4f}", start.X(), start.Y(), start.Z(), zdest) << std::endl;
4326     std::cout << std::format("GetStepDistortion: fieldInt=({:E},{:E},{:E})", fieldInt.X(), fieldInt.Y(), fieldInt.Z()) << std::endl;
4327     std::cout << std::format("GetStepDistortion: delta=({:E},{:E},{:E})", deltaX, deltaY, deltaZ) << std::endl;
4328   }
4329 
4330   // if (std::abs(deltaZ / zdist) > 0.25)
4331   if (false)
4332   {
4333     std::cout << "GetStepDistortion produced a very large zdistortion!" << std::endl;
4334     std::cout << std::format("GetStepDistortion: zdist={:.4f}, deltaZ={:.4f}, Delta/z={:.4f}", zdist, deltaZ, deltaZ / zdist) << std::endl;
4335     std::cout << std::format("GetStepDistortion: average field Z:  Ez={:.4f}V/cm, Bz={:.4f}T", EfieldZ / (V / cm), BfieldZ / Tesla) << std::endl;
4336     std::cout << std::format("GetStepDistortion:  (c0,c1,c2)=({:E},{:E},{:E})", c0, c1, c2) << std::endl;
4337     std::cout << std::format("GetStepDistortion:  EintOverEz==({:E},{:E},{:E})", EintOverEz.X(), EintOverEz.Y(), EintOverEz.Z()) << std::endl;
4338     std::cout << std::format("GetStepDistortion:  BintOverBz==({:E},{:E},{:E})", BintOverBz.X(), BintOverBz.Y(), BintOverBz.Z()) << std::endl;
4339     std::cout << std::format("GetStepDistortion: ({:.4f},{:.4f},{:.4f}) to z={:.4f}", start.X(), start.Y(), start.Z(), zdest) << std::endl;
4340     std::cout << std::format("GetStepDistortion: EfieldInt=({:E},{:E},{:E})", fieldInt.X(), fieldInt.Y(), fieldInt.Z()) << std::endl;
4341     std::cout << std::format("GetStepDistortion: BfieldInt=({:E},{:E},{:E})", fieldIntB.X(), fieldIntB.Y(), fieldIntB.Z()) << std::endl;
4342     std::cout << std::format("GetStepDistortion: delta=({:E},{:E},{:E})", deltaX, deltaY, deltaZ) << std::endl;
4343 
4344     // assert(false);
4345   }
4346 
4347   if (std::abs(deltaX) < 1E-20 && !(chargeCase == NoSpacecharge))
4348   {
4349     std::cout << std::format("GetStepDistortion produced a very small deltaX: {:E}", deltaX) << std::endl;
4350     std::cout << std::format("GetStepDistortion:  (c0,c1,c2)=({:E},{:E},{:E})", c0, c1, c2) << std::endl;
4351     std::cout << std::format("GetStepDistortion:  EintOverEz==({:E},{:E},{:E})", EintOverEz.X(), EintOverEz.Y(), EintOverEz.Z()) << std::endl;
4352     std::cout << std::format("GetStepDistortion:  BintOverBz==({:E},{:E},{:E})", BintOverBz.X(), BintOverBz.Y(), BintOverBz.Z()) << std::endl;
4353     std::cout << std::format("GetStepDistortion: ({:.4f},{:.4f},{:.4f}) to z={:.4f}", start.X(), start.Y(), start.Z(), zdest) << std::endl;
4354     std::cout << std::format("GetStepDistortion: fieldInt=({:E},{:E},{:E})", fieldInt.X(), fieldInt.Y(), fieldInt.Z()) << std::endl;
4355     std::cout << std::format("GetStepDistortion: delta=({:E},{:E},{:E})", deltaX, deltaY, deltaZ) << std::endl;
4356 
4357     // assert(1==2);
4358   }
4359 
4360   // if (!(std::abs(deltaX) < 1E3))
4361   if (false)
4362   {
4363     std::cout << std::format("GetStepDistortion:  (c0,c1,c2)=({:E},{:E},{:E})", c0, c1, c2) << std::endl;
4364     std::cout << std::format("GetStepDistortion:  EintOverEz==({:E},{:E},{:E})", EintOverEz.X(), EintOverEz.Y(), EintOverEz.Z()) << std::endl;
4365     std::cout << std::format("GetStepDistortion:  BintOverBz==({:E},{:E},{:E})", BintOverBz.X(), BintOverBz.Y(), BintOverBz.Z()) << std::endl;
4366     std::cout << std::format("GetStepDistortion: ({:.4f},{:.4f},{:.4f}) (rp)=({:.4f},{:.4f}) to z={:.4f}",
4367                              start.X(), start.Y(), start.Z(), start.Perp(), start.Phi(), zdest)
4368               << std::endl;
4369     std::cout << std::format("GetStepDistortion: fieldInt=({:E},{:E},{:E})", fieldInt.X(), fieldInt.Y(), fieldInt.Z()) << std::endl;
4370     std::cout << std::format("GetStepDistortion: delta=({:E},{:E},{:E})", deltaX, deltaY, deltaZ) << std::endl;
4371     exit(1);
4372   }
4373 
4374   // deltaZ=0;//temporary removal.
4375 
4376   TVector3 shift(deltaX, deltaY, deltaZ);
4377   if (debug_distortionScale.Mag() > 0)
4378   {  // debug code to scale the resulting distortions
4379     shift.RotateZ(-start.Phi());
4380     // TVector3 localScale=debug_distortionScale;
4381     // localScale.RotateZ(start.Phi());
4382     shift.SetXYZ(shift.X() * debug_distortionScale.X(), shift.Y() * debug_distortionScale.Y(), shift.Z() * debug_distortionScale.Z());
4383     shift.RotateZ(start.Phi());
4384   }
4385 
4386   return shift;
4387 }
4388 
4389 // putting all the getters here out of the way:
4390 std::string AnnularFieldSim::GetLookupString()
4391 {
4392   if (lookupCase == LookupCase::Full3D)
4393   {
4394     return std::format("Full3D ({} x {} x {}) with ({} x {} x {}) roi", nr, nphi, nz, nr_roi, nphi_roi, nz_roi);
4395   }
4396 
4397   if (lookupCase == LookupCase::PhiSlice)
4398   {
4399     return std::format("PhiSlice ({} x {} x {}) with ({} x 1 x {}) roi", nr, nphi, nz, nr_roi, nz_roi);
4400   }
4401 
4402   return "broken";
4403 }
4404 std::string AnnularFieldSim::GetGasString()
4405 {
4406   return std::format("vdrift={:.2f}cm/us, Enom={:.2f}V/cm, Bnom={:.2f}T, omtau={:.4E}", vdrift / (cm / us), Enominal / (V / cm), Bnominal / Tesla, omegatau_nominal);
4407 }
4408 
4409 std::string AnnularFieldSim::GetFieldString()
4410 {
4411   return std::format("{}, {}", Efieldname, Bfieldname);
4412 }
4413 
4414 // NOLINTNEXTLINE(misc-no-recursion)
4415 TVector3 AnnularFieldSim::GetFieldAt(const TVector3 &pos)
4416 {
4417   // assume pos is in native units (see header)
4418 
4419   int r;
4420   int p;
4421   int z;
4422 
4423   if (GetRindexAndCheckBounds(pos.Perp(), &r) == BoundsCase::OutOfBounds)
4424   {
4425     return zero_vector;
4426   }
4427   if (GetPhiIndexAndCheckBounds(FilterPhiPos(pos.Phi()), &p) == BoundsCase::OutOfBounds)
4428   {
4429     return zero_vector;
4430   }
4431   if (GetZindexAndCheckBounds(pos.Z(), &z) == BoundsCase::OutOfBounds)
4432   {
4433     if (hasTwin)
4434     {
4435       return twin->GetFieldAt(pos);
4436     }
4437     return zero_vector;
4438   }
4439   return Efield->Get(r, p, z);
4440 }
4441 
4442 // NOLINTNEXTLINE(misc-no-recursion)
4443 TVector3 AnnularFieldSim::GetBFieldAt(const TVector3 &pos)
4444 {
4445   // assume pos is in native units (see header)
4446 
4447   int r;
4448   int p;
4449   int z;
4450 
4451   if (GetRindexAndCheckBounds(pos.Perp(), &r) == BoundsCase::OutOfBounds)
4452   {
4453     return zero_vector;
4454   }
4455   if (GetPhiIndexAndCheckBounds(FilterPhiPos(pos.Phi()), &p) == BoundsCase::OutOfBounds)
4456   {
4457     return zero_vector;
4458   }
4459   if (GetZindexAndCheckBounds(pos.Z(), &z) == BoundsCase::OutOfBounds)
4460   {
4461     if (hasTwin)
4462     {
4463       return twin->GetBFieldAt(pos);
4464     }
4465     return zero_vector;
4466   }
4467   return Bfield->Get(r, p, z);
4468 }
4469 
4470 // NOLINTNEXTLINE(misc-no-recursion)
4471 float AnnularFieldSim::GetChargeAt(const TVector3 &pos)
4472 {
4473   int z;
4474   BoundsCase zbound = GetZindexAndCheckBounds(pos.Z(), &z);  //==BoundsCase::OutOfBounds) return zero_vector;
4475   if (zbound == OutOfBounds)
4476   {
4477     if (hasTwin)
4478     {
4479       return twin->GetChargeAt(pos);
4480     }
4481     std::cout << std::format("Caution:  tried to read charge at zbin={}!  No twin available to handle this", z) << std::endl;
4482     return -999;
4483   }
4484 
4485   return q->GetChargeAtPosition(pos.Perp(), FilterPhiPos(pos.Phi()), pos.Z());  // because tvectors take position to be -phi to phi, we always have to filter.
4486   // actually, we should probably just yield to that assumption in more places to speed this up.
4487   /*
4488   //assume pos is in native units (see header)
4489   int r, p, z;
4490 
4491   //get the bounds, but we don't want to actually check the cases, because the charge can go outside the vector region.
4492   r = GetRindex(pos.Perp());
4493   p = GetPhiIndex(pos.Phi());
4494 
4495   BoundsCase zbound = GetZindexAndCheckBounds(pos.Z(), &z);  //==BoundsCase::OutOfBounds) return zero_vector;
4496   if (zbound == OutOfBounds && hasTwin) return twin->GetChargeAt(pos);
4497   return q->Get(r, p, z);
4498   */
4499 }