ruvector/examples/climate-consciousness/src/data.rs
ruvnet 96d8fdc172 chore(workspace): cargo fmt — mechanical whitespace fix across 427 files
Pre-existing rustfmt drift across the workspace was blocking CI's
`Rustfmt` check on PR #373 + PR #377. Running plain `cargo fmt`
reformats 427 files; no semantic changes, no logic changes, no
behavior changes — just what rustfmt already wanted.

None of the touched files are in ruvector-rabitq, ruvector-rulake,
or the new mirror-rulake workflow — those were already fmt-clean
per the per-crate checks on commits 5a4b0d782, 5f32fd450, f5003bc7b.
Drift is in cognitum-gate-kernel, mcp-brain, nervous-system,
prime-radiant, ruqu-core, ruvector-attention, ruvector-mincut,
ruvix/* and sub-crates, plus several examples.

Verified post-fmt:
  cargo check -p ruvector-rabitq -p ruvector-rulake            → clean
  cargo clippy -p ... -p ... --all-targets -- -D warnings      → clean
  cargo test   -p ... -p ... --release                         → 82/82 pass

Intentionally does NOT touch clippy drift — many more warnings
(missing docs, precision-loss casts, too-many-args, unsafe-safety-
docs) spread across unrelated crates, each category a cross-cutting
design decision that deserves its own review.

With this commit Rustfmt CI goes green on PR #373 and PR #377.
Clippy will still fail — that's honest pre-existing state for a
separate dedicated PR.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-04-24 10:44:02 -04:00

271 lines
9.5 KiB
Rust

//! Climate teleconnection data generation.
//!
//! Builds correlation matrices for 7 major climate indices based on known
//! teleconnection strengths. Generates neutral baseline and El Nino active
//! variants.
use rand::Rng;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
/// Climate index identifiers.
pub const INDEX_ENSO: usize = 0; // El Nino Southern Oscillation (Nino3.4)
pub const INDEX_NAO: usize = 1; // North Atlantic Oscillation
pub const INDEX_PDO: usize = 2; // Pacific Decadal Oscillation
pub const INDEX_AMO: usize = 3; // Atlantic Multidecadal Oscillation
pub const INDEX_IOD: usize = 4; // Indian Ocean Dipole
pub const INDEX_SAM: usize = 5; // Southern Annular Mode
pub const INDEX_QBO: usize = 6; // Quasi-Biennial Oscillation
pub const INDEX_NAMES: &[&str] = &["ENSO", "NAO", "PDO", "AMO", "IOD", "SAM", "QBO"];
pub const N_INDICES: usize = 7;
/// Regional subsets of climate indices.
pub const PACIFIC_INDICES: &[usize] = &[INDEX_ENSO, INDEX_PDO, INDEX_IOD];
pub const ATLANTIC_INDICES: &[usize] = &[INDEX_NAO, INDEX_AMO];
pub const POLAR_INDICES: &[usize] = &[INDEX_SAM, INDEX_QBO];
pub const REGION_NAMES: &[&str] = &["Pacific", "Atlantic", "Polar"];
pub fn all_regions() -> Vec<(&'static str, &'static [usize])> {
vec![
(REGION_NAMES[0], PACIFIC_INDICES),
(REGION_NAMES[1], ATLANTIC_INDICES),
(REGION_NAMES[2], POLAR_INDICES),
]
}
/// Climate mode correlation data.
pub struct ClimateCorrelations {
pub n_indices: usize,
/// Flat row-major correlation matrix (symmetric, diagonal = 1.0).
pub correlations: Vec<f64>,
/// Variant label.
pub variant: String,
}
/// Transition probability matrix for consciousness analysis.
pub struct TransitionMatrix {
pub size: usize,
pub data: Vec<f64>,
}
/// Build the neutral baseline correlation matrix.
///
/// Known teleconnection strengths (approximate, from literature):
/// - ENSO <-> IOD: strong (0.5-0.7)
/// - ENSO <-> PDO: moderate (0.3-0.5)
/// - NAO <-> AMO: moderate (0.2-0.4)
/// - QBO <-> ENSO: weak but real (0.1-0.2)
/// - SAM: mostly independent
pub fn build_neutral_correlations() -> ClimateCorrelations {
let n = N_INDICES;
let mut corr = vec![0.0f64; n * n];
let mut rng = ChaCha8Rng::seed_from_u64(42);
// Set diagonal to 1.0 (self-correlation)
for i in 0..n {
corr[i * n + i] = 1.0;
}
// Define known teleconnection strengths (symmetric)
let connections: &[(usize, usize, f64, f64)] = &[
// (index_a, index_b, min_corr, max_corr)
(INDEX_ENSO, INDEX_IOD, 0.50, 0.70), // Strong: ENSO-IOD coupling
(INDEX_ENSO, INDEX_PDO, 0.30, 0.50), // Moderate: Pacific basin coupling
(INDEX_NAO, INDEX_AMO, 0.20, 0.40), // Moderate: Atlantic coupling
(INDEX_QBO, INDEX_ENSO, 0.10, 0.20), // Weak: stratosphere-troposphere
(INDEX_PDO, INDEX_IOD, 0.10, 0.25), // Weak: Indo-Pacific coupling
(INDEX_ENSO, INDEX_NAO, 0.05, 0.15), // Weak: Pacific-Atlantic bridge
(INDEX_ENSO, INDEX_SAM, 0.05, 0.15), // Weak: tropical-polar link
(INDEX_AMO, INDEX_PDO, 0.05, 0.10), // Very weak: inter-basin
(INDEX_SAM, INDEX_QBO, 0.03, 0.08), // Very weak: polar-stratosphere
(INDEX_NAO, INDEX_QBO, 0.05, 0.12), // Weak: NAO-QBO link
(INDEX_SAM, INDEX_NAO, 0.02, 0.06), // Very weak: bipolar
(INDEX_IOD, INDEX_AMO, 0.02, 0.06), // Very weak: Indian-Atlantic
(INDEX_AMO, INDEX_SAM, 0.01, 0.04), // Negligible
(INDEX_IOD, INDEX_NAO, 0.01, 0.04), // Negligible
(INDEX_IOD, INDEX_SAM, 0.01, 0.03), // Negligible
(INDEX_PDO, INDEX_NAO, 0.02, 0.06), // Very weak
(INDEX_PDO, INDEX_SAM, 0.01, 0.04), // Negligible
(INDEX_QBO, INDEX_AMO, 0.02, 0.05), // Very weak
(INDEX_QBO, INDEX_PDO, 0.03, 0.08), // Very weak
(INDEX_QBO, INDEX_IOD, 0.02, 0.06), // Very weak
(INDEX_QBO, INDEX_SAM, 0.03, 0.08), // Very weak
];
for &(a, b, min_c, max_c) in connections {
let c = rng.gen_range(min_c..max_c);
corr[a * n + b] = c;
corr[b * n + a] = c;
}
ClimateCorrelations {
n_indices: n,
correlations: corr,
variant: "Neutral".into(),
}
}
/// Build the El Nino active variant.
///
/// During El Nino events, ENSO correlations strengthen by ~50%,
/// and the Pacific basin becomes more tightly coupled.
pub fn build_elnino_correlations() -> ClimateCorrelations {
let mut data = build_neutral_correlations();
data.variant = "El Nino Active".into();
let n = data.n_indices;
// Boost all ENSO correlations by 50%
for j in 0..n {
if j != INDEX_ENSO {
let boosted = (data.correlations[INDEX_ENSO * n + j] * 1.5).min(0.95);
data.correlations[INDEX_ENSO * n + j] = boosted;
data.correlations[j * n + INDEX_ENSO] = boosted;
}
}
// Also boost intra-Pacific correlations
let pacific_boost: &[(usize, usize)] = &[(INDEX_PDO, INDEX_IOD)];
for &(a, b) in pacific_boost {
let boosted = (data.correlations[a * n + b] * 1.3).min(0.90);
data.correlations[a * n + b] = boosted;
data.correlations[b * n + a] = boosted;
}
data
}
/// Convert a correlation matrix to a transition probability matrix.
///
/// Method (same approach as the CMB example):
/// 1. Use absolute correlations as coupling strengths.
/// 2. Apply a sharpness exponent (alpha = 2.0) to emphasize strong connections.
/// 3. Row-normalize to get transition probabilities.
pub fn correlation_to_tpm(data: &ClimateCorrelations) -> TransitionMatrix {
let n = data.n_indices;
let alpha = 2.0;
let mut tpm = vec![0.0f64; n * n];
for i in 0..n {
let mut row_sum = 0.0f64;
for j in 0..n {
let c = data.correlations[i * n + j].abs();
let val = c.powf(alpha);
tpm[i * n + j] = val;
row_sum += val;
}
// Row-normalize
if row_sum > 1e-30 {
for j in 0..n {
tpm[i * n + j] /= row_sum;
}
}
}
TransitionMatrix { size: n, data: tpm }
}
/// Extract a sub-TPM for a subset of climate indices.
pub fn extract_sub_tpm(tpm: &TransitionMatrix, indices: &[usize]) -> TransitionMatrix {
let n = indices.len();
let mut sub = vec![0.0f64; n * n];
for (si, &ii) in indices.iter().enumerate() {
let row_sum: f64 = indices.iter().map(|&ij| tpm.data[ii * tpm.size + ij]).sum();
for (sj, &ij) in indices.iter().enumerate() {
sub[si * n + sj] = tpm.data[ii * tpm.size + ij] / row_sum.max(1e-30);
}
}
TransitionMatrix { size: n, data: sub }
}
/// Generate a null-model correlation matrix by shuffling off-diagonal entries.
pub fn generate_null_tpm(data: &ClimateCorrelations, rng: &mut impl rand::Rng) -> TransitionMatrix {
let n = data.n_indices;
let mut corr = data.correlations.clone();
// Collect upper-triangular off-diagonal values
let mut upper: Vec<f64> = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
upper.push(corr[i * n + j]);
}
}
// Shuffle
for k in (1..upper.len()).rev() {
let swap_idx = rng.gen_range(0..=k);
upper.swap(k, swap_idx);
}
// Put back (symmetric)
let mut idx = 0;
for i in 0..n {
for j in (i + 1)..n {
corr[i * n + j] = upper[idx];
corr[j * n + i] = upper[idx];
idx += 1;
}
}
let shuffled = ClimateCorrelations {
n_indices: n,
correlations: corr,
variant: "Null".into(),
};
correlation_to_tpm(&shuffled)
}
/// Generate monthly seasonal variation of correlations.
///
/// ENSO peaks in boreal winter (DJF), weakens in spring (MAM).
/// NAO is strongest in winter, weaker in summer.
pub fn generate_monthly_tpms(base: &ClimateCorrelations) -> Vec<(String, TransitionMatrix)> {
let months = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
let n = base.n_indices;
// ENSO seasonal modulation: peaks in DJF (months 0,1,11)
let enso_seasonal = [1.3, 1.2, 0.9, 0.7, 0.6, 0.5, 0.5, 0.6, 0.7, 0.9, 1.1, 1.3];
// NAO seasonal modulation: peaks in DJF
let nao_seasonal = [1.4, 1.3, 1.0, 0.7, 0.5, 0.4, 0.4, 0.5, 0.7, 0.9, 1.1, 1.3];
months
.iter()
.enumerate()
.map(|(m, &name)| {
let mut corr = base.correlations.clone();
// Modulate ENSO connections
for j in 0..n {
if j != INDEX_ENSO {
corr[INDEX_ENSO * n + j] *= enso_seasonal[m];
corr[j * n + INDEX_ENSO] *= enso_seasonal[m];
// Clamp to valid correlation range
corr[INDEX_ENSO * n + j] = corr[INDEX_ENSO * n + j].min(0.95);
corr[j * n + INDEX_ENSO] = corr[j * n + INDEX_ENSO].min(0.95);
}
}
// Modulate NAO connections
for j in 0..n {
if j != INDEX_NAO {
corr[INDEX_NAO * n + j] *= nao_seasonal[m];
corr[j * n + INDEX_NAO] *= nao_seasonal[m];
corr[INDEX_NAO * n + j] = corr[INDEX_NAO * n + j].min(0.95);
corr[j * n + INDEX_NAO] = corr[j * n + INDEX_NAO].min(0.95);
}
}
let monthly = ClimateCorrelations {
n_indices: n,
correlations: corr,
variant: format!("{} modulated", name),
};
(name.to_string(), correlation_to_tpm(&monthly))
})
.collect()
}