# # Laser with Filter Cavities

# An intuitive and straightforward approach to calculate the spectrum of a laser is to filter the emitted light. We can do this by coupling filter cavities with different detunings to the main cavity and observe the photon number in these 'filters', see for example [K. Debnath et al., Phys Rev A 98, 063837 (2018)](https://journals.aps.org/pra/abstract/10.1103/PhysRevA.98.063837).

# The main goal of this example is to combine two indexed Hilbert spaces, where one will be scaled and the other evaluated. The model is basically the same as for the [superradiant laser](https://qojulia.github.io/QuantumCumulants.jl/stable/examples/superradiant_laser_indexed/) example, but with the additional terms due to the filter cavities. The Hamiltonian of this system is

# ```math
# \begin{equation}
# H = - \Delta a^\dagger a + g \sum\limits_{j=1}^{N} (a^\dagger \sigma^{12}_{j} + a \sigma^{21}_{j}) - \sum\limits_{i=1}^{M} \delta_i b_i^\dagger b_i +  g_f \sum\limits_{i=1}^{M} (a^\dagger b_i + a b_i^\dagger),
# \end{equation}
# ```

# where $\delta_i$ is the detuning of the $i$-th filter cavity and $g_f$ the coupling to the normal cavity. Furthermore, their decay rate is $\kappa_f$.

# We start by loading the packages.

using QuantumCumulants
using OrdinaryDiffEqTsit5, ModelingToolkitBase
using Plots

# We create the parameters of the system including the $\texttt{IndexedVariable}$ $\delta_i$. For the atoms and filter cavities we only need one Hilbert space each. We define the indices for each Hilbert space and use them to create $\texttt{IndexedOperators}$.

@variables κ g gf κf R Γ Δ ν N M # Parameters
δ(i) = IndexedVariable(:δ, i)

hc = FockSpace(:cavity) # Hilbert spaces
hf = FockSpace(:filter)
ha = NLevelSpace(:atom, 2)
h = hc ⊗ hf ⊗ ha

i = Index(h, :i, M, hf) # Indices
j = Index(h, :j, N, ha)

@qnumbers a::Destroy(h, 1)
b(k) = IndexedOperator(Destroy(h, :b, 2), k)
nothing # hide

# `i` is bound by the Hamiltonian sums and the dissipator. The canonical
# free-index slot on the filter Hilbert space mints `i_2` (lex-first
# declared index, suffix 2) to keep state names disjoint from H's bound
# scope. Per-atom state lookups thus use `i_2(k) = i_2_k`, not `i(k)`.
b(k::Integer) = IndexedOperator(Destroy(h, :b, 2), i(2)(k))
σ(α, β, k) = IndexedOperator(Transition(h, :σ, α, β, 3), k)
σ(α, β, k::Integer) = IndexedOperator(Transition(h, :σ, α, β, 3), j(k))
nothing # hide

# We define the Hamiltonian using symbolic sums and define the individual dissipative processes. For an indexed jump operator the (symbolic) sum is build in the Liouvillian, in this case corresponding to individual decay processes.

H =
    Δ * Σ(σ(2, 2, j), j) +
    Σ(δ(i) * b(i)'b(i), i) +
    gf * (Σ(a' * b(i) + a * b(i)', i)) +
    g * (Σ(a' * σ(1, 2, j) + a * σ(2, 1, j), j)) # Hamiltonian

J = [a, b(i), σ(1, 2, j), σ(2, 1, j), σ(2, 2, j)] # Jumps & rates
rates = [κ, κf, Γ, R, ν]
nothing # hide

# We derive the equation for $\langle a^\dagger a \rangle$ and complete the system automatically in second order.

eqs = meanfield(a'a, H, J; rates = rates, order = 2)

eqs_c = complete!(deepcopy(eqs));
nothing # hide

# Now we assume that all atoms behave identically, but we want to obtain the equations for 20 different filter cavities. To this end we $\texttt{scale}$ the Hilbert space of the atoms and $\texttt{evaluate}$ the filter cavities. Specifying the Hilbert space is done with the kwarg $\texttt{h}$, which can either be the specific Hilbert space or its acts-on number. Evaluating the filter cavities requires a numeric upper bound for the used $\texttt{Index}$, we provide this with a dictionary on the kwarg $\texttt{limits}$.

M_ = 20
eqs_sc = scale(eqs_c; h = [3])
eqs_eval = evaluate(eqs_sc; limits = Dict(M => M_))
println("Number of eqs.: $(length(eqs_eval))")

# To calculate the dynamics of the system we create a system of ordinary differential equations, which can be used by [DifferentialEquations.jl](https://diffeq.sciml.ai/stable/). Finally we need to define the numerical parameters and the initial state of the system.

sys = System(eqs_eval; name = :sys)
sys_c = mtkcompile(sys)
nothing # hide

#

u0 = zeros(ComplexF64, length(eqs_eval)) # Initial state


N_ = 200 # System parameters
Γ_ = 1.0
Δ_ = 0Γ_
g_ = 1Γ_
κ_ = 100Γ_
R_ = 10Γ_
ν_ = 1Γ_

gf_ = 0.1Γ_
κf_ = 0.1Γ_
δ_ls = [0:(1 / M_):(1 - 1 / M_);] * 10Γ_

pmap = parameter_map(
    eqs_eval, Dict(
        Γ => Γ_, κ => κ_, g => g_, κf => κf_, gf => gf_, R => R_,
        δ(i) => δ_ls,
        Δ => Δ_, ν => ν_, N => N_,
    )
)
dict = merge(initial_values(eqs_eval, u0), pmap)
prob = ODEProblem(sys_c, dict, (0.0, 10.0 / κf_))
nothing # hide

#

sol = solve(prob, Tsit5(); abstol = 1.0e-10, reltol = 1.0e-10, maxiters = 1.0e7) # Solve the numeric problem

t = sol.t
n = abs.(get_solution(sol, a'a, eqs_eval).(t))
n_b(i) = abs.(get_solution(sol, b(i)'b(i), eqs_eval).(t))
n_f = [abs(get_solution(sol, b(i)'b(i), eqs_eval)(t[end])) for i in 1:M_] ./
    (abs(get_solution(sol, b(1)'b(1), eqs_eval)(t[end])))
nothing # hide

#

p1 = plot(t, n_b(1), alpha = 0.5, ylabel = "⟨bᵢ⁺bᵢ⟩", legend = false) # Plot results
for i in 2:M_
    plot!(t, n_b(i), alpha = 0.5, legend = false)
end

p2 = plot(
    [-reverse(δ_ls); δ_ls],
    [reverse(n_f); n_f],
    xlabel = "δ/Γ",
    ylabel = "intensity",
    legend = false,
)
plot(p1, p2, layout = (1, 2), size = (700, 300))

# ## Package versions

# These results were obtained using the following versions:

using InteractiveUtils
versioninfo()

using Pkg
Pkg.status(
    ["QuantumCumulants", "OrdinaryDiffEqTsit5", "ModelingToolkitBase", "Plots"],
    mode = PKGMODE_MANIFEST,
)
