Skip to main content

phenomenological_rendezvous/
pattern.rs

1//! Submodality pattern definitions and helpers.
2
3use serde::{Deserialize, Serialize};
4
5/// Minimum brightness (normalized).
6pub const BRIGHTNESS_MIN: f32 = 0.0;
7/// Maximum brightness (normalized).
8pub const BRIGHTNESS_MAX: f32 = 1.0;
9/// Minimum color temperature (Kelvin).
10pub const COLOR_TEMP_MIN: f32 = 2000.0;
11/// Maximum color temperature (Kelvin).
12pub const COLOR_TEMP_MAX: f32 = 10_000.0;
13/// Minimum focal distance (normalized).
14pub const FOCAL_DISTANCE_MIN: f32 = 0.0;
15/// Maximum focal distance (normalized).
16pub const FOCAL_DISTANCE_MAX: f32 = 1.0;
17/// Minimum volume (normalized).
18pub const VOLUME_MIN: f32 = 0.0;
19/// Maximum volume (normalized).
20pub const VOLUME_MAX: f32 = 1.0;
21/// Minimum tempo (BPM).
22pub const TEMPO_MIN: f32 = 0.0;
23/// Maximum tempo (BPM).
24pub const TEMPO_MAX: f32 = 300.0;
25/// Minimum pitch (Hz).
26pub const PITCH_MIN: f32 = 20.0;
27/// Maximum pitch (Hz).
28pub const PITCH_MAX: f32 = 20_000.0;
29/// Minimum temperature (Celsius).
30pub const TEMPERATURE_MIN: f32 = 10.0;
31/// Maximum temperature (Celsius).
32pub const TEMPERATURE_MAX: f32 = 40.0;
33/// Minimum movement (normalized).
34pub const MOVEMENT_MIN: f32 = 0.0;
35/// Maximum movement (normalized).
36pub const MOVEMENT_MAX: f32 = 1.0;
37/// Minimum arousal (normalized).
38pub const AROUSAL_MIN: f32 = 0.0;
39/// Maximum arousal (normalized).
40pub const AROUSAL_MAX: f32 = 1.0;
41
42/// A submodality pattern as described in the paper.
43///
44/// This mirrors the SubmodalityPattern pseudo-code and keeps raw values in
45/// their natural units. Normalization to `[0, 1]` is handled separately.
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct SubmodalityPattern {
48    /// Brightness, normalized to `[0.0, 1.0]`.
49    pub brightness: f32,
50    /// Color temperature in Kelvin (2000–10000).
51    pub color_temp: f32,
52    /// Focal distance, normalized to `[0.0, 1.0]`.
53    pub focal_distance: f32,
54    /// Volume, normalized to `[0.0, 1.0]`.
55    pub volume: f32,
56    /// Tempo in BPM (0–300).
57    pub tempo: f32,
58    /// Pitch in Hertz (20–20000).
59    pub pitch: f32,
60    /// Temperature in Celsius.
61    pub temperature: f32,
62    /// Movement, normalized to `[0.0, 1.0]`.
63    pub movement: f32,
64    /// Arousal, normalized to `[0.0, 1.0]`.
65    pub arousal: f32,
66}
67
68impl SubmodalityPattern {
69    /// Create a neutral baseline pattern for initialization and testing.
70    ///
71    /// "Neutral" means unit-range fields are centered or zeroed, and absolute
72    /// scale fields are set to commonly used midpoints. This is a placeholder
73    /// baseline and should be replaced with domain-specific defaults later.
74    pub fn zeros() -> Self {
75        Self {
76            brightness: 0.5,
77            color_temp: 6500.0,
78            focal_distance: 0.5,
79            volume: 0.5,
80            tempo: 0.0,
81            pitch: 440.0,
82            temperature: 20.0,
83            movement: 0.0,
84            arousal: 0.0,
85        }
86    }
87
88    /// Normalize this pattern into `[0, 1]` ranges for distance calculations.
89    ///
90    /// The normalization uses fixed min/max ranges for each dimension. These
91    /// ranges are reference defaults and may need tuning or calibration in
92    /// real deployments based on sensors and user populations.
93    ///
94    /// Temperature normalization assumes a `10..=40` Celsius operating window
95    /// as a placeholder until domain-specific bounds are defined.
96    pub fn normalize(&self) -> NormalizedPattern {
97        NormalizedPattern {
98            brightness: clamp01(self.brightness),
99            color_temp: clamp01((self.color_temp - COLOR_TEMP_MIN) / (COLOR_TEMP_MAX - COLOR_TEMP_MIN)),
100            focal_distance: clamp01(self.focal_distance),
101            volume: clamp01(self.volume),
102            tempo: clamp01(self.tempo / TEMPO_MAX),
103            pitch: clamp01((self.pitch - PITCH_MIN) / (PITCH_MAX - PITCH_MIN)),
104            temperature: clamp01((self.temperature - TEMPERATURE_MIN) / (TEMPERATURE_MAX - TEMPERATURE_MIN)),
105            movement: clamp01(self.movement),
106            arousal: clamp01(self.arousal),
107        }
108    }
109}
110
111/// A fully normalized submodality pattern with values in `[0, 1]`.
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct NormalizedPattern {
114    /// Normalized brightness.
115    pub brightness: f32,
116    /// Normalized color temperature.
117    pub color_temp: f32,
118    /// Normalized focal distance.
119    pub focal_distance: f32,
120    /// Normalized volume.
121    pub volume: f32,
122    /// Normalized tempo.
123    pub tempo: f32,
124    /// Normalized pitch.
125    pub pitch: f32,
126    /// Normalized temperature.
127    pub temperature: f32,
128    /// Normalized movement.
129    pub movement: f32,
130    /// Normalized arousal.
131    pub arousal: f32,
132}
133
134fn clamp01(value: f32) -> f32 {
135    if value < 0.0 {
136        0.0
137    } else if value > 1.0 {
138        1.0
139    } else {
140        value
141    }
142}
143
144/// Map a 16-bit integer into a floating-point range `[min, max]`.
145///
146/// `val` is interpreted as an unsigned 16-bit sample, where `0` maps to `min`
147/// and `u16::MAX` maps to `max`.
148pub fn quantize_u16_to_range(val: u16, min: f32, max: f32) -> f32 {
149    let fraction = f32::from(val) / f32::from(u16::MAX);
150    min + (max - min) * fraction
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn pattern_json_round_trip() {
159        let pattern = SubmodalityPattern::zeros();
160        let json = serde_json::to_string(&pattern).expect("serialize");
161        let decoded: SubmodalityPattern = serde_json::from_str(&json).expect("deserialize");
162        assert_eq!(pattern, decoded);
163    }
164}