Skip to main content

dreamwell_engine/input/
wave.rs

1//! DreamWaveController — decoherence-based observer representation.
2//!
3//! The observer exists as a coherent wave form (particle cloud) that decoheres
4//! into zone-specific classical forms (humanoid, vehicle, fluid) when entering
5//! decoherence fields. The transition uses the quantum decoherence kernel:
6//!
7//!   coherence(t) = exp(-Gamma * t)
8//!
9//! where Gamma is the zone's decoherence rate. This is not metaphor — it is the
10//! same exponential decay that governs off-diagonal density matrix elements
11//! in open quantum systems (Lindblad master equation).
12//!
13//! Clean Compute: all state is inline arrays. Zero allocation per frame.
14//! Pre-allocated bone target buffer reused every frame.
15
16/// The observer's physical representation form.
17/// Transitions between forms are governed by DecoherenceFields on zones.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum WaveForm {
20    /// Coherent particle cloud — golden ratio spiral distribution.
21    /// No skeleton. Pure procedural math. Cheapest representation.
22    #[default]
23    Particle,
24    /// Humanoid skeleton — particles cluster at bone positions.
25    /// FBX animation clips drive bone transforms.
26    Humanoid,
27    /// Rigid vehicle — particles form hull shape.
28    Vehicle,
29    /// Volume-preserving fluid mass.
30    Fluid,
31}
32
33impl WaveForm {
34    pub fn label(self) -> &'static str {
35        match self {
36            Self::Particle => "Particle",
37            Self::Humanoid => "Humanoid",
38            Self::Vehicle => "Vehicle",
39            Self::Fluid => "Fluid",
40        }
41    }
42}
43
44/// A spatial decoherence field attached to a Zone or POI.
45/// When the observer enters the field radius, the wave form begins
46/// collapsing into the target form at rate gamma.
47///
48/// Mathematical model: Lindblad decoherence kernel
49///   coherence(t) = exp(-gamma * t_in_field)
50///   target_weight = 1.0 - coherence(t)
51///
52/// The field strength falls off spatially:
53///   effective_gamma = gamma * max(0, 1 - dist/radius)
54#[derive(Debug, Clone)]
55pub struct DecoherenceField {
56    /// Center position of the field in world space.
57    pub position: [f32; 3],
58    /// Field radius — decoherence begins when observer enters this radius.
59    pub radius: f32,
60    /// Target wave form after full collapse.
61    pub target_form: WaveForm,
62    /// Decoherence rate Gamma (per second). Higher = faster collapse.
63    /// Typical: 2.0 (gentle, ~1s), 8.0 (fast, ~0.25s), 20.0 (instant snap).
64    pub gamma: f32,
65    /// Animation clip name for the target form (e.g., "climb_stairs").
66    /// Empty string = use zone default idle.
67    pub animation_clip: String,
68    /// PropertyTag that triggered this field (for debugging/attestation).
69    pub source_tag: String,
70}
71
72impl DecoherenceField {
73    /// Compute effective decoherence rate at a given distance from field center.
74    /// Returns 0.0 if outside radius (no decoherence).
75    #[inline]
76    pub fn effective_gamma(&self, observer_pos: &[f32; 3]) -> f32 {
77        let dx = observer_pos[0] - self.position[0];
78        let dy = observer_pos[1] - self.position[1];
79        let dz = observer_pos[2] - self.position[2];
80        let dist = (dx * dx + dy * dy + dz * dz).sqrt();
81        if dist >= self.radius {
82            return 0.0;
83        }
84        self.gamma * (1.0 - dist / self.radius)
85    }
86}
87
88/// Tracks the observer's current decoherence state.
89/// This is the density matrix diagonal — how "collapsed" the wave form is.
90#[derive(Debug, Clone)]
91pub struct DecoherenceState {
92    /// Current coherence level [0.0, 1.0].
93    /// 1.0 = fully coherent (pure particle). 0.0 = fully collapsed (target form).
94    pub coherence: f32,
95    /// Time spent in the current decoherence field (seconds).
96    pub time_in_field: f32,
97    /// Active decoherence rate (from the strongest nearby field).
98    pub active_gamma: f32,
99    /// Current wave form (what the observer IS right now).
100    pub current_form: WaveForm,
101    /// Target wave form (what the observer is COLLAPSING INTO, if any).
102    pub target_form: Option<WaveForm>,
103    /// Re-coherence rate when leaving a field. Default: gamma * 0.5 (slower return).
104    pub recoherence_rate: f32,
105    /// Whether the observer is currently inside a decoherence field.
106    pub in_field: bool,
107    /// Animation clip active during collapse (from the field).
108    pub active_clip: String,
109}
110
111impl Default for DecoherenceState {
112    fn default() -> Self {
113        Self {
114            coherence: 1.0,
115            time_in_field: 0.0,
116            active_gamma: 0.0,
117            current_form: WaveForm::Particle,
118            target_form: None,
119            recoherence_rate: 2.0,
120            in_field: false,
121            active_clip: String::new(),
122        }
123    }
124}
125
126/// DreamWaveController — the observer's wave form representation.
127/// Wraps ParticleController with zone-driven decoherence transitions.
128///
129/// The controller exists in one of two regimes:
130/// 1. Coherent: ParticleController drives position and shape (cheap, procedural)
131/// 2. Decohering/Collapsed: Target form's bone positions blend with particle positions
132///
133/// The transition between regimes uses the Lindblad decoherence kernel:
134///   coherence(t) = exp(-Gamma * t)
135///
136/// Per-particle position during transition:
137///   pos = lerp(particle_offset, bone_target, 1.0 - coherence)
138pub struct DreamWaveController {
139    /// The underlying particle controller (always exists, drives position/velocity).
140    pub particle: super::particle::ParticleController,
141    /// Current decoherence state.
142    pub decoherence: DecoherenceState,
143    /// Quantum locomotion density matrix state.
144    pub quantum: QuantumLocomotionState,
145    /// Per-particle target positions (bone positions for humanoid, hull points for vehicle).
146    /// Pre-allocated, reused every frame (Clean Compute).
147    pub bone_targets: Vec<[f32; 3]>,
148    /// Per-particle coherence weight (for GPU upload).
149    /// 1.0 = at particle position, 0.0 = at bone target.
150    pub particle_weights: Vec<f32>,
151    /// Active decoherence fields in range (sorted by distance, max 4).
152    /// Reserved for future per-frame field sorting/caching.
153    #[allow(dead_code)]
154    active_fields: Vec<DecoherenceField>,
155    /// Scratch buffer for zone field parsing.
156    /// Reserved for future zone tag batch parsing.
157    #[allow(dead_code)]
158    field_scratch: Vec<DecoherenceField>,
159}
160
161impl DreamWaveController {
162    pub fn new(position: [f32; 3], particle_count: u32) -> Self {
163        Self {
164            particle: super::particle::ParticleController::new(position, particle_count),
165            decoherence: DecoherenceState::default(),
166            quantum: QuantumLocomotionState::new(particle_count),
167            bone_targets: vec![[0.0; 3]; particle_count as usize],
168            particle_weights: vec![1.0; particle_count as usize],
169            active_fields: Vec::with_capacity(4),
170            field_scratch: Vec::with_capacity(8),
171        }
172    }
173
174    /// Update the wave controller from input + zone decoherence fields.
175    /// Returns true if the observer moved this frame.
176    pub fn update(
177        &mut self,
178        forward: bool,
179        back: bool,
180        left: bool,
181        right: bool,
182        jump: bool,
183        sprint: bool,
184        camera_yaw: f32,
185        dt: f32,
186        fields: &[DecoherenceField],
187    ) -> bool {
188        // 1. Update underlying particle position/velocity/locomotion.
189        let moved = self
190            .particle
191            .update(forward, back, left, right, jump, sprint, camera_yaw, dt);
192
193        // 1b. Quantum locomotion density matrix tick.
194        let moving = forward || back || left || right;
195        let input_bias = [
196            if !moving { 2.0 } else { 0.0 },                           // Idle bias when not moving
197            if moving && !sprint { 2.0 } else { 0.0 },                 // Moving bias
198            if jump { 3.0 } else { 0.0 },                              // Jump bias on press
199            if self.particle.landing_timer > 0.0 { 2.0 } else { 0.0 }, // Landing
200            if sprint && moving { 2.0 } else { 0.0 },                  // Sprint bias
201        ];
202        // Decoherence from terrain contact
203        let epsilon = if self.particle.grounded { 0.3 * dt } else { 0.05 * dt };
204        let _measured_mode = self.quantum.tick(dt, &input_bias, epsilon);
205
206        // 2. Find the strongest decoherence field affecting us.
207        let pos = &self.particle.position;
208        let mut strongest_gamma = 0.0f32;
209        let mut strongest_field: Option<&DecoherenceField> = None;
210        for field in fields {
211            let g = field.effective_gamma(pos);
212            if g > strongest_gamma {
213                strongest_gamma = g;
214                strongest_field = Some(field);
215            }
216        }
217
218        // 3. Apply decoherence kernel.
219        if let Some(field) = strongest_field {
220            // Entering or continuing in a field.
221            self.decoherence.in_field = true;
222            self.decoherence.active_gamma = strongest_gamma;
223
224            // Sprint modulates collapse rate: shift = 2x faster decoherence.
225            let modulated_gamma = if sprint { strongest_gamma * 2.0 } else { strongest_gamma };
226
227            self.decoherence.time_in_field += dt;
228
229            // Lindblad decoherence kernel: coherence decays exponentially.
230            // coherence(t) = exp(-Gamma * t)
231            self.decoherence.coherence = (-modulated_gamma * self.decoherence.time_in_field).exp();
232
233            // Set target form from field.
234            if self.decoherence.target_form.is_none() || self.decoherence.target_form != Some(field.target_form) {
235                self.decoherence.target_form = Some(field.target_form);
236                self.decoherence.active_clip = field.animation_clip.clone();
237            }
238
239            // Fully collapsed threshold.
240            if self.decoherence.coherence < 0.01 {
241                self.decoherence.coherence = 0.0;
242                self.decoherence.current_form = field.target_form;
243            }
244        } else {
245            // Outside all fields — re-cohere back to particle.
246            self.decoherence.in_field = false;
247            self.decoherence.time_in_field = 0.0;
248
249            if self.decoherence.coherence < 1.0 {
250                // Re-coherence: exponential return to particle form.
251                // recoherence(t) = 1 - exp(-recoherence_rate * dt)
252                let rate = self.decoherence.recoherence_rate;
253                self.decoherence.coherence += (1.0 - self.decoherence.coherence) * (1.0 - (-rate * dt).exp());
254
255                if self.decoherence.coherence > 0.99 {
256                    self.decoherence.coherence = 1.0;
257                    self.decoherence.current_form = WaveForm::Particle;
258                    self.decoherence.target_form = None;
259                    self.decoherence.active_clip.clear();
260                }
261            }
262        }
263
264        // 4. Update per-particle weights for GPU.
265        let w = self.decoherence.coherence;
266        for pw in &mut self.particle_weights {
267            *pw = w;
268        }
269
270        moved
271    }
272
273    /// Current coherence level (1.0 = fully particle, 0.0 = fully collapsed).
274    pub fn coherence(&self) -> f32 {
275        self.decoherence.coherence
276    }
277
278    /// Current wave form.
279    pub fn current_form(&self) -> WaveForm {
280        self.decoherence.current_form
281    }
282
283    /// Whether the observer is actively decohering.
284    pub fn is_decohering(&self) -> bool {
285        self.decoherence.in_field && self.decoherence.coherence > 0.01
286    }
287
288    /// Whether the observer is fully collapsed into a target form.
289    pub fn is_collapsed(&self) -> bool {
290        self.decoherence.coherence < 0.01
291    }
292
293    /// Whether the observer is re-cohering back to particle.
294    pub fn is_recohering(&self) -> bool {
295        !self.decoherence.in_field && self.decoherence.coherence < 0.99
296    }
297
298    /// Set bone target positions for the current target form.
299    /// Called when FBX skeleton is evaluated.
300    pub fn set_bone_targets(&mut self, bones: &[[f32; 3]]) {
301        // Map particles to nearest bones via spatial proximity.
302        let particle_count = self.particle.particle_count as usize;
303        let offsets = super::particle::particle_sphere_offsets(self.particle.particle_count);
304        let params = self.particle.shape_params();
305        let radius = self.particle.cloud_radius * params.radius_scale;
306
307        for i in 0..particle_count.min(self.bone_targets.len()) {
308            if i >= offsets.len() {
309                break;
310            }
311            // Particle world position (particle distribution).
312            let px = self.particle.position[0] + offsets[i][0] * params.stretch[0] * radius;
313            let py = self.particle.position[1] + offsets[i][1] * params.stretch[1] * radius;
314            let pz = self.particle.position[2] + offsets[i][2] * params.stretch[2] * radius;
315
316            // Find nearest bone.
317            let mut best_dist_sq = f32::MAX;
318            let mut best_bone = [0.0f32; 3];
319            for bone in bones {
320                let dx = px - bone[0];
321                let dy = py - bone[1];
322                let dz = pz - bone[2];
323                let d2 = dx * dx + dy * dy + dz * dz;
324                if d2 < best_dist_sq {
325                    best_dist_sq = d2;
326                    best_bone = *bone;
327                }
328            }
329            self.bone_targets[i] = best_bone;
330        }
331    }
332}
333
334// ═══════════════════════════════════════════════════════════════════════════════
335// Quantum Locomotion — density matrix, Hamiltonian evolution, dephasing,
336// Born rule measurement, interference kernels.
337//
338// The observer's locomotion state is modeled as a 5-dimensional Hilbert space
339// H_loc ≅ C^5 with basis states |Idle⟩, |Moving⟩, |Jumping⟩, |Landing⟩, |Sprinting⟩.
340//
341// The density matrix ρ carries both mode populations (diagonal) and inter-mode
342// coherence (off-diagonal). Coherent evolution via a Hamiltonian generates
343// interference that visibly affects particle geometry — this is not metaphor.
344// ═══════════════════════════════════════════════════════════════════════════════
345
346/// Complex number for density matrix operations. No external deps.
347#[derive(Debug, Clone, Copy, Default)]
348pub struct Complex {
349    pub re: f32,
350    pub im: f32,
351}
352
353impl Complex {
354    pub const ZERO: Self = Self { re: 0.0, im: 0.0 };
355    pub const ONE: Self = Self { re: 1.0, im: 0.0 };
356
357    pub fn new(re: f32, im: f32) -> Self {
358        Self { re, im }
359    }
360    pub fn from_real(r: f32) -> Self {
361        Self { re: r, im: 0.0 }
362    }
363
364    pub fn conj(self) -> Self {
365        Self {
366            re: self.re,
367            im: -self.im,
368        }
369    }
370    pub fn norm_sq(self) -> f32 {
371        self.re * self.re + self.im * self.im
372    }
373    pub fn norm(self) -> f32 {
374        self.norm_sq().sqrt()
375    }
376
377    pub fn mul(self, other: Self) -> Self {
378        Self {
379            re: self.re * other.re - self.im * other.im,
380            im: self.re * other.im + self.im * other.re,
381        }
382    }
383
384    pub fn add(self, other: Self) -> Self {
385        Self {
386            re: self.re + other.re,
387            im: self.im + other.im,
388        }
389    }
390
391    pub fn sub(self, other: Self) -> Self {
392        Self {
393            re: self.re - other.re,
394            im: self.im - other.im,
395        }
396    }
397
398    pub fn scale(self, s: f32) -> Self {
399        Self {
400            re: self.re * s,
401            im: self.im * s,
402        }
403    }
404
405    /// e^{i*theta} = cos(theta) + i*sin(theta)
406    #[allow(dead_code)]
407    pub fn exp_i(theta: f32) -> Self {
408        Self {
409            re: theta.cos(),
410            im: theta.sin(),
411        }
412    }
413}
414
415/// 5x5 density matrix for the locomotion Hilbert space H_loc = C^5.
416/// Basis: |I>=Idle, |M>=Moving, |J>=Jumping, |L>=Landing, |S>=Sprinting.
417///
418/// Hermitian: rho_dag = rho (entries[i][j] = conj(entries[j][i]))
419/// Positive semidefinite: all eigenvalues >= 0
420/// Trace 1: sum_k rho_kk = 1
421///
422/// This is the standard density matrix from quantum information theory.
423/// Diagonal entries are mode populations. Off-diagonal entries carry coherence.
424pub struct DensityMatrix5 {
425    /// Flat 5x5 complex entries. entries[i*5+j] = rho_{ij}.
426    pub entries: [Complex; 25],
427}
428
429impl DensityMatrix5 {
430    /// Pure state |k><k| — all population in mode k.
431    pub fn pure_state(k: usize) -> Self {
432        let mut entries = [Complex::ZERO; 25];
433        if k < 5 {
434            entries[k * 5 + k] = Complex::ONE;
435        }
436        Self { entries }
437    }
438
439    /// Create from a state vector (5 complex amplitudes).
440    /// rho = |psi><psi| where |psi> = sum alpha_k |k>
441    pub fn from_state_vector(amplitudes: &[Complex; 5]) -> Self {
442        let mut entries = [Complex::ZERO; 25];
443        for i in 0..5 {
444            for j in 0..5 {
445                entries[i * 5 + j] = amplitudes[i].mul(amplitudes[j].conj());
446            }
447        }
448        Self { entries }
449    }
450
451    /// Trace: sum_k rho_kk
452    pub fn trace(&self) -> f32 {
453        (0..5).map(|k| self.entries[k * 5 + k].re).sum()
454    }
455
456    /// Diagonal populations (mode probabilities).
457    pub fn populations(&self) -> [f32; 5] {
458        let mut p = [0.0f32; 5];
459        for k in 0..5 {
460            p[k] = self.entries[k * 5 + k].re;
461        }
462        p
463    }
464
465    /// Total off-diagonal coherence magnitude: sum_{i<j} |rho_{ij}|
466    pub fn coherence_magnitude(&self) -> f32 {
467        let mut c = 0.0f32;
468        for i in 0..5 {
469            for j in (i + 1)..5 {
470                c += self.entries[i * 5 + j].norm();
471            }
472        }
473        c
474    }
475
476    /// Completely dephase: zero all off-diagonals. rho -> Delta(rho).
477    pub fn dephased(&self) -> Self {
478        let mut result = Self {
479            entries: [Complex::ZERO; 25],
480        };
481        for k in 0..5 {
482            result.entries[k * 5 + k] = self.entries[k * 5 + k];
483        }
484        result
485    }
486
487    /// Partial dephasing channel: rho -> (1-eps)rho + eps*Delta(rho).
488    /// eps in [0,1]. eps=0: no change. eps=1: full dephasing.
489    pub fn apply_dephasing(&mut self, epsilon: f32) {
490        let retain = 1.0 - epsilon.clamp(0.0, 1.0);
491        for i in 0..5 {
492            for j in 0..5 {
493                if i != j {
494                    self.entries[i * 5 + j] = self.entries[i * 5 + j].scale(retain);
495                }
496            }
497        }
498    }
499
500    /// Apply unitary evolution: rho -> U * rho * U_dag.
501    /// U is a 5x5 complex matrix (flat, row-major).
502    pub fn apply_unitary(&mut self, u: &[Complex; 25]) {
503        // Compute U * rho
504        let mut u_rho = [Complex::ZERO; 25];
505        for i in 0..5 {
506            for j in 0..5 {
507                let mut sum = Complex::ZERO;
508                for k in 0..5 {
509                    sum = sum.add(u[i * 5 + k].mul(self.entries[k * 5 + j]));
510                }
511                u_rho[i * 5 + j] = sum;
512            }
513        }
514        // Compute (U * rho) * U_dag
515        for i in 0..5 {
516            for j in 0..5 {
517                let mut sum = Complex::ZERO;
518                for k in 0..5 {
519                    sum = sum.add(u_rho[i * 5 + k].mul(u[j * 5 + k].conj()));
520                }
521                self.entries[i * 5 + j] = sum;
522            }
523        }
524    }
525
526    /// Born rule measurement: p_k = rho_kk.
527    /// Returns mode index sampled from probability distribution.
528    /// Uses deterministic seed for reproducible gameplay.
529    pub fn measure(&self, seed: u64) -> usize {
530        let pops = self.populations();
531        // Deterministic pseudo-random from seed.
532        let mut s = seed;
533        s = s.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
534        let r = (s >> 33) as f32 / (1u64 << 31) as f32; // [0, 1)
535        let mut cumulative = 0.0;
536        for (i, &p) in pops.iter().enumerate() {
537            cumulative += p;
538            if r < cumulative {
539                return i;
540            }
541        }
542        4 // fallback: Sprint
543    }
544}
545
546/// Locomotion Hamiltonian H_loc = H_bias + H_input + H_couple + H_phase.
547/// Generates the unitary U_tick = exp(-i*H*dt/hbar_eff) each frame.
548pub struct LocomotionHamiltonian {
549    /// Effective reduced Planck constant (engine tuning parameter).
550    pub hbar_eff: f32,
551    /// Bias energies per mode [E_I, E_M, E_J, E_L, E_S].
552    pub bias: [f32; 5],
553    /// Coupling strengths between adjacent modes.
554    /// Order: omega_IM, omega_MS, omega_MJ, omega_JL, omega_LI
555    pub couplings: [f32; 5],
556}
557
558impl Default for LocomotionHamiltonian {
559    fn default() -> Self {
560        Self {
561            hbar_eff: 1.0,
562            bias: [0.0, 0.2, 0.5, 0.3, 0.4],      // Idle lowest energy
563            couplings: [1.0, 0.8, 0.6, 0.9, 0.7], // IM, MS, MJ, JL, LI
564        }
565    }
566}
567
568impl LocomotionHamiltonian {
569    /// Build the 5x5 Hamiltonian matrix from current parameters + input.
570    /// input_bias: per-mode input contribution [b_I, b_M, b_J, b_L, b_S].
571    pub fn build_matrix(&self, input_bias: &[f32; 5], phase_time: f32) -> [Complex; 25] {
572        let mut h = [Complex::ZERO; 25];
573
574        // Diagonal: bias + input + phase drift
575        for k in 0..5 {
576            h[k * 5 + k] = Complex::from_real(self.bias[k] + input_bias[k] + 0.1 * (k as f32 * phase_time).sin());
577        }
578
579        // Off-diagonal couplings (Hermitian: h_ij = h_ji*)
580        // Coupling pairs: (I,M)=0, (M,S)=1, (M,J)=2, (J,L)=3, (L,I)=4
581        let pairs: [(usize, usize); 5] = [(0, 1), (1, 4), (1, 2), (2, 3), (3, 0)];
582        for (idx, &(i, j)) in pairs.iter().enumerate() {
583            let w = Complex::from_real(self.couplings[idx]);
584            h[i * 5 + j] = w;
585            h[j * 5 + i] = w.conj(); // Hermitian
586        }
587
588        h
589    }
590
591    /// Compute U = exp(-i*H*dt/hbar_eff) via second-order Taylor expansion.
592    /// U = I - i*H*dt/hbar + (1/2)(-i*H*dt/hbar)^2
593    /// Sufficient for small dt.
594    pub fn compute_unitary(&self, h: &[Complex; 25], dt: f32) -> [Complex; 25] {
595        let scale = dt / self.hbar_eff;
596        let mut u = [Complex::ZERO; 25];
597
598        // U = I
599        for k in 0..5 {
600            u[k * 5 + k] = Complex::ONE;
601        }
602
603        // U -= i*H*scale  (first order)
604        for i in 0..5 {
605            for j in 0..5 {
606                // -i * h_ij * scale
607                let ih = Complex::new(h[i * 5 + j].im * scale, -h[i * 5 + j].re * scale);
608                u[i * 5 + j] = u[i * 5 + j].sub(ih);
609            }
610        }
611
612        // U += (1/2)(-i*H*scale)^2 = -(1/2)*H^2*scale^2  (second order)
613        let s2 = scale * scale * 0.5;
614        for i in 0..5 {
615            for j in 0..5 {
616                let mut h2_ij = Complex::ZERO;
617                for k in 0..5 {
618                    h2_ij = h2_ij.add(h[i * 5 + k].mul(h[k * 5 + j]));
619                }
620                u[i * 5 + j] = u[i * 5 + j].sub(h2_ij.scale(s2));
621            }
622        }
623
624        u
625    }
626}
627
628/// Interference kernel kappa_{n,ij} in C^3 for particle n between modes i and j.
629/// Controls how off-diagonal coherence rho_{ij} affects particle geometry.
630///
631/// The particle position with interference is:
632///   x_n = root + sum_k rho_kk * g_{n,k} + 2*sum_{i<j} Re[kappa_{n,ij} * rho_{ij}]
633///
634/// This is the decisive test from QUANTUM.md S7:
635///   If exists n: x_n(rho) != x_n(Delta(rho)), then coherence is not metaphor — it's simulation.
636pub struct InterferenceKernels {
637    /// For each mode pair (i,j) with i<j, a 3D complex displacement.
638    /// 10 pairs x 3 axes x 2 (re,im) = 60 f32 values per particle.
639    /// We store kernels for a representative subset of particles.
640    /// Pair order: (0,1),(0,2),(0,3),(0,4),(1,2),(1,3),(1,4),(2,3),(2,4),(3,4)
641    pub kernels: Vec<[[Complex; 3]; 10]>,
642}
643
644impl InterferenceKernels {
645    /// Generate interference kernels for N particles.
646    /// Each kernel is designed to create visible wave-like distortion
647    /// when the corresponding mode coherence is nonzero.
648    pub fn generate(particle_count: u32) -> Self {
649        let n = particle_count as usize;
650        let mut kernels = Vec::with_capacity(n);
651        let golden = (1.0 + 5.0f32.sqrt()) / 2.0;
652
653        for p in 0..n {
654            let t = p as f32 / n.max(1) as f32;
655            let mut pair_kernels = [[Complex::ZERO; 3]; 10];
656
657            // Each mode pair gets a distinct interference pattern.
658            // The pattern creates wave-like ripples, spirals, or oscillations
659            // that are ONLY visible when off-diagonal coherence is nonzero.
660
661            // (I,M) pair 0: Idle<->Moving creates horizontal wave ripple
662            pair_kernels[0] = [
663                Complex::new(0.3 * (t * 12.0).sin(), 0.1 * (t * 8.0).cos()),
664                Complex::new(0.0, 0.15 * (t * 10.0).sin()),
665                Complex::new(0.2 * (t * 6.0).cos(), 0.0),
666            ];
667
668            // (I,J) pair 1: Idle<->Jump creates vertical expansion pulse
669            pair_kernels[1] = [
670                Complex::new(0.0, 0.1 * (t * 4.0).sin()),
671                Complex::new(0.4 * (t * 8.0).sin(), 0.2 * (t * golden * 5.0).cos()),
672                Complex::new(0.0, 0.1 * (t * 4.0).cos()),
673            ];
674
675            // (I,L) pair 2: Idle<->Land creates compression ripple
676            pair_kernels[2] = [
677                Complex::new(0.15 * (t * 16.0).sin(), 0.0),
678                Complex::new(-0.3 * (t * 12.0).cos(), 0.1),
679                Complex::new(0.15 * (t * 16.0).cos(), 0.0),
680            ];
681
682            // (I,S) pair 3: Idle<->Sprint creates elongation wave
683            pair_kernels[3] = [
684                Complex::new(0.1, 0.2 * (t * 20.0).sin()),
685                Complex::new(0.0, 0.05),
686                Complex::new(0.35 * (t * 14.0).sin(), 0.15 * (t * 7.0).cos()),
687            ];
688
689            // (M,J) pair 4: Move<->Jump creates arc transition
690            pair_kernels[4] = [
691                Complex::new(0.1 * (t * 6.0).cos(), 0.0),
692                Complex::new(0.25 * (t * 10.0).sin(), 0.2 * (t * 5.0).cos()),
693                Complex::new(0.15 * (t * 8.0).sin(), 0.0),
694            ];
695
696            // (M,L) pair 5: Move<->Land creates ground-impact spread
697            pair_kernels[5] = [
698                Complex::new(0.2 * (t * 14.0).sin(), 0.1 * (t * 7.0).cos()),
699                Complex::new(-0.15 * (t * 18.0).cos(), 0.0),
700                Complex::new(0.2 * (t * 14.0).cos(), -0.1 * (t * 7.0).sin()),
701            ];
702
703            // (M,S) pair 6: Move<->Sprint creates velocity streak
704            pair_kernels[6] = [
705                Complex::new(0.05, 0.1 * (t * 22.0).sin()),
706                Complex::new(0.0, 0.05),
707                Complex::new(0.3 * (t * 18.0).sin(), 0.2 * (t * 9.0).cos()),
708            ];
709
710            // (J,L) pair 7: Jump<->Land creates bounce oscillation
711            pair_kernels[7] = [
712                Complex::new(0.1 * (t * 8.0).cos(), 0.0),
713                Complex::new(0.35 * (t * 16.0).sin(), -0.15 * (t * 8.0).cos()),
714                Complex::new(0.1 * (t * 8.0).sin(), 0.0),
715            ];
716
717            // (J,S) pair 8: Jump<->Sprint creates aerial streak
718            pair_kernels[8] = [
719                Complex::new(0.15 * (t * 10.0).sin(), 0.1),
720                Complex::new(0.2 * (t * 12.0).cos(), 0.15 * (t * 6.0).sin()),
721                Complex::new(0.25 * (t * 15.0).sin(), 0.0),
722            ];
723
724            // (L,S) pair 9: Land<->Sprint creates recovery burst
725            pair_kernels[9] = [
726                Complex::new(0.2 * (t * 20.0).sin(), 0.1 * (t * 10.0).cos()),
727                Complex::new(-0.1 * (t * 24.0).cos(), 0.05),
728                Complex::new(0.15 * (t * 16.0).cos(), 0.15 * (t * 8.0).sin()),
729            ];
730
731            kernels.push(pair_kernels);
732        }
733
734        Self { kernels }
735    }
736
737    /// Compute interference displacement for particle n from density matrix.
738    /// Returns the 3D displacement caused by off-diagonal coherence.
739    /// This is: 2*sum_{i<j} Re[kappa_{n,ij} * rho_{ij}]
740    pub fn interference_displacement(&self, n: usize, rho: &DensityMatrix5) -> [f32; 3] {
741        if n >= self.kernels.len() {
742            return [0.0; 3];
743        }
744        let k = &self.kernels[n];
745        let mut disp = [0.0f32; 3];
746
747        // Iterate over all 10 pairs (i<j)
748        let pairs: [(usize, usize); 10] = [
749            (0, 1),
750            (0, 2),
751            (0, 3),
752            (0, 4),
753            (1, 2),
754            (1, 3),
755            (1, 4),
756            (2, 3),
757            (2, 4),
758            (3, 4),
759        ];
760        for (idx, &(i, j)) in pairs.iter().enumerate() {
761            let rho_ij = rho.entries[i * 5 + j];
762            for axis in 0..3 {
763                // 2 * Re[kappa * rho_ij]
764                let product = k[idx][axis].mul(rho_ij);
765                disp[axis] += 2.0 * product.re;
766            }
767        }
768
769        disp
770    }
771}
772
773/// Full quantum locomotion state — wraps density matrix, Hamiltonian, kernels.
774/// This is the quantum simulation subsystem that satisfies QUANTUM.md S11.
775pub struct QuantumLocomotionState {
776    /// The density matrix rho in C^{5x5}.
777    pub rho: DensityMatrix5,
778    /// The Hamiltonian governing coherent evolution.
779    pub hamiltonian: LocomotionHamiltonian,
780    /// Interference kernels for particle rendering.
781    pub kernels: InterferenceKernels,
782    /// Cumulative phase time (for H_phase).
783    pub phase_time: f32,
784    /// Frame counter for measurement seed.
785    pub frame: u64,
786}
787
788impl QuantumLocomotionState {
789    pub fn new(particle_count: u32) -> Self {
790        // Initialize in Idle pure state: rho = |I><I|
791        Self {
792            rho: DensityMatrix5::pure_state(0),
793            hamiltonian: LocomotionHamiltonian::default(),
794            kernels: InterferenceKernels::generate(particle_count),
795            phase_time: 0.0,
796            frame: 0,
797        }
798    }
799
800    /// Tick the quantum locomotion state.
801    /// 1. Build input bias from player state
802    /// 2. Hamiltonian evolution (unitary)
803    /// 3. Decoherence channel (terrain/contact)
804    /// 4. Returns Born-rule measured mode
805    pub fn tick(&mut self, dt: f32, input_bias: &[f32; 5], decoherence_epsilon: f32) -> usize {
806        self.phase_time += dt;
807        self.frame += 1;
808
809        // 1. Build Hamiltonian
810        let h = self.hamiltonian.build_matrix(input_bias, self.phase_time);
811
812        // 2. Unitary evolution: rho -> U*rho*U_dag
813        let u = self.hamiltonian.compute_unitary(&h, dt);
814        self.rho.apply_unitary(&u);
815
816        // 3. Decoherence channel: rho -> (1-eps)rho + eps*Delta(rho)
817        self.rho.apply_dephasing(decoherence_epsilon);
818
819        // 4. Born rule measurement
820        self.rho.measure(self.frame)
821    }
822
823    /// Compute particle position with interference.
824    /// pos = root + sum_k rho_kk * g_{n,k} + interference_displacement(n)
825    pub fn particle_position_with_interference(
826        &self,
827        n: usize,
828        root: [f32; 3],
829        mode_templates: &[[f32; 3]; 5],
830    ) -> [f32; 3] {
831        let pops = self.rho.populations();
832        let interference = self.kernels.interference_displacement(n, &self.rho);
833
834        let mut pos = root;
835        for k in 0..5 {
836            pos[0] += pops[k] * mode_templates[k][0];
837            pos[1] += pops[k] * mode_templates[k][1];
838            pos[2] += pops[k] * mode_templates[k][2];
839        }
840        pos[0] += interference[0];
841        pos[1] += interference[1];
842        pos[2] += interference[2];
843
844        pos
845    }
846}
847
848/// Parsed result from a decoherence PropertyTag string.
849#[derive(Debug, Clone, PartialEq)]
850pub enum DecoherenceTagValue {
851    /// A form tag: target wave form + optional animation clip name.
852    Form(WaveForm, String),
853    /// A gamma override tag: decoherence rate value.
854    Gamma(f32),
855}
856
857/// Parse a decoherence PropertyTag string.
858/// Format: "decohere:form[:clip]" or "gamma:rate"
859/// Examples:
860///   "decohere:humanoid:climb_stairs" -> (Humanoid, "climb_stairs")
861///   "decohere:fluid" -> (Fluid, "")
862///   "gamma:4.0" -> Gamma(4.0)
863pub fn parse_decoherence_tag(tag: &str) -> Option<DecoherenceTagValue> {
864    if let Some(rest) = tag.strip_prefix("decohere:") {
865        let mut parts = rest.splitn(2, ':');
866        let form_str = parts.next()?;
867        let form = match form_str {
868            "humanoid" => WaveForm::Humanoid,
869            "vehicle" => WaveForm::Vehicle,
870            "fluid" => WaveForm::Fluid,
871            "particle" => WaveForm::Particle,
872            _ => return None,
873        };
874        let clip = parts.next().unwrap_or("").to_string();
875        Some(DecoherenceTagValue::Form(form, clip))
876    } else if let Some(rest) = tag.strip_prefix("gamma:") {
877        let rate: f32 = rest.parse().ok()?;
878        Some(DecoherenceTagValue::Gamma(rate))
879    } else {
880        None
881    }
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887
888    #[test]
889    fn wave_form_default_is_particle() {
890        assert_eq!(WaveForm::default(), WaveForm::Particle);
891    }
892
893    #[test]
894    fn decoherence_field_outside_radius_zero() {
895        let field = DecoherenceField {
896            position: [0.0, 0.0, 0.0],
897            radius: 5.0,
898            target_form: WaveForm::Humanoid,
899            gamma: 4.0,
900            animation_clip: String::new(),
901            source_tag: String::new(),
902        };
903        // Observer at distance 10 — well outside radius 5.
904        let g = field.effective_gamma(&[10.0, 0.0, 0.0]);
905        assert_eq!(g, 0.0, "gamma should be 0 outside field radius");
906    }
907
908    #[test]
909    fn decoherence_field_at_center_full_gamma() {
910        let field = DecoherenceField {
911            position: [5.0, 0.0, 5.0],
912            radius: 10.0,
913            target_form: WaveForm::Humanoid,
914            gamma: 8.0,
915            animation_clip: String::new(),
916            source_tag: String::new(),
917        };
918        // Observer at the exact center of the field.
919        let g = field.effective_gamma(&[5.0, 0.0, 5.0]);
920        assert!(
921            (g - 8.0).abs() < 1e-5,
922            "gamma at center should equal field gamma (8.0), got {}",
923            g
924        );
925    }
926
927    #[test]
928    fn decoherence_field_at_edge_near_zero() {
929        let field = DecoherenceField {
930            position: [0.0, 0.0, 0.0],
931            radius: 10.0,
932            target_form: WaveForm::Fluid,
933            gamma: 4.0,
934            animation_clip: String::new(),
935            source_tag: String::new(),
936        };
937        // Observer at distance 9.99 (just inside edge).
938        let g = field.effective_gamma(&[9.99, 0.0, 0.0]);
939        assert!(g < 0.01, "gamma near the edge should be near zero, got {}", g);
940        assert!(g > 0.0, "gamma just inside edge should be >0, got {}", g);
941    }
942
943    #[test]
944    fn dream_wave_controller_starts_coherent() {
945        let ctrl = DreamWaveController::new([0.0, 1.0, 0.0], 64);
946        assert!(
947            (ctrl.coherence() - 1.0).abs() < 1e-5,
948            "new controller should start fully coherent (1.0), got {}",
949            ctrl.coherence()
950        );
951        assert_eq!(ctrl.current_form(), WaveForm::Particle);
952        assert!(!ctrl.is_decohering());
953        assert!(!ctrl.is_collapsed());
954        assert!(!ctrl.is_recohering());
955    }
956
957    #[test]
958    fn dream_wave_controller_decoheres_in_field() {
959        let mut ctrl = DreamWaveController::new([0.0, 1.0, 0.0], 64);
960        let fields = vec![DecoherenceField {
961            position: [0.0, 1.0, 0.0],
962            radius: 10.0,
963            target_form: WaveForm::Humanoid,
964            gamma: 4.0,
965            animation_clip: "idle".into(),
966            source_tag: "decohere:humanoid".into(),
967        }];
968
969        // Run several frames inside the field.
970        for _ in 0..60 {
971            ctrl.update(false, false, false, false, false, false, 0.0, 0.016, &fields);
972        }
973
974        assert!(
975            ctrl.coherence() < 1.0,
976            "coherence should decrease inside a field, got {}",
977            ctrl.coherence()
978        );
979        assert!(ctrl.decoherence.in_field, "should be marked as in_field");
980        assert_eq!(
981            ctrl.decoherence.target_form,
982            Some(WaveForm::Humanoid),
983            "target form should be Humanoid"
984        );
985    }
986
987    #[test]
988    fn dream_wave_controller_recoheres_outside_field() {
989        let mut ctrl = DreamWaveController::new([0.0, 1.0, 0.0], 64);
990        let fields = vec![DecoherenceField {
991            position: [0.0, 1.0, 0.0],
992            radius: 10.0,
993            target_form: WaveForm::Humanoid,
994            gamma: 8.0,
995            animation_clip: String::new(),
996            source_tag: String::new(),
997        }];
998
999        // Decohere for a while.
1000        for _ in 0..120 {
1001            ctrl.update(false, false, false, false, false, false, 0.0, 0.016, &fields);
1002        }
1003        let coherence_in_field = ctrl.coherence();
1004        assert!(
1005            coherence_in_field < 0.5,
1006            "should be significantly decohered after 120 frames at gamma=8, got {}",
1007            coherence_in_field
1008        );
1009
1010        // Now update outside any field — re-coherence should begin.
1011        let no_fields: Vec<DecoherenceField> = vec![];
1012        for _ in 0..300 {
1013            ctrl.update(false, false, false, false, false, false, 0.0, 0.016, &no_fields);
1014        }
1015
1016        assert!(
1017            ctrl.coherence() > coherence_in_field,
1018            "coherence should increase after leaving field: was {}, now {}",
1019            coherence_in_field,
1020            ctrl.coherence()
1021        );
1022        assert!(!ctrl.decoherence.in_field, "should not be in_field after leaving");
1023    }
1024
1025    #[test]
1026    fn parse_decoherence_tag_humanoid() {
1027        let result = parse_decoherence_tag("decohere:humanoid:climb_stairs");
1028        assert_eq!(
1029            result,
1030            Some(DecoherenceTagValue::Form(WaveForm::Humanoid, "climb_stairs".into()))
1031        );
1032    }
1033
1034    #[test]
1035    fn parse_decoherence_tag_gamma() {
1036        let result = parse_decoherence_tag("gamma:4.0");
1037        assert_eq!(result, Some(DecoherenceTagValue::Gamma(4.0)));
1038    }
1039
1040    // ── Additional coverage ────────────────────────────────────────────
1041
1042    #[test]
1043    fn parse_decoherence_tag_fluid_no_clip() {
1044        let result = parse_decoherence_tag("decohere:fluid");
1045        assert_eq!(result, Some(DecoherenceTagValue::Form(WaveForm::Fluid, String::new())));
1046    }
1047
1048    #[test]
1049    fn parse_decoherence_tag_unknown_form_returns_none() {
1050        assert_eq!(parse_decoherence_tag("decohere:dragon"), None);
1051    }
1052
1053    #[test]
1054    fn parse_decoherence_tag_unrelated_returns_none() {
1055        assert_eq!(parse_decoherence_tag("some_other_tag"), None);
1056    }
1057
1058    #[test]
1059    fn parse_decoherence_tag_gamma_invalid_returns_none() {
1060        assert_eq!(parse_decoherence_tag("gamma:notanumber"), None);
1061    }
1062
1063    #[test]
1064    fn wave_form_labels() {
1065        assert_eq!(WaveForm::Particle.label(), "Particle");
1066        assert_eq!(WaveForm::Humanoid.label(), "Humanoid");
1067        assert_eq!(WaveForm::Vehicle.label(), "Vehicle");
1068        assert_eq!(WaveForm::Fluid.label(), "Fluid");
1069    }
1070
1071    #[test]
1072    fn dream_wave_controller_fully_collapses() {
1073        let mut ctrl = DreamWaveController::new([0.0, 1.0, 0.0], 32);
1074        let fields = vec![DecoherenceField {
1075            position: [0.0, 1.0, 0.0],
1076            radius: 10.0,
1077            target_form: WaveForm::Vehicle,
1078            gamma: 20.0, // very fast
1079            animation_clip: String::new(),
1080            source_tag: String::new(),
1081        }];
1082
1083        // Run many frames with high gamma — should fully collapse.
1084        for _ in 0..200 {
1085            ctrl.update(false, false, false, false, false, false, 0.0, 0.016, &fields);
1086        }
1087
1088        assert!(ctrl.is_collapsed(), "should be fully collapsed");
1089        assert_eq!(ctrl.current_form(), WaveForm::Vehicle);
1090        assert_eq!(ctrl.coherence(), 0.0);
1091    }
1092
1093    #[test]
1094    fn dream_wave_controller_particle_weights_match_coherence() {
1095        let mut ctrl = DreamWaveController::new([0.0, 1.0, 0.0], 16);
1096        let fields = vec![DecoherenceField {
1097            position: [0.0, 1.0, 0.0],
1098            radius: 10.0,
1099            target_form: WaveForm::Humanoid,
1100            gamma: 4.0,
1101            animation_clip: String::new(),
1102            source_tag: String::new(),
1103        }];
1104
1105        ctrl.update(false, false, false, false, false, false, 0.0, 0.016, &fields);
1106        let c = ctrl.coherence();
1107        for &w in &ctrl.particle_weights {
1108            assert!(
1109                (w - c).abs() < 1e-5,
1110                "particle weight {} should match coherence {}",
1111                w,
1112                c
1113            );
1114        }
1115    }
1116
1117    // ── Quantum Locomotion Ablation Tests ──────────────────────────────
1118
1119    #[test]
1120    fn ablation_interference_changes_output() {
1121        // THE DECISIVE TEST: x_n(rho) != x_n(Delta(rho)) for some n.
1122        let mut state = QuantumLocomotionState::new(64);
1123
1124        // Create a superposition state with nonzero off-diagonals.
1125        let sq2 = 1.0 / 2.0f32.sqrt();
1126        state.rho = DensityMatrix5::from_state_vector(&[
1127            Complex::new(sq2, 0.0), // Idle
1128            Complex::new(sq2, 0.0), // Moving
1129            Complex::ZERO,
1130            Complex::ZERO,
1131            Complex::ZERO,
1132        ]);
1133
1134        let root = [0.0; 3];
1135        let templates: [[f32; 3]; 5] = [
1136            [0.0, 0.0, 0.0],  // Idle
1137            [0.0, 0.0, 1.0],  // Moving
1138            [0.0, 1.0, 0.0],  // Jump
1139            [0.0, -0.5, 0.0], // Land
1140            [0.0, 0.0, 2.0],  // Sprint
1141        ];
1142
1143        // Full quantum position
1144        let pos_quantum = state.particle_position_with_interference(0, root, &templates);
1145
1146        // Dephased position (remove off-diagonals)
1147        let dephased = state.rho.dephased();
1148        let old_rho_entries = state.rho.entries;
1149        state.rho = dephased;
1150        let pos_dephased = state.particle_position_with_interference(0, root, &templates);
1151        state.rho.entries = old_rho_entries;
1152
1153        // THE TEST: they must differ
1154        let diff_sq = (pos_quantum[0] - pos_dephased[0]).powi(2)
1155            + (pos_quantum[1] - pos_dephased[1]).powi(2)
1156            + (pos_quantum[2] - pos_dephased[2]).powi(2);
1157
1158        assert!(
1159            diff_sq > 1e-6,
1160            "QUANTUM.md S7 FAILED: interference does not affect output. diff_sq={diff_sq}"
1161        );
1162    }
1163
1164    #[test]
1165    fn density_matrix_trace_one() {
1166        let rho = DensityMatrix5::pure_state(2);
1167        assert!((rho.trace() - 1.0).abs() < 1e-6);
1168    }
1169
1170    #[test]
1171    fn density_matrix_from_state_vector_trace() {
1172        let sq2 = 1.0 / 2.0f32.sqrt();
1173        let rho = DensityMatrix5::from_state_vector(&[
1174            Complex::new(sq2, 0.0),
1175            Complex::new(sq2, 0.0),
1176            Complex::ZERO,
1177            Complex::ZERO,
1178            Complex::ZERO,
1179        ]);
1180        assert!((rho.trace() - 1.0).abs() < 1e-6);
1181        // Off-diagonal should be nonzero
1182        assert!(rho.coherence_magnitude() > 0.1);
1183    }
1184
1185    #[test]
1186    fn dephasing_removes_coherence() {
1187        let sq2 = 1.0 / 2.0f32.sqrt();
1188        let rho = DensityMatrix5::from_state_vector(&[
1189            Complex::new(sq2, 0.0),
1190            Complex::new(sq2, 0.0),
1191            Complex::ZERO,
1192            Complex::ZERO,
1193            Complex::ZERO,
1194        ]);
1195        let dephased = rho.dephased();
1196        assert!(dephased.coherence_magnitude() < 1e-6);
1197        assert!((dephased.trace() - 1.0).abs() < 1e-6);
1198    }
1199
1200    #[test]
1201    fn hamiltonian_evolution_preserves_trace() {
1202        let mut state = QuantumLocomotionState::new(16);
1203        let input = [1.0, 0.5, 0.0, 0.0, 0.0];
1204        state.tick(1.0 / 60.0, &input, 0.0);
1205        // Trace should remain ~1.0 (small numerical drift acceptable)
1206        assert!(
1207            (state.rho.trace() - 1.0).abs() < 0.1,
1208            "Trace drifted: {}",
1209            state.rho.trace()
1210        );
1211    }
1212
1213    #[test]
1214    fn born_rule_measurement_valid() {
1215        let rho = DensityMatrix5::pure_state(2); // |J>
1216                                                 // Measuring a pure |J> state should always return 2
1217        for seed in 0..100 {
1218            assert_eq!(rho.measure(seed), 2);
1219        }
1220    }
1221
1222    #[test]
1223    fn quantum_locomotion_full_tick_cycle() {
1224        let mut state = QuantumLocomotionState::new(32);
1225        // Simulate 120 ticks with moving input and mild decoherence (dt-scaled)
1226        for _ in 0..120 {
1227            state.tick(1.0 / 60.0, &[0.0, 2.0, 0.0, 0.0, 0.0], 0.02);
1228        }
1229        // After sustained Moving bias, Moving population should be significant
1230        let pops = state.rho.populations();
1231        assert!(pops[1] > 0.1, "Moving population should be significant: {:?}", pops);
1232        // Trace should remain close to 1.0
1233        let trace: f32 = pops.iter().sum();
1234        assert!((trace - 1.0).abs() < 0.2, "Trace should be near 1.0: {}", trace);
1235    }
1236}