Skip to content

Commit d9312dd

Browse files
committed
Improve implementation of Radiocarbon, add efficient logcdf, logccdf, etc.
1 parent 29bb2f0 commit d9312dd

File tree

4 files changed

+65
-26
lines changed

4 files changed

+65
-26
lines changed

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ Distributions = "0.15 - 0.25"
2323
Isoplot = "0.3"
2424
KernelDensity = "0.4 - 0.6"
2525
LsqFit = "0.7 - 0.13"
26-
NaNStatistics = "0.6.38"
26+
NaNStatistics = "0.6.39"
2727
Plots = "1"
2828
ProgressMeter = "1"
2929
QuadGK = "2"
3030
Reexport = "0.2, 1.0"
3131
SpecialFunctions = "0.10, 1, 2"
32-
StatGeochemBase = "0.5.9, 0.6"
32+
StatGeochemBase = "0.6.2"
3333
julia = "1"

src/Utilities.jl

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -152,28 +152,42 @@
152152
struct Radiocarbon{T<:Real} <: ContinuousUnivariateDistribution
153153
μ::T
154154
σ::T
155-
dist::Vector{T}
156155
ldist::Vector{T}
156+
lcdist::Vector{T}
157+
lccdist::Vector{T}
157158
end
158159

159160
function Radiocarbon::T, σ::T, ldist::Vector{T}) where {T<:Real}
160-
dist = exp.(ldist)
161-
dist ./= sum(dist) * 1 # Normalize
162-
return Radiocarbon{T}(μ, σ, dist, ldist)
161+
# Ensure normalization
162+
normconst = sum(exp.(ldist)) * one(T)
163+
ldist .-= normconst
164+
165+
# Cumulative distributions
166+
lcdist = nanlogcumsumexp(ldist)
167+
lccdist = nanlogcumsumexp(ldist, reverse=true)
168+
169+
return Radiocarbon{T}(μ, σ, ldist, lcdist, lccdist)
163170
end
164171

165172
function Radiocarbon(Age_14C::Real, Age_14C_sigma::Real, calibration::NamedTuple=intcal13)
166173
@assert calibration.Age_Calendar == 1:1:length(calibration.Age_14C)
167174
@assert step(calibration.Age_Calendar) == calibration.dt == 1
168175

169-
ldist = normproduct_ll.(Age_14C, Age_14C_sigma, calibration.Age_14C, calibration.Age_sigma)
170-
176+
ldist = normlogproduct.(Age_14C, Age_14C_sigma, calibration.Age_14C, calibration.Age_sigma)
171177
dist = exp.(ldist)
172-
dist ./= sum(dist) * calibration.dt # Normalize
178+
179+
# Normalize
180+
normconst = sum(dist) * calibration.dt
181+
dist ./= normconst
182+
ldist .-= normconst
173183
μ = histmean(dist, calibration.Age_Calendar)
174184
σ = histstd(dist, calibration.Age_Calendar, corrected=false)
175185

176-
return Radiocarbon(μ, σ, dist, ldist)
186+
# Cumulative distributions
187+
lcdist = nanlogcumsumexp(ldist)
188+
lccdist = nanlogcumsumexp(ldist, reverse=true)
189+
190+
return Radiocarbon(μ, σ, ldist, lcdist, lccdist)
177191
end
178192

179193
## Conversions
@@ -190,11 +204,32 @@
190204
Base.eltype(::Type{Radiocarbon{T}}) where {T} = T
191205

192206
## Evaluation
193-
@inline function Distributions.pdf(d::Radiocarbon{T}, x::Real) where {T}
194-
return linterp_at_index(d.dist, x, zero(T))
195-
end
207+
@inline Distributions.pdf(d::Radiocarbon, x::Real) = exp(logpdf(d, x))
196208
@inline function Distributions.logpdf(d::Radiocarbon{T}, x::Real) where {T}
197-
return linterp_at_index(d.ldist, x, -maxintfloat(T))
209+
if firstindex(d.ldist) <= x <= lastindex(d.ldist)
210+
linterp_at_index(d.ldist, x, -maxintfloat(T))
211+
else
212+
# Treat as a single Normal distrbution if outside of the calibration range
213+
logpdf(Normal(d.μ, d.σ), x)
214+
end
215+
end
216+
@inline Distributions.cdf(d::Radiocarbon, x::Real) = exp(logcdf(d, x))
217+
@inline function Distributions.logcdf(d::Radiocarbon{T}, x::Real) where {T}
218+
if firstindex(d.lcdist) <= x <= lastindex(d.lcdist)
219+
linterp_at_index(d.lcdist, x, -maxintfloat(T))
220+
else
221+
# Treat as a single Normal distrbution if outside of the calibration range
222+
logcdf(Normal(d.μ, d.σ), x)
223+
end
224+
end
225+
@inline Distributions.ccdf(d::Radiocarbon, x::Real) = exp(logccdf(d, x))
226+
@inline function Distributions.logccdf(d::Radiocarbon{T}, x::Real) where {T}
227+
if firstindex(d.lccdist) <= x <= lastindex(d.lccdist)
228+
linterp_at_index(d.lccdist, x, -maxintfloat(T))
229+
else
230+
# Treat as a single Normal distrbution if outside of the calibration range
231+
logccdf(Normal(d.μ, d.σ), x)
232+
end
198233
end
199234

