Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 40 additions & 0 deletions docs/src/transforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,46 @@ deredden
deredden!
```

## Redshift

Spectra can be shifted in wavelength via cosmological redshift or Doppler velocity. Both transformations operate only on the spectral axis; flux values are preserved as-is.

**Cosmological redshift** shifts by a dimensionless parameter `z`:

```jldoctest
julia> using Spectra

julia> spec = spectrum(collect(4000.0:1000.0:8000.0), ones(5));

julia> shifted = redshift(spec, 0.5);

julia> spectral_axis(shifted) ≈ spectral_axis(spec) .* 1.5
true
```

**Doppler shift** shifts by a radial velocity `v`. Pass a `Unitful` velocity or a plain number (interpreted as m/s). Set `relativistic=true` for the full relativistic formula:

```jldoctest
julia> using Spectra, Unitful

julia> spec = spectrum(collect(4000.0:1000.0:8000.0), ones(5));

julia> shifted = doppler_shift(spec, 100u"km/s");

julia> shifted_rel = doppler_shift(spec, 100u"km/s"; relativistic=true);
```

Both `redshift` and `doppler_shift` return a new spectrum. In-place variants `redshift!` and `doppler_shift!` are also available.

### API/Reference

```@docs
redshift
redshift!
doppler_shift
doppler_shift!
```

## Resampling

External interpolators, e.g., from [DataInterpolations.jl](https://github.com/SciML/DataInterpolations.jl) or [Interpolations.jl](https://github.com/JuliaMath/Interpolations.jl), can be used to resample spectra onto a given wavelength grid. Starting with a sample spectrum `spec`, we first create a [`SpectrumResampler`](@ref) object `resampler` which stores the initial spectrum and interpolator `interp` together. We then apply this object to the wavelength grid of our choice to produce the resampled spectrum. We show example usage in the docstring below:
Expand Down
1 change: 1 addition & 0 deletions src/Spectra.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export SingleSpectrum, IFUSpectrum, EchelleSpectrum

# Transforms
export SpectrumResampler, redden, redden!, deredden, deredden!
export redshift, redshift!, doppler_shift, doppler_shift!

# Utilities
export blackbody #, line_flux, equivalent_width
Expand Down
105 changes: 105 additions & 0 deletions src/transforms/redshift.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
const _c_mps = Float64(ustrip(c_0))

_velocity_ratio(v::Real) = v / _c_mps
_velocity_ratio(v::Quantity) = ustrip(u"m/s", v) / _c_mps

"""
redshift!(spec::AbstractSpectrum, z::Real)

In-place version of [`redshift`](@ref).
"""
function redshift!(spec::AbstractSpectrum, z::Real)
spec.spectral_axis = spectral_axis(spec) .* (1 + z)
return spec
end

"""
redshift(spec::AbstractSpectrum, z::Real)

Apply a cosmological redshift to a spectrum, returning a new spectrum with shifted wavelengths.

The observed wavelength is related to the rest-frame wavelength by

``λ_\\mathrm{obs} = λ_\\mathrm{rest} \\cdot (1 + z)``

where `z` is the cosmological redshift parameter. Only the spectral axis is transformed;
flux density values are not corrected for the stretching of the wavelength bins.

# Arguments
- `spec`: The input spectrum.
- `z`: Redshift parameter. Positive values redshift (longer wavelengths),
negative values blueshift (shorter wavelengths).

# Examples
```jldoctest
julia> spec = spectrum(collect(4000.0:1000.0:8000.0), ones(5));

julia> shifted = redshift(spec, 0.1);

julia> spectral_axis(shifted) ≈ spectral_axis(spec) .* 1.1
true
```

See also [`doppler_shift`](@ref) for velocity-based Doppler shifting.
"""
function redshift(spec::AbstractSpectrum, z::Real)
shifted = deepcopy(spec)
redshift!(shifted, z)
return shifted
end

"""
doppler_shift!(spec::AbstractSpectrum, v; relativistic=false)

In-place version of [`doppler_shift`](@ref).
"""
function doppler_shift!(spec::AbstractSpectrum, v; relativistic::Bool = false)
β = _velocity_ratio(v)
if relativistic
spec.spectral_axis = spectral_axis(spec) .* sqrt((1 + β) / (1 - β))
else
spec.spectral_axis = spectral_axis(spec) .* (1 + β)
end
return spec
end

"""
doppler_shift(spec::AbstractSpectrum, v; relativistic=false)

Apply a Doppler shift to a spectrum, returning a new spectrum with shifted wavelengths.

**Non-relativistic** (default):

``λ_\\mathrm{obs} = λ_\\mathrm{rest} \\cdot \\left(1 + \\frac{v}{c}\\right)``

**Relativistic** (`relativistic=true`):

``λ_\\mathrm{obs} = λ_\\mathrm{rest} \\cdot \\sqrt{\\frac{1 + v/c}{1 - v/c}}``

Only the spectral axis is transformed; flux density values are preserved as-is.

# Arguments
- `spec`: The input spectrum.
- `v`: Radial velocity. A `Unitful.Quantity` with velocity dimensions is automatically
converted using the speed of light in vacuum. A plain `Real` is interpreted as m/s.
- `relativistic`: If `true`, use the relativistic Doppler formula. Default is `false`.

# Examples
```jldoctest
julia> spec = spectrum(collect(4000.0:1000.0:8000.0), ones(5));

julia> v = 1000.0; # 1000 m/s

julia> shifted = doppler_shift(spec, v);

