ANASEN_analysis/scratch/FitBeamAxis.C
Vignesh Sitaraman ec43a81030 modified: TrackRecon.C implemented and applied beamtilt parameter calcualtion and application
modified:   run_27Al.sh
	new file:   scratch/FitBeamAxis.C code to figure out beamtilt
2026-09-07 09:57:13 -04:00

261 lines
11 KiB
C

// FitBeamAxis.C
// ---------------------------------------------------------------------------
// Single-pass beam-axis line fit. Replaces the iterative
// FitBeamAxis.C + iterate_beam.sh scheme, which diverges.
//
// WHY THE ITERATIVE SCHEME FAILS
// ------------------------------
// FitBeamAxis.C fits beamAxis_all_vertexX_vs_Z and feeds the residual back as a
// correction to BEAM_AXIS_X / BEAM_TILT_X. But that vertex comes from
// beamVertex(), which returns the track's point of closest approach to the
// ASSUMED beam line -- so the reconstructed vertex is pulled onto whatever axis
// the run was configured with, by construction. Its residual therefore measures
// acceptance asymmetry and geometry artifacts far more than it measures "how far
// off is my assumed axis". Feeding that back adds a roughly fixed bias each pass
// with no restoring force, so the parameters drift monotonically and never
// converge. Measured behaviour: step L2 grew 1.48 -> 2.65 over 6 passes at full
// step, and still grew (1.27 -> 1.50 over 10 passes) at relax=0.3 -- damping only
// scales the drift rate, which is the signature of a wrong-direction update
// rather than overshoot.
//
// WHAT THIS DOES INSTEAD
// ----------------------
// TrackRecon.C's fillBeamProfile() also histograms where each track CROSSES a set
// of fixed z-planes (beamAxis_<tag>_crossX_z**, crossY_z**). A track crossing is a
// property of the track alone -- it never references the assumed beam axis. Tracks
// originate on the true beam line and fan out with roughly uniform azimuth, so at
// each plane the crossing distribution centres on the true beam position at that z.
// Take a robust centre per plane, line-fit those centres against z, and the axis
// falls out directly. One pass, no iteration, no feedback.
//
// WHICH TRACKS FEED THE PLANES
// ----------------------------
// fillBeamProfile() only sends a track to the crossing planes when its PC z is a
// real measurement: a1c2 (cathode charge division), a2c0 (two wires, geometry
// fixes z), and a1c1 whose a1c1_cfrac_pcz() solve landed in band. Bare a1c1 and
// a1c0 are excluded -- their z is a Gaussian dither about the raw wire z, which
// puts them on the assumed axis by construction and would make this fit return
// whatever axis the run was configured with. The vertexX_vs_Z and pocaDist
// diagnostics in the same folder are still filled for every topology, so their
// entry counts will not match the crossing planes'. Do not fit those.
//
// CAVEAT -- azimuthal symmetry. The per-plane centre is only unbiased if
// acceptance is uniform in phi. Dead channels break that (this analysis already
// excludes QQQ wedge 48/49, ring 63, and several anode wires). The median is used
// rather than the mean to blunt the resulting asymmetric tails, but a strongly
// lopsided acceptance will still pull the centre. If the fitted axis looks
// implausible, check the crossX/crossY distributions at a few planes for
// visible asymmetry before trusting the number.
//
// USAGE:
// root -l -b -q 'scratch/FitBeamAxisCrossings.C("Output_27Al/Output_27Al.root")'
// root -l -b -q 'scratch/FitBeamAxisCrossings.C("out.root", -440, 40, "all")'
// root -l -b -q 'scratch/FitBeamAxisCrossings.C("out.root", -440, 40, "elastic")'
//
// Arguments:
// filename TrackRecon.C output ROOT file
// zmin/zmax only planes whose z falls in this window are used in the line fit.
// Defaults span every plane; narrow it to exclude planes you have
// reason to distrust, not to chase a better chi2.
// tag which branch's crossings to use. "all" pools everything; the
// per-branch tags are "reaction_<channel>_<det>" (e.g.
// "reaction_m27Alax_qqq", "..._sx3") and "elastic_<DET>" ("elastic_QQQ",
// "elastic_SX3"). Fitting the qqq and sx3 tags separately is the check
// that a tilt is the beam and not one branch's acceptance: same beam,
// different geometry and phi coverage, so they must agree.
// Which tags exist at all depends on run configuration -- on a dataset
// where doPCSX3ClusterAnalysis / doPCQQQClusterAnalysis /
// process_alpha_proton_scattering are false and OUT_DIR is not
// Output_p, the sx3a1c2, qqqa1c2, apCoinc and elastic_* tags are never
// filled and "all" is exactly the reaction_* tags pooled.
//
// NOTE on chi2/ndf: medianAndError reports a STATISTICS-ONLY uncertainty
// (~8 um at a 4e5-entry plane) while observed plane-to-plane scatter in a
// well-behaved region is ~0.4 mm, i.e. ~50x larger. chi2/ndf is therefore
// inflated by a missing systematic term and will not approach 1 even for a
// correct axis. Judge the fit on whether the medians lie on a straight line,
// not on chi2/ndf alone.
//
// Prints the fitted BEAM_AXIS_X/Y and BEAM_TILT_X/Y as ready-to-paste export
// lines. These are ABSOLUTE values, not increments -- set them directly in
// run_27Al.sh; do NOT add them to the current values.
// ---------------------------------------------------------------------------
#include <TFile.h>
#include <TH1.h>
#include <TKey.h>
#include <TDirectory.h>
#include <TGraphErrors.h>
#include <TF1.h>
#include <TCanvas.h>
#include <TMath.h>
#include <TString.h>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <string>
#include <vector>
// Must match fillBeamProfile() in TrackRecon.C. The range follows the physical
// vertex acceptance (z_entrance = -454 mm up to the +100 mm cut), not a
// symmetric window about 0, and stops well below the QQQ plane at z = 105 --
// see the comment in fillBeamProfile() for why planes near a detector are
// unusable.
static const double kZLo = -440.0;
static const double kZHi = 40.0;
static const int kNSlice = 16;
static TH1 *findTH1(TDirectory *dir, const std::string &name)
{
if (TObject *o = dir->Get(name.c_str()))
if (o && o->InheritsFrom(TH1::Class()) && !o->InheritsFrom("TH2"))
return static_cast<TH1 *>(o);
TIter next(dir->GetListOfKeys());
while (TKey *key = static_cast<TKey *>(next()))
{
TObject *obj = key->ReadObj();
if (!obj)
continue;
if (obj->InheritsFrom(TDirectory::Class()))
{
if (TH1 *h = findTH1(static_cast<TDirectory *>(obj), name))
return h;
}
else if (obj->InheritsFrom(TH1::Class()) && !obj->InheritsFrom("TH2") && name == obj->GetName())
return static_cast<TH1 *>(obj);
}
return nullptr;
}
// Robust centre of a crossing distribution: the median. Chosen over the mean
// because the crossing distribution has long tails (tracks originating far from
// this plane) that are not necessarily symmetric once dead channels bite.
// Uncertainty on the median ~ 1.253 * sigma / sqrt(N), with sigma taken from the
// interquartile range (IQR/1.349) so a few wild tails don't inflate it.
static bool medianAndError(TH1 *h, double &med, double &err, double &nEff, int minEntries = 500)
{
if (!h)
return false;
double n = h->Integral();
if (n < minEntries)
return false;
double probs[3] = {0.25, 0.5, 0.75};
double q[3] = {0, 0, 0};
h->GetQuantiles(3, q, probs);
med = q[1];
double sigma = (q[2] - q[0]) / 1.349; // IQR -> Gaussian-equivalent sigma
if (sigma <= 0.0)
return false;
err = 1.253 * sigma / TMath::Sqrt(n);
nEff = n;
return true;
}
static bool fitOneProjection(TFile *f, const std::string &base, const char *coord,
double zmin, double zmax, double z0,
double &axis0, double &tilt, double &axis0_err, double &tilt_err,
double &chi2ndf, int &nUsed)
{
std::vector<double> zs, cs, ces, zes;
std::cout << " plane z (mm) median (mm) +/- entries\n";
for (int k = 0; k < kNSlice; k++)
{
double sliceW = (kZHi - kZLo) / kNSlice;
double zPlane = kZLo + (k + 0.5) * sliceW;
if (zPlane < zmin || zPlane > zmax)
continue;
char kbuf[8];
snprintf(kbuf, sizeof(kbuf), "%02d", k);
TH1 *h = findTH1(f, base + "cross" + coord + "_z" + kbuf);
double med = 0, err = 0, nEff = 0;
if (!medianAndError(h, med, err, nEff))
{
std::cout << " " << zPlane << "\t(skipped -- missing or too few entries)\n";
continue;
}
std::cout << " " << zPlane << "\t\t" << med << "\t" << err << "\t" << (long)nEff << "\n";
zs.push_back(zPlane);
zes.push_back(0.0);
cs.push_back(med);
ces.push_back(err);
}
nUsed = static_cast<int>(zs.size());
if (nUsed < 3)
{
std::cout << " [" << coord << "] only " << nUsed << " usable planes -- need >= 3 for a line fit\n";
return false;
}
TGraphErrors g(nUsed, zs.data(), cs.data(), zes.data(), ces.data());
TF1 fL("fL", Form("[0]+[1]*(x-%.8g)", z0), zmin, zmax);
fL.SetParameters(cs[nUsed / 2], 0.0);
if (g.Fit(&fL, "QRN") != 0)
{
std::cout << " [" << coord << "] line fit failed\n";
return false;
}
axis0 = fL.GetParameter(0);
tilt = fL.GetParameter(1);
axis0_err = fL.GetParError(0);
tilt_err = fL.GetParError(1);
chi2ndf = (fL.GetNDF() > 0) ? fL.GetChisquare() / fL.GetNDF() : -1.0;
return true;
}
void FitBeamAxis(const char *filename, double zmin = -440.0, double zmax = 40.0,
const char *tag = "all")
{
TFile *f = TFile::Open(filename, "READ");
if (!f || f->IsZombie())
{
std::cerr << "Cannot open file: " << filename << std::endl;
return;
}
double z0 = 0.0;
if (const char *s = std::getenv("BEAM_AXIS_Z0"))
z0 = std::atof(s);
std::string base = std::string("beamAxis_") + tag + "_";
std::cout << "\n============ Beam-axis fit (track crossings) ============\n";
std::cout << "file: " << filename << "\n";
std::cout << "tag: " << tag << " (histogram prefix: " << base << ")\n";
std::cout << "range: " << zmin << " <= z <= " << zmax << " mm\n";
std::cout << "z0: " << z0 << " mm (BEAM_AXIS_Z0)\n\n";
double bx = 0, tx = 0, bxe = 0, txe = 0, chi2x = -1;
double by = 0, ty = 0, bye = 0, tye = 0, chi2y = -1;
int nx = 0, ny = 0;
std::cout << "--- X crossings ---\n";
bool okx = fitOneProjection(f, base, "X", zmin, zmax, z0, bx, tx, bxe, txe, chi2x, nx);
std::cout << "\n--- Y crossings ---\n";
bool oky = fitOneProjection(f, base, "Y", zmin, zmax, z0, by, ty, bye, tye, chi2y, ny);
std::cout << "\n--- fitted beam line ---\n";
if (okx)
std::cout << " x(z0) = " << bx << " +/- " << bxe << " mm, dx/dz = " << tx << " +/- " << txe
<< " (" << tx * 1000.0 << " mrad, chi2/ndf = " << chi2x << ", " << nx << " planes)\n";
if (oky)
std::cout << " y(z0) = " << by << " +/- " << bye << " mm, dy/dz = " << ty << " +/- " << tye
<< " (" << ty * 1000.0 << " mrad, chi2/ndf = " << chi2y << ", " << ny << " planes)\n";
if (!okx || !oky)
{
std::cout << "\nOne or both projections failed -- not emitting exports.\n";
std::cout << "=========================================================\n\n";
f->Close();
return;
}
std::cout << "\n--- set these directly (ABSOLUTE values, do not add to current) ---\n";
std::cout << "export BEAM_AXIS_X=" << bx << "\n";
std::cout << "export BEAM_AXIS_Y=" << by << "\n";
std::cout << "export BEAM_AXIS_Z0=" << z0 << "\n";
std::cout << "export BEAM_TILT_X=" << tx << "\n";
std::cout << "export BEAM_TILT_Y=" << ty << "\n";
std::cout << "=========================================================\n\n";
f->Close();
}