Skip to content

Commit 94496fe

Browse files
committed
First version of the code which can produce plots! Look totally wrong: some major issue in the physics.
1 parent 89d845e commit 94496fe

6 files changed

Lines changed: 328 additions & 1 deletion

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using Janus
2+
using Gnuplot
3+
4+
# Load trajectory data (same loader for GSB and SE!)
5+
traj_gs = load_trajectory("gsb/gsb_ene.dat", "gsb/gsb_osc.dat")
6+
7+
# Laser pulse (5 fs Gaussian, no dispersion)
8+
laser = LaserPulse=5.0)
9+
10+
# Experimental parameters
11+
ω_pump = 3.0 # eV
12+
ω_probe = collect(range(1, 6; length=200)) # eV grid
13+
14+
# GSB spectrum
15+
S_gsb = spectrum(GSB(), traj_gs, laser, ω_pump, ω_probe)
16+
17+
@show S_gsb
18+
19+
@gp "set xlabel 'Probe energy (eV)'" "set ylabel 'Signal (arb.)'" :-
20+
@gp :- "set xrange [0:50]"
21+
@gp :- "set yrange [1:200]"
22+
@gp :- S_gsb' "with image title 'GSB'"
23+
Gnuplot.save("gsb.png", term="pngcairo size 550,350 fontscale 0.8")
24+
25+
# SE -- same loader, different signal type
26+
traj_se = load_trajectory("se/se_ene.dat", "se/se_osc.dat")
27+
S_se = spectrum(SE(), traj_se, laser, ω_pump, ω_probe)
28+
29+
@show S_se
30+
31+
# Time-resolved 2D map
32+
@gp "set xlabel 'Time step'" "set ylabel 'Probe index'" :-
33+
@gp :- "set xrange [0:50]"
34+
@gp :- "set yrange [1:200]"
35+
@gp :- S_se' "with image title 'SE'"
36+
Gnuplot.save("se.png", term="pngcairo size 550,350 fontscale 0.8")
37+
38+
# different pump and probe lasers
39+
pump_laser = LaserPulse=5.0, envelope=Gaussian())
40+
probe_laser = LaserPulse=10.0, envelope=Sech())
41+
S_nd = spectrum(GSB(), traj_gs, pump_laser, ω_pump, probe_laser, ω_probe)
42+
43+
@gp "set xlabel 'Time step'" "set ylabel 'Probe index'" :-
44+
@gp :- "set xrange [0:50]"
45+
@gp :- "set yrange [1:200]"
46+
@gp :- S_nd' "with image title 'different pump and probe lasers'"
47+
Gnuplot.save("gsb_weird_laser.png", term="pngcairo size 550,350 fontscale 0.8")

src/Janus.jl

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
module Janus
22

3-
# Write your package code here.
3+
# Janus.jl
4+
# Simple implementation of Doorway-Window approach for time-dependent spectrscopy
5+
6+
using DelimitedFiles
7+
using LinearAlgebra: mul!
8+
9+
10+
include("constants.jl")
11+
export eV2Ha, Ha2eV, fs2au, au2fs, nm2eV, eV2nm, eV2invcm, invcm2eV
12+
13+
include("types.jl")
14+
export SignalType, GSB, SE, ESA
15+
export Envelope, Gaussian, Lorentzian, Sech
16+
export Trajectory, LaserPulse, Dephasing
17+
18+
include("io.jl")
19+
export load_trajectory
20+
export delta_energy, n_states, n_steps
21+
22+
include("dw.jl")
23+
export dipole_moment, spectral_envelope
24+
export doorway, window, spectrum
425

526
end
27+

