Open in Colab

On Colab the first import exfor_tools downloads and unpacks the EXFOR database, which takes a few minutes.

 1# Google Colab setup: installs jitr and this notebook's extra dependencies.
 2# Does nothing when the notebook is run anywhere else.
 3import sys
 4
 5if "google.colab" in sys.modules:
 6    import subprocess
 7
 8    subprocess.run(
 9        [sys.executable, "-m", "pip", "install", "-q", "jitr", "exfor-tools"],
10        check=True,
11    )

Analyzing power of the quasi-elastic \((\vec{p},n)\) reaction

In 1976 Gosset, Mayer and Escudié measured the differential analyzing power \(A_y(\theta)\) of the quasi-elastic \((p,n)\) reaction to the isobaric analog state (IAS) on nine targets, with a 22.8 MeV polarized proton beam at Saclay (Phys. Rev. C 14, 878). Those data are EXFOR entry O0669.

Their point was that \(A_y\) is a far sharper probe of the Lane potential than the cross section is. In the macroscopic Lane model the nucleon-nucleus optical potential splits into isoscalar and isovector parts,

\[ U = U_0 + \frac{4}{A} U_1\, \vec{t} \cdot \vec{T}, \]

which for the proton-target, neutron-analog and neutron-target channels gives

\[ U_{pT} = U_0 - \frac{2 T_0}{A} U_1, \qquad U_{nA} = U_0 + \frac{2 (T_0 - 1)}{A} U_1, \qquad U_{nT} = U_0 + \frac{2 T_0}{A} U_1, \]

with \(T_0 = (N - Z)/2\). The off-diagonal part of \(U_1\) drives the charge-exchange transition between \(|T_0 T_0\rangle\) and its analog \(|T_0, T_0 - 1\rangle\). Because the transition carries \(l = s = j = 0\), only two amplitudes survive — one conserving and one inverting the projectile spin projection — and the analyzing power is

\[ A_y = \frac{2\,\mathrm{Im}(Y X^*)}{|X|^2 + |Y|^2}. \]

The spin-flip amplitude \(Y\) is built from the difference between the \(j = l + \tfrac{1}{2}\) and \(j = l - \tfrac{1}{2}\) radial matrix elements.

This notebook solves the two coupled Lane channels exactly with jitr.xs.lane_pn rather than in DWBA, and propagates the posteriors of three uncertainty-quantified global optical potentials — KDUQ, WLH and CHUQ — plus a deterministic JLMB calculation, through to \(A_y\) for all nine targets.

Notw that, while \(^{49}\)Ti, \(^{117}\)Sn and \(^{165}\)Ho all have non-zero ground-state spin, the measured transitions transfer no angular momentum, so the target spin is a spectator and the spin-1/2 geometry used here is still reasonable.

 1import os
 2
 3import matplotlib.pyplot as plt
 4import numpy as np
 5import periodictable
 6
 7import jitr
 8from jitr.folding import ILDAFolder
 9from jitr.folding.jlm import (
10    lambda_v0,
11    lambda_v1,
12    lambda_vso,
13    lambda_w0,
14    lambda_w1,
15    lambda_wso,
16    potential_JLMB,
17    spin_orbit_jlmb,
18)
19from jitr.optical_potentials import chuq, kduq, wlh
20from jitr.utils.density import density_table
21from jitr.xs import lane_pn
22
23# set JITR_QUICK=1 to thin the posterior draws; used by the CI notebook run
24QUICK = os.environ.get("JITR_QUICK", "0") == "1"

Pulling the measurements from EXFOR

Each of the nine targets is a subentry of O0669, tabulated as an analyzing power against the center-of-mass angle, together with the excitation energy of the analog state in the residual nucleus. That excitation energy is what fixes the outgoing neutron energy, so we read it from the entry to set the kinematics in the exit channels.

