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}