src/constants.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const Ha2eV = 27.2113862459 # Hartree -> eV
2+
const eV2Ha = 1.0 / Ha2eV # eV -> Hartree
3+
const fs2au = 41.341374575751 # fs -> a.u. time
4+
const au2fs = 1.0 / fs2au # a.u. time -> fs
5+
6+
# Careful! Energy is reciprocal of wavelength; these are dangerously named
7+
const eV2nm = 1239.84
8+
const nm2eV = 1.0 / eV2nm
9+
10+
const eV2invcm = 8065.5439373492107
11+
const invcm2eV = 1.0 / eV2invcm
12+
13+
# Factorial constants for Taylor expansions; a little bit OTT perhaps
14+
# I wonder if this actually gets any speed increase? Only used in Gaussian wavepackets? Are
15+
# those called repeatedly for the Window?
16+
const _inv_fact2 = 1 / factorial(2)
17+
const _inv_fact3 = 1 / factorial(3)
18+
const _inv_fact4 = 1 / factorial(4)
19+
const _inv_fact5 = 1 / factorial(5)
20+

src/dw.jl

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# magic broadcast with @.
2+
# factor 3//2 due to isotropic overage of dipole
3+
dipole_moment_sq(f, ΔE) = @. 3//2 * f / ΔE
4+
5+
# DOORWAYS
6+
7+
# "Beware of Doors." - Neverwhere
8+
9+
# GSB
10+
function doorway(::GSB, traj::Trajectory, pump::LaserPulse, ω_pump::Real, frame::Int)
11+
ΔE = delta_energy(traj, frame)
12+
μ² = dipole_moment_sq(traj.oscillators[:, frame], ΔE)
13+
Δω = @. ω_pump - ΔE
14+
τ_au = pump.τ * fs2au
15+
E = spectral_envelope(Δω, τ_au, pump.envelope, pump)
16+
return sum(@. E^2 * μ²) # returns a scalar, no mutation
17+
end
18+
19+
# SE
20+
function doorway(::SE, traj::Trajectory, pump::LaserPulse, ω_pump::Real, frame::Int)
21+
s = traj.active_state[frame]
22+
ΔE = traj.energies[s + 1, frame] - traj.energies[1, frame]
23+
μ² = dipole_moment_sq(traj.oscillators[s, frame], ΔE)
24+
Δω = ω_pump - ΔE
25+
τ_au = pump.τ * fs2au
26+
E = spectral_envelope(Δω, τ_au, pump.envelope, pump)
27+
return E^2 * μ² # scalar, single active state
28+
end
29+
30+
# WINDOWS
31+
32+
# GSB
33+
function window(::GSB, traj::Trajectory, probe::LaserPulse, ω_probe::AbstractVector)
34+
ΔE = delta_energy(traj) # (n_states-1) x n_steps
35+
μ² = dipole_moment_sq(traj.oscillators, ΔE) # (n_states-1) x n_steps
36+
n_t = n_steps(traj)
37+
n_ω = length(ω_probe)
38+
τ_au = probe.τ * fs2au
39+
40+
W = zeros(n_t, n_ω)
41+
for (j, ω) in enumerate(ω_probe)
42+
Δω = @. ω .- ΔE # (n_states-1) x n_steps
43+
E = spectral_envelope(Δω, τ_au, probe.envelope, probe)
44+
@views W[:, j] .= vec(sum(@. E^2 * μ²; dims=1)) # sum over states
45+
end
46+
return W
47+
end
48+
49+
# SE
50+
function window(::SE, traj::Trajectory, probe::LaserPulse, ω_probe::AbstractVector)
51+
n_t = n_steps(traj)
52+
n_ω = length(ω_probe)
53+
τ_au = probe.τ * fs2au
54+
55+
# Extract active-state ΔE and oscillator for each frame
56+
ΔE_active = Vector{Float64}(undef, n_t)
57+
f_active = Vector{Float64}(undef, n_t)
58+
for t in 1:n_t
59+
s = traj.active_state[t]
60+
ΔE_active[t] = traj.energies[s + 1, t] - traj.energies[1, t]
61+
f_active[t] = traj.oscillators[s, t]
62+
end
63+
64+
μ² = dipole_moment_sq(f_active, ΔE_active)
65+
# Guard against Inf/NaN from zero ΔE (trajectory at ground state)
66+
μ² .= ifelse.(isfinite.(μ²), μ², 0.0)
67+
68+
W = zeros(n_t, n_ω)
69+
for (j, ω) in enumerate(ω_probe)
70+
Δω = @. ω - ΔE_active
71+
E = spectral_envelope(Δω, τ_au, probe.envelope, probe)
72+
@views W[:, j] .= @. E^2 * μ²
73+
end
74+
return W
75+
end
76+
77+
# OK, generate the spectrum!
78+
79+
# Convenience function for same LaserPulse in both pump and probe
80+
function spectrum(sig::SignalType, traj::Trajectory,
81+
laser::LaserPulse, ω_pump::Real, ω_probe::AbstractVector)
82+
spectrum(sig, traj, laser, ω_pump, laser, ω_probe)
83+
end
84+
85+
"""
86+
Pump-probe spectrum: doorway (n_steps,) .* window (n_steps, n_probe).
87+
88+
User-facing frequencies ω_pump, ω_probe are in **eV**; internally converted
89+
to Hartree to match trajectory energies.
90+
91+
Returns: (n_steps, n_probe) matrix. Sum over time steps for integrated signal.
92+
"""
93+
function spectrum(sig::SignalType, traj::Trajectory,
94+
pump::LaserPulse, ω_pump::Real,
95+
probe::LaserPulse, ω_probe::AbstractVector)
96+
ω_pu_au = ω_pump * eV2Ha
97+
ω_pr_au = ω_probe .* eV2Ha
98+
d = [doorway(sig, traj, pump, ω_pu_au, t) for t in 1:n_steps(traj)]
99+
W = window(sig, traj, probe, ω_pr_au)
100+
return d .* W
101+
end
102+
103+