1import exfor_tools
2
3print(exfor_tools.__version__)
Using database version X4-2024-12-31 located in: /home/kyle/db/exfor/unpack_exfor-2024/X4-2024-12-31
1.3
 1from exfor_tools import ExforEntry
 2from exfor_tools.reaction import Reaction as ExforReaction
 3
 4# the nine targets of Table I, in the order the paper presents them
 5TARGETS = [
 6    (49, 22),
 7    (56, 26),
 8    (64, 28),
 9    (70, 30),
10    (90, 40),
11    (96, 40),
12    (117, 50),
13    (165, 67),
14    (208, 82),
15]
16PROTON, NEUTRON = (1, 1), (1, 0)
17
18
19def nuclide(A, Z):
20    return rf"$^{{{A}}}${periodictable.elements[Z].symbol}"
21
22
23datasets = []
24for A, Z in TARGETS:
25    entry = ExforEntry(
26        "O0669",
27        ExforReaction(
28            target=(A, Z), projectile=PROTON, product=NEUTRON, residual=(A, Z + 1)
29        ),
30        quantity="Ay",
31    )
32    (measurement,) = entry.measurements
33    datasets.append(
34        {
35            "A": A,
36            "Z": Z,
37            "name": f"{A}{periodictable.elements[Z].symbol}",
38            "residual_name": f"{A}{periodictable.elements[Z + 1].symbol}",
39            "label": nuclide(A, Z),
40            "subentry": measurement.subentry,
41            "E_lab": measurement.Einc,
42            "E_IAS": measurement.Ex,
43            "theta": measurement.x,
44            "Ay": measurement.y,
45            "Ay_err": measurement.statistical_err,
46        }
47    )
48
49print(entry.bibtex())
@article{JGosset1976_O0669001,
  author = {J.Gosset and B.Mayer and J.L.Escudie},
  title = {Quasielastic (P,N) Reactions Induced By Polarized Protons.},
  journal = {Physical Review, Part C, Nuclear Physics},
  year = {1976},
  volume = {14},
  pages = {878},
  note = {EXFOR reference: Physical Review, Part C, Nuclear Physics 14, 878 (1976); EXFOR subent: O0669001}
}
 1header = f"{'target':>8} {'residual':>10} {'subentry':>10} {'E_IAS [MeV]':>12} {'points':>7} {'angular range [deg]':>22}"
 2print(header)
 3print("-" * len(header))
 4for d in datasets:
 5    angles = f"{d['theta'][0]:.1f} - {d['theta'][-1]:.1f}"
 6    print(
 7        f"{d['name']:>8} {d['residual_name']:>10} {d['subentry']:>10} "
 8        f"{d['E_IAS']:>12.2f} {len(d['theta']):>7} {angles:>22}"
 9    )
10print(
11    f"\n{sum(len(d['theta']) for d in datasets)} points, all at E_p = {datasets[0]['E_lab']} MeV"
12)
  target   residual   subentry  E_IAS [MeV]  points    angular range [deg]
--------------------------------------------------------------------------
    49Ti        49V   O0669002         5.36      18           16.6 - 103.2
    56Fe       56Co   O0669003         3.51      23           15.7 - 125.1
    64Ni       64Cu   O0669004         6.70      19           16.7 - 106.4
    70Zn       70Ga   O0669005         8.12      20           16.0 - 114.3
    90Zr       90Nb   O0669006         5.03      22           15.4 - 124.3
    96Zr       96Nb   O0669007        11.07      18            14.4 - 98.7
   117Sn      117Sb   O0669008        11.18      17            15.0 - 94.9
   165Ho      165Er   O0669009        15.49       8            16.9 - 61.0
   208Pb      208Bi   O0669010        15.33       4            17.3 - 31.4

149 points, all at E_p = 22.8 MeV

Compiling one coupled-channels workspace per target

jitr.xs.lane_pn.Workspace solves the two-channel problem

\[ \left[T_l + U_{pp} - E_p\right] u_p + U_1 u_n = 0, \qquad \left[T_l + U_{nn} - E_n\right] u_n + U_1 u_p = 0 \]

