| name | copula-bivariate |
| description | How to add a bivariate copula to stochastic-rs-copulas. Invoke when implementing Clayton, Frank, Gumbel, Joe, Plackett, FGM-style families, or any 2-d Archimedean / extreme-value copula. |
Copula bivariate — stochastic-rs-copulas
Bivariate copulas in stochastic-rs-copulas implement the
BivariateExt trait, exposing pdf / cdf / inverse / partial
derivative / parametric tau / sampling. The §6.12 audit fix-pass
(rc.0) caught a Frank-tau formula bug where a custom Newton iteration
diverged on positive correlations; the rc.1 fix routed everything
through roots::find_root_brent, which is the canonical pattern.
This SKILL codifies the trait surface, the bounds / invalid-θ
contract, the compute_theta pattern (closed-form when possible,
Brent root-find otherwise), and the parametric-tau-recovery test that
prevents the §6.12-class regression.
(Note: NCopula2DExt was removed in v2.0 — bivariate samplers are
all consolidated under BivariateExt.)
1. The trait surface
pub trait BivariateExt {
fn theta_bounds(&self) -> (f64, f64);
fn invalid_thetas(&self) -> &[f64];
fn compute_theta(&self, tau: f64) -> f64;
fn cdf(&self, u: f64, v: f64) -> f64;
fn pdf(&self, u: f64, v: f64) -> f64;
fn percent_point(&self, p: f64, v: f64) -> f64;
fn partial_derivative(&self, u: f64, v: f64) -> f64;
fn sample(&self, n: usize) -> ndarray::Array2<f64>;
}
2. The struct skeleton
use roots::SimpleConvergency;
use roots::find_root_brent;
pub struct Clayton {
pub theta: f64,
pub seed: u64,
}
impl BivariateExt for Clayton {
fn theta_bounds(&self) -> (f64, f64) {
(-1.0, f64::INFINITY)
}
fn invalid_thetas(&self) -> &[f64] {
&[0.0]
}
fn compute_theta(&self, tau: f64) -> f64 {
2.0 * tau / (1.0 - tau)
}
fn cdf(&self, u: f64, v: f64) -> f64 {
let θ = self.theta;
(u.powf(-θ) + v.powf(-θ) - 1.0).powf(-1.0 / θ)
}
}
3. The compute_theta pattern — closed-form > Brent > custom
The §6.12 trap was a custom Newton iteration on Frank's
τ → θ map that diverged on positive correlations because the
secant initialisation hit a flat region. The mandate:
-
Closed-form first. If τ(θ) inverts analytically (Clayton, Gumbel,
FGM), use it. Cite the textbook formula in a comment.
-
Brent's method second. When no closed form exists, use
roots::find_root_brent:
fn compute_theta(&self, tau: f64) -> f64 {
let f = |theta: f64| -> f64 { tau_from_theta(theta) - tau };
let bounds = self.theta_bounds();
let lo = bounds.0.max(-50.0).max(self.theta_bounds().0 + 1e-6);
let hi = bounds.1.min( 50.0).min(self.theta_bounds().1 - 1e-6);
let mut conv = SimpleConvergency { eps: 1e-12, max_iter: 100 };
find_root_brent(lo, hi, &f, &mut conv).unwrap_or(0.0)
}
Brent is bracketing → guaranteed convergence on a sign-change. Newton
isn't.
-
Never roll a custom Newton / secant. The §6.12 trap shipped
exactly this: a custom hand-rolled iteration with no convergence
proof.
4. Sampling — Rosenblatt transform
The standard 2-d copula sampler:
fn sample(&self, n: usize) -> ndarray::Array2<f64> {
let mut rng = StdRng::seed_from_u64(self.seed);
let unif = Uniform::new_inclusive(0.0, 1.0);
let mut out = ndarray::Array2::<f64>::zeros((n, 2));
for i in 0..n {
let v = unif.sample(&mut rng);
let p = unif.sample(&mut rng);
let u = self.percent_point(p, v);
out[[i, 0]] = u;
out[[i, 1]] = v;
}
out
}
The percent_point(p, v) step is where the inversion happens. For
copulas where this isn't closed-form (FGM-class, Joe), invert
partial_derivative(u, v) = p w.r.t. u via Brent again.
5. Mandatory test: parametric-τ recovery
#[test]
fn parametric_tau_recovery() {
let target_tau = 0.5;
let cop = Clayton {
theta: Clayton { theta: 0.0, seed: 0 }.compute_theta(target_tau),
seed: 42,
};
let samples = cop.sample(50_000);
let empirical_tau = compute_kendall_tau(&samples);
assert!(
(empirical_tau - target_tau).abs() < 0.02,
"τ recovery: target {target_tau}, got {empirical_tau}"
);
}
This is the regression test that catches the §6.12 class. Without it,
a wrong compute_theta looks fine on the data side but produces
samples with a different τ than requested. Pin the seed, pin a
50_000-sample bound on the empirical-τ noise.
6. Anti-patterns
- Do not roll a custom Newton in
compute_theta. Use Brent.
- Do not silently return
0.0 from compute_theta on
out-of-domain τ. Validate bounds at construction or in
compute_theta and panic with a useful message.
- Do not branch on
if theta == 0.0 for the degenerate case in the
hot loop. Pre-check at construction; the sampler should never see
theta = 0 for Clayton.
- Do not sample without a seed.
seed: u64 field is mandatory for
reproducibility.
7. Reference impls
Clayton (clayton.rs) — closed-form compute_theta, Rosenblatt
sampling.
Frank (frank.rs) — Brent-based compute_theta (rc.1 fix; was
custom Newton).
Gumbel (gumbel.rs) — closed-form via Archimedean generator.
FGM (fgm.rs) — Farlie-Gumbel-Morgenstern; bounded τ ∈ [-2/9, 2/9].
Related SKILLs
add-mc-variance-reduction — when copula sampling is part of an MC
pricer using common random numbers.
python-bindings — for the PyClayton etc. wrappers.