Skip to content

Commit 12d30d5

Browse files
robhentgesRobert Hentges
andauthored
Add Das-like function with exponential right tail (#42)
* Revert "delete das-function, focused on CB" Adds das-like function implementations back * Refactorized Crystal-Ball/Das Function * Deleted json setting * added to gitignore * Removed old comment --------- Co-authored-by: Robert Hentges <robert.hentges@cern.ch>
1 parent d64ec82 commit 12d30d5

File tree

7 files changed

+488
-0
lines changed

7 files changed

+488
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/Manifest.toml
22
/docs/Manifest.toml
33
/docs/build/
4+
.vscode/settings.json

src/DistributionsHEP.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,8 @@ include("bifurcated-gaussian.jl")
3535
export DoubleSidedBifurcatedCrystalBall
3636
include("double-sided-bifurcated-crystal-ball.jl")
3737

38+
export DoubleSidedBifurcatedCrystalBallDas
39+
include("exponential-tail.jl")
40+
include("double-sided-bifurcated-crystal-ball-das.jl")
41+
3842
end
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# Common parameter validation for double-sided bifurcated crystal ball
2+
function _check_double_sided_bifurcated_crystal_ball_das_params(σ::T, αL::T, nL::T, αR::T) where {T<:Real}
3+
σ > zero(T) || error("σ (scale) must be positive.")
4+
αL > zero(T) || error("αL (left transition point) must be positive.")
5+
nL > zero(T) || error("nL (left power-law exponent) must be positive.")
6+
αR > zero(T) || error("αR (right transition point) must be positive.")
7+
end
8+
9+
function ExponentialTail(
10+
BifGauss::BifurcatedGaussian{T},
11+
x0::T
12+
) where {T<:Real}
13+
G_x0 = pdf(BifGauss, x0)
14+
L_x0 = _log_derivative_bifurcated_gaussian(BifGauss, x0)
15+
return ExponentialTail(G_x0, L_x0, x0)
16+
end
17+
18+
"""
19+
DoubleSidedBifurcatedCrystalBallDas{T<:Real} <: ContinuousUnivariateDistribution
20+
21+
The Double-sided Bifurcated Crystal Ball/Das distribution is a probability distribution commonly used in high-energy physics
22+
to model various lossy processes. It extends the Bifurcated Gaussian distribution by adding a power-law tail on the left side and an exponential tail on the right side
23+
of the asymmetric Gaussian core.
24+
25+
The distribution consists of:
26+
- A left power-law tail for x < xL
27+
- A bifurcated Gaussian core for xL ≤ x ≤ xR
28+
- A right exponential tail for x > xR
29+
30+
The bifurcated Gaussian core uses different scale parameters on the left (σL) and right (σR) sides of the mean μ,
31+
controlled by the asymmetry parameter ψ via κ = tanh(ψ), where σL = σ(1 + κ) and σR = σ(1 - κ).
32+
33+
# Arguments
34+
- `μ`: The mean of the bifurcated Gaussian core.
35+
- `σ`: The base scale parameter of the bifurcated Gaussian core. Must be positive.
36+
- `ψ`: The asymmetry parameter controlling the difference between left and right scales via κ = tanh(ψ).
37+
- `αL`: The left transition point parameter, defining where the left power-law tail begins (in units of σL). Must be positive.
38+
- `nL`: The exponent parameter for the left power-law tail. Must be positive. The effective exponent is NL = √(1 + nL²) > 1.
39+
- `αR`: The right transition point parameter, defining where the right exponential tail begins (in units of σR). Must be positive.
40+
41+
The transition points are calculated as:
42+
- xL = μ - αL * σL (left transition)
43+
- xR = μ + αR * σR (right transition)
44+
45+
The struct stores precomputed constants for efficient PDF, CDF, and quantile calculations.
46+
47+
# Example
48+
```julia
49+
using DistributionsHEP
50+
using Plots
51+
52+
# Create a double-sided bifurcated crystal ball distribution
53+
d = DoubleSidedBifurcatedCrystalBallDas(0.0, 1.0, 0.25, 0.5, 1.25, 0.75) # μ, σ, ψ, αL, nL, αR
54+
55+
# Evaluate PDF, CDF, and quantile
56+
pdf(d, 0.0)
57+
cdf(d, 1.0)
58+
quantile(d, 0.5)
59+
60+
# Plot the distribution
61+
plot(-5, 5, x->pdf(d, x))
62+
```
63+
"""
64+
struct DoubleSidedBifurcatedCrystalBallDas{T<:Real} <: ContinuousUnivariateDistribution
65+
BifGauss::BifurcatedGaussian{T} # Bifurcated Gaussian core distribution (contains μ, σ, ψ, σL, σR)
66+
left_tail::CrystalBallTail{T} # Left tail parameters (G(x0), N, L(x0), x0)
67+
right_tail::ExponentialTail{T} # Right tail parameters (G(x0), L(x0), x0)
68+
norm_const::T # Normalization constant N
69+
70+
function DoubleSidedBifurcatedCrystalBallDas(μ::T, σ::T, ψ::T, αL::T, nL::T, αR::T) where {T<:Real}
71+
_check_double_sided_bifurcated_crystal_ball_das_params(σ, αL, nL, αR)
72+
73+
# Calculate kappa
74+
κ = tanh(ψ)
75+
76+
# Calculate scales for left and right sides
77+
σL = σ * (1 + κ)
78+
σR = σ * (1 - κ)
79+
80+
# Calculate transition points
81+
xL = μ - αL * σL
82+
xR = μ + αR * σR
83+
84+
# Pre-compute constants
85+
BifGauss = BifurcatedGaussian(μ, σ, ψ)
86+
87+
# Create tail structures using clean 4-parameter formulation
88+
left_tail = CrystalBallTail(BifGauss, nL, xL)
89+
right_tail = ExponentialTail(BifGauss, xR)
90+
91+
# Calculate normalization constant using _integral
92+
left_tail_contribution = _integral(left_tail, left_tail.x0)
93+
right_tail_contribution = -_integral(right_tail, right_tail.x0) # Right tail has negative L_x0, so negate
94+
# Integral from xL to xR = cdf(BifGauss, xR) - cdf(BifGauss, xL)
95+
core_contribution = cdf(BifGauss, xR) - cdf(BifGauss, xL)
96+
N = one(T) / (left_tail_contribution + right_tail_contribution + core_contribution)
97+
98+
new{T}(BifGauss, left_tail, right_tail, N)
99+
end
100+
end
101+
102+
"""
103+
_compute_transition_cdf_values(d::DoubleSidedBifurcatedCrystalBallDas)
104+
105+
Compute the CDF values at the two transition points (left and right) of the Double-sided Bifurcated Crystal Ball/Das distribution.
106+
107+
Returns a tuple `(cdf_at_xL, cdf_at_xR)` where:
108+
- `cdf_at_xL` is the CDF value at the left transition point (x = μ - αL * σL)
109+
- `cdf_at_xR` is the CDF value at the right transition point (x = μ + αR * σR)
110+
"""
111+
function _compute_transition_cdf_values(d::DoubleSidedBifurcatedCrystalBallDas{T}) where {T<:Real}
112+
cdf_at_xL = d.norm_const * _integral(d.left_tail, d.left_tail.x0)
113+
# Integral from xL to xR = cdf(BifGauss, xR) - cdf(BifGauss, xL)
114+
core_contribution = cdf(d.BifGauss, d.right_tail.x0) - cdf(d.BifGauss, d.left_tail.x0)
115+
cdf_at_xR = cdf_at_xL + d.norm_const * core_contribution
116+
117+
return cdf_at_xL, cdf_at_xR
118+
end
119+
120+
# Convenience constructors
121+
DoubleSidedBifurcatedCrystalBallDas(μ::Real, σ::Real, ψ::Real, αL::Real, nL::Real, αR::Real) =
122+
DoubleSidedBifurcatedCrystalBallDas(promote(μ, σ, ψ, αL, nL, αR)...)
123+
124+
function Distributions.pdf(d::DoubleSidedBifurcatedCrystalBallDas{T}, x::Real) where {T<:Real}
125+
# Left power-law tail
126+
if x < d.left_tail.x0
127+
offset = x - d.left_tail.x0
128+
return d.norm_const * _value(d.left_tail, offset)
129+
end
130+
# Bifurcated Gaussian core
131+
if x <= d.right_tail.x0
132+
return d.norm_const * pdf(d.BifGauss, x)
133+
end
134+
# Right exponential tail
135+
offset = x - d.right_tail.x0
136+
return d.norm_const * _value(d.right_tail, offset)
137+
end
138+
139+
function Distributions.cdf(d::DoubleSidedBifurcatedCrystalBallDas{T}, x::Real) where {T<:Real}
140+
cdf_at_xL, cdf_at_xR = _compute_transition_cdf_values(d)
141+
142+
if x <= d.left_tail.x0
143+
# Left power-law tail
144+
return d.norm_const * _integral(d.left_tail, x)
145+
elseif x >= d.right_tail.x0
146+
# Right exponential tail
147+
integral_at_x = _integral(d.right_tail, x)
148+
integral_at_x0R = _integral(d.right_tail, d.right_tail.x0)
149+
return cdf_at_xR + d.norm_const * (integral_at_x - integral_at_x0R)
150+
else
151+
# Bifurcated Gaussian core
152+
# Integral from xL to x = cdf(BifGauss, x) - cdf(BifGauss, xL)
153+
core_contribution = cdf(d.BifGauss, x) - cdf(d.BifGauss, d.left_tail.x0)
154+
return cdf_at_xL + d.norm_const * core_contribution
155+
end
156+
end
157+
158+
function Distributions.quantile(d::DoubleSidedBifurcatedCrystalBallDas{T}, p::Real) where {T<:Real}
159+
if p < zero(T) || p > one(T)
160+
throw(DomainError(p, "Probability p must be in [0,1]."))
161+
end
162+
p == zero(T) && return T(-Inf)
163+
p == one(T) && return T(Inf)
164+
165+
cdf_at_xL, cdf_at_xR = _compute_transition_cdf_values(d)
166+
167+
if p <= cdf_at_xL
168+
# Left power-law tail
169+
tail_integral = p / d.norm_const
170+
return _integral_inversion(d.left_tail, tail_integral)
171+
elseif p >= cdf_at_xR
172+
integral_at_x0R = _integral(d.right_tail, d.right_tail.x0)
173+
tail_integral = (p - cdf_at_xR) / d.norm_const + integral_at_x0R
174+
return _integral_inversion(d.right_tail, tail_integral)
175+
else
176+
target_cdf = (p - cdf_at_xL) / d.norm_const + cdf(d.BifGauss, d.left_tail.x0)
177+
return quantile(d.BifGauss, target_cdf)
178+
end
179+
end
180+
181+
Distributions.maximum(d::DoubleSidedBifurcatedCrystalBallDas{T}) where {T<:Real} = T(Inf)
182+
Distributions.minimum(d::DoubleSidedBifurcatedCrystalBallDas{T}) where {T<:Real} = T(-Inf)
183+
184+
# Distributions.jl interface methods
185+
Distributions.location(d::DoubleSidedBifurcatedCrystalBallDas) = d.BifGauss.μ
186+
Distributions.scale(d::DoubleSidedBifurcatedCrystalBallDas) = d.BifGauss.σ
187+
# Compute αL, nL, αR, nR from tail structs when needed for params()
188+
# For bifurcated: x0L = μ - αL*σL, x0R = μ + αR*σR, and N = sqrt(1 + n²) → n = sqrt(N² - 1)
189+
Distributions.params(d::DoubleSidedBifurcatedCrystalBallDas) = (
190+
d.BifGauss.μ,
191+
d.BifGauss.σ,
192+
d.BifGauss.ψ,
193+
(d.BifGauss.μ - d.left_tail.x0) / d.BifGauss.σL, # αL = (μ - x0L) / σL
194+
sqrt(d.left_tail.N^2 - 1), # nL = sqrt(N² - 1)
195+
(d.right_tail.x0 - d.BifGauss.μ) / d.BifGauss.σR # αR = (x0R - μ) / σR
196+
)
197+
Distributions.partype(::DoubleSidedBifurcatedCrystalBallDas{T}) where {T} = T

src/exponential-tail.jl

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Tail parameters for Exponential tail distribution
2+
#
3+
struct ExponentialTail{T<:Real}
4+
G_x0::T # G(x0): Core PDF at transition point x0
5+
L_x0::T # L(x0): Logarithmic derivative of core PDF at transition point x0
6+
x0::T # x0: Absolute transition point
7+
end
8+
9+
function _norm_const(tail::ExponentialTail{T}) where {T<:Real}
10+
(; G_x0, L_x0) = tail
11+
return G_x0 / L_x0
12+
end
13+
14+
function _value(tail::ExponentialTail{T}, offset::T) where {T<:Real}
15+
(; G_x0, L_x0) = tail
16+
return G_x0 * exp(L_x0 * offset)
17+
end
18+
19+
"""
20+
_integral(t::ExponentialTail, a)
21+
22+
Compute the integral of the ExponentialTail function.
23+
24+
For left tail (L_x0 > 0): integral from -∞ to a
25+
For right tail (L_x0 < 0): returns -integral from [a, +∞]
26+
27+
The integral is computed using the antiderivative of the tail function:
28+
∫[-Inf*sign(L) to a] G_x0 / L_x0 * exp(L_x0 * (x - x0)) dx
29+
30+
For L_x0 < 0, the result is the negative of the integral from a to +∞.
31+
32+
Returns the integral value.
33+
"""
34+
function _integral(t::ExponentialTail{T}, a::Real) where {T<:Real}
35+
(; G_x0, L_x0, x0) = t
36+
# Compute offset from transition point
37+
a_T = T(a)
38+
offset_a = a_T - x0
39+
const_tail = _norm_const(t)
40+
return const_tail * exp(L_x0 * offset_a)
41+
end
42+
43+
"""
44+
_integral_inversion(t::ExponentialTail, integral)
45+
46+
Find `a` such that the integral relationship holds.
47+
48+
For left tail (L_x0 > 0): finds `a` such that integral from -∞ to `a` equals the given `integral` value.
49+
For right tail (L_x0 < 0): finds `a` such that `_integral(t, a) = integral` (where integral should be negative).
50+
51+
Returns the value of `a` (in absolute coordinates).
52+
"""
53+
function _integral_inversion(t::ExponentialTail{T}, integral::Real) where {T<:Real}
54+
(; L_x0, x0) = t
55+
const_tail = _norm_const(t)
56+
#
57+
integral_T = T(integral)
58+
ratio = integral_T / const_tail
59+
offset_a = log(ratio) / L_x0
60+
# Convert back to absolute coordinate
61+
a = x0 + offset_a
62+
return a
63+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ using Test
88
include("test-secant.jl")
99
include("test-bifurcated-gaussian.jl")
1010
include("test-double-sided-bifurcated-crystal-ball.jl")
11+
include("test-double-sided-bifurcated-crystal-ball-das.jl")
1112
end

0 commit comments

Comments
 (0)