for every \((l, j)\), with an incoming wave in the proton channel only.

 1E_LAB = 22.8
 2LMAX = 25
 3ANGLES = np.radians(np.linspace(1.0, 180.0, 180))
 4
 5solvers = {}
 6for d in datasets:
 7    A, Z = d["A"], d["Z"]
 8    reaction = jitr.reactions.Reaction(
 9        target=(A, Z), projectile=PROTON, product=NEUTRON, residual=(A, Z + 1)
10    )
11    # "El" so that the neutron potential is evaluated for n + residual
12    exit_reaction = jitr.reactions.Reaction(
13        target=(A, Z + 1), projectile=NEUTRON, process="El"
14    )
15    kinematics_entrance = reaction.kinematics(E_LAB)
16    kinematics_exit = reaction.kinematics_exit(
17        kinematics_entrance, residual_excitation_energy=d["E_IAS"]
18    )
19
20    a = jitr.utils.interaction_range(A) * kinematics_entrance.k + 2 * np.pi
21    nbasis = jitr.utils.suggested_basis_size(a)
22    solver = solvers.setdefault(nbasis, jitr.rmatrix.Solver(nbasis))
23
24    d.update(
25        reaction=reaction,
26        exit_reaction=exit_reaction,
27        kinematics_entrance=kinematics_entrance,
28        kinematics_exit=kinematics_exit,
29        nbasis=nbasis,
30        channel_radius_fm=a / kinematics_entrance.k,
31        workspace=lane_pn.Workspace(
32            reaction,
33            kinematics_entrance,
34            kinematics_exit,
35            solver,
36            ANGLES,
37            LMAX,
38            a / kinematics_entrance.k,
39        ),
40    )
41
42for d in datasets:
43    print(
44        f"{d['name']:>8}  E_n = {d['kinematics_exit'].Elab:5.2f} MeV  "
45        f"a = {d['channel_radius_fm']:5.2f} fm  nbasis = {d['nbasis']:2d}"
46    )
    49Ti  E_n = 15.92 MeV  a = 10.47 fm  nbasis = 20
    56Fe  E_n = 13.78 MeV  a = 10.66 fm  nbasis = 20
    64Ni  E_n = 13.50 MeV  a = 10.85 fm  nbasis = 20
    70Zn  E_n = 13.11 MeV  a = 10.99 fm  nbasis = 20
    90Zr  E_n = 10.74 MeV  a = 11.40 fm  nbasis = 20
    96Zr  E_n = 10.99 MeV  a = 11.52 fm  nbasis = 20
   117Sn  E_n =  8.96 MeV  a = 11.88 fm  nbasis = 20
   165Ho  E_n =  6.05 MeV  a = 12.58 fm  nbasis = 25
   208Pb  E_n =  3.72 MeV  a = 13.10 fm  nbasis = 25

Propagating the optical model posteriors

Let’s propagate the KDUQ, WLH and CHUQ posteriors through the \((p,n)\) \(A_y\).

Nothing here is a fit to these data: the potentials were calibrated to elastic scattering, and the Lane transition potential is taken to be the isovector difference

\[ U_1(E) = -\left(U_n(E - Q - E_x) - U_p(E)\right) \frac{\sqrt{|N - Z|}}{N - Z - 1}, \]

evaluated separately for the central and spin-orbit terms — so the isovector spin-orbit form factor that the analyzing power is sensitive to comes entirely from the difference between the proton and neutron spin-orbit potentials of each global parameterization. Note that the neutron and proton potentials going into \(U_1\) are evaluated at the energies of their respective channels.

