///|
/// An explicit model candidate, including Frank branch and rotation.
pub(all) struct Candidate {
label : String
family : Family
rotation : Rotation
options : FitOptions
}
///|
/// Ranked model fit plus normalized AIC weight.
pub struct RankedModel {
label : String
fit : FitResult
delta_aic : Double
weight : Double
}
///|
/// A compact standard model set; negative dependence is represented explicitly.
pub fn standard_candidates() -> Array[Candidate] {
let result : Array[Candidate] = [
{
label: "independence",
family: Independence,
rotation: R0,
options: default_fit_options(Independence),
},
]
for (label, family) in [("clayton", Clayton), ("gumbel", Gumbel)] {
for
(suffix, rotation) in [
("", R0),
("-90", R90),
("-180", R180),
("-270", R270),
] {
result.push({
label: label + suffix,
family,
rotation,
options: default_fit_options(family),
})
}
}
result.push({
label: "frank-positive",
family: Frank,
rotation: R0,
options: default_fit_options(Frank),
})
result.push({
label: "frank-negative",
family: Frank,
rotation: R0,
options: { lower: -30.0, upper: -0.01, tolerance: 1.0e-6, iterations: 80, },
})
result
}
///|
/// AIC comparison on identical data. Failure/nonconvergence is explicit;
/// unsuccessful candidates are never silently discarded.
pub fn compare_models(
candidates : Array[Candidate],
data : Array[Pair],
) -> Array[RankedModel] raise CopulaError {
validate_uniforms(data, 3)
if candidates.length() < 1 || candidates.length() > 32 {
raise BudgetExceeded("candidate count must be in [1,32]")
}
let scored : Array[(String, FitResult)] = []
for i in 0.. 80 {
raise InvalidInput("candidate label must have 1 to 80 characters")
}
for j in 0.. FitResult raise CopulaError {
fit(family, pseudo_observations(raw), rotation~)
}