julia> spectral_axis(shifted) ≈ spectral_axis(spec) .* (1 + v / 299792458.0)
true
```

See also [`redshift`](@ref) for cosmological redshift.
"""
function doppler_shift(spec::AbstractSpectrum, v; relativistic::Bool = false)
shifted = deepcopy(spec)
doppler_shift!(shifted, v; relativistic)
return shifted
end
1 change: 1 addition & 0 deletions src/transforms/transforms.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
include("redden.jl")
include("redshift.jl")

# ## Broadening ops
# include("kernels.jl")
Expand Down
148 changes: 148 additions & 0 deletions test/transforms/redshift.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using Spectra:
redshift,
redshift!,
doppler_shift,
doppler_shift!

const C_MPS = 299792458.0

function mock_spectrum_redshift(; use_units::Bool = false)
wave = collect(range(4000.0, 8000.0, length = 100))
flux = @. 1e14 / (wave^5 * (exp(1 / (wave * 6700)) - 1))
if use_units
wave *= u"angstrom"
flux *= u"erg/s/cm^2/angstrom"
end
spectrum(wave, flux, name = "Test Redshift Spectrum")
end

@testset "redshift" begin
@testset "Cosmological redshift z=$z" for z in [0.0, 0.1, 0.5, 1.0, 2.0]
spec = mock_spectrum_redshift()
original_wave = copy(spectral_axis(spec))
original_flux = copy(flux_axis(spec))
original_meta = deepcopy(spec.meta)

shifted = @inferred redshift(spec, z)

@test spectral_axis(shifted) ≈ original_wave .* (1 + z)
@test flux_axis(shifted) ≈ original_flux
@test shifted.meta == original_meta

# Original unchanged
@test spectral_axis(spec) ≈ original_wave
@test flux_axis(spec) ≈ original_flux
end

@testset "Cosmological redshift in-place" begin
spec = mock_spectrum_redshift()
expected_wave = spectral_axis(spec) .* 1.5

result = @inferred redshift!(spec, 0.5)
@test result === spec
@test spectral_axis(spec) ≈ expected_wave
end

@testset "Cosmological redshift roundtrip" begin
spec = mock_spectrum_redshift()
original_wave = copy(spectral_axis(spec))

shifted = redshift(spec, 1.0)
unshifted = redshift(shifted, -0.5)
@test spectral_axis(unshifted) ≈ original_wave
end

@testset "Cosmological redshift with Unitful" begin
spec = mock_spectrum_redshift(use_units = true)
original_wave = copy(spectral_axis(spec))

shifted = @inferred redshift(spec, 0.5)
@test spectral_axis(shifted) ≈ original_wave .* 1.5
@test flux_axis(shifted) ≈ flux_axis(spec)
end
end

@testset "doppler_shift" begin
@testset "Non-relativistic Doppler" begin
spec = mock_spectrum_redshift()
original_wave = copy(spectral_axis(spec))
original_flux = copy(flux_axis(spec))
original_meta = deepcopy(spec.meta)

v = 1e5 # 100 km/s in m/s
shifted = @inferred doppler_shift(spec, v)
expected = original_wave .* (1 + v / C_MPS)

@test spectral_axis(shifted) ≈ expected
@test flux_axis(shifted) ≈ original_flux
@test shifted.meta == original_meta

# Original unchanged
@test spectral_axis(spec) ≈ original_wave
@test flux_axis(spec) ≈ original_flux
end

@testset "Relativistic Doppler" begin
spec = mock_spectrum_redshift()
original_wave = copy(spectral_axis(spec))

v = 1e5 # 100 km/s in m/s
β = v / C_MPS
shifted = @inferred doppler_shift(spec, v; relativistic = true)
expected = original_wave .* sqrt((1 + β) / (1 - β))

@test spectral_axis(shifted) ≈ expected
end

@testset "Non-relativistic ≈ relativistic at low v" begin
spec = mock_spectrum_redshift()

v = 100.0 # 100 m/s — very non-relativistic
nr = doppler_shift(spec, v; relativistic = false)
rel = doppler_shift(spec, v; relativistic = true)
@test spectral_axis(nr) ≈ spectral_axis(rel) rtol = 1e-10
end

@testset "Doppler in-place" begin
spec = mock_spectrum_redshift()
v = 5e4
expected_wave = spectral_axis(spec) .* (1 + v / C_MPS)

result = @inferred doppler_shift!(spec, v)
@test result === spec
@test spectral_axis(spec) ≈ expected_wave
end

@testset "Doppler with Unitful velocity" begin
spec = mock_spectrum_redshift()
original_wave = copy(spectral_axis(spec))

v = 100.0u"km/s"
shifted = @inferred doppler_shift(spec, v)
β = 1e5 / C_MPS # 100 km/s = 1e5 m/s
expected = original_wave .* (1 + β)
@test spectral_axis(shifted) ≈ expected

shifted_rel = @inferred doppler_shift(spec, v; relativistic = true)
expected_rel = original_wave .* sqrt((1 + β) / (1 - β))
@test spectral_axis(shifted_rel) ≈ expected_rel
end

@testset "Doppler with Unitful spectrum and velocity" begin
spec = mock_spectrum_redshift(use_units = true)
original_wave = copy(spectral_axis(spec))

v = 100.0u"km/s"
shifted = @inferred doppler_shift(spec, v)
β = 1e5 / C_MPS
@test spectral_axis(shifted) ≈ original_wave .* (1 + β)
@test flux_axis(shifted) ≈ flux_axis(spec)
end

@testset "Zero velocity" begin
spec = mock_spectrum_redshift()
shifted = doppler_shift(spec, 0.0)
@test spectral_axis(shifted) ≈ spectral_axis(spec)
@test flux_axis(shifted) ≈ flux_axis(spec)
end
end
Loading