CHUQ deserves a word: CH89 is Lane-consistent by construction, so it is the one built-in whose proton and neutron potentials really are the \(U_{pT}\) and \(U_{nA}\) of the equations above, and whose difference therefore really is \(U_1\). For KDUQ and WLH the same construction is an approximation, and the diagonal potentials may already absorb part of the coupling.

 1from tqdm import tqdm
 2
 3OMPS = {
 4    "KDUQ": (
 5        kduq.KDUQ(PROTON),
 6        kduq.KDUQ(NEUTRON),
 7        kduq.get_samples(PROTON),
 8        kduq.get_samples(NEUTRON),
 9    ),
10    "WLH": (
11        wlh.WLH(PROTON),
12        wlh.WLH(NEUTRON),
13        wlh.get_samples(PROTON),
14        wlh.get_samples(NEUTRON),
15    ),
16    # CH89 is Lane consistent: one parameter set serves both projectiles
17    "CHUQ": (chuq.CHUQ(), chuq.CHUQ(), chuq.get_samples(), chuq.get_samples()),
18}
19
20
21def propagate(omp_p, omp_n, samples_p, samples_n):
22    n_draws = min(len(samples_p), len(samples_n))
23    if QUICK:
24        n_draws = min(n_draws, 50)
25
26    bands = []
27    for d in tqdm(datasets):
28        workspace = d["workspace"]
29        rgrid = workspace.radial_grid()
30        Ay = np.zeros((len(ANGLES), n_draws))
31        dsdo = np.zeros((len(ANGLES), n_draws))
32        for j in range(n_draws):
33            U_p, U_p_so, U_coulomb = omp_p(
34                rgrid, d["reaction"], d["kinematics_entrance"], *samples_p[j]
35            )
36            U_n, U_n_so, _ = omp_n(
37                rgrid, d["exit_reaction"], d["kinematics_exit"], *samples_n[j]
38            )
39            _, S = workspace.rsmatrix(U_coulomb, U_p, U_p_so, U_n, U_n_so)
40            observables = workspace.observables_from_smatrix(S)
41            Ay[:, j] = observables.Ay
42            dsdo[:, j] = observables.dsdo
43        bands.append(
44            {
45                "Ay": np.percentile(Ay, [16, 84], axis=1),
46                "Ay_median": np.median(Ay, axis=1),
47                "dsdo": np.percentile(dsdo, [16, 84], axis=1),
48                "dsdo_median": np.median(dsdo, axis=1),
49            }
50        )
51    return n_draws, bands
1predictions = {}
2for name, args in OMPS.items():
3    n_draws, bands = propagate(*args)
4    predictions[name] = bands
5    print(f"{name}: {n_draws} posterior draws x {len(datasets)} targets")
  0%|                                                                         | 0/9 [00:00<?, ?it/s]/home/kyle/umich/jitr/src/jitr/optical_potentials/kduq.py:470: RuntimeWarning: overflow encountered in exp
  d2 = d2_0 + d2_A / (1 + np.exp((A - d2_A3) / d2_A2))
100%|█████████████████████████████████████████████████████████████████| 9/9 [00:26<00:00,  2.90s/it]
KDUQ: 416 posterior draws x 9 targets
100%|█████████████████████████████████████████████████████████████████| 9/9 [00:45<00:00,  5.01s/it]
WLH: 1000 posterior draws x 9 targets
100%|█████████████████████████████████████████████████████████████████| 9/9 [00:09<00:00,  1.04s/it]
CHUQ: 208 posterior draws x 9 targets

A semi-microscopic comparison: JLMB

JLMB is a different kind of potential: a local-density folding of the Jeukenne-Lejeune-Mahaux nuclear-matter g-matrix over a Gogny D1M density, with energy-dependent normalizations \(\lambda_V, \lambda_W, \lambda_{V_1}, \lambda_{W_1}\) and a Scheerbaum spin-orbit form factor. It is the one potential here with a genuine isovector form factor rather than one obtained by differencing.

It is not uncertainty quantified, so it contributes a single curve, not a band.

 1def jlmb_potentials(d):
 2    workspace = d["workspace"]
 3    rgrid = workspace.radial_grid()
 4    folder = ILDAFolder(r_max=15.0, n_quad=200)
 5
 6    def channel(A, Z, projectile, energy, coulomb):
 7        table = density_table(A, Z, model="d1m")
 8        rho_n = folder.interp_to_quad(table.radial_grid, table.neutron_density_grid)
 9        rho_p = folder.interp_to_quad(table.radial_grid, table.proton_density_grid)
