Skip to main content

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}