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
This commit is contained in:
Vignesh Sitaraman 2026-09-07 09:57:13 -04:00
parent 7f936f6a53
commit ec43a81030
4 changed files with 403 additions and 45 deletions

View File

@ -1,21 +1,11 @@
# ANASEN Analysis — working notes for Claude # ANASEN Analysis — working notes for Claude
## Agent behavior ## Agent behavior
- **Architecture & Remotes:**
- `upstream` points to the canonical public repo (read-only reference).
- `origin` points to your private sandbox repo (read/write scratchpad).
- **NEVER attempt to push to `upstream`.** All upstream synchronization is strictly one-way (`git fetch upstream` -> merge).
- **Upstream Sync Routine:**
- Before applying requested features or edits, sync with `upstream` if requested.
- Never pull into a dirty working tree. Use: `git stash` -> `git fetch upstream` -> `git merge upstream/devel_vignesh` -> `git stash pop`.
- If a merge conflict occurs in analysis macros (`TrackRecon.C`, `MakeVertex.C`, kinematics scripts, or calibration tables), **STOP immediately**. Do not attempt to guess physics logic; flag the exact conflict lines and await instruction.
- **Commits & Pushes to `origin`:**
- Commit only when a coherent, discrete change is finished or when explicitly asked. Write concise, conventional commit messages.
- Push **only** to `origin` (`git push origin <branch>`). Push after completing a requested unit of work so the sandbox stays backed up.
- ALWAYS output a brief, step-by-step plan before modifying files or running commands. - ALWAYS output a brief, step-by-step plan before modifying files or running commands.
- Commit only when explicitly asked. Do not push to remote on your own initiative; pushing is allowed only when the session's instructions name a branch to push to (e.g. a `claude/*` working branch), and never to `devel_vignesh` or any other shared branch.
- **Targeted edits only.** Never regenerate a whole file to make a local change. - **Targeted edits only.** Never regenerate a whole file to make a local change.
- **Never read `MakeVertex.C`** - **Never read `MakeVertex.C`** (4,003 lines, 240 KB). Same rule as `TrackRecon.C` below if you must touch it: grep, then Read with offset/limit.
- **Never read `TrackRecon.C` end to end** (~4,250 lines; ~63k tokens each, and it stays in the prefix for the rest of the session). Grep for the symbol, then Read with offset/limit around the hit. If a question genuinely needs the whole file, say so first. Do not explore the codebase open-endedly. - **Never read `TrackRecon.C` end to end** (4,268 lines, 218 KB; ~60k tokens, and it stays in the prefix for the rest of the session). Grep for the symbol, then Read with offset/limit around the hit. If a question genuinely needs the whole file, say so first. Do not explore the codebase open-endedly.
- **Do not spawn subagents** for work that can be done inline — each starts cold and re-derives context already loaded. Worth it only for wide parallel searches where the conclusion is all that's needed. - **Do not spawn subagents** for work that can be done inline — each starts cold and re-derives context already loaded. Worth it only for wide parallel searches where the conclusion is all that's needed.
- **You cannot verify your own work here.** No test suite, no CI, no data in the repo; validation is visual, via ROOT `TBrowser`, on a machine that has the data. Say explicitly when a change is unverified — never report a physics change as working because it compiled. The dangerous bugs are silent: a flipped sign in a kinematics term, an inverted spline direction, a wrong argument order, an off-by-one in a wire mask. None of them throw. The run completes, the plot renders, and the spectrum is wrong. - **You cannot verify your own work here.** No test suite, no CI, no data in the repo; validation is visual, via ROOT `TBrowser`, on a machine that has the data. Say explicitly when a change is unverified — never report a physics change as working because it compiled. The dangerous bugs are silent: a flipped sign in a kinematics term, an inverted spline direction, a wrong argument order, an off-by-one in a wire mask. None of them throw. The run completes, the plot renders, and the spectrum is wrong.
- **Review once per coherent change, not per edit.** Scale depth to blast radius: scratch macros, shell, and docs are shallow; `TrackRecon.C`, `MakeVertex.C`, `Armory/`, or any calibration fit is deep and should tolerate uncertain findings — silent failure is what's being hunted. Run `/security-review` only for file I/O or shell interpolation in the `run_*.sh` drivers; it is not a physics tool. - **Review once per coherent change, not per edit.** Scale depth to blast radius: scratch macros, shell, and docs are shallow; `TrackRecon.C`, `MakeVertex.C`, `Armory/`, or any calibration fit is deep and should tolerate uncertain findings — silent failure is what's being hunted. Run `/security-review` only for file I/O or shell interpolation in the `run_*.sh` drivers; it is not a physics tool.
@ -34,8 +24,7 @@ All config is `getenv`-driven; the run scripts `export` it. Defaults are in `Tra
Physics / results-affecting: Physics / results-affecting:
`DATASET` `reactiondata` `CO2percent` `pressure_in_torr` `CATHODE_GAIN` `PC_ENERGY_CALIBRATION` `DATASET` `reactiondata` `CO2percent` `pressure_in_torr` `CATHODE_GAIN` `PC_ENERGY_CALIBRATION`
`source_vertex` `CUTLIST` `DITHER_SIGMA` `RNG_SEED` `BEAM_AXIS_X` `BEAM_AXIS_Y` `source_vertex` `CUTLIST` `DITHER_SIGMA` `RNG_SEED` `BEAM_AXIS_X` `BEAM_AXIS_Y`
`timecut_low` `timecut_high` `DISABLE_BAD_ANODE_WIRES` `A1C1_LOWBAND_RFACTOR` `A1C1_CFRAC_SPLIT` `timecut_low` `timecut_high` `DISABLE_BAD_ANODE_WIRES` `A1C1_LOWBAND_RFACTOR`
`A1C1_ANODEE_COFF` `A1C1_ANODEE_REF` `A1C1_MISSING_FMAX`
`A1C1_Z_SCALE_QQQ` `A1C1_Z_SCALE_SX3` `A1C1_Z_OFF_QQQ` `A1C1_Z_OFF_SX3` `A1C1_Z_SCALE_QQQ` `A1C1_Z_SCALE_SX3` `A1C1_Z_OFF_QQQ` `A1C1_Z_OFF_SX3`
Plumbing: `OUT_DIR` `RUN_NUMBER` `FLUSH_BARRIER` `MAX_RSS_MB` `MEMCHECK_STRIDE` Plumbing: `OUT_DIR` `RUN_NUMBER` `FLUSH_BARRIER` `MAX_RSS_MB` `MEMCHECK_STRIDE`
@ -44,9 +33,9 @@ Plumbing: `OUT_DIR` `RUN_NUMBER` `FLUSH_BARRIER` `MAX_RSS_MB` `MEMCHECK_STRIDE`
It multiplies `catima.dedx` when the lookup tables are generated. Changing it invalidates every `.dat` table — re-run `Eloss.py` (the run scripts do this automatically before processing). Tables are keyed `eloss_calculations/<species>_lookup_<E>MeV_<P>torr_<CO2>pc.dat` (e.g. `alpha_lookup_50MeV_250torr_3pc.dat`). It multiplies `catima.dedx` when the lookup tables are generated. Changing it invalidates every `.dat` table — re-run `Eloss.py` (the run scripts do this automatically before processing). Tables are keyed `eloss_calculations/<species>_lookup_<E>MeV_<P>torr_<CO2>pc.dat` (e.g. `alpha_lookup_50MeV_250torr_3pc.dat`).
## Footguns ## Footguns
- **`HistPlotter` keys `oMap` by histogram NAME only.** The folder argument does not namespace. Two `Fill` calls with the same name in different folders merge silently — no warning, no error, wrong plot. Always make names unique unless required. - **`HistPlotter` keys `oMap` by histogram NAME only.** The folder argument does not namespace. Two `Fill` calls with the same name and the *same* folder merge silently — no warning, no error, wrong plot. A same-name/different-folder (or toplevel-vs-folder) clash does print to `stderr` and then merges anyway, but the run scripts send stdout+stderr to `/dev/null`, so in a batch run it is invisible either way. Always make names unique.
- **SiPC coincidence sits at negative dt.** The real band is roughly `-450 < t_si - t_pc < -200`, not near zero. Do not "correct" a timing gate toward zero. - **SiPC coincidence sits at negative dt.** The band is centred well below zero, not near it. The one gate of record is `siPcCoincident()` in `TrackRecon.C` (`kSiPcDtMin = -500`, `kSiPcDtMax = 150` ns); anything else is a plotting range, not a cut. Do not "correct" a timing gate toward zero.
- **RNG**: one shared `anasenRandom` (`TRandom3`, seed 4357, `RNG_SEED` to override) — do not introduce fresh `TRandom3(0)` instances; it breaks reproducibility. - **RNG**: in `TrackRecon.C`, one shared `anasenRandom` (`TRandom3`, seed 4357, `RNG_SEED` to override), aliased by reference wherever a local `rnd`/`rand` appears — do not introduce fresh `TRandom3(0)` instances; it breaks reproducibility. `MakeVertex.C` predates this and still has ~12 independent default-seeded `TRandom3` objects; do not assume its output is reproducible against `TrackRecon.C`.
- **Pass By Reference in Event Loops:** Always iterate via `const auto&` over cluster/hit vectors to prevent expensive struct copies in multi-million event runs. - **Pass By Reference in Event Loops:** Always iterate via `const auto&` over cluster/hit vectors to prevent expensive struct copies in multi-million event runs.
## Invariants to check on any physics change ## Invariants to check on any physics change

View File

@ -42,8 +42,8 @@ Int_t colors[40] = {
bool process_alpha_proton_scattering = false, bool process_alpha_proton_scattering = false,
doMiscHistograms = true, doMiscHistograms = true,
doRawHistos = false, doRawHistos = false,
doPCSX3ClusterAnalysis = false, doPCSX3ClusterAnalysis = true,
doPCQQQClusterAnalysis = false, doPCQQQClusterAnalysis = true,
doOldAnalysis = false, doOldAnalysis = false,
BenchMark = false, BenchMark = false,
onewire_analysis = true, onewire_analysis = true,
@ -75,6 +75,9 @@ double source_vertex = 53.0,
a1c1_z_off_sx3 = 2.52614, a1c1_z_off_sx3 = 2.52614,
beam_axis_x = 0.0, beam_axis_x = 0.0,
beam_axis_y = 0.0, beam_axis_y = 0.0,
beam_axis_z0 = 0.0, // reference z at which (beam_axis_x, beam_axis_y) is specified
beam_tilt_x = 0.0, // dx/dz slope of the beam axis
beam_tilt_y = 0.0, // dy/dz slope of the beam axis
ta_foil_z_mm = 0.0, ta_foil_z_mm = 0.0,
alpha_source_mev = 5.486; alpha_source_mev = 5.486;
@ -122,23 +125,111 @@ inline SiPcPid classifyByAnodeDe(double anodeE_MeV)
return (anodeE_MeV < kProtonAlphaAnodeDeGate_MeV) ? SiPcPid::kProton : SiPcPid::kAlpha; return (anodeE_MeV < kProtonAlphaAnodeDeGate_MeV) ? SiPcPid::kProton : SiPcPid::kAlpha;
} }
inline TVector3 beamVertex(const TVector3 &si, const TVector3 &dir) inline TVector3 beamAxisOrigin()
{ {
double d = dir.X() * dir.X() + dir.Y() * dir.Y(); return TVector3(beam_axis_x, beam_axis_y, beam_axis_z0);
double t = (d > 0.0) ? -((si.X() - beam_axis_x) * dir.X() + (si.Y() - beam_axis_y) * dir.Y()) / d : 0.0;
return si + t * dir;
} }
inline double beamPerp(const TVector3 &p) inline TVector3 beamAxisDirUnit()
{ {
return TMath::Sqrt((p.X() - beam_axis_x) * (p.X() - beam_axis_x) + (p.Y() - beam_axis_y) * (p.Y() - beam_axis_y)); return TVector3(beam_tilt_x, beam_tilt_y, 1.0).Unit();
} }
// A point on the beam axis at height z. Every theta/phi reference point used to be
// spelled beamAxisPoint(z), which silently ignored BEAM_AXIS_X/Y even though
// Begin() prints them as configured parameters and pcEnergyCalibrationAccumulate
// already built its source_pos the correct way.
inline TVector3 beamAxisPoint(double z) inline TVector3 beamAxisPoint(double z)
{ {
return TVector3(beam_axis_x, beam_axis_y, z); double dz = z - beam_axis_z0;
return TVector3(beam_axis_x + beam_tilt_x * dz,
beam_axis_y + beam_tilt_y * dz,
z);
}
inline TVector3 pocaOnTrack(const TVector3 &si, const TVector3 &dir,
const TVector3 &p, const TVector3 &d)
{
TVector3 e = dir;
double ee = e.Dot(e);
double de = d.Dot(e);
double denom = ee - de * de; // = |e|^2 * sin^2(angle between beam and track)
double t = 0.0;
if (denom > 1e-18 * (ee > 0.0 ? ee : 1.0))
{
TVector3 w = si - p;
double dw = d.Dot(w);
double ew = e.Dot(w);
t = (de * dw - ew) / denom;
}
return si + t * e;
}
inline TVector3 beamVertex(const TVector3 &si, const TVector3 &dir)
{
return pocaOnTrack(si, dir, beamAxisOrigin(), beamAxisDirUnit());
}
inline TVector3 beamVertexNominal(const TVector3 &si, const TVector3 &dir)
{
return pocaOnTrack(si, dir, TVector3(0.0, 0.0, 0.0), TVector3(0.0, 0.0, 1.0));
}
inline double beamPerp(const TVector3 &p)
{
TVector3 p0 = beamAxisOrigin();
TVector3 d = beamAxisDirUnit();
TVector3 w = p - p0;
TVector3 perp = w - d * w.Dot(d);
return perp.Mag();
}
inline void fillBeamProfile(HistPlotter *plotter, const TVector3 &vertex,
const TVector3 &si, const TVector3 &dir, const std::string &tag,
bool axisSafe)
{
const std::string folder = "BeamAxis";
const std::string all = "beamAxis_all_";
const std::string one = "beamAxis_" + tag + "_";
double vx = vertex.X(), vy = vertex.Y(), vz = vertex.Z();
double pocaDist = beamPerp(vertex);
plotter->Fill2D(all + "vertexX_vs_Z", 250, -450, 50, 200, -50, 50, vz, vx, folder);
plotter->Fill2D(all + "vertexY_vs_Z", 250, -450, 50, 200, -50, 50, vz, vy, folder);
plotter->Fill1D(all + "pocaDist", 400, 0, 100, pocaDist, folder);
// Per-branch copy so anomalies can be traced to a specific reconstruction path.
plotter->Fill2D(one + "vertexX_vs_Z", 250, -450, 50, 200, -50, 50, vz, vx, folder);
plotter->Fill2D(one + "vertexY_vs_Z", 250, -450, 50, 200, -50, 50, vz, vy, folder);
plotter->Fill1D(one + "pocaDist", 400, 0, 100, pocaDist, folder);
const double zLo = -440.0, zHi = 40.0;
const int nSlice = 16;
const double sliceW = (zHi - zLo) / nSlice; // 40 mm
if (vz >= zLo && vz < zHi)
{
int is = static_cast<int>((vz - zLo) / sliceW);
if (is >= 0 && is < nSlice)
{
char buf[8];
snprintf(buf, sizeof(buf), "%02d", is);
plotter->Fill2D(all + "vertexXY_z" + buf, 200, -50, 50, 200, -50, 50, vx, vy, folder);
}
}
double dz = dir.Z();
double vzBin = beamVertexNominal(si, dir).Z();
if (axisSafe && TMath::Abs(dz) > 1e-6 && vzBin >= zLo && vzBin < zHi)
{
int k = static_cast<int>((vzBin - zLo) / sliceW);
if (k >= 0 && k < nSlice)
{
double zPlane = zLo + (k + 0.5) * sliceW;
double s = (zPlane - si.Z()) / dz;
double cx = si.X() + s * dir.X();
double cy = si.Y() + s * dir.Y();
if (TMath::Abs(cx) < 100.0 && TMath::Abs(cy) < 100.0)
{
char kbuf[8];
snprintf(kbuf, sizeof(kbuf), "%02d", k);
plotter->Fill1D(all + "crossX_z" + kbuf, 800, -100, 100, cx, folder);
plotter->Fill1D(all + "crossY_z" + kbuf, 800, -100, 100, cy, folder);
plotter->Fill1D(one + "crossX_z" + kbuf, 800, -100, 100, cx, folder);
plotter->Fill1D(one + "crossY_z" + kbuf, 800, -100, 100, cy, folder);
}
}
}
} }
struct PCPath struct PCPath
@ -587,7 +678,16 @@ void TrackRecon::Begin(TTree * /*tree*/)
beam_axis_x = std::atof(getenv("BEAM_AXIS_X")); beam_axis_x = std::atof(getenv("BEAM_AXIS_X"));
if (getenv("BEAM_AXIS_Y")) if (getenv("BEAM_AXIS_Y"))
beam_axis_y = std::atof(getenv("BEAM_AXIS_Y")); beam_axis_y = std::atof(getenv("BEAM_AXIS_Y"));
std::cout << "Beam-axis origin (x,y) = (" << beam_axis_x << ", " << beam_axis_y << ") mm" << std::endl; if (getenv("BEAM_AXIS_Z0"))
beam_axis_z0 = std::atof(getenv("BEAM_AXIS_Z0"));
if (getenv("BEAM_TILT_X"))
beam_tilt_x = std::atof(getenv("BEAM_TILT_X"));
if (getenv("BEAM_TILT_Y"))
beam_tilt_y = std::atof(getenv("BEAM_TILT_Y"));
std::cout << "Beam axis line: (x,y) = (" << beam_axis_x << ", " << beam_axis_y
<< ") mm at z0 = " << beam_axis_z0
<< " mm, tilts (dx/dz, dy/dz) = (" << beam_tilt_x << ", " << beam_tilt_y << ")"
<< std::endl;
if (doPCEnergyCalibration) if (doPCEnergyCalibration)
std::cout << "PC energy calibration ON: alpha source = " << alpha_source_mev std::cout << "PC energy calibration ON: alpha source = " << alpha_source_mev
<< " MeV, source position = (" << beam_axis_x << ", " << beam_axis_y << ", " << source_vertex << " MeV, source position = (" << beam_axis_x << ", " << beam_axis_y << ", " << source_vertex
@ -2045,6 +2145,8 @@ void protonAlphaHistograms(HistPlotter *plotter, const std::vector<Event> &QQQ_E
// purely longitudinal direction. // purely longitudinal direction.
TVector3 r_rhoMin_fix = beamVertex(x1, v); TVector3 r_rhoMin_fix = beamVertex(x1, v);
double vertex_z = r_rhoMin_fix.Z(); double vertex_z = r_rhoMin_fix.Z();
fillBeamProfile(plotter, r_rhoMin_fix, x1, v, "apCoinc",
pcevent.multi1 == 1 && pcevent.multi2 == 2);
double theta_q = (qqqevent.pos - beamAxisPoint(vertex_z)).Theta(); double theta_q = (qqqevent.pos - beamAxisPoint(vertex_z)).Theta();
// double theta_q = (qqqevent.pos - r_rhoMin_fix).Theta(); // double theta_q = (qqqevent.pos - r_rhoMin_fix).Theta();
double sinTheta_customV = TMath::Sin(theta_q); double sinTheta_customV = TMath::Sin(theta_q);
@ -2485,6 +2587,7 @@ void PCSX3ClusterAnalysis(HistPlotter *plotter, const std::vector<Event> &QQQ_Ev
// beam axis at (0,0), silently ignoring BEAM_AXIS_X/Y, and had no guard for a // beam axis at (0,0), silently ignoring BEAM_AXIS_X/Y, and had no guard for a
// purely longitudinal direction. // purely longitudinal direction.
TVector3 r_rhoMin_fix = beamVertex(x1, v); TVector3 r_rhoMin_fix = beamVertex(x1, v);
fillBeamProfile(plotter, r_rhoMin_fix, x1, v, "sx3a1c2", true); // a1c2 only, gated above
plotter->Fill1D("VertexRecon_pczfix_sx3", 800, -300, 300, r_rhoMin_fix.Z(), "Vertex_Reconstruction"); plotter->Fill1D("VertexRecon_pczfix_sx3", 800, -300, 300, r_rhoMin_fix.Z(), "Vertex_Reconstruction");
plotter->Fill1D("VertexRecon_pczfix", 800, -300, 300, r_rhoMin_fix.Z(), "Vertex_Reconstruction"); plotter->Fill1D("VertexRecon_pczfix", 800, -300, 300, r_rhoMin_fix.Z(), "Vertex_Reconstruction");
plotter->Fill1D("pczfix_A1C2_1d_sx3", 600, -200, 200, pcz_fix, "PCZ_Recon"); plotter->Fill1D("pczfix_A1C2_1d_sx3", 600, -200, 200, pcz_fix, "PCZ_Recon");
@ -3036,6 +3139,7 @@ void PCQQQClusterAnalysis(HistPlotter *plotter, const std::vector<Event> &QQQ_Ev
// beam axis at (0,0), silently ignoring BEAM_AXIS_X/Y, and had no guard for a // beam axis at (0,0), silently ignoring BEAM_AXIS_X/Y, and had no guard for a
// purely longitudinal direction. // purely longitudinal direction.
TVector3 r_rhoMin_fix = beamVertex(x1, v); TVector3 r_rhoMin_fix = beamVertex(x1, v);
fillBeamProfile(plotter, r_rhoMin_fix, x1, v, "qqqa1c2", true); // a1c2 only, gated above
double sinTheta_customV = TMath::Sin((qqqevent.pos - beamAxisPoint(r_rhoMin_fix.Z())).Theta()); double sinTheta_customV = TMath::Sin((qqqevent.pos - beamAxisPoint(r_rhoMin_fix.Z())).Theta());
plotter->Fill2D("dE3_E_CathodeQQQR_A1C2_TC1_PC" + std::to_string(phicut), 400, 0, 30, 800, 0, 10000, qqqevent.Energy1, pcevent.Energy2 * sinTheta_customV, "PID_dE_E"); plotter->Fill2D("dE3_E_CathodeQQQR_A1C2_TC1_PC" + std::to_string(phicut), 400, 0, 30, 800, 0, 10000, qqqevent.Energy1, pcevent.Energy2 * sinTheta_customV, "PID_dE_E");
@ -3593,6 +3697,8 @@ void protonAlphaElastic_core(HistPlotter *plotter, const std::vector<Event> &Si_
TVector3 x2f(pcXY.X(), pcXY.Y(), pcz_fix); TVector3 x2f(pcXY.X(), pcXY.Y(), pcz_fix);
TVector3 r_rhoMin_fix = beamVertex(sievent.pos, x2f - sievent.pos); TVector3 r_rhoMin_fix = beamVertex(sievent.pos, x2f - sievent.pos);
double vertex_z = r_rhoMin_fix.Z(); double vertex_z = r_rhoMin_fix.Z();
const bool axisSafe = (multi2 == 2) || (multi1 == 2 && multi2 == 0);
fillBeamProfile(plotter, r_rhoMin_fix, sievent.pos, x2f - sievent.pos, "elastic_" + det, axisSafe);
if (vertex_z < z_entrance) if (vertex_z < z_entrance)
return; return;
double theta = (sievent.pos - r_rhoMin_fix).Theta(); double theta = (sievent.pos - r_rhoMin_fix).Theta();
@ -3629,7 +3735,7 @@ void protonAlphaElastic_core(HistPlotter *plotter, const std::vector<Event> &Si_
plotter->Fill1D(rx + "_VertexReconZ" + ejtag + sfx, 800, -400, 400, vertex_z, pmlabel); plotter->Fill1D(rx + "_VertexReconZ" + ejtag + sfx, 800, -400, 400, vertex_z, pmlabel);
plotter->Fill2D(rx + "_VertexReconXY" + ejtag + sfx, 200, -100, 100, 200, -100, 100, r_rhoMin_fix.X(), r_rhoMin_fix.Y(), pmlabel); plotter->Fill2D(rx + "_VertexReconXY" + ejtag + sfx, 200, -100, 100, 200, -100, 100, r_rhoMin_fix.X(), r_rhoMin_fix.Y(), pmlabel);
plotter->Fill2D(rx + "_Ef_vs_theta" + ejtag + sfx, 100, 0, 180, 800, 0, 10, theta * 180 / M_PI, Efix, pmlabel); plotter->Fill2D(rx + "_Ef_vs_theta" + ejtag + sfx, 100, 0, 180, 800, 0, 10, theta * 180 / M_PI, Efix, pmlabel);
plotter->Fill2D(rx + "_Ex_vs_theta" + ejtag + sfx, 360, 0, 180, 800, -10, 10, theta * 180 / M_PI, Ex, pmlabel); plotter->Fill2D(rx + "_Ex_vs_theta" + ejtag + sfx, 720, 0, 180, 800, -10, 10, theta * 180 / M_PI, Ex, pmlabel);
plotter->Fill2D(rx + "_Ex_vs_phi" + ejtag + sfx, 180, -180, 180, 800, -10, 10, sievent.pos.Phi() * 180 / M_PI, Ex, pmlabel); plotter->Fill2D(rx + "_Ex_vs_phi" + ejtag + sfx, 180, -180, 180, 800, -10, 10, sievent.pos.Phi() * 180 / M_PI, Ex, pmlabel);
for (const auto &pcevent : PC_Events) for (const auto &pcevent : PC_Events)
@ -3970,6 +4076,9 @@ static void reaction_ax_core(HistPlotter *plotter, const std::vector<Event> &Si_
TVector3 x2f(pcXY.X(), pcXY.Y(), pcz_fix); TVector3 x2f(pcXY.X(), pcXY.Y(), pcz_fix);
TVector3 r_rhoMin_fix = beamVertex(sievent.pos, x2f - sievent.pos); TVector3 r_rhoMin_fix = beamVertex(sievent.pos, x2f - sievent.pos);
double vertex_z = r_rhoMin_fix.Z(); double vertex_z = r_rhoMin_fix.Z();
const bool axisSafe = (topo1 == "a1c2fix") || (topo1 == "a2c0") || (topo2 == "a1c1_inband");
fillBeamProfile(plotter, r_rhoMin_fix, sievent.pos, x2f - sievent.pos,
"reaction_" + globaltag + "_" + det, axisSafe);
if (beamPerp(r_rhoMin_fix) > perp_cut || vertex_z < z_entrance) if (beamPerp(r_rhoMin_fix) > perp_cut || vertex_z < z_entrance)
return; return;
@ -4029,7 +4138,7 @@ static void reaction_ax_core(HistPlotter *plotter, const std::vector<Event> &Si_
plotter->Fill1D(rx + "_Ex_from" + ejtag + t + sfx, 600, -10, 20, Ex, pmlabel); plotter->Fill1D(rx + "_Ex_from" + ejtag + t + sfx, 600, -10, 20, Ex, pmlabel);
plotter->Fill2D(rx + "_VertexReconZ_vs_Ef" + ejtag + t + sfx, 800, -400, 400, 800, 0, ef_max, vertex_z, Efix, pmlabel); plotter->Fill2D(rx + "_VertexReconZ_vs_Ef" + ejtag + t + sfx, 800, -400, 400, 800, 0, ef_max, vertex_z, Efix, pmlabel);
plotter->Fill2D(rx + "_VertexReconZ_vs_Ex" + ejtag + t + sfx, 800, -400, 400, 600, -10, 20, vertex_z, Ex, pmlabel); plotter->Fill2D(rx + "_VertexReconZ_vs_Ex" + ejtag + t + sfx, 800, -400, 400, 600, -10, 20, vertex_z, Ex, pmlabel);
plotter->Fill2D(rx + "_Ex_vs_theta" + ejtag + t + sfx, 360, 0, 180, 600, -10, 20, theta * 180 / M_PI, Ex, pmlabel); plotter->Fill2D(rx + "_Ex_vs_theta" + ejtag + t + sfx, 720, 0, 180, 600, -10, 20, theta * 180 / M_PI, Ex, pmlabel);
if (ebeam_kin_MeV > 0.0) if (ebeam_kin_MeV > 0.0)
plotter->Fill2D(rx + "_BeamEnergy_ETrack_vs_EKin" + ejtag + t + sfx, 400, 0, beamE0 * 1.5, 400, 0, beamE0 * 1.5, plotter->Fill2D(rx + "_BeamEnergy_ETrack_vs_EKin" + ejtag + t + sfx, 400, 0, beamE0 * 1.5, 400, 0, beamE0 * 1.5,
@ -4069,6 +4178,7 @@ static void reaction_ax_core(HistPlotter *plotter, const std::vector<Event> &Si_
} }
plotter->Fill1D(rx + "_pczfix" + sfx, 600, -300, 300, pcz_fix, pmlabel); plotter->Fill1D(rx + "_pczfix" + sfx, 600, -300, 300, pcz_fix, pmlabel);
plotter->Fill2D(rx + "_Ef_vs_theta" + ejtag + sfx, 100, 0, 180, 800, 0, ef_max, theta * 180 / M_PI, Efix, pmlabel); plotter->Fill2D(rx + "_Ef_vs_theta" + ejtag + sfx, 100, 0, 180, 800, 0, ef_max, theta * 180 / M_PI, Efix, pmlabel);
plotter->Fill2D(rx + "_Ex_vs_theta" + ejtag + sfx, 720, 0, 180, 800, -20, 20, theta * 180 / M_PI, Ex, pmlabel);
plotter->Fill2D(rx + "_Ex_vs_phi" + ejtag + sfx, 45, -180, 180, 600, -10, 20, phi * 180 / M_PI, Ex, pmlabel); plotter->Fill2D(rx + "_Ex_vs_phi" + ejtag + sfx, 45, -180, 180, 600, -10, 20, phi * 180 / M_PI, Ex, pmlabel);
for (const auto &pcevent : PC_Events) for (const auto &pcevent : PC_Events)
@ -4107,7 +4217,7 @@ static void reaction_ax_core(HistPlotter *plotter, const std::vector<Event> &Si_
plotter->Fill2D(rx + "_dEgasCalib_vs_VertexZ" + ejtag + sfx, 800, -400, 400, 800, 0, 0.6, vertex_z, anodeE_MeV, pmlabel); plotter->Fill2D(rx + "_dEgasCalib_vs_VertexZ" + ejtag + sfx, 800, -400, 400, 800, 0, 0.6, vertex_z, anodeE_MeV, pmlabel);
plotter->Fill2D(rx + "_dEgasRaw_vs_VertexZ" + ejtag + sfx, 800, -400, 400, 800, 0, 20000, vertex_z, anodeE, pmlabel); plotter->Fill2D(rx + "_dEgasRaw_vs_VertexZ" + ejtag + sfx, 800, -400, 400, 800, 0, 20000, vertex_z, anodeE, pmlabel);
plotter->Fill2D(rx + "_dEgasRaw_vs_theta" + ejtag + sfx, 180, 0, 180, 800, 0, 20000, theta * 180 / M_PI, anodeE, pmlabel); plotter->Fill2D(rx + "_dEgasRaw_vs_theta" + ejtag + sfx, 180, 0, 180, 800, 0, 20000, theta * 180 / M_PI, anodeE, pmlabel);
plotter->Fill2D(rx + "_dEgasCalib_vs_theta" + ejtag + sfx, 360, 0, 180, 800, 0, 0.6, theta * 180 / M_PI, anodeE_MeV, pmlabel); plotter->Fill2D(rx + "_dEgasCalib_vs_theta" + ejtag + sfx, 720, 0, 180, 800, 0, 0.6, theta * 180 / M_PI, anodeE_MeV, pmlabel);
plotter->Fill2D(rx + "_dEgasCalib_vs_phi" + ejtag + sfx, 90, -180, 180, 800, 0, 0.6, phi * 180 / M_PI, anodeE_MeV, pmlabel); plotter->Fill2D(rx + "_dEgasCalib_vs_phi" + ejtag + sfx, 90, -180, 180, 800, 0, 0.6, phi * 180 / M_PI, anodeE_MeV, pmlabel);
// if (anodeCh >= 0) // if (anodeCh >= 0)
// plotter->Fill2D(rx + "_dEgasCalib_vs_E" + ejtag + sfx + "_anode" + pad2(anodeCh), 400, 0, ef_max, 800, 0, 0.6, sievent.Energy1, anodeE_MeV, pmlabel); // plotter->Fill2D(rx + "_dEgasCalib_vs_E" + ejtag + sfx + "_anode" + pad2(anodeCh), 400, 0, ef_max, 800, 0, 0.6, sievent.Energy1, anodeE_MeV, pmlabel);

View File

@ -11,14 +11,12 @@ export CATHODE_GAIN=3.0
export source_vertex=-200.0 export source_vertex=-200.0
export DEDX_SCALE=0.89 export DEDX_SCALE=0.89
export CUTLIST=cuts_list.txt export CUTLIST=cuts_list.txt
export BEAM_AXIS_X=0
export BEAM_AXIS_Y=0 export BEAM_AXIS_Z0=-200
# Tilted-beam parameters: (x, y) at BEAM_AXIS_Z0 (default 0), plus dx/dz and dy/dz slopes. export BEAM_AXIS_X=-0.136398
# Default (zeros) reproduces the original vertical beam bit-for-bit. Fit them export BEAM_AXIS_Y=0.034824
# with scratch/iterate_beam.sh + scratch/FitBeamAxis.C. export BEAM_TILT_X=-0.00083798
export BEAM_AXIS_Z0=${BEAM_AXIS_Z0:-0} export BEAM_TILT_Y=0.00191377
export BEAM_TILT_X=${BEAM_TILT_X:-0}
export BEAM_TILT_Y=${BEAM_TILT_Y:-0}
echo "Pre-compiling TrackRecon.C safely on a single core..." echo "Pre-compiling TrackRecon.C safely on a single core..."
root -q -l -b -e '.L TrackRecon.C++O' root -q -l -b -e '.L TrackRecon.C++O'
@ -64,11 +62,11 @@ export -f process_run
echo "Starting parallel processing..." echo "Starting parallel processing..."
# time parallel --bar -j 12 process_run ::: {24..41} 44 45 46 {50..59} # time parallel --bar -j 12 process_run ::: {24..41} 44 45 46 {50..59}
time parallel --bar -j 12 process_run ::: {24..41} # time parallel --bar -j 12 process_run ::: {24..41}
# time parallel --bar -j 10 process_run ::: 44 45 46 {50..59} # time parallel --bar -j 10 process_run ::: 44 45 46 {50..59}
# time parallel --bar -j 1 process_run ::: 48 # pc without coincidence # time parallel --bar -j 1 process_run ::: 48 # pc without coincidence
# mv "${CURRENT_OUT_DIR}/results_run048.root" "Output_27Al_run48/." # mv "${CURRENT_OUT_DIR}/results_run048.root" "Output_27Al_run48/."
# time parallel --bar -j 10 process_run ::: 62 63 66 67 68 73 74 {78..89} time parallel --bar -j 10 process_run ::: {24..41} 44 45 46 {50..59} 62 63 66 67 68 73 74 {78..89}
# time parallel --bar -j 4 process_run ::: 62 63 66 67 68 # time parallel --bar -j 4 process_run ::: 62 63 66 67 68
# time parallel --bar -j 1 process_run ::: 73 # time parallel --bar -j 1 process_run ::: 73
# time parallel --bar -j 1 process_run ::: 74 # time parallel --bar -j 1 process_run ::: 74

261
scratch/FitBeamAxis.C Normal file
View File

@ -0,0 +1,261 @@
// 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();
}