10        V_C_quad = folder.V_coulomb(rho_p) if coulomb else None
11        V, W = potential_JLMB(
12            folder,
13            rho_n,
14            rho_p,
15            projectile,
16            (A, Z),
17            energy,
18            V_C=V_C_quad,
19            parameterization="talys",
20            lambda_V=float(lambda_v0(energy)),
21            lambda_W=float(lambda_w0(energy, mode=0)),
22            lambda_V1=float(lambda_v1(energy)),
23            lambda_W1=float(lambda_w1(energy, mode=0)),
24            t_r=1.25,
25            t_i=1.35,
26            r_out=rgrid,
27        )
28        spin_orbit = (
29            float(lambda_vso(energy)) + 1j * float(lambda_wso(energy))
30        ) * spin_orbit_jlmb(folder.r_q, rho_n, rho_p, projectile, r_out=rgrid)
31        coulomb_out = folder.V_coulomb(rho_p, r_out=rgrid) if coulomb else None
32        return (
33            np.asarray(V + 1j * W, dtype=np.complex128),
34            np.asarray(spin_orbit, dtype=np.complex128),
35            (
36                None
37                if coulomb_out is None
38                else np.asarray(coulomb_out, dtype=np.complex128)
39            ),
40        )
41
42    U_p, U_p_so, U_coulomb = channel(
43        d["A"], d["Z"], PROTON, d["kinematics_entrance"].Elab, coulomb=True
44    )
45    U_n, U_n_so, _ = channel(
46        d["A"], d["Z"] + 1, NEUTRON, d["kinematics_exit"].Elab, coulomb=False
47    )
48    return U_coulomb, U_p, U_p_so, U_n, U_n_so
49
50
51jlmb = []
52for d in datasets:
53    jlmb.append(d["workspace"].observables(*jlmb_potentials(d)))
54print(f"JLMB evaluated for {len(jlmb)} targets")
JLMB evaluated for 9 targets

Plot the measurements against the calculations

 1BANDS = {"KDUQ": "#0072B2", "WLH": "#D55E00", "CHUQ": "#009E73"}
 2JLMB_INK = "#333333"
 3angles_deg = np.degrees(ANGLES)
 4
 5
 6def grid_figure(ylabel):
 7    fig, axes = plt.subplots(3, 3, figsize=(12, 10), sharex=True)
 8    fig.subplots_adjust(hspace=0.12, wspace=0.22)
 9    for ax in axes[-1]:
10        ax.set_xlabel(r"$\theta_{\rm c.m.}$ [deg]")
11    for ax in axes[:, 0]:
12        ax.set_ylabel(ylabel)
13    for ax in axes.flat:
14        ax.set_xlim(0, 140)
15        ax.tick_params(direction="in", top=True, right=True)
16        for side in ax.spines.values():
17            side.set_linewidth(0.8)
18    return fig, axes
19
20
21fig, axes = grid_figure(r"$A_y$")
22for i, (ax, d, curve) in enumerate(zip(axes.flat, datasets, jlmb, strict=True)):
23    for name, color in BANDS.items():
24        lo, hi = predictions[name][i]["Ay"]
25        ax.fill_between(angles_deg, lo, hi, color=color, alpha=0.35, lw=0, label=name)
26        # ax.plot(angles_deg, predictions[name][i]["Ay_median"], color=color, lw=1.2)
27    ax.plot(angles_deg, curve.Ay, color=JLMB_INK, lw=1.2, ls="--", label="JLMB")
28    ax.errorbar(
29        d["theta"],
30        d["Ay"],
31        yerr=d["Ay_err"],
32        fmt="o",
33        ms=3.5,
34        color="k",
35        lw=0.9,
36        capsize=1.5,
37        label="Gosset (1976)",
38    )
39    ax.axhline(0.0, color="0.75", lw=0.7, zorder=0)
40    ax.set_ylim(-1.05, 1.05)
41    ax.text(0.04, 0.06, d["label"], transform=ax.transAxes, fontsize=13)
42
43axes[0, 0].legend(loc="upper right", fontsize=8, frameon=False, ncols=2)
44fig.suptitle(r"$(\vec{p},n)$ analyzing power to the IAS at $E_p = 22.8$ MeV", y=0.92)
45plt.show()
../../_images/974a9d6fbee1f7988f164dd563c4e3b803b04aee41405a9bf00004eb00b8f5de.png