///|
/// Explicit failures; numerical singularities are not silently clipped.
pub(all) suberror CopulaError {
InvalidInput(String)
BudgetExceeded(String)
NumericalFailure(String)
} derive(Debug)
///|
/// Supported bivariate analytic families.
pub(all) enum Family {
Independence
Clayton
Gumbel
Frank
} derive(Debug)
///|
/// Rotation of a base copula on the unit square.
pub(all) enum Rotation {
R0
R90
R180
R270
} derive(Debug)
///|
/// Construction validates parameters. Fields cannot be changed by consumers.
pub struct Copula {
family : Family
theta : Double
rotation : Rotation
} derive(Debug)
///|
/// Owned pair of finite observations. Validation is performed by operations.
pub(all) struct Pair {
x : Double
y : Double
} derive(Debug)
///|
fn finite(x : Double, label : String) -> Unit raise CopulaError {
if x.is_nan() || x.is_inf() {
raise InvalidInput(label + " must be finite")
}
}
///|
fn probability(x : Double, label : String) -> Unit raise CopulaError {
finite(x, label)
if x < 0.0 || x > 1.0 {
raise InvalidInput(label + " must be in [0,1]")
}
}
///|
fn interior(x : Double, label : String) -> Unit raise CopulaError {
probability(x, label)
if x <= 0.0 || x >= 1.0 {
raise InvalidInput(label + " must be strictly between 0 and 1")
}
}
///|
fn validate_pairs(
data : Array[Pair],
minimum : Int32,
) -> Unit raise CopulaError {
if data.length() < minimum {
raise InvalidInput("insufficient paired observations")
}
if data.length() > 4096 {
raise BudgetExceeded("at most 4096 paired observations")
}
for item in data {
finite(item.x, "x")
finite(item.y, "y")
}
}
///|
pub fn validate_uniforms(
data : Array[Pair],
minimum : Int32,
) -> Unit raise CopulaError {
validate_pairs(data, minimum)
for item in data {
interior(item.x, "u")
interior(item.y, "v")
}
}
///|
pub fn checked_probability(x : Double) -> Double raise CopulaError {
if x.is_nan() || x.is_inf() || x < -1.0e-10 || x > 1.0000000001 {
raise NumericalFailure("probability outside numerical tolerance")
}
x.max(0.0).min(1.0)
}
///|
/// Create an analytic model in its documented numerically bounded domain.
/// Clayton: [0.01,30], Gumbel: [1,20], Frank: [-30,-0.01] or [0.01,30].
/// Independence requires theta=0. Use it for the exact independence limit.
pub fn Copula::new(
family : Family,
theta : Double,
rotation~ : Rotation = R0,
) -> Copula raise CopulaError {
finite(theta, "theta")
let valid = match family {
Independence => theta == 0.0
Clayton => theta >= 0.01 && theta <= 30.0
Gumbel => theta >= 1.0 && theta <= 20.0
Frank => theta.abs() >= 0.01 && theta.abs() <= 30.0
}
if !valid {
raise InvalidInput("theta outside supported family range")
}
{ family, theta, rotation, }
}
///|
/// Exact independent model.
pub fn independence() -> Copula {
{ family: Independence, theta: 0.0, rotation: R0, }
}
///|
pub extend CopulaError with @debug.Debug::{to_repr}
///|
pub extend Family with @debug.Debug::{to_repr}
///|
pub extend Rotation with @debug.Debug::{to_repr}
///|
pub extend Copula with @debug.Debug::{to_repr}
///|
pub extend Pair with @debug.Debug::{to_repr}