qualia_core_db/modalities/
diffusion.rs1use crate::NQuin;
2use std::borrow::Cow;
3use std::sync::mpsc;
4
5pub fn trigger_diffusion(graph_id: &str) -> bool {
9 !graph_id.is_empty()
10}
11
12pub async fn execute_diffusion_pass(graph: &mut [NQuin]) -> Result<(), String> {
13 if graph.is_empty() {
14 return Ok(());
15 }
16
17 let instance = wgpu::Instance::default();
18 let adapter = instance
19 .request_adapter(&wgpu::RequestAdapterOptions {
20 power_preference: wgpu::PowerPreference::HighPerformance,
21 ..Default::default()
22 })
23 .await
24 .map_err(|e| format!("Failed to find wgpu adapter: {e}"))?;
25
26 let (device, queue) = adapter
27 .request_device(&wgpu::DeviceDescriptor::default())
28 .await
29 .map_err(|e| format!("Failed to create device: {}", e))?;
30
31 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
32 label: Some("Diffusion Shader"),
33 source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("../shaders/diffusion.wgsl"))),
34 });
35
36 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
37 label: Some("Diffusion Pipeline"),
38 layout: None,
39 module: &shader,
40 entry_point: Some("main"),
41 compilation_options: Default::default(),
42 cache: None,
43 });
44
45 let bytes: &[u8] = bytemuck::cast_slice(graph);
47
48 let storage_buffer = device.create_buffer(&wgpu::BufferDescriptor {
49 label: Some("Graph Buffer"),
50 size: bytes.len() as wgpu::BufferAddress,
51 usage: wgpu::BufferUsages::STORAGE
52 | wgpu::BufferUsages::COPY_DST
53 | wgpu::BufferUsages::COPY_SRC,
54 mapped_at_creation: false,
55 });
56
57 queue.write_buffer(&storage_buffer, 0, bytes);
58
59 let staging_buffer = device.create_buffer(&wgpu::BufferDescriptor {
60 label: Some("Staging Buffer"),
61 size: bytes.len() as wgpu::BufferAddress,
62 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
63 mapped_at_creation: false,
64 });
65
66 let bind_group_layout = pipeline.get_bind_group_layout(0);
67 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
68 label: Some("Diffusion Bind Group"),
69 layout: &bind_group_layout,
70 entries: &[wgpu::BindGroupEntry {
71 binding: 0,
72 resource: storage_buffer.as_entire_binding(),
73 }],
74 });
75
76 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
77 label: Some("Diffusion Encoder"),
78 });
79
80 {
81 let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
82 label: Some("Diffusion Pass"),
83 timestamp_writes: None,
84 });
85 cpass.set_pipeline(&pipeline);
86 cpass.set_bind_group(0, &bind_group, &[]);
87
88 let num_u32s = (bytes.len() / 4) as u32;
90 let workgroups = (num_u32s + 63) / 64;
91 cpass.dispatch_workgroups(workgroups, 1, 1);
92 }
93
94 encoder.copy_buffer_to_buffer(
95 &storage_buffer,
96 0,
97 &staging_buffer,
98 0,
99 bytes.len() as wgpu::BufferAddress,
100 );
101
102 queue.submit(Some(encoder.finish()));
103
104 let buffer_slice = staging_buffer.slice(..);
105 let (tx, rx) = mpsc::channel();
106 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
107 tx.send(result).unwrap();
108 });
109
110 let _ = device.poll(wgpu::PollType::wait_indefinitely());
111
112 if let Ok(Ok(())) = rx.recv() {
113 let data = buffer_slice
114 .get_mapped_range()
115 .expect("wgpu buffer map_range failed");
116 let out_quins: &[NQuin] = bytemuck::cast_slice(&data);
117 graph.copy_from_slice(out_quins);
118 drop(data);
119 staging_buffer.unmap();
120 Ok(())
121 } else {
122 Err("Failed to read back from GPU".to_string())
123 }
124}
125
126pub fn diffuse_step(beliefs: &[f32], edges: &[(usize, usize)], rate: f32, out: &mut [f32]) -> bool {
137 let n = beliefs.len();
138 if out.len() < n {
139 return false;
140 }
141 for i in 0..n {
142 let mut sum = 0.0f32;
143 let mut deg = 0u32;
144 for &(a, b) in edges {
145 if a == i {
146 sum += beliefs[b];
147 deg += 1;
148 } else if b == i {
149 sum += beliefs[a];
150 deg += 1;
151 }
152 }
153 out[i] = if deg == 0 {
154 beliefs[i]
155 } else {
156 let avg = sum / deg as f32;
157 beliefs[i] + rate * (avg - beliefs[i])
158 };
159 }
160 true
161}
162
163#[inline]
166pub fn inject_energy(belief: f32, energy: f32) -> f32 {
167 (belief + energy).clamp(0.0, 1.0)
168}
169
170pub fn anneal_accept(delta: f32, temperature: f32, rand01: f32) -> bool {
173 if delta <= 0.0 {
174 return true;
175 }
176 if temperature <= 0.0 {
177 return false;
178 }
179 rand01 < (-delta / temperature).exp()
180}
181
182#[inline]
184pub fn cool(temperature: f32, cooling_rate: f32) -> f32 {
185 temperature * cooling_rate
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::NQuin;
192
193 #[test]
194 fn cpu_belief_diffusion_moves_toward_neighbours() {
195 let beliefs = [1.0f32, 0.0, 0.0];
197 let edges = [(0usize, 1usize), (1, 2)];
198 let mut out = [0.0f32; 3];
199 assert!(diffuse_step(&beliefs, &edges, 0.5, &mut out));
200 assert!((out[0] - 0.5).abs() < 1e-6);
202 assert!((out[1] - 0.25).abs() < 1e-6);
204 assert!((out[2] - 0.0).abs() < 1e-6);
206 }
207
208 #[test]
209 fn energy_injection_and_annealing() {
210 assert!((inject_energy(0.6, 0.5) - 1.0).abs() < 1e-6, "clamps to 1");
211 assert!((inject_energy(0.2, 0.3) - 0.5).abs() < 1e-6);
212 assert!(anneal_accept(-1.0, 1.0, 0.99));
214 assert!(
215 anneal_accept(1.0, 10.0, 0.5),
216 "hot → likely accept an uphill move"
217 );
218 assert!(
219 !anneal_accept(5.0, 0.01, 0.5),
220 "cold → reject an uphill move"
221 );
222 assert!(
223 !anneal_accept(1.0, 0.0, 0.0),
224 "zero temperature → no uphill moves"
225 );
226 assert!((cool(10.0, 0.9) - 9.0).abs() < 1e-6);
228 }
229
230 #[test]
231 fn test_execute_diffusion_pass() {
232 let mut graph = vec![NQuin::default(); 10];
233
234 graph[0].subject = 2; graph[1].subject = 3; let res = pollster::block_on(async { execute_diffusion_pass(&mut graph).await });
241 assert!(res.is_ok());
242
243 assert_eq!(graph[0].subject, 4294967299);
245 assert_eq!(graph[1].subject, 4294967299);
247 }
248}