use crate::error::PrivacyError;
use crate::types::{AttractorKind, ChaosTelemetry};
use serde::{Deserialize, Serialize};
extern crate alloc;
use alloc::vec::Vec;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChuaParams {
pub alpha: f64,
pub beta: f64,
pub m0: f64,
pub m1: f64,
pub dt: f64,
}
impl Default for ChuaParams {
fn default() -> Self {
Self {
alpha: 9.0,
beta: 14.286,
m0: -1.0 / 7.0,
m1: 2.0 / 7.0,
dt: 0.01,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct ChuaState {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl ChuaState {
pub fn default_ic() -> Self {
Self { x: 0.7, y: 0.0, z: 0.0 }
}
}
#[derive(Debug, Clone)]
pub struct ChuaAttractor {
pub params: ChuaParams,
pub state: ChuaState,
pub lyapunov: f64,
steps: u64,
variance_acc: f64,
prev_x: f64,
}
impl ChuaAttractor {
pub fn new() -> Self {
Self::with_params(ChuaParams::default(), ChuaState::default_ic())
}
pub fn with_params(params: ChuaParams, ic: ChuaState) -> Self {
Self {
params,
state: ic,
lyapunov: 4.5,
steps: 0,
variance_acc: 0.0,
prev_x: ic.x,
}
}
fn f(&self, x: f64) -> f64 {
let m0 = self.params.m0;
let m1 = self.params.m1;
m1 * x + 0.5 * (m0 - m1) * ((x + 1.0).abs() - (x - 1.0).abs())
}
fn derivatives(&self, s: &ChuaState) -> (f64, f64, f64) {
let alpha = self.params.alpha;
let beta = self.params.beta;
let dxdt = alpha * (s.y - s.x - self.f(s.x));
let dydt = s.x - s.y + s.z;
let dzdt = -beta * s.y;
(dxdt, dydt, dzdt)
}
#[allow(clippy::manual_clamp)]
pub fn step(&mut self) {
let dt = self.params.dt;
let s = self.state;
let (k1x, k1y, k1z) = self.derivatives(&s);
let s2 = ChuaState {
x: s.x + 0.5 * dt * k1x,
y: s.y + 0.5 * dt * k1y,
z: s.z + 0.5 * dt * k1z,
};
let (k2x, k2y, k2z) = self.derivatives(&s2);
let s3 = ChuaState {
x: s.x + 0.5 * dt * k2x,
y: s.y + 0.5 * dt * k2y,
z: s.z + 0.5 * dt * k2z,
};
let (k3x, k3y, k3z) = self.derivatives(&s3);
let s4 = ChuaState {
x: s.x + dt * k3x,
y: s.y + dt * k3y,
z: s.z + dt * k3z,
};
let (k4x, k4y, k4z) = self.derivatives(&s4);
self.state.x += dt / 6.0 * (k1x + 2.0 * k2x + 2.0 * k3x + k4x);
self.state.y += dt / 6.0 * (k1y + 2.0 * k2y + 2.0 * k3y + k4y);
self.state.z += dt / 6.0 * (k1z + 2.0 * k2z + 2.0 * k3z + k4z);
let dx = (self.state.x - self.prev_x).abs();
self.variance_acc = 0.99 * self.variance_acc + 0.01 * dx;
self.prev_x = self.state.x;
if self.variance_acc > 1e-10 {
self.lyapunov = (self.variance_acc * 1000.0).ln().max(0.0).min(10.0);
}
self.steps += 1;
}
pub fn sample_bits(&mut self, n: usize) -> Vec<u8> {
let mut bits = Vec::with_capacity(n.div_ceil(8));
let mut byte = 0u8;
let mut bit_pos = 0u8;
for _ in 0..n {
self.step();
let bit = ((self.state.x > 0.0) ^ (self.state.z > 0.0)) as u8;
byte |= bit << bit_pos;
bit_pos += 1;
if bit_pos == 8 {
bits.push(byte);
byte = 0;
bit_pos = 0;
}
}
if bit_pos > 0 {
bits.push(byte);
}
bits
}
pub fn is_stalled(&self) -> bool {
self.steps > 100 && self.variance_acc < 0.01
}
pub fn perturbation(&self) -> f64 {
let t_norm = (self.steps as f64) / 1000.0;
let delta = self.state.x * (self.lyapunov * t_norm).exp();
delta.tanh() }
pub fn telemetry(&self) -> ChaosTelemetry {
ChaosTelemetry {
lyapunov: self.lyapunov,
h_min: if self.is_stalled() { 0.0 } else { 0.99 },
passed: !self.is_stalled() && self.lyapunov >= 4.5,
attractor: AttractorKind::Chua,
}
}
pub fn validate(&self) -> Result<(), PrivacyError> {
if self.is_stalled() {
return Err(PrivacyError::AttractorStalled(self.lyapunov));
}
Ok(())
}
}
impl Default for ChuaAttractor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chua_steps() {
let mut chua = ChuaAttractor::new();
for _ in 0..1000 {
chua.step();
}
assert!(chua.state.x.abs() > 1e-10 || chua.state.y.abs() > 1e-10);
}
#[test]
fn test_sample_bits_length() {
let mut chua = ChuaAttractor::new();
let bits = chua.sample_bits(256);
assert_eq!(bits.len(), 32); }
#[test]
fn test_perturbation_bounded() {
let mut chua = ChuaAttractor::new();
for _ in 0..100 {
chua.step();
}
let p = chua.perturbation();
assert!(p > -1.0 && p < 1.0);
}
}