Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
dc6ef92
Add multi-wavelength spectroscopy tutorial
aditya-pandey-dev Mar 20, 2026
c20e924
Fix: add newline at end of Project.toml
aditya-pandey-dev Mar 20, 2026
f15060a
Fix: remove strict compat bounds causing DustExtinction conflict
aditya-pandey-dev Mar 20, 2026
699466e
Fix: remove Spectra.jl from docs/Project.toml
aditya-pandey-dev Mar 20, 2026
b7ea6ad
Fix: revert docs/Project.toml to original deps
aditya-pandey-dev Mar 20, 2026
6830528
Fix: restore all required deps in docs/Project.toml
aditya-pandey-dev Mar 20, 2026
38d30bd
Fix: remove test_tutorial.jl from test/ directory
aditya-pandey-dev Mar 20, 2026
cdea606
Fix: address all cgarling review comments
Mar 31, 2026
249da20
Fix: address all cgarling review comments
Apr 1, 2026
e7c0108
Fix: use plain julia blocks (Spectra.jl not in General registry yet)
Apr 1, 2026
734911c
Fix: correct Cosmology.jl UUID in docs/Project.toml
Apr 1, 2026
ffe896c
Fix: revert docs/Project.toml to original - plain julia blocks need n…
Apr 1, 2026
fa112c1
Fix: complete tutorial rewrite - CairoMakie, no Plots.jl, all mentor …
Apr 1, 2026
8b26c8e
Fix: use real SDSS DR14 spectrum with ivar-based uncertainties in Part 1
Apr 1, 2026
56c3006
Update docs/src/tutorials/multi-wavelength-spectroscopy.md
icweaver May 15, 2026
d00ac88
Update docs/src/tutorials/multi-wavelength-spectroscopy.md
icweaver May 15, 2026
528b74f
Update docs/src/tutorials/multi-wavelength-spectroscopy.md
icweaver May 15, 2026
3a651be
Update docs/src/tutorials/multi-wavelength-spectroscopy.md
icweaver May 15, 2026
5ba3cab
Merge branch 'main' into add-spectroscopy-tutorial
icweaver May 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ makedocs(
"tutorials/tabular-data.md",
"tutorials/curve-fit.md",
],
"Spectroscopy" => [
"Multi-wavelength spectroscopy" => "tutorials/multi-wavelength-spectroscopy.md",
],
],
"Package Ecosystem" => "ecosystem.md",
case_studies,
Expand Down
4 changes: 4 additions & 0 deletions docs/src/tutorials/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,7 @@ The following tutorials show how to use Julia to perform common tasks in astrono
* [Load tabular data from a FITS file and plot acceleration of nearby stars](@ref tabular-data)

* [Curve fitting: least square and Bayesian](@ref curve-fit)

### Spectroscopy

* [Multi-wavelength spectroscopy: loading, transforming, and analyzing spectra](@ref tutorial-multiwavelength-spectroscopy)
191 changes: 191 additions & 0 deletions docs/src/tutorials/multi-wavelength-spectroscopy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# [Multi-wavelength spectroscopy](@id tutorial-multiwavelength-spectroscopy)

Spectroscopy is one of the most powerful tools in an astronomer's toolkit. By splitting
light into its component wavelengths, we can learn about a source's chemical composition,
temperature, velocity, and much more.

In this tutorial we walk through a realistic end-to-end spectroscopy workflow using:

- [`Spectra.jl`](https://juliaastro.org/Spectra/stable/): core spectral types
- [`Unitful.jl`](https://painterqubits.github.io/Unitful.jl/stable/) + [`UnitfulAstro.jl`](https://juliaastro.org/UnitfulAstro/stable/): physical units
- [`Measurements.jl`](https://github.com/JuliaPhysics/Measurements.jl): uncertainty propagation
- [`DustExtinction.jl`](https://juliaastro.org/DustExtinction/stable/): dust dereddening
- [`Cosmology.jl`](https://juliaastro.org/Cosmology/stable/): cosmological distances
- [CairoMakie](https://docs.makie.org/stable/): plotting

## Setup
```julia
using Unitful, UnitfulAstro, DustExtinction
using Measurements, Cosmology, CairoMakie, SkyCoords
using Spectra
```

## Part 1: Loading a real SDSS spectrum

We use a publicly available spectrum from SDSS DR14 — a galaxy on
plate 1323, MJD 52797, fiber 12. The FITS file stores flux in units
of 10⁻¹⁷ erg s⁻¹ cm⁻² Å⁻¹ and an `ivar` (inverse-variance) column
that encodes real per-pixel measurement uncertainties.
```julia
using Downloads, FITSIO, Unitful, UnitfulAstro, Measurements
Comment thread
icweaver marked this conversation as resolved.

# Download the spectrum (skipped if already present)
sdss_url = "https://dr14.sdss.org/optical/spectrum/view/data/format=fits/spec=lite" *
"?plateid=1323&mjd=52797&fiberid=12"
sdss_file = joinpath(@__DIR__, "sdss_example.fits")
isfile(sdss_file) || Downloads.download(sdss_url, sdss_file)

# Read spectral columns
f = FITS(sdss_file)
loglam = read(f[2], "loglam") # log₁₀(λ / Å)
flux_raw = read(f[2], "flux") # 10⁻¹⁷ erg s⁻¹ cm⁻² Å⁻¹
ivar = read(f[2], "ivar") # inverse variance
close(f)

# Convert wavelength + attach units
wave_raw = 10 .^ loglam # Å (plain numbers for dust map)
wave_aa = wave_raw .* u"angstrom" # with units

# Real per-pixel σ from ivar: σ = 1/√ivar
sigma_raw = map(iv -> iv > 0 ? 1/sqrt(iv) : 0.0, ivar)

# Attach units + Measurements.jl uncertainties
flux_meas = ((flux_raw .± sigma_raw) .* 1e-17) * u"erg/s/cm^2/angstrom"

spec = spectrum(wave_aa, flux_meas)

println("Wavelength : ", round(wave_raw[1], digits=1),
" — ", round(wave_raw[end], digits=1), " Å")
println("Pixels : ", length(wave_raw))
println("Example px : ", flux_meas[100]) # shows value ± uncertainty
```

Plot the spectrum with its uncertainty band:
```julia
wv = ustrip.(wave_aa)
fv = Measurements.value.(ustrip.(flux_meas))
fe = Measurements.uncertainty.(ustrip.(flux_meas))

fig, ax = lines(wv, fv;
axis = (xlabel = "Wavelength (Å)",
ylabel = "Flux Density (erg s⁻¹ cm⁻² Å⁻¹)",
title = "SDSS Galaxy — plate 1323, fiber 12"))
band!(ax, wv, fv .- fe, fv .+ fe; color = (:steelblue, 0.3))
fig
Comment thread
icweaver marked this conversation as resolved.
```

## Part 2: Physical unit conversions

[`Unitful.jl`](https://painterqubits.github.io/Unitful.jl/stable/) makes it easy to
convert between wavelength, frequency, and energy representations.
```julia
Ha_aa = 6563.0u"angstrom"
println("Hα = ", Ha_aa,
" = ", uconvert(u"nm", Ha_aa),
" = ", uconvert(u"μm", Ha_aa))

# Convert Fλ → Fν (Jansky)
c0 = 2.99792458e18u"angstrom/s"
Fla = 2.5e-17u"erg/s/cm^2/angstrom"
Fnu = uconvert(u"erg/s/cm^2/Hz", Fla * Ha_aa^2 / c0)
println("Fλ = ", Fla)
println("Fν = ", uconvert(u"Jy", Fnu))
```

## Part 3: Dust extinction and dereddening

We use [`DustExtinction.jl`](https://juliaastro.org/DustExtinction/stable/) with the
Schlegel, Finkbeiner & Davis (1998) dust map and the CCM89 extinction law.
```julia
# Convert RA/Dec → Galactic coordinates for SFD98 dust map
eq = ICRSCoords(deg2rad(178.90417), deg2rad(0.66278))
gal = convert(GalCoords, eq)

dustmap = SFD98Map()
ebv = dustmap(gal.l, gal.b)
Av = 3.1 * ebv
println("E(B-V) = ", round(ebv, digits=4), " Av = ", round(Av, digits=4), " mag")

# Apply CCM89 extinction law
ext = CCM89(Rv = 3.1)
A_lambda = ext.(wave_raw) .* Av
corr = exp10.(-A_lambda ./ 2.5)
flux_dered = flux_meas ./ corr

fig2, ax2 = lines(wave_aa, flux_meas; label = "Raw",
axis = (title = "CCM89 Dust Dereddening",))
lines!(ax2, wave_aa, flux_dered; label = "Dereddened", color = :orange)
axislegend(ax2)
fig2
```

## Part 4: Blackbody spectra and stellar luminosity density

`blackbody` returns spectral radiance B(λ, T). The stellar luminosity density is:
```math
L_\lambda(\lambda, T) = 4\pi^2 R^2 \, B(\lambda, T)
```
```julia
R_sun = 6.957e10u"cm"

# Wavelength and temperature must carry units
wave_bb = collect(range(3000, 10000, length = 500)) .* u"angstrom"
bb_solar = blackbody(wave_bb, 5778.0u"K")
B_solar = bb_solar.flux
L_solar = 4π^2 .* R_sun.^2 .* B_solar

println("BB peak radiance : ",
round(maximum(ustrip.(B_solar)), sigdigits = 3), " erg/s/cm²/Å/sr")
println("Peak L_λ (1 R☉) : ",
round(ustrip(uconvert(u"erg/s/angstrom", maximum(L_solar))), sigdigits = 3),
" erg/s/Å")

# Compare multiple temperatures — wave_synth with units
wave_synth = collect(range(1000, 20000, length = 1000)) .* u"angstrom"
temps = [3_000u"K", 6_000u"K", 10_000u"K", 30_000u"K"]
labels_bb = ["3 000 K", "6 000 K", "10 000 K", "30 000 K"]
colors_bb = [:red, :orange, :steelblue, :purple]

fig3, ax3 = lines(wave_synth,
blackbody(wave_synth, temps[1]).flux;
label = labels_bb[1], color = colors_bb[1],
axis = (title = "Blackbody Spectra",
yscale = log10, yticks = LogTicks(LinearTicks(5))))

for (T, lab, col) in zip(temps[2:end], labels_bb[2:end], colors_bb[2:end])
lines!(ax3, wave_synth,
blackbody(wave_synth, T).flux;
label = lab, color = col)
end
axislegend(ax3, position = :rt)
fig3
Comment thread
icweaver marked this conversation as resolved.
```

## Part 5: Cosmological redshift and surface-brightness dimming

For a source at redshift z, the observed flux density is:
```math
F_\lambda^{\rm obs}(\lambda_{\rm obs}) =
\frac{(1+z)\,L_\lambda(\lambda_{\rm rest})}{4\pi\,d_L^2}
```
```julia
cosmo = cosmology()
wave_rest = collect(range(3000, 10000, length = 500)) .* u"angstrom"
bb_rest = blackbody(wave_rest, 10_000u"K")
L_lam = 4π^2 .* R_sun.^2 .* bb_rest.flux

fig4, ax4 = lines(ustrip.(wave_rest), ustrip.(bb_rest.flux);
label = "z = 0", color = :black,
axis = (xlabel = "Wavelength (Å)",
ylabel = "Flux Density (erg s⁻¹ cm⁻² Å⁻¹)",
title = "Cosmological Surface-Brightness Dimming"))

for (z, col) in zip([0.5, 1.0, 2.0], [:blue, :green, :red])
d_L = uconvert(u"cm", luminosity_dist(cosmo, z))
w_obs = wave_rest .* (1 + z)
F_obs = L_lam .* (1 + z) ./ (4π .* d_L.^2)
lines!(ax4, ustrip.(w_obs), ustrip.(F_obs); label = "z = $z", color = col)
end
axislegend(ax4, position = :rt)
fig4
Comment on lines +177 to +190
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fig4, ax4 = lines(ustrip.(wave_rest), ustrip.(bb_rest.flux);
label = "z = 0", color = :black,
axis = (xlabel = "Wavelength (Å)",
ylabel = "Flux Density (erg s⁻¹ cm⁻² Å⁻¹)",
title = "Cosmological Surface-Brightness Dimming"))
for (z, col) in zip([0.5, 1.0, 2.0], [:blue, :green, :red])
d_L = uconvert(u"cm", luminosity_dist(cosmo, z))
w_obs = wave_rest .* (1 + z)
F_obs = L_lam .* (1 + z) ./ (4π .* d_L.^2)
lines!(ax4, ustrip.(w_obs), ustrip.(F_obs); label = "z = $z", color = col)
end
axislegend(ax4, position = :rt)
fig4
fig4, ax4 = lines(ustrip.(wave_rest), ustrip.(bb_rest.flux);
label = "z = 0", color = :black,
axis = (xlabel = "Wavelength (Å)",
ylabel = "Flux Density (erg s⁻¹ cm⁻² Å⁻¹)",
yscale = log10, yticks=LogTicks(LinearTicks(5)),
title = "Cosmological Surface-Brightness Dimming"))
for (z, col) in zip([0.1, 0.5, 1.0], [:blue, :green, :red])
d_L = uconvert(u"cm", luminosity_dist(cosmo, z))
w_obs = wave_rest .* (1 + z)
F_obs = L_lam .* (1 + z) ./ (4π .* d_L.^2)
lines!(ax4, ustrip.(w_obs), ustrip.(F_obs); label = "z = $z", color = col)
end
axislegend(ax4, position = :rt)
fig4

Changed to display a little better

```
Loading