qualia_core_db/wgsl_forge/physics/
kinematics.rs1use crate::wgsl_forge::ForgeError;
17
18pub const KIN_STEP_WGSL: &str = include_str!("../../shaders/kinematics.wgsl");
20pub const KIN_STEP_ENTRY: &str = "nbody_step";
22pub const KIN_STRIDE: usize = 8;
24
25pub fn nbody_step_cpu(state_in: &[f32], dt: f32, soft: f32, coupling: f32) -> Vec<f32> {
30 let count = state_in.len() / KIN_STRIDE;
31 let mut out = vec![0.0f32; state_in.len()];
32 for i in 0..count {
33 let bi = i * KIN_STRIDE;
34 let pix = state_in[bi];
35 let piy = state_in[bi + 1];
36 let piz = state_in[bi + 2];
37 let qi = state_in[bi + 7];
38
39 let mut fx = 0.0f32;
40 let mut fy = 0.0f32;
41 let mut fz = 0.0f32;
42 for j in 0..count {
43 if j == i {
44 continue;
45 }
46 let bj = j * KIN_STRIDE;
47 let rx = pix - state_in[bj];
48 let ry = piy - state_in[bj + 1];
49 let rz = piz - state_in[bj + 2];
50 let r2 = rx * rx + ry * ry + rz * rz + soft;
51 let inv = coupling * qi * state_in[bj + 7] / (r2 * r2.sqrt());
52 fx += rx * inv;
53 fy += ry * inv;
54 fz += rz * inv;
55 }
56
57 let mass = state_in[bi + 6];
58 let inv_m = if mass != 0.0 { 1.0 / mass } else { 0.0 };
59 let vx = state_in[bi + 3] + fx * inv_m * dt;
60 let vy = state_in[bi + 4] + fy * inv_m * dt;
61 let vz = state_in[bi + 5] + fz * inv_m * dt;
62
63 out[bi] = pix + vx * dt;
64 out[bi + 1] = piy + vy * dt;
65 out[bi + 2] = piz + vz * dt;
66 out[bi + 3] = vx;
67 out[bi + 4] = vy;
68 out[bi + 5] = vz;
69 out[bi + 6] = mass;
70 out[bi + 7] = qi;
71 }
72 out
73}
74
75pub fn nbody_step_gpu(
81 state_in: &[f32],
82 dt: f32,
83 soft: f32,
84 coupling: f32,
85) -> Result<Vec<f32>, ForgeError> {
86 use crate::wgsl_forge::execute::{
87 BindingUsage, QualiaCompute, WgpuComputeContext, WgpuPipeline,
88 };
89 use crate::wgsl_forge::Schedule;
90
91 if state_in.is_empty() || state_in.len() % KIN_STRIDE != 0 {
92 return Err(ForgeError::GpuValidation(format!(
93 "nbody_step_gpu: state length {} is not a non-zero multiple of {KIN_STRIDE}",
94 state_in.len()
95 )));
96 }
97 let count = state_in.len() / KIN_STRIDE;
98 let capacity = (state_in.len() * 8).max(4 << 20);
99 let mut ctx = WgpuComputeContext::new(capacity)?;
100
101 let view_in = ctx.allocate_and_write(
102 bytemuck::cast_slice(state_in),
103 0,
104 0,
105 BindingUsage::StorageRead,
106 )?;
107 let zeros = vec![0.0f32; state_in.len()];
108 let view_out = ctx.allocate_and_write(
109 bytemuck::cast_slice(&zeros),
110 1,
111 0,
112 BindingUsage::StorageReadWrite,
113 )?;
114 let params = [dt, soft, coupling];
115 let view_params = ctx.allocate_and_write(
116 bytemuck::cast_slice(¶ms),
117 2,
118 0,
119 BindingUsage::StorageRead,
120 )?;
121
122 let buffers = vec![view_in, view_out, view_params];
123 let pipeline = WgpuPipeline::compile(&ctx, KIN_STEP_WGSL, KIN_STEP_ENTRY)?;
124 let schedule = Schedule {
125 workgroup_size: 64,
126 ..Default::default()
127 };
128 pipeline.dispatch(&buffers, &schedule, count)?;
129 let mut out = ctx.read_buffer_f32(&view_out)?;
130 out.truncate(state_in.len());
131 Ok(out)
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::wgsl_forge::validate::validate_wgsl;
138
139 #[test]
141 fn kinematics_wgsl_validates() {
142 let report = validate_wgsl(KIN_STEP_WGSL).expect("kinematics WGSL must naga-validate");
143 assert!(
144 report.entry_points.iter().any(|e| e == KIN_STEP_ENTRY),
145 "validated module must expose {KIN_STEP_ENTRY}; got {:?}",
146 report.entry_points
147 );
148 }
149
150 #[test]
153 fn nbody_oracle_like_charges_repel() {
154 let state = vec![
156 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, ];
159 let out = nbody_step_cpu(&state, 0.1, 1e-4, 1.0);
160 assert!(out[0] < 0.0, "left particle should move −x, got {}", out[0]);
162 assert!(
163 out[8] > 1.0,
164 "right particle should move +x, got {}",
165 out[8]
166 );
167 assert!((out[3] + out[11]).abs() < 1e-5, "momentum not conserved");
169 assert_eq!(out[6], 1.0);
171 assert_eq!(out[7], 1.0);
172 }
173
174 #[test]
176 fn nbody_oracle_opposite_charges_attract() {
177 let state = vec![
178 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, -1.0, ];
181 let out = nbody_step_cpu(&state, 0.1, 1e-4, 1.0);
182 assert!(out[0] > 0.0, "left particle should move +x, got {}", out[0]);
183 assert!(
184 out[8] < 1.0,
185 "right particle should move −x, got {}",
186 out[8]
187 );
188 }
189
190 #[test]
193 #[serial_test::serial(gpu)]
194 fn nbody_gpu_matches_oracle() {
195 if !crate::wgsl_forge::test_gpu_available() {
196 return;
197 }
198 let count = 128usize;
199 let mut state = Vec::with_capacity(count * KIN_STRIDE);
200 for i in 0..count {
201 let f = i as f32;
202 state.extend_from_slice(&[
203 (f * 0.21) - 12.0,
204 (f * 0.13) - 8.0,
205 (f * 0.07) - 4.0,
206 0.0,
207 0.0,
208 0.0,
209 1.0 + (f % 3.0),
210 if i % 2 == 0 { 1.0 } else { -1.0 },
211 ]);
212 }
213 let (dt, soft, coupling) = (0.005f32, 1e-2, 1.0);
214 let expected = nbody_step_cpu(&state, dt, soft, coupling);
215 let gpu = nbody_step_gpu(&state, dt, soft, coupling).expect("nbody_step_gpu");
216 assert_eq!(gpu.len(), expected.len());
217 for (g, e) in gpu.iter().zip(expected.iter()) {
218 let tol = 1e-3 * e.abs().max(1.0);
219 assert!((g - e).abs() <= tol, "GPU/CPU mismatch: {g} vs {e}");
220 }
221 }
222}