qualia_core_db/render/sense.rs
1//! Phase 4 — the **sense path** (the input twin of the renderer; STELLAR §D).
2//!
3//! The renderer projects the manifold → percept (output). The sense path is its twin: a physical
4//! signal → the manifold → a **discrete Fact** the values/epistemic layer can reason over. It is
5//! the same `∫Ψ > τ → Fact` bridge ([`crate::modalities::manifold_logic`]) the legal-logic layer
6//! already uses, with a microphone front-end.
7//!
8//! ## What this is (honest scope)
9//! * **Acoustic (microphone) is the live band** — real forward DSP: a Hann-windowed DFT of PCM
10//! samples → magnitude bins → dominant tonal bin. (There is no forward STFT elsewhere in the
11//! crate; the `audio::stft_bake` path *synthesises* spectra for the sidecar raster, it does not
12//! *analyse* a captured signal. So this is the analysis primitive.)
13//! * **RF / Wi-Fi CSI is DEFERRED** ([`band_available`]) — it needs an SDR / radio plus explicit
14//! hardware permission, and may never be available in-browser. Documented, not stubbed-as-done.
15//!
16//! ## The rails (RENDERER_DEFINITION §8; the memories) — load-bearing here
17//! * **Every sense runs under the deontic/standpoint gate.** [`sense_permitted`] **fails closed**:
18//! no Active `PERMIT` consent for *this agent + this environment* ⇒ **refused**. That is
19//! *surveillance-refusal by construction* — the default is to **not** capture. An Active `FORBID`
20//! always wins.
21//! * **Own-environment + consent.** The consent norm binds `(agent) PERMIT capture(environment)`;
22//! sensing is authorised per environment the agent holds consent for.
23//! * **Biometrics never leave the device.** The pipeline emits **only a discrete symbolic Fact**
24//! (an acoustic-tone-detected quin + the dominant frequency / energy scalars). The raw samples
25//! are never stored, returned, or embedded — there is no voiceprint, no audio, in the output.
26//! * **Delegates, does not reinvent the logic.** The continuous→discrete decision is
27//! `manifold_logic::continuous_to_fact`; the consent decision is `logic::deontic`. Only the
28//! forward DSP and the field-packing live here.
29
30use crate::modalities::logic::deontic::{
31 compile_norm_quin, evaluate_deontic_contract, DeonticStatus, DeonticVerdict, OP_FORBID,
32 OP_PERMIT,
33};
34use crate::modalities::manifold_logic::{continuous_to_fact, integrate_abs};
35use crate::{q_hash, NQuin};
36
37/// Number of DFT magnitude bins computed for a captured frame (analysis resolution).
38pub const SENSE_BINS: usize = 64;
39
40/// Max consent norms evaluated in one [`sense_permitted`] pass (stack-bounded, zero-heap).
41pub const MAX_SENSE_NORMS: usize = 32;
42
43/// Predicate stamp for a percept→fact quin.
44pub const P_PERCEIVED: u64 = q_hash("urn:qualia:sense:perceived");
45/// Property-path for a capture/sense action governed by a consent norm.
46pub const P_SENSE_CAPTURE: u64 = q_hash("urn:qualia:sense:capture");
47/// The discrete fact emitted when acoustic energy crosses the bridge threshold.
48pub const FACT_ACOUSTIC_TONE: u64 = q_hash("urn:qualia:sense:acousticToneDetected");
49
50// ── forward DSP (the microphone analysis — real, not synthesis) ──────────────────────────────────
51
52/// Hann window coefficient for sample `n` of a `len`-sample frame (reduces spectral leakage).
53#[inline]
54pub fn hann(n: usize, len: usize) -> f64 {
55 if len <= 1 {
56 return 1.0;
57 }
58 let x = (2.0 * core::f64::consts::PI * n as f64) / (len as f64 - 1.0);
59 0.5 - 0.5 * x.cos()
60}
61
62/// Forward windowed DFT magnitudes: for each bin `k` in `0..out.len()`, write `|X_k|` of the
63/// Hann-windowed real `samples`. Zero-heap — results go in the caller's `out` slice.
64pub fn dft_magnitudes(samples: &[f64], out: &mut [f64]) {
65 let n = samples.len().max(1);
66 for (k, mag) in out.iter_mut().enumerate() {
67 let w = -2.0 * core::f64::consts::PI * k as f64 / n as f64;
68 let mut re = 0.0;
69 let mut im = 0.0;
70 for (i, &s) in samples.iter().enumerate() {
71 let ws = s * hann(i, samples.len());
72 let ang = w * i as f64;
73 re += ws * ang.cos();
74 im += ws * ang.sin();
75 }
76 *mag = (re * re + im * im).sqrt();
77 }
78}
79
80/// Centre frequency (Hz) of DFT bin `bin` for an `fft_size`-point transform at `sample_rate`.
81#[inline]
82pub fn bin_to_hz(bin: usize, sample_rate: f64, fft_size: usize) -> f64 {
83 bin as f64 * sample_rate / fft_size.max(1) as f64
84}
85
86/// The dominant **tonal** bin (skipping DC bin 0): `(bin, magnitude)`, or `None` if there is no
87/// non-DC bin.
88pub fn dominant_bin(mags: &[f64]) -> Option<(usize, f64)> {
89 let mut best: Option<(usize, f64)> = None;
90 for (i, &m) in mags.iter().enumerate().skip(1) {
91 match best {
92 Some((_, bm)) if bm >= m => {}
93 _ => best = Some((i, m)),
94 }
95 }
96 best
97}
98
99// ── consent gate (deontic; fail-closed; surveillance-refusal) ────────────────────────────────────
100
101/// Build a consent norm for sensing: `(agent) OPCODE capture(environment)` in a `frame`, optionally
102/// expiring. Consent is `OP_PERMIT`; a prohibition is `OP_FORBID`.
103pub fn sense_norm(
104 agent: u64,
105 opcode: u8,
106 environment: u64,
107 frame: u64,
108 expiry_unix32: u32,
109) -> NQuin {
110 compile_norm_quin(
111 agent,
112 opcode,
113 P_SENSE_CAPTURE,
114 environment,
115 frame,
116 expiry_unix32,
117 false,
118 )
119}
120
121/// **The sense gate.** `true` iff `agent` has Active consent to capture `environment`: there is an
122/// Active `PERMIT` bound to `(agent, environment)` **and** no Active `FORBID` bound to it.
123///
124/// **Fails closed:** no consent norm (or an over-capacity / unevaluable set) ⇒ `false` (refuse).
125/// This is the surveillance-refusal default — nothing is sensed without explicit consent.
126pub fn sense_permitted(agent: u64, environment: u64, norms: &[NQuin], now_unix: u32) -> bool {
127 if norms.len() > MAX_SENSE_NORMS {
128 return false; // fail closed
129 }
130 let mut out = [DeonticVerdict::default(); MAX_SENSE_NORMS];
131 let n = match evaluate_deontic_contract(norms, now_unix, &mut out) {
132 Ok(n) => n,
133 Err(_) => return false, // fail closed
134 };
135 let mut consented = false;
136 for v in &out[..n] {
137 if v.status != DeonticStatus::Active
138 || v.norm.subject != agent
139 || v.norm.object != environment
140 {
141 continue;
142 }
143 match v.opcode {
144 OP_FORBID => return false, // an active prohibition always wins → refuse
145 OP_PERMIT => consented = true,
146 _ => {}
147 }
148 }
149 consented
150}
151
152// ── percept → fact ───────────────────────────────────────────────────────────────────────────────
153
154/// Pack the percept scalars (dominant Hz, integrated energy) into one `metadata` word — the only
155/// quantitative residue that leaves the sense path. `hz` in the high 32 bits, `energy` in the low.
156#[inline]
157pub fn pack_percept(hz: f32, energy: f32) -> u64 {
158 ((hz.to_bits() as u64) << 32) | energy.to_bits() as u64
159}
160
161/// Inverse of [`pack_percept`].
162#[inline]
163pub fn unpack_percept(metadata: u64) -> (f32, f32) {
164 (
165 f32::from_bits((metadata >> 32) as u32),
166 f32::from_bits((metadata & 0xFFFF_FFFF) as u32),
167 )
168}
169
170/// Build the discrete percept→fact NQuin: *where* sensed (`subject = environment`), the percept
171/// stamp ([`P_PERCEIVED`]), *what* (`object = fact_id`), the consenting standpoint (`context`), and
172/// the dominant-Hz / energy scalars in `metadata`. No raw audio — only this discrete fact.
173pub fn perceived_fact_quin(
174 environment: u64,
175 standpoint: u64,
176 fact_id: u64,
177 dominant_hz: f32,
178 energy: f32,
179) -> NQuin {
180 let metadata = pack_percept(dominant_hz, energy);
181 let mut q = NQuin {
182 subject: environment,
183 predicate: P_PERCEIVED,
184 object: fact_id,
185 context: standpoint,
186 metadata,
187 parity: 0,
188 };
189 q.parity = q.subject ^ q.predicate ^ q.object ^ q.context ^ q.metadata;
190 q
191}
192
193/// The outcome of a gated sense attempt.
194#[derive(Debug, Clone, Copy, PartialEq)]
195pub enum SenseOutcome {
196 /// The consent gate denied capture (surveillance-refusal). No signal was turned into a fact.
197 Refused,
198 /// Consented, but the integrated signal did not cross the bridge threshold — no fact.
199 BelowThreshold,
200 /// A discrete percept→fact NQuin was emitted (carries no raw audio).
201 Fact(NQuin),
202}
203
204/// **The Phase-4 pipeline.** Microphone PCM `samples` → (consent gate) → forward DSP → the
205/// `∫Ψ > τ → Fact` bridge → a discrete Fact NQuin, or a refusal.
206///
207/// Order is deliberate: the **consent gate runs first** — if `agent` has no Active consent to
208/// capture `environment`, the function returns [`SenseOutcome::Refused`] and the signal is never
209/// turned into anything. When consented, the integrated time-domain energy is thresholded by the
210/// inherited bridge; on a crossing, the dominant tonal frequency (forward DFT) is attached to a
211/// discrete fact. Raw `samples` never leave this call.
212#[allow(clippy::too_many_arguments)]
213pub fn sense_acoustic_to_fact(
214 samples: &[f64],
215 sample_rate: f64,
216 threshold: f64,
217 agent: u64,
218 environment: u64,
219 standpoint: u64,
220 norms: &[NQuin],
221 now_unix: u32,
222) -> SenseOutcome {
223 // 1) Consent gate FIRST — surveillance-refusal default.
224 if !sense_permitted(agent, environment, norms, now_unix) {
225 return SenseOutcome::Refused;
226 }
227 // 2) The inherited bridge decides IF a percept crosses into a fact (∫Ψ > τ).
228 match continuous_to_fact(samples, threshold, FACT_ACOUSTIC_TONE) {
229 None => SenseOutcome::BelowThreshold,
230 Some(fact_id) => {
231 // 3) Forward DSP decides WHAT (the dominant tonal frequency).
232 let mut mags = [0.0_f64; SENSE_BINS];
233 dft_magnitudes(samples, &mut mags);
234 let dominant_hz = dominant_bin(&mags)
235 .map(|(bin, _)| bin_to_hz(bin, sample_rate, samples.len()))
236 .unwrap_or(0.0);
237 let energy = integrate_abs(samples);
238 SenseOutcome::Fact(perceived_fact_quin(
239 environment,
240 standpoint,
241 fact_id,
242 dominant_hz as f32,
243 energy as f32,
244 ))
245 }
246 }
247}
248
249// ── sense bands (acoustic live; RF deferred) ─────────────────────────────────────────────────────
250
251/// The physical bands the sense path can ingest.
252#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum SenseBand {
254 /// Microphone acoustic — **live** (this module).
255 Acoustic,
256 /// RF / Wi-Fi CSI — **deferred**: needs an SDR / radio + explicit hardware permission, may
257 /// never be available in-browser (STELLAR §D). Honestly not implemented.
258 RadioFrequency,
259}
260
261/// Whether a band is available for live sensing. Only [`SenseBand::Acoustic`] is; RF is deferred.
262#[inline]
263pub fn band_available(band: SenseBand) -> bool {
264 matches!(band, SenseBand::Acoustic)
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 fn agent() -> u64 {
272 q_hash("did:example:listener")
273 }
274 fn environment() -> u64 {
275 q_hash("urn:qualia:env:my-room")
276 }
277 fn standpoint() -> u64 {
278 q_hash("urn:qualia:frame:home")
279 }
280
281 /// A pure sine of `freq` Hz at `sr`, `n` samples, amplitude 1.0.
282 fn sine(freq: f64, sr: f64, n: usize) -> Vec<f64> {
283 (0..n)
284 .map(|i| (2.0 * core::f64::consts::PI * freq * i as f64 / sr).sin())
285 .collect()
286 }
287
288 #[test]
289 fn dft_detects_a_pure_tone() {
290 // 1000 Hz @ 16 kHz, 256-pt DFT → bin spacing 62.5 Hz → exact bin 16.
291 let s = sine(1000.0, 16000.0, 256);
292 let mut mags = [0.0; SENSE_BINS];
293 dft_magnitudes(&s, &mut mags);
294 let (bin, _) = dominant_bin(&mags).unwrap();
295 assert_eq!(bin, 16);
296 assert!((bin_to_hz(bin, 16000.0, 256) - 1000.0).abs() < 1e-6);
297 }
298
299 #[test]
300 fn percept_pack_round_trip() {
301 let (hz, e) = unpack_percept(pack_percept(1000.0, 162.5));
302 assert!((hz - 1000.0).abs() < 1e-3 && (e - 162.5).abs() < 1e-3);
303 }
304
305 #[test]
306 fn rf_band_is_deferred() {
307 assert!(band_available(SenseBand::Acoustic));
308 assert!(!band_available(SenseBand::RadioFrequency));
309 }
310
311 /// RAIL: surveillance-refusal — with no consent norm, a loud signal yields NO fact.
312 #[test]
313 fn no_consent_refuses_even_a_loud_signal() {
314 let s = sine(1000.0, 16000.0, 256);
315 let out = sense_acoustic_to_fact(
316 &s,
317 16000.0,
318 50.0,
319 agent(),
320 environment(),
321 standpoint(),
322 &[],
323 100,
324 );
325 assert_eq!(out, SenseOutcome::Refused);
326 }
327
328 /// PHASE-4 ACCEPTANCE: consented mic frame → STFT/DFT → a discrete Fact NQuin via the bridge.
329 #[test]
330 fn consented_tone_emits_a_discrete_fact() {
331 let s = sine(1000.0, 16000.0, 256);
332 let permit = sense_norm(agent(), OP_PERMIT, environment(), standpoint(), 0);
333 let out = sense_acoustic_to_fact(
334 &s,
335 16000.0,
336 50.0,
337 agent(),
338 environment(),
339 standpoint(),
340 &[permit],
341 100,
342 );
343 match out {
344 SenseOutcome::Fact(q) => {
345 // The fact is the discrete percept — WHAT (tone), WHERE (env), no raw audio.
346 assert_eq!(q.object, FACT_ACOUSTIC_TONE);
347 assert_eq!(q.subject, environment());
348 assert_eq!(q.predicate, P_PERCEIVED);
349 assert_eq!(
350 q.parity,
351 q.subject ^ q.predicate ^ q.object ^ q.context ^ q.metadata
352 );
353 let (hz, energy) = unpack_percept(q.metadata);
354 assert!((hz - 1000.0).abs() < 1.0, "dominant ~1000 Hz, got {hz}");
355 assert!(energy > 50.0);
356 }
357 other => panic!("expected a Fact, got {other:?}"),
358 }
359 }
360
361 /// Consented but silent → the bridge does not cross → no fact.
362 #[test]
363 fn consented_silence_yields_no_fact() {
364 let silence = [0.0_f64; 256];
365 let permit = sense_norm(agent(), OP_PERMIT, environment(), standpoint(), 0);
366 let out = sense_acoustic_to_fact(
367 &silence,
368 16000.0,
369 50.0,
370 agent(),
371 environment(),
372 standpoint(),
373 &[permit],
374 100,
375 );
376 assert_eq!(out, SenseOutcome::BelowThreshold);
377 }
378
379 /// An Active FORBID overrides consent → refused even with a strong signal.
380 #[test]
381 fn active_forbid_overrides_consent() {
382 let s = sine(1000.0, 16000.0, 256);
383 let permit = sense_norm(agent(), OP_PERMIT, environment(), standpoint(), 0);
384 let forbid = sense_norm(agent(), OP_FORBID, environment(), standpoint(), 0);
385 let out = sense_acoustic_to_fact(
386 &s,
387 16000.0,
388 50.0,
389 agent(),
390 environment(),
391 standpoint(),
392 &[permit, forbid],
393 100,
394 );
395 assert_eq!(out, SenseOutcome::Refused);
396 }
397
398 /// Consent for a DIFFERENT environment does not authorise this one (own-environment).
399 #[test]
400 fn consent_is_per_environment() {
401 let s = sine(1000.0, 16000.0, 256);
402 let other_env = q_hash("urn:qualia:env:someone-elses-room");
403 let permit = sense_norm(agent(), OP_PERMIT, other_env, standpoint(), 0);
404 let out = sense_acoustic_to_fact(
405 &s,
406 16000.0,
407 50.0,
408 agent(),
409 environment(),
410 standpoint(),
411 &[permit],
412 100,
413 );
414 assert_eq!(out, SenseOutcome::Refused);
415 }
416}