200235
## Statistics

test/testRadiocarbon.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ for i = 1:nSamples
3737
smpl.Age_sigma[i] = std(samples)
3838

3939
# Populate smpl.Params with log likelihood for each sample
40-
smpl.Params[:,i] = normproduct_ll.(smpl.Age_14C[i], smpl.Age_14C_sigma[i], calibration.Age_14C, calibration.Age_sigma)
40+
smpl.Params[:,i] = normlogproduct.(smpl.Age_14C[i], smpl.Age_14C_sigma[i], calibration.Age_14C, calibration.Age_sigma)
4141
end
4242

4343
# Run stratigraphic model
@@ -111,7 +111,7 @@ for i = 1:nSamples
111111
smpl.Age_sigma[i] = std(samples)
112112

113113
# Populate smpl.Params with log likelihood for each sample
114-
smpl.Params[:,i] = normproduct_ll.(smpl.Age_14C[i], smpl.Age_14C_sigma[i], calibration.Age_14C, calibration.Age_sigma)
114+
smpl.Params[:,i] = normlogproduct.(smpl.Age_14C[i], smpl.Age_14C_sigma[i], calibration.Age_14C, calibration.Age_sigma)
115115
end
116116

117117
# Run stratigraphic model

test/testUtilities.jl

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,30 @@
3030

3131
d = Radiocarbon(1000, 10, intcal13)
3232
@test d isa Radiocarbon{Float64}
33-
@test pdf(d, 950) 0.0036210273644940918
34-
@test logpdf(d, 950) -3.4446721311475414
35-
@test pdf.(d, [900, 950, 1000]) [3.372067127114498e-7, 0.0036210273644940918, 1.5582187464154278e-12]
33+
@test pdf(d, 950) 0.0006472245090905895
34+
@test logpdf(d, 950) -7.342817323513984
35+
@test pdf.(d, [900, 950, 1000]) [6.333126104255167e-8, 0.0006472245090905895, 2.652591331229418e-13]
36+
@test cdf.(d, [-1, 900, 950, 1000, 1e5]) [0.0, 0.000563737957020387, 0.18096988328291203, 0.18312332434946413, 1.0]
37+
@test ccdf.(d, [-1, 900, 950, 1000, 1e5]) [1.0, 0.18255964979458322, 0.0028006656465210025, 7.114355524701459e-11, 0.0]
38+
@test logcdf.(d, [-1, 900, 950, 1000, 1e5]) [-7649.088264850034, -7.480921029645386, -1.7094246522629235, -1.6975954495627632, -0.0]
39+
@test logccdf.(d, [-1, 900, 950, 1000, 1e5]) [-0.0, -1.700678311173327, -5.8778981591541335, -23.366321375298313, -8.706770436253259e7]
3640

3741
@test eltype(d) === partype(d) === Float64
38-
@test location(d) 927.2875322569857
39-
@test scale(d) 7.412902965559571
42+
@test location(d) 927.2556461305643
43+
@test scale(d) 7.5077650707247
4044

41-
@test mean(d) 927.2875322569857
42-
@test var(d) 7.412902965559571^2
43-
@test std(d) 7.412902965559571
45+
@test mean(d) 927.2556461305643
46+
@test var(d) 7.5077650707247^2
47+
@test std(d) 7.5077650707247
4448

4549
## --- Other utility functions for log likelihoods
4650

4751
@test strat_ll(66.02, BilinearExponential(-5.9345103368858085, 66.00606672179812, 0.17739474265630253, 70.57882331291309, 0.6017142541555505)) (-3.251727600957773 -5.9345103368858085)
48-
@test strat_ll(950, Radiocarbon(1000, 10, intcal13)) -3.4446721311475414
52+
@test strat_ll(950, Radiocarbon(1000, 10, intcal13)) -7.342817323513984
4953

5054
@test strat_ll([0.0, 0.0], [Normal(0,1), Normal(0,1)]) 0
5155
@test strat_ll([0.0, 0.5], [Normal(0,1), Uniform(0,1)]) -0.9189385332046728
5256
@test strat_ll([0.0, 0.5, 66.02], [Normal(0,1), Uniform(0,1), BilinearExponential(-5.9345103368858085, 66.00606672179812, 0.17739474265630253, 70.57882331291309, 0.6017142541555505)]) (-0.9189385332046728 -3.251727600957773 -5.9345103368858085)
53-
@test strat_ll([0.0, 0.5, 66.02, 900], [Normal(0,1), Uniform(0,1), BilinearExponential(-5.9345103368858085, 66.00606672179812, 0.17739474265630253, 70.57882331291309, 0.6017142541555505), Radiocarbon(1000, 10, intcal13)]) -22.831420814939655
57+
@test strat_ll([0.0, 0.5, 66.02, 900], [Normal(0,1), Uniform(0,1), BilinearExponential(-5.9345103368858085, 66.00606672179812, 0.17739474265630253, 70.57882331291309, 0.6017142541555505), Radiocarbon(1000, 10, intcal13)]) -26.680063245418374
5458

5559
## ---

0 commit comments

Comments
 (0)