src/io.jl

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# ZagHop ASCII format (?) currently we just have the examples with Wavemixings to test with
2+
3+
function load_trajectory(energy_file::AbstractString, oscillator_file::AbstractString)
4+
# Energy file: #header, then columns [time, cst, etot, epot, qe001, qe002, ...]
5+
raw = readdlm(energy_file; comments=true, comment_char='#')
6+
7+
time = @view raw[:, 1]
8+
active_state = Int.(@view raw[:, 2])
9+
10+
# Columns 5:end are the adiabatic state energies (skip time, cst, etot, epot)
11+
# Transpose so rows = states, columns = time steps
12+
energies = permutedims(@view raw[:, 5:end])
13+
14+
# Handle duplicate time steps (surface hops produce repeated entries)
15+
keep = _unique_timesteps(time, active_state)
16+
17+
# Oscillator file: no header, columns are oscillator strengths per transition
18+
osc_raw = readdlm(oscillator_file; comments=true, comment_char='#')
19+
oscillators = permutedims(osc_raw)
20+
21+
Trajectory(
22+
time[keep],
23+
active_state[keep],
24+
energies[:, keep],
25+
max.(oscillators[:, keep], 0.0), # clamp negative oscillator strengths
26+
)
27+
end
28+
29+
function _unique_timesteps(time, active_state)
30+
# When a surface hop occurs, ZagHop seems to write the same time step twice (old state
31+
# then new state). Keep only the last entry for each time.
32+
keep = trues(length(time))
33+
for i in 1:(length(time) - 1)
34+
if time[i] == time[i + 1]
35+
keep[i] = false
36+
end
37+
end
38+
return keep
39+
end
40+

