# Cross-section pipeline (ANASEN active target) This folder turns data into a cross section for any reaction in the ANASEN active target. There are three steps, one script each, and one config file per study shared by all three: ``` root -l -b -q 'extract_yields.C+("examples/27Al_ap_runA.toml")' # 1. ROOT -> yields_.csv ~/myenv/bin/python -B geometric_efficiency.py examples/27Al_ap_runA.toml # 2. detectors.py -> eps_.csv ~10 s ~/myenv/bin/python -B cross_section.py examples/27Al_ap_runA.toml # 3. -> xs_.csv, xs.png ~3 s ``` Run them in WSL from this folder (`~/ANASEN_analysis/cross_section`). Step 1 needs ROOT; steps 2 and 3 need `~/myenv/bin/python`, not ROOT. | file | what it does | |---|---| | `extract_yields.C` | **Step 1, the only step that reads the data.** It reads a TH1/TH2, or fills a histogram from a TTree with a cut, and writes a plain CSV histogram. | | `geometric_efficiency.py` | Step 2: geometric efficiency per bin, a toy Monte Carlo of the reaction, with every hit rule taken from `detectors.py`. | | `detectors.py` | **The detectors as they are now** (Run A): QQQ3, the four analysed SX3s, the counter, the beam tube and the PCB hole, plus the hit rules. To change the geometry, edit this file. | | `cross_section.py` | Step 3: reads only CSVs (the yields and an efficiency) and computes σ(E_cm) or dσ/dΩ(θ_cm). | | `common.py` | What the three scripts share: the config, the beam track E(z), the bins. | | `examples/` | One commented config per study: `27Al_ap_runA.toml` and `paap_ta_runs_hist.toml`. | | `examples/reference/` | Barnard 1964 (EXFOR A1113) for p + ⁴He. | | `inputs/` | The beam track (`beam_tracks.csv`) and the TALYS states (`talys_27Al_ap.csv`). The p + ⁴He track is read from `../eloss_calculations/`. | | `out//` | Each study's outputs: `yields_.csv`, `eps_.csv`, `xs_.csv`, `xs.png`. | ## Starting a new study 1. Copy the closest example in `examples/` (excitation function: `27Al_ap_runA.toml`; angular distribution: `paap_ta_runs_hist.toml`) and give it its own `out_dir`. 2. Fill in `[extract]` for your histogram or tree, then run step 1 and look at `yields_.csv`. 3. Fill in `[reaction]`, `[beam]` and `[bins]`, then run step 2. Check that `eps_.csv` is non-zero where you expect events. 4. Fill in `[target]`, `[acceptance]` and any `[[reference]]`, then run step 3. If the detectors have changed, edit `detectors.py`, not the configs. ![27Al(a,p)](out/27Al_ap/xs.png) ## The formula | mode | result | formula | |---|---|---| | `excitation` | σ(E_cm), mb | Y / (N_b · n · dz · ε) | | `angular` | dσ/dΩ(θ_cm) in one E_cm (or z) window, mb/sr | Y / (N_b · n · dz · ε · ΔΩ) | - **Y:** the yield in the bin. - **N_b:** the number of beam particles. - **n:** target atoms per cm² per mm of path. - **dz:** the length of beam path over which the beam's E_cm lies in the bin, read off the beam track E(z). - **ε:** the efficiency of the bin. ## Step 1: `extract_yields.C`, data to CSV `[extract] source` says where the events are: - `"hist"`: a TH1 or TH2 in a ROOT file. A TH2 is projected on the axis that holds `variable`, or kept 2D with `angle_axis` for the angular mode. - `"tree"`: a ROOT TTree (`file` may be a glob or a list of runs), filled into `bins` after `cut`. Cuts use `and` / `or` (the macro turns them into `&&` / `||`) and ROOT's TTree::Draw syntax. **Histogram paths** are `"folder/name"`. - **They are case-sensitive.** Production files of different vintages spell the detector differently: the 27Al outputs use `_m27Alax+misc_qqq/...`, the proton runs' `Output_p/Al_protons.root` uses `elastic+misc_QQQ_a/...`. Put the spelling in `[det_vars]`: ```toml dets = ["qqq", "sx3"] # the names detectors.py knows [det_vars.qqq] DET = "QQQ" [det_vars.sx3] DET = "SX3" [extract] hist = "elastic+misc_{DET}_a/elastic_VertexReconZ_a_{DET}" ``` - **A name that itself contains `/`** (e.g. `dE3_E_AnodeQQQR_A1C2_(vertex_fix_z/100)=0.000000`) is found too. - **If the path doesn't exist,** the macro stops and names the closest histogram paths in the file. The CSV it writes, `/yields_.csv`, is the hand-off between the steps. `cross_section.py` reads nothing else, so a yield made any other way works too as long as it is in this format: ``` 1D: _lo, _hi, counts, variance 2D: _lo, _hi, theta_cm_lo, theta_cm_hi, counts, variance (angular mode) ``` `` is `z` (vertex z, mm), `e_beam` (MeV) or `ecm` (MeV). The bins may be finer than the analysis bins and need not line up with them. Step 3 shares each one out by overlap. It is run like this: ``` root -l -b -q 'extract_yields.C+("examples/27Al_ap_runA.toml")' root -l -b -q 'extract_yields.C+("examples/27Al_ap_runA.toml", "out/elsewhere")' # optional output folder ``` - **Trees:** it fills the histogram with `TTree::Draw` on a `TChain`, so `file` may be a glob or a list of runs. - **Cuts:** write them with `and` / `or` (e.g. `"det == 1 and e_si > 0.5"`). The macro turns them into `&&` / `||`. - **TOML:** the macro has a small reader that handles only what these configs use: strings, numbers, one-line lists and `{start, stop, step}` tables. It stops with an error on anything else. - **Errors:** a missing file or tree, or a tree with no entries, stops it with exit code 1 rather than writing zeros. - **One edge case:** an entry exactly on a tree histogram's last upper edge goes to ROOT's overflow. ## Step 2: `geometric_efficiency.py`, ε from the detector geometry The toy runs once per bin: - vertices uniform over the bin's path (angular mode: over the window); - beam energy at each vertex from the nominal track; - the residual state drawn from fixed weights, or from TALYS state cross sections at the bin's E_cm; - isotropic CM emission (angular mode: inside the θ_cm bin); - relativistic two-body kinematics and straight tracks. The particle the silicon sees can be the ejectile or the recoil (`detect`). θ_cm is always the ejectile's, so with `detect = "recoil"` the bins are still in the ejectile's CM angle. For p + ⁴He that is the proton's angle, Barnard's convention, which TrackRecon's `elastic_VertexZ_vs_ThetaCM` histograms also use. The output has one column per condition, each times the azimuthal coverage: | column | the particle... | |---|---| | `eps_si` | reaches the analysed silicon (solid angle) | | `eps_si_thr` | ...and has a vertex energy above the Si threshold. This is the ch5 "solid angle" convention: a kinematic cut, not geometry. | | `eps_counter` | `eps_si`, and it crosses the guard, anode and cathode radii inside the counter | | **`eps_geometric`** | `eps_counter`, and it clears the beam tube and the PCB hole. **This is the geometric efficiency.** | Each column has a Monte Carlo error, `_rel_err`. Gas energy loss, Si thresholds after the gas and the counter's deposit threshold are **not** included, because they are not geometry. That is why the adopted `eps_tube` of `../scratch/xs_27Al_lowE_2026-10-15` is lower than `eps_geometric`; to use it instead, see the commented `[acceptance]` block in the example. The angular distribution is isotropic. ## Step 3: `cross_section.py` `[acceptance] source` picks ε: - `"geometric"`: step 2's file; `column` chooses which efficiency. - `"csv"`: any file, matched on a bin edge or interpolated. - `"value"`: one number for every bin. - `"none"`: ε = 1, which gives σ_app. `[[acceptance.extra]]` multiplies in further factors, such as an intrinsic efficiency. The output `xs_.csv` keeps every intermediate number: the bins, z ranges, dz, counts, σ_app, ε, σ and its errors (stat, ε, total), and the references. The header lines record every input. The N_b and n uncertainties are a common scale, so the header gives them as a percentage and they stay out of the error columns. ## The config (TOML) Paths are relative to the config file, and `~` is the home folder. `{det}` is replaced by each entry of `dets`, and so is every key of `[det_vars.]`. Commented templates in `examples/`: - `27Al_ap_runA.toml`: a TH2 from the merged Run A production file, all three steps. - `paap_ta_runs_hist.toml`: p + ⁴He elastic **straight from a TrackRecon.C production file**, as for 27Al. It reads the TH2 `elastic_VertexZ_vs_ThetaCM_cath_exgate_a_` (vertex z × proton θ_cm). `protonAlphaElastic_core` fills that histogram since 2026-10-16 in every species folder, in four variants: all, `_cath`, `_exgate`, `_cath_exgate`. - Input: `~/ANASEN_analysis/Output_p/results_run022.root` (run 22 alone, run_tr.sh block 5 with CUTLIST), N_b = 1.35 pA × 672.65 s = 5.67 × 10⁹, vertex window z = −170 to −53 mm. - On 2026-10-16 the merged runs 18–22 agreed with a per-hit route to 2–4% in QQQ3 counts and about 3% in dσ/dΩ. | section | used by | keys | |---|---|---| | top | all | `title`, `mode` (`excitation` / `angular`), `dets` (names in `detectors.py`: `qqq`, `sx3`), `out_dir` | | `[extract]` histogram | 1 | `source = "hist"`, `file`, `hist`, `variable`, `axis` (the axis holding `variable`), `angle_axis` (keep it 2D), `other_range` | | `[extract]` tree | 1 | `source = "tree"`, `file` (glob or list), `tree`, `variable`, `branch`, `bins`, `cut`, `weight_branch`, `angle_branch` + `angle_bins` | | `[reaction]` | 2, 3 | `beam_mass_u`, `target_mass_u`; for step 2 also `ejectile_mass_u`, `recoil_mass_u`, `detect`, and `states` *or* `states_csv`; optional `ecm_per_ebeam` | | `[beam]` | 2, 3 | track: `track_csv` + `track_e_col` (+ `nominal_e_col`, `track_z_col`), *or* `eloss_table` + `e_entrance_mev` + `z_entrance_mm`. N_b: `n_beam` *or* `charge_c` *or* `current_a` + `time_s`; `charge_state`, `n_beam_rel_err`. For Run A runs, `../runlog/beam_exposure.py` gives `n_beam` for any set of runs from the elog's cup readings | | `[bins]` | 2, 3 | `ecm_edges` (a list or `{start, stop, step}`); angular: `theta_edges_deg` and `window_ecm` *or* `window_z_mm` | | `[efficiency]` | 2 | `n_per_bin` (default 400 000), `seed` | | `[target]` | 3 | `pressure_torr`, `temperature_k`, `mole_fraction`, `atoms_per_molecule`, `rel_err`; *or* `n_per_cm2_per_mm` | | `[yield]` | 3 | `csv`: only to read yields from somewhere other than step 1's output | | `[acceptance]` | 3 | `source` and its keys (see above), `[[acceptance.extra]]` | | `[[reference]]` | 3 | `csv`, `x_col`, `y_col`, `label`, `scale`, `dets` | | `[plot]` | 3 | `yscale`, `xlim`, `ylim`, `xlabel`, `ylabel` | ## Example: p + ⁴He elastic scattering, Ta run 22 (`examples/paap_ta_runs_hist.toml`) ``` root -l -b -q 'extract_yields.C+("examples/paap_ta_runs_hist.toml")' ~/myenv/bin/python -B geometric_efficiency.py examples/paap_ta_runs_hist.toml ~/myenv/bin/python -B cross_section.py examples/paap_ta_runs_hist.toml ``` dσ/dΩ(θ_cm) from the α recoils (`detect = "recoil"`, bins in the proton's θ_cm, Barnard's convention), against Barnard, Jones & Weil (1964). Histogram: `elastic_VertexZ_vs_ThetaCM_cath_exgate_a_` (cathode topologies, graphical α cut, |E_x| < 1 MeV). The merged runs 18–22 gave QQQ3 0.44–0.54 × Barnard at 132–156°; SX3 is at 1–3%. Not in `detectors.py`: the Ta holder rod (~28% of the azimuth downstream of each foil) and Si thresholds after the gas. ## Checks (run in the Windows repo, `/mnt/c/Users/sengi/ANASEN_analysis/cross_section`; the scripts are not in this copy) - 27Al(α,p), Run A: yields, dz and nominal E_cm equal the earlier study's in every bin; ε_si_thr agrees with its independent toy within the MC errors. - Synthetic events with a known σ, ε and dσ/dΩ are returned with unit-Gaussian pulls. - `extract_yields.C` and the Python version gave byte-identical yields on every example. **Not verified:** the physics inputs are whatever the config and `detectors.py` say. The checks show the arithmetic and the bookkeeping are right, and that `detectors.py` reproduces the geometry of `anasen_detectability.py`. They cannot show that the geometry, a track or an N_b is right. In particular: - The dE/dx scale of a `.dat` table is not written in the file. The live 250 Torr / 3% tables were regenerated on 2026-10-16 and match the 0.962 track to 3 keV. - `detectors.py` gives QQQ3 0.967 of the azimuth, but TrackRecon drops QQQ pixels without a calibration, which leaves about 0.675. QQQ3 ε is therefore about 1.4× too high, and its cross sections about 1.4× too low. - Where only a sliver of solid angle is open, ε is tiny and σ explodes. Example: QQQ3 at E_cm 5.4 MeV, ε = 1.5 × 10⁻⁵, behind the beam tube. The error bar shows it; no ε cut is applied.