Scattering probabilities for TullyModelTwo

In this section we aim to reproduce the results of Fig. 5 from [1]. This figure presents the scattering outcomes when a particle interacts with Tully's model 2 with an increasing magnitude of incident kinetic energy.

To reproduce this figure, first, let's set up our system parameters:

using NQCDynamics

sim = Simulation{FSSH}(Atoms(2000), TullyModelTwo())
Simulation{FSSH{Float64}}:
  Atoms{Float64}([:X], [0], [2000.0])
  TullyModelTwo{Float64, Float64, Float64, Float64, Float64}
  a: Float64 0.1
  b: Float64 0.28
  c: Float64 0.015
  d: Float64 0.06
  e: Float64 0.05

!!! note Atomic units

Recall that all of are units are atomic by default, this mass of 2000 is similar to
that of a hydrogen atom.

Each data point in the figure is obtained from an ensemble average of trajectories. We can use our Ensembles setup to run a set of trajectories for every single momentum value. Firstly, we can prepare the parts that will be the same for every ensemble:

using ComponentArrays: ComponentVector

output = OutputStateResolvedScattering1D(sim, :adiabatic)
OutputStateResolvedScattering1D{Simulation{FSSH{Float64}, NQCDynamics.Calculators.DiabaticCalculator{Float64, TullyModelTwo{Float64, Float64, Float64, Float64, Float64}, 2, 4}, Float64, Unitful.Quantity{Int64, 𝚯, Unitful.FreeUnits{(K,), 𝚯, nothing}}, InfiniteCell}}(Simulation{FSSH{Float64}}:
  Atoms{Float64}([:X], [0], [2000.0])
  TullyModelTwo{Float64, Float64, Float64, Float64, Float64}
  a: Float64 0.1
  b: Float64 0.28
  c: Float64 0.015
  d: Float64 0.06
  e: Float64 0.05
, :adiabatic)

Here, we are using the OutputStateResolvedScattering1D along with the MeanReduction which will give us the average scattering outcome from the entire ensemble. Each trajectory outputs the scattering outcome along with its final adiabatic state, and the reduction computes the average over all trajectories.

Next, we can choose how many trajectories we want to perform for each ensemble, and choose the range of momentum values:

trajectories = 500
momenta = 9:2:50
9:2:49
Range notation

Here we uses Julia's range operator to generate a set of values from 9 to 50 with a spacing of 2: 9, 11, 13, ..., 49. The final value of 50 is not included since a step size of 2 starting from 9 allows us to include only odd numbers.

Since each ensemble requires different initial conditions, we will specify the trajectory timespan and the distribution inside the loop. Before the loop begins, we will create an empty list to store the results, and append to this list after every iteration. The distribution we create produces initial conditions where each trajectory has momentum k and starts at a position of -5.

result = []
for k in momenta # Iterate through each momentum value
    v = k / sim.atoms.masses[1] # Starting velocity
    r = -5 # Starting position
    tspan = (0, 2abs(r)/v)
    distribution = DynamicalDistribution(v, -5, size(sim)) * PureState(1, Adiabatic())

    out = run_dynamics(sim, tspan, distribution;
        saveat=tspan[end], trajectories, output, reduction=MeanReduction()
    )

    push!(result, out[:OutputStateResolvedScattering1D])
end

result
21-element Vector{Any}:
 ComponentVector{Float64}(reflection = [0.012, 0.0], transmission = [0.988, 0.0])
 ComponentVector{Float64}(reflection = [0.008, 0.002], transmission = [0.98, 0.01])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.868, 0.132])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.866, 0.134])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.932, 0.068])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.964, 0.036])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.966, 0.034])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.826, 0.174])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.692, 0.308])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.482, 0.518])
 ⋮
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.33, 0.67])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.412, 0.588])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.498, 0.502])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.66, 0.34])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.782, 0.218])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.894, 0.106])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.936, 0.064])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [0.984, 0.016])
 ComponentVector{Float64}(reflection = [0.0, 0.0], transmission = [1.0, 0.0])
Adaptive timespan

Since the trajectories with larger momentum will exit the scattering region sooner, we scale the timespan to save computational time. Using tspan = (0, 2abs(r)/v) allows enough time such that a particle will be able to travel a total distance of 2r at a constant velocity of v. This is sufficient to ensure the particle has left the interaction region. Alternatively, we could define a callback to terminate the simulation early.

Now we can plot our simulation results. We format this plot to match Fig. 3 from [18] which also reproduces Fig. 5 from [1]. We manage to reproduce the FSSH results quite accurately by visual comparison, though a larger number of trajectories would lead to better convergence. Since all of the examples run during the documentation build, we use a minimal number of trajectories to optimise the build time.

using Plots

plot(
    xlabel="Incidence momentum / a.u.",
    ylabel="Scattering probability"
)

r1 = [r.reflection[1] for r in result]
t1 = [r.transmission[1] for r in result]
t2 = [r.transmission[2] for r in result]

scatter!(momenta, r1; label="R1", color=:red)
scatter!(momenta, t1; label="T1", color=:green)
scatter!(momenta, t2; label="T2", color=:blue)
Example block output

As in [18], R1, T1, T2 refer to reflection on state 1, transmission on state 1 and transmission on state 2 respectively. For this model, surface hopping is successful in closely approximating the exact quantum result, especially at higher momentum values. Refer to [1] and [18] for a detailed discussion of the results.