diff --git a/src/constants.jl b/src/constants.jl index cf7235b..080ace3 100644 --- a/src/constants.jl +++ b/src/constants.jl @@ -2,7 +2,7 @@ module PhysicalConstants import Base.sqrt -export μB, ħ, c, e, ϵ₀, α, kB, eye3, c_rank1, c_rank2 +export μB, ħ, c, e, ϵ₀, α, kB, gₛ, eye3, c_rank1, c_rank2 """ PhysicalConstant(x::Real) @@ -31,6 +31,8 @@ const ϵ₀ = PhysicalConstant(8.85418782e-12, "(s^4A^2) / (m^3 kg)") const α = PhysicalConstant(0.007297352557920479, "") """`kB` = 1.38064852e-23 ``m^2kg/(s^2K)``""" const kB = PhysicalConstant(1.38064852e-23, "m^2kg/(s^2K)") +"""`gₛ` = -2.00231930436256""" +const gₛ = PhysicalConstant(-2.00231930436256, "") ############################################################################################# # 3D real-space tensors diff --git a/src/ions.jl b/src/ions.jl index d24ea4b..03875c8 100644 --- a/src/ions.jl +++ b/src/ions.jl @@ -1,6 +1,6 @@ using WignerSymbols: wigner3j, wigner6j using LinearAlgebra: cross -#using .PhysicalConstants: e, ħ, α, μB, e, eye3, c_rank1, c_rank2 +#using .PhysicalConstants: e, ħ, α, μB, e, gₛ, eye3, c_rank1, c_rank2 using IonSim.PhysicalConstants export Ion, @@ -396,6 +396,7 @@ function transitionwavelength(I::Ion, transition::Tuple; B = 0, ignore_starkshif transitionfrequency(I, transition, B = B, ignore_starkshift = ignore_starkshift) end +#TODO: Add M1s here!! """ leveltransitions(I::Ion) Returns all allowed transitions between levels of `I` as a vector of `Tuple{String,String}`. @@ -554,6 +555,28 @@ function matrix_element( units_factor = abs(e * Efield / (2ħ) * sqrt(15 * A12 / (α * c * k^3))) return units_factor * hyperfine_factor * geometric_factor / 2π end + elseif multipole == "M1" + # as implemented, this will error as it requires an API change to include l and s. + # I'd like to consider adding "term" as a data structure. Almost always, these arguments are + # being passed together, and I think it would be useful to have them in a structure that's + # easy to unpack, even if just to ease the syntax burden on the user + if abs(q) > 1 + return 0 + else + hyperfine_factor = abs( + sqrt((2 * f1 + 1) * (2 * f2 + 1) * (2 * j1 + 1) * (2 * j2 + 1)) * wigner6j(j2, I, f2, f1, 1, j1) * + ( + wigner6j(l, j1, s, j2, l, 1) sqrt((2 * l + 1) * (l + 1) * l) + + gₛ * wigner6j(s, j1, l, j2, s, 1) sqrt((2 * s + 1) * (s + 1) * s) + )) + geometric_factor = abs( + sqrt(2j2 + 1) * + wigner3j(f2, 1, f1, -m2, q, m1) * + (transpose(c_rank1[q + 2, :]) * cross(khat_rotated, ϵhat_rotated)) + ) + units_factor = e * mu_B * Efield / c + return units_factor * hyperfine_factor * geometric_factor / 2π + end else @error "calculation of atomic transition matrix element for transition type $multipole not currently supported" end diff --git a/src/lasers.jl b/src/lasers.jl deleted file mode 100644 index 46b9247..0000000 --- a/src/lasers.jl +++ /dev/null @@ -1,107 +0,0 @@ -using .PhysicalConstants: c - -export Laser - -""" - Laser(;λ=nothing, E=0, Δ=0, ϵ=(x̂+ŷ)/√2, k=ẑ, ϕ=0, pointing::Array{Tuple{Int,Real}}) - -The physical parameters defining laser light. -**args** -* `λ::Union{Real,Nothing}`: the wavelength of the laser in meters -* `E::Union{Function,Real}`: magnitude of the E-field in V/m -* `Δ`: static detuning from f = c/λ in [Hz] -* `ϵ::NamedTuple`: (ϵ.x, ϵ.y, ϵ.z), polarization direction, requires norm of 1 -* `k::NamedTuple`: (k.x, k.y, k.z), propagation direction, requires norm of 1 -* `ϕ::Union{Function,Real}`: time-dependent phase. of course, this can also be used to model a - time-dependent detuning. Units are in radians. Note: if this is set to a function of time, - then when constructing a Hamiltonian with the `hamiltonian` function, the units of time - will be as specified by the `timescale` keyword argument. -* `pointing`: an array of `Tuple{Int,Real}` for describing ion-laser pointing configuration. - (first element of the tuple is the index for an ion and the second element is the scaling - factor for the laser's Efield which must be between 0 and 1). -""" -mutable struct Laser - λ::Union{Real, Nothing} - E::Function - Δ::Real - ϵ::NamedTuple{(:x, :y, :z)} - k::NamedTuple{(:x, :y, :z)} - ϕ::Function - pointing::Vector - function Laser(; - λ = nothing, - E::TE = 0, - Δ = 0, - ϵ = (x̂ + ŷ) / √2, - k = ẑ, - ϕ::Tϕ = 0, - pointing = Array{Tuple{Int, <:Real}}(undef, 0) - ) where {TE, Tϕ} - rtol = 1e-6 - @assert isapprox(norm(ϵ), 1, rtol = rtol) "!(|ϵ| = 1)" - @assert isapprox(norm(k), 1, rtol = rtol) "!(|k| = 1)" - # @assert isapprox(ndot(ϵ, k), 0, rtol=rtol) "!(ϵ ⟂ k)" - # Above commented out until we figure out a better place to put this warning - a = pointing - (ion_num, scaling) = map(x -> getfield.(a, x), fieldnames(eltype(a))) - @assert length(ion_num) == length(unique(ion_num)) ( - "a laser is pointing at the same ion twice" - ) - for s in scaling - @assert 0 <= s <= 1 "must have s ∈ [0,1]" - end - TE <: Number ? Et(t) = E : Et = E - Tϕ <: Number ? ϕt(t) = ϕ : ϕt = ϕ - return new(λ, Et, Δ, ϵ, k, ϕt, pointing) - end - # for copying - Laser(λ, E, Δ, ϵ, k, ϕ, pointing) = new(λ, E, Δ, ϵ, k, ϕ, pointing) -end - -function Base.:(==)(L1::Laser, L2::Laser) - for field in fieldnames(Laser) - if getfield(L1, field) != getfield(L2, field) - return false - end - end - return true -end - -function Base.print(L::Laser) - println("λ: ", L.λ, " m") - println("Δ: ", L.Δ, " Hz") - println("ϵ̂: ", "(x=$(L.ϵ.x), y=$(L.ϵ.y), z=$(L.ϵ.z))") - println("k̂: ", "(z=$(L.k.x), y=$(L.k.y), z=$(L.k.z))") - println("E(t=0): ", "$(L.E(0.0)) V/m") - return println("ϕ(t=0): ", "$(L.ϕ(0.0)) ⋅ 2π") -end - -function Base.setproperty!(L::Laser, s::Symbol, v::Tv) where {Tv} - rtol = 1e-6 - if s == :ϵ - @assert isapprox(norm(v), 1, rtol = rtol) "!(|ϵ| = 1)" - # if ! isapprox(ndot(L.k, v), 0, rtol=rtol) - # @warn "!(ϵ ⟂ k)" - # end - elseif s == :k - @assert isapprox(norm(v), 1, rtol = rtol) "!(|k| = 1)" - # if ! isapprox(ndot(v, L.ϵ), 0, rtol=rtol) - # @warn "!(ϵ ⟂ k)" - # end - elseif s == :pointing - b = Tv <: Vector{Tuple{Int64, Float64}} || Tv <: Vector{Tuple{Int64, Int64}} - @assert b "type != Vector{Tuple{Int,Real}}" - (ion_num, scaling) = map(x -> getfield.(v, x), fieldnames(eltype(v))) - @assert length(ion_num) == length(unique(ion_num)) ( - "a laser is pointing at the same ion twice" - ) - for s in scaling - @assert 0 <= s <= 1 "must have s ∈ [0,1]" - end - elseif s == :E || s == :ϕ - Tv <: Number ? vt(t) = v : vt = v - Core.setproperty!(L, s, vt) - return - end - return Core.setproperty!(L, s, v) -end diff --git a/src/lightfields.jl b/src/lightfields.jl new file mode 100644 index 0000000..dc4e11e --- /dev/null +++ b/src/lightfields.jl @@ -0,0 +1,187 @@ +using .PhysicalConstants: c, ϵ₀ + +export Laser, Microwave + +abstract type LightField end + +function Base.:(==)(LF1::LightField, LF2::LightField) + if typeof(LF1) != typeof(LF2) + return false + end + for field in fieldnames(typeof(LF1)) + if getfield(LF1, field) != getfield(LF2, field) + return false + end + end + return true +end + +function Base.print(LF::LightField) + println("λ: ", LF.λ, " m") + println("Δ: ", LF.Δ, " Hz") + println("ϵ̂: ", "(x=$(LF.ϵ.x), y=$(LF.ϵ.y), z=$(LF.ϵ.z))") + println("k̂: ", "(z=$(LF.k.x), y=$(LF.k.y), z=$(LF.k.z))") + println("E(t=0): ", "$(LF.E(0.0)) V/m") + return println("ϕ(t=0): ", "$(LF.ϕ(0.0)) ⋅ 2π") +end + + +""" + Laser(;λ=nothing, E=0, Δ=0, ϵ=(x̂+ŷ)/√2, k=ẑ, ϕ=0, pointing::Array{Tuple{Int,Real}}) + +The physical parameters defining laser light. +**args** +* `λ::Union{Real,Nothing}`: the wavelength of the laser in meters +* `E::Union{Function,Real}`: magnitude of the E-field in V/m +* `Δ`: static detuning from f = c/λ in [Hz] +* `ϵ::NamedTuple`: (ϵ.x, ϵ.y, ϵ.z), polarization direction, requires norm of 1 +* `k::NamedTuple`: (k.x, k.y, k.z), propagation direction, requires norm of 1 +* `ϕ::Union{Function,Real}`: time-dependent phase. of course, this can also be used to model a + time-dependent detuning. Units are in radians. Note: if this is set to a function of time, + then when constructing a Hamiltonian with the `hamiltonian` function, the units of time + will be as specified by the `timescale` keyword argument. +* `pointing`: an array of `Tuple{Int,Real}` for describing ion-laser pointing configuration. + (first element of the tuple is the index for an ion and the second element is the scaling + factor for the laser's Efield which must be between 0 and 1). +""" +mutable struct Laser <: LightField + λ::Union{Real, Nothing} + E::Function + Δ::Real + ϵ::NamedTuple{(:x, :y, :z)} + k::NamedTuple{(:x, :y, :z)} + ϕ::Function + pointing::Vector + function Laser(; + λ = nothing, + E::TE = 0, + Δ = 0, + ϵ = (x̂ + ŷ) / √2, + k = ẑ, + ϕ::Tϕ = 0, + pointing = Array{Tuple{Int, <:Real}}(undef, 0) + ) where {TE, Tϕ} + rtol = 1e-6 + @assert isapprox(norm(ϵ), 1, rtol = rtol) "!(|ϵ| = 1)" + @assert isapprox(norm(k), 1, rtol = rtol) "!(|k| = 1)" + # @assert isapprox(ndot(ϵ, k), 0, rtol=rtol) "!(ϵ ⟂ k)" + # Above commented out until we figure out a better place to put this warning + a = pointing + (ion_num, scaling) = map(x -> getfield.(a, x), fieldnames(eltype(a))) + @assert length(ion_num) == length(unique(ion_num)) ( + "a laser is pointing at the same ion twice" + ) + for s in scaling + @assert 0 <= s <= 1 "must have s ∈ [0,1]" + end + TE <: Number ? Et(t) = E : Et = E + Tϕ <: Number ? ϕt(t) = ϕ : ϕt = ϕ + return new(λ, Et, Δ, ϵ, k, ϕt, pointing) + end + # for copying + Laser(λ, E, Δ, ϵ, k, ϕ, pointing) = new(λ, E, Δ, ϵ, k, ϕ, pointing) +end + +function Base.setproperty!(L::Laser, s::Symbol, v::Tv) where {Tv} + rtol = 1e-6 + if s == :ϵ + @assert isapprox(norm(v), 1, rtol = rtol) "!(|ϵ| = 1)" + # if ! isapprox(ndot(L.k, v), 0, rtol=rtol) + # @warn "!(ϵ ⟂ k)" + # end + elseif s == :k + @assert isapprox(norm(v), 1, rtol = rtol) "!(|k| = 1)" + # if ! isapprox(ndot(v, L.ϵ), 0, rtol=rtol) + # @warn "!(ϵ ⟂ k)" + # end + elseif s == :pointing + b = Tv <: Vector{Tuple{Int64, Float64}} || Tv <: Vector{Tuple{Int64, Int64}} + @assert b "type != Vector{Tuple{Int,Real}}" + (ion_num, scaling) = map(x -> getfield.(v, x), fieldnames(eltype(v))) + @assert length(ion_num) == length(unique(ion_num)) ( + "a laser is pointing at the same ion twice" + ) + for s in scaling + @assert 0 <= s <= 1 "must have s ∈ [0,1]" + end + elseif s == :E || s == :ϕ + Tv <: Number ? vt(t) = v : vt = v + Core.setproperty!(L, s, vt) + return + end + return Core.setproperty!(L, s, v) +end + +# This could probably be made into one function with Unitfuls? +laser_from_intensity(λ, i, Δ, ϵ, k, ϕ, pointing) = Laser(λ, sqrt(2i / (c*ϵ₀)), Δ, ϵ, k, ϕ, pointing) +laser_from_waist_power(λ, P, Δ, ϵ, k, ϕ, pointing, w₀) = laser_from_intensity(λ, 2P/(π*w₀^2), Δ, ϵ, k, ϕ, pointing) + +""" + Microwave(;λ=nothing, E=0, Δ=0, ϵ=(x̂+ŷ)/√2, k=ẑ, ϕ=0) + +The physical parameters defining microwave radiation. +**args** +* `λ::Union{Real,Nothing}`: the wavelength of the microwave in meters +* `E::Union{Function,Real}`: magnitude of the E-field in V/m +* `Δ`: static detuning from f = c/λ in [Hz] +* `ϵ::NamedTuple`: (ϵ.x, ϵ.y, ϵ.z), polarization direction, requires norm of 1 +* `k::NamedTuple`: (k.x, k.y, k.z), propagation direction, requires norm of 1 +* `ϕ::Union{Function,Real}`: time-dependent phase. of course, this can also be used to model a + time-dependent detuning. Units are in radians. Note: if this is set to a function of time, + then when constructing a Hamiltonian with the `hamiltonian` function, the units of time + will be as specified by the `timescale` keyword argument. +""" +mutable struct Microwave <: LightField + λ::Union{Real, Nothing} + E::Function + Δ::Real + ϵ::NamedTuple{(:x, :y, :z)} + k::NamedTuple{(:x, :y, :z)} + ϕ::Function + pointing::Array{Tuple{Int, <:Real}}(undef, 0) + function Microwave(; + λ = nothing, + E::TE = 0, + Δ = 0, + ϵ = (x̂ + ŷ) / √2, + k = ẑ, + ϕ::Tϕ = 0, + ) where {TE, Tϕ} + rtol = 1e-6 + @assert isapprox(norm(ϵ), 1, rtol = rtol) "!(|ϵ| = 1)" + @assert isapprox(norm(k), 1, rtol = rtol) "!(|k| = 1)" + # @assert isapprox(ndot(ϵ, k), 0, rtol=rtol) "!(ϵ ⟂ k)" + # Above commented out until we figure out a better place to put this warning + TE <: Number ? Et(t) = E : Et = E + Tϕ <: Number ? ϕt(t) = ϕ : ϕt = ϕ + + return new(λ, Et, Δ, ϵ, k, ϕt, []) + end + # for copying + Microwave(λ, E, Δ, ϵ, k, ϕ) = new(λ, E, Δ, ϵ, k, ϕ) +end + + +function Base.setproperty!(MW::Microwave, s::Symbol, v::Tv) where {Tv} + rtol = 1e-6 + if s == :ϵ + @assert isapprox(norm(v), 1, rtol = rtol) "!(|ϵ| = 1)" + # if ! isapprox(ndot(L.k, v), 0, rtol=rtol) + # @warn "!(ϵ ⟂ k)" + # end + elseif s == :k + @assert isapprox(norm(v), 1, rtol = rtol) "!(|k| = 1)" + # if ! isapprox(ndot(v, L.ϵ), 0, rtol=rtol) + # @warn "!(ϵ ⟂ k)" + # end + elseif s == :E || s == :ϕ + Tv <: Number ? vt(t) = v : vt = v + Core.setproperty!(MW, s, vt) + return + end + return Core.setproperty!(MW, s, v) +end + +# This could probably be made into one function with Unitfuls? +microwave_from_B(λ, B, Δ, ϵ, k, ϕ) = Microwave(λ, B*c, Δ, ϵ, k, ϕ) +microwave_from_intensity(λ, i, Δ, ϵ, k, ϕ, pointing) = Microwave(λ, sqrt(2i / (c*ϵ₀)), Δ, ϵ, k, ϕ) \ No newline at end of file diff --git a/src/traps.jl b/src/traps.jl index a4321ae..3d3a349 100644 --- a/src/traps.jl +++ b/src/traps.jl @@ -31,7 +31,8 @@ interacting with laser light. * `∇B`: Magnitude of the B-field gradient. We assume that the gradient always points along the z-direction. [Tesla / meter] * `δB::Function`: Time-dependence of the B-field [Tesla] -* `lasers::Array{<:Laser}`: For each laser in the array, the pointing field should contain +* `lasers::Array{<:LightField}`: Microwaves are globally addressed. + For each laser in the array, the pointing field should contain an array of `Tuple{Int,Real}`. The first element specifies the index of an ion in the `ions` field that the laser interacts with. The second element specifies a scaling factor for the strength of that interaction (to be used, e.g., for @@ -66,7 +67,7 @@ mutable struct Trap Bhat::NamedTuple{(:x, :y, :z)} ∇B::Real δB::Function - lasers::Array{<:Laser} + lasers::Array{<:LightField} #rename to lightfields? basis::CompositeBasis _cnst_δB::Bool function Trap(; @@ -173,15 +174,18 @@ function get_basis(T::Trap)::CompositeBasis end """ - global_beam!(T::Trap, laser::Laser) -Set `laser` to shine with full intensity on all ions in `Trap`. + global_beam!(T::Trap, lightfield::LightField) +Set `lightfield` to shine with full intensity on all ions in `Trap`. """ -function global_beam!(T::Trap, laser::Laser) +function global_beam!(T::Trap, lightfield::LightField) for n in eachindex(T.configuration.ions) - push!(laser.pointing, (n, 1.0)) + push!(lightfield.pointing, (n, 1.0)) end end +# TODO: Bfield_from_pi_time, for M1 transitions +# TODO: Bfield_from_rabi_frequency + """ Efield_from_pi_time( pi_time::Real, Bhat::NamedTuple{(:x,:y,:z)}, laser::Laser, ion::Ion, @@ -343,7 +347,7 @@ end """ Bfield(T::Trap, ion::Ion) -Retuns the value of the magnetic field in `T` at the location of `ion`, including both the trap's overall B-field and its B-field gradient. +Returns the value of the magnetic field in `T` at the location of `ion`, including both the trap's overall B-field and its B-field gradient. """ function Bfield(T::Trap, ion::Ion) @assert ionintrap(T, ion) "trap does not contain ion" @@ -397,16 +401,16 @@ transitionwavelength( ) """ - matrix_element(I::Ion, transition::Tuple, T::Trap, laser::Laser, time::Real) + matrix_element(I::Ion, transition::Tuple, T::Trap, lightfield::LightField, time::Real) Calls `matrix_element(I::Ion, transition::Tuple, Efield::Real, khat::NamedTuple, ϵhat::NamedTuple, Bhat::NamedTuple=(;z=1))` with `Efield`, `khat`, and `ϵhat` evaluated for `laser` at time `time`, and `Bhat` evaluated for `T`. One may alternatively replace `ion` with `ion_index`::Int, which instead specifies the index of the intended ion within `T`. """ -matrix_element(I::Ion, transition::Tuple, T::Trap, laser::Laser, time::Real) = - matrix_element(I, transition, laser.E(time), laser.k, laser.ϵ, T.Bhat) -matrix_element(ion_index::Int, transition::Tuple, T::Trap, laser::Laser, time::Real) = - matrix_element(T.configuration.ions[ion_index], transition, T, laser, time) +matrix_element(I::Ion, transition::Tuple, T::Trap, lightfield::LightField, time::Real) = + matrix_element(I, transition, lightfield.E(time), lightfield.k, lightfield.ϵ, T.Bhat) +matrix_element(ion_index::Int, transition::Tuple, T::Trap, lightfield::LightField, time::Real) = + matrix_element(T.configuration.ions[ion_index], transition, T, lightfield, time) """ zeeman_shift(I::Ion, sublevel, T::Trap)