Elastic scattering with LocalOpticalPotential

LocalOpticalPotential provides a useful interface for defining optical potentials.

1import numpy as np
2from matplotlib import pyplot as plt
3from pandas import DataFrame
4from tqdm import tqdm
1from jitr.optical_potentials import LocalOpticalPotential
2from jitr.reactions.reaction import Reaction
3from jitr.rmatrix import Solver as SolverKernel
4from jitr.utils import utils
5from jitr.xs import elastic
1neutron = (1, 0)
2proton = (1, 1)
1target = (208, 82)
2projectile = proton
3energy_lab = 24
4reaction = Reaction(target=target, projectile=projectile, process="El")
5kinematics = reaction.kinematics(energy_lab)
1# set the channel radius, number of nodes, and number of partial waves
2interaction_range_fm = 1.2 * (208 ** (1 / 3)) + 1
3channel_radius_dimensionless = utils.suggested_dimensionless_channel_radius(
4    interaction_range_fm, kinematics.k
5)
6channel_radius = channel_radius_dimensionless / kinematics.k
7N = utils.suggested_basis_size(channel_radius_dimensionless)
8lmax = 50
 1# build a solver for the system and reaction of interest
 2print(f"Compiling solver for {reaction} at {energy_lab} MeV")
 3print(f" - channel radius {channel_radius:1.2f} fm")
 4print(f" - {N} nodes")
 5print(f" - {lmax} partial waves")
 6
 7solver = elastic.DifferentialWorkspace.build_from_system(
 8    reaction=reaction,
 9    kinematics=kinematics,
10    channel_radius_fm=channel_radius,
11    solver=SolverKernel(N),
12    lmax=lmax,
13    angles=np.linspace(0.1, np.pi, 180),
14)
15rgrid = solver.radial_grid()
16print("Done!")
Compiling solver for 208-Pb(p,el) at 24 MeV
 - channel radius 13.94 fm
 - 25 nodes
 - 50 partial waves
Done!

Use the built-in LocalOpticalPotential class

Although, in general, one can define whatever interaction they want. This is just a handy tool.

1omp = LocalOpticalPotential()
2omp.params
['Vv',
 'rv',
 'av',
 'Wv',
 'rw',
 'aw',
 'Wd',
 'Vd',
 'rd',
 'ad',
 'Vso',
 'Wso',
 'rso',
 'aso',
 'rC']

Randomly generate a bunch of parameters

1means = np.array([56, 1.2, 0.7, 5, 1.2, 0.7, 13, 6, 1.3, 0.9, 8, 4, 1.1, 0.7, 1.2])
2std_devs = np.array(
3    [3, 0.05, 0.03, 0.1, 0.05, 0.05, 1, 0.1, 0.1, 0.1, 0.2, 1, 0.1, 0.05, 0.1]
4)
5samples = np.random.multivariate_normal(means, np.diag(std_devs) ** 2, 1000)
1df = DataFrame(samples, columns=omp.params)
2df.head()
Vv rv av Wv rw aw Wd Vd rd ad Vso Wso rso aso rC
0 51.100403 1.193229 0.694053 5.001168 1.204845 0.698625 11.593592 6.002358 1.180502 0.987147 8.170491 3.076880 1.085694 0.734179 1.339886
1 52.833976 1.219670 0.686945 4.859437 1.260052 0.711278 11.806969 5.940825 1.241372 0.904550 8.085328 4.380284 1.151938 0.721191 1.342671
2 60.490654 1.111695 0.690334 5.017723 1.266576 0.709783 11.567632 6.207810 1.380022 0.802054 8.088928 3.947422 1.042329 0.771521 1.315199
3 55.871365 1.194629 0.746419 5.118907 1.167164 0.682690 13.338335 6.052000 1.309950 0.599037 7.906294 4.470666 1.194999 0.675212 1.157940
4 55.460503 1.117543 0.733219 4.896455 1.210000 0.658962 14.617597 6.307500 1.341588 0.890594 8.172465 4.120561 1.102040 0.620034 1.185585

How do we calculate observables?

We are using a jitr.xs.elastic.DifferentialWorkspace, which is set up specifically to interface with omp and other classes that have the same structure

1help(elastic.DifferentialWorkspace.xs)
Help on function xs in module jitr.xs.elastic:

xs(self, central_potential: 'npt.ArrayLike', spin_orbit_potential: 'npt.ArrayLike | None' = None, coulomb_potential: 'npt.ArrayLike | None' = None) -> 'ElasticXS'
    Return differential and integral elastic observables.

Exactly the information that the solver workspace needs is what is provided by the omp class:

1help(omp.evaluate)
Help on method evaluate in module jitr.optical_potentials.omp:

evaluate(rgrid: 'ArrayOrScalar', reaction_model: 'reaction.Reaction', kinematics_model: 'kinematics.ChannelKinematics', Vv: 'float', rv: 'float', av: 'float', Wv: 'float', rw: 'float', aw: 'float', Wd: 'float', Vd: 'float', rd: 'float', ad: 'float', Vso: 'float', Wso: 'float', rso: 'float', aso: 'float', rC: 'float') -> 'tuple[PotentialArray, PotentialArray, PotentialArray | ArrayOrScalar]' method of jitr.optical_potentials.omp.LocalOpticalPotential instance
    Evaluate the local optical-potential terms on ``rgrid``.

Do you see the vision?

Now running calculations is simple! Once it’s been compiled, it’s fast:

 1%%time
 2num_samples, num_params = df.shape
 3xs_ratio = np.zeros((num_samples, solver.angles.size))
 4Ay = np.zeros((num_samples, solver.angles.size))
 5rgrid = solver.radial_grid()
 6
 7for i in tqdm(range(num_samples)):
 8    central_term, spin_orbit_term, coulomb_term = omp(
 9        rgrid,
10        reaction,
11        kinematics,
12        *samples[i, :],
13    )
14    xs = solver.xs(central_term, spin_orbit_term, coulomb_term)
15    xs_ratio[i, :] = xs.dsdo / solver.rutherford
16    Ay[i, :] = xs.Ay
100%|██████████████████████████████████████████████████████████| 1000/1000 [00:09<00:00, 100.51it/s]
CPU times: user 9.68 s, sys: 265 ms, total: 9.94 s
Wall time: 9.95 s

1plt.fill_between(
2    np.rad2deg(solver.angles), *np.percentile(xs_ratio, [5, 95], axis=0), alpha=0.5
3)
4plt.xlabel(r"$\theta$ [deg]")
5plt.ylabel(
6    r"$\frac{d \sigma}{d\Omega} / \frac{d \sigma_{\text{R}}}{d\Omega}$ [dimensionless]"
7)
Text(0, 0.5, '$\\frac{d \\sigma}{d\\Omega} / \\frac{d \\sigma_{\\text{R}}}{d\\Omega}$ [dimensionless]')
../../_images/82507e2217dd9479a44989928c5062fcee94f00b5b283622ee2fb83b7dcc4d61.png
1plt.fill_between(
2    np.rad2deg(solver.angles), *np.percentile(Ay, [5, 95], axis=0), alpha=0.5
3)
4plt.xlabel(r"$\theta$ [deg]")
5plt.ylabel(r"$A_y$ [dimensionless]")
Text(0, 0.5, '$A_y$ [dimensionless]')
../../_images/a8d88745934f9c2a75797ad90cb37cdf65e5b6bcf1fd4f9fa13709b9f799f3bc.png