src/types.jl

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Abstract types for dispatch
2+
3+
abstract type SignalType end
4+
struct GSB <: SignalType end # Ground State Bleach
5+
struct SE <: SignalType end # Stimulated Emission
6+
struct ESA <: SignalType end # Excited State Absorption
7+
8+
abstract type Envelope end
9+
struct Gaussian <: Envelope end
10+
struct Lorentzian <: Envelope end
11+
struct Sech <: Envelope end
12+
13+
14+
15+
"""
16+
Trajectory data from a non-adiabatic (surface-hopping kind presumed) simulation.
17+
18+
Fields:
19+
- `time::Vector{Float64}`: simulation times (fs)
20+
- `active_state::Vector{Int}`: occupied electronic state (density?) at each step
21+
- `energies::Matrix{Float64}`: adiabatic energies (n_states x n_steps), Hartree
22+
- `oscillators::Matrix{Float64}`: oscillator strengths ((n_states-1) x n_steps)
23+
"""
24+
struct Trajectory
25+
time::Vector{Float64}
26+
active_state::Vector{Int}
27+
energies::Matrix{Float64}
28+
oscillators::Matrix{Float64}
29+
end
30+
31+
n_states(t::Trajectory) = size(t.energies, 1)
32+
n_steps(t::Trajectory) = size(t.energies, 2)
33+
34+
"""ΔE_e = E_e - E_ground for all excited states at one frame."""
35+
delta_energy(traj::Trajectory, frame::Int) =
36+
@views traj.energies[2:end, frame] .- traj.energies[1, frame]
37+
38+
"""ΔE matrix for all frames: (n_states-1) x n_steps."""
39+
delta_energy(traj::Trajectory) =
40+
@views traj.energies[2:end, :] .- traj.energies[1:1, :]
41+
42+
43+
"""
44+
Laser pulse characteristics (envelope shape, duration, dispersion).
45+
(Nb: central frequency passed elsewhere; this is just arb pulse!)
46+
47+
Fields:
48+
- `envelope::E`: spectral envelope shape (Gaussian, Lorentzian, Sech, etc.)
49+
- `τ::Float64`: pulse duration (fs)
50+
- `D2`--`D5`: dispersion coefficients (fs^n), default 0
51+
"""
52+
@kwdef struct LaserPulse{E<:Envelope}
53+
envelope::E = Gaussian()
54+
τ::Float64
55+
D2::Float64 = 0.0
56+
D3::Float64 = 0.0
57+
D4::Float64 = 0.0
58+
D5::Float64 = 0.0
59+
end
60+
61+
"""Dephasing rate for dispersed and 2D calculations."""
62+
@kwdef struct Dephasing
63+
γ::Float64 = 0.01 # eV
64+
end
65+
# not used yet
66+
67+
# Nb: spectra envelope stuff needs to be down here, as depends on LaserPulse having been ready by Julia
68+
"""
69+
spectral_envelope(Δω, τ_au, envelope, pulse)
70+
71+
Spectral field envelope E(Δω, τ) in the frequency domain.
72+
73+
Returns the field amplitude. Signal formulas use E² (proportional to intensity).
74+
All envelopes include τ_au prefactor for normalisation.
75+
"""
76+
function spectral_envelope(Δω, τ_au::Real, ::Gaussian, p::LaserPulse)
77+
@. τ_au * exp(
78+
-(Δω * τ_au)^2 / 4 - (
79+
_inv_fact2 * p.D2 * Δω^2 +
80+
_inv_fact3 * p.D3 * Δω^3 +
81+
_inv_fact4 * p.D4 * Δω^4 +
82+
_inv_fact5 * p.D5 * Δω^5
83+
)
84+
)
85+
end
86+
87+
# Lorentzian
88+
function spectral_envelope(Δω, τ_au::Real, ::Lorentzian, ::LaserPulse)
89+
@. τ_au * exp(-2 * abs(Δω) * τ_au)
90+
end
91+
92+
# Sech
93+
function spectral_envelope(Δω, τ_au::Real, ::Sech, ::LaserPulse)
94+
@. τ_au * sech* Δω * τ_au / 2)
95+
end

0 commit comments

Comments
 (0)