1use ahash::AHashMap;
17use tantivy::tokenizer::{LowerCaser, SimpleTokenizer, TextAnalyzer, TokenStream};
18
19use wm_core::Coordinate5D;
20
21#[derive(Debug, Clone, PartialEq)]
23pub struct SemanticScores {
24 pub x: f32,
26 pub y: f32,
28 pub z: f32,
30}
31
32impl SemanticScores {
33 #[must_use]
35 pub const fn neutral() -> Self {
36 Self {
37 x: 0.5,
38 y: 0.5,
39 z: 0.5,
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
46struct SemanticAnchors {
47 logic: &'static [&'static str],
48 emotion: &'static [&'static str],
49 micro: &'static [&'static str],
50 macro_: &'static [&'static str],
51 time: &'static [&'static str],
52 space: &'static [&'static str],
53}
54
55impl Default for SemanticAnchors {
56 fn default() -> Self {
57 Self {
58 logic: &[
59 "algorithm",
60 "code",
61 "data",
62 "function",
63 "method",
64 "system",
65 "process",
66 "structure",
67 "analysis",
68 "compute",
69 "parameter",
70 "model",
71 "formula",
72 "theorem",
73 "proof",
74 "derive",
75 "calculate",
76 "measure",
77 "metric",
78 "logic",
79 "rational",
80 "objective",
81 "systematic",
82 "technical",
83 "engineering",
84 ],
85 emotion: &[
86 "feel",
87 "feeling",
88 "emotion",
89 "love",
90 "fear",
91 "joy",
92 "sad",
93 "happy",
94 "angry",
95 "hope",
96 "care",
97 "beauty",
98 "art",
99 "soul",
100 "heart",
101 "passion",
102 "dream",
103 "wonder",
104 "intuition",
105 "empathy",
106 "spirit",
107 "subjective",
108 "personal",
109 "emotional",
110 "expressive",
111 ],
112 micro: &[
113 "detail",
114 "specific",
115 "small",
116 "local",
117 "individual",
118 "element",
119 "atom",
120 "bit",
121 "byte",
122 "cell",
123 "node",
124 "token",
125 "word",
126 "line",
127 "step",
128 "tiny",
129 "precise",
130 "exact",
131 "narrow",
132 "component",
133 "unit",
134 "instance",
135 ],
136 macro_: &[
137 "global",
138 "universe",
139 "network",
140 "architecture",
141 "framework",
142 "theory",
143 "paradigm",
144 "concept",
145 "abstract",
146 "broad",
147 "general",
148 "whole",
149 "total",
150 "infinite",
151 "cosmic",
152 "universal",
153 "grand",
154 "scale",
155 "overview",
156 "ecosystem",
157 "pattern",
158 "horizon",
159 ],
160 time: &[
161 "time",
162 "when",
163 "before",
164 "after",
165 "now",
166 "then",
167 "past",
168 "future",
169 "present",
170 "moment",
171 "duration",
172 "temporal",
173 "chronological",
174 "history",
175 "timeline",
176 "schedule",
177 "deadline",
178 "period",
179 "phase",
180 "cycle",
181 "event",
182 "sequence",
183 ],
184 space: &[
185 "space",
186 "where",
187 "here",
188 "there",
189 "location",
190 "position",
191 "area",
192 "region",
193 "zone",
194 "place",
195 "distance",
196 "spatial",
197 "coordinate",
198 "map",
199 "geometry",
200 "layout",
201 "boundary",
202 "field",
203 "domain",
204 "environment",
205 "context",
206 ],
207 }
208 }
209}
210
211pub struct SemanticEncoder {
217 anchors: SemanticAnchors,
218}
219
220impl Default for SemanticEncoder {
221 fn default() -> Self {
222 Self::new()
223 }
224}
225
226impl SemanticEncoder {
227 #[must_use]
229 pub fn new() -> Self {
230 Self {
231 anchors: SemanticAnchors::default(),
232 }
233 }
234
235 #[must_use]
242 pub fn encode(&self, text: &str) -> SemanticScores {
243 let freqs = self.term_frequencies(text);
244 let x = self.axis_score(&freqs, self.anchors.logic, self.anchors.emotion);
245 let y = self.axis_score(&freqs, self.anchors.micro, self.anchors.macro_);
246 let z = self.axis_score(&freqs, self.anchors.time, self.anchors.space);
247 SemanticScores { x, y, z }
248 }
249
250 #[must_use]
252 pub fn encode_coordinate(
253 &self,
254 text: &str,
255 temporal_weight: f32,
256 importance: f32,
257 ) -> Coordinate5D {
258 let scores = self.encode(text);
259 Coordinate5D::from_semantic(scores.x, scores.y, scores.z, temporal_weight, importance)
260 }
261
262 fn term_frequencies(&self, text: &str) -> AHashMap<String, f32> {
264 let mut freqs: AHashMap<String, f32> = AHashMap::new();
265 let mut analyzer = TextAnalyzer::builder(SimpleTokenizer::default())
266 .filter(LowerCaser)
267 .build();
268 let mut stream = analyzer.token_stream(text);
269 while stream.advance() {
270 *freqs.entry(stream.token().text.clone()).or_insert(0.0) += 1.0;
271 }
272 freqs
273 }
274
275 fn axis_score(
277 &self,
278 freqs: &AHashMap<String, f32>,
279 neg_pole: &[&str],
280 pos_pole: &[&str],
281 ) -> f32 {
282 let neg = self.pole_score(freqs, neg_pole);
283 let pos = self.pole_score(freqs, pos_pole);
284 let smoothing = 0.5;
285 (pos + smoothing) / 2.0f32.mul_add(smoothing, neg + pos)
286 }
287
288 fn pole_score(&self, freqs: &AHashMap<String, f32>, terms: &[&str]) -> f32 {
290 let mut score = 0.0f32;
291 for term in terms {
292 if let Some(&freq) = freqs.get(*term) {
293 score += 1.0 + freq.ln();
295 }
296 }
297 score
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn neutral_text_returns_midpoint() {
307 let encoder = SemanticEncoder::new();
308 let scores = encoder.encode("the quick brown fox jumps over the lazy dog");
309 assert!((scores.x - 0.5).abs() < 0.01);
311 assert!((scores.y - 0.5).abs() < 0.01);
312 assert!((scores.z - 0.5).abs() < 0.01);
313 }
314
315 #[test]
316 fn empty_text_returns_neutral() {
317 let encoder = SemanticEncoder::new();
318 let scores = encoder.encode("");
319 assert_eq!(scores, SemanticScores::neutral());
320 }
321
322 #[test]
323 fn logic_text_scores_toward_zero_x() {
324 let encoder = SemanticEncoder::new();
325 let scores = encoder.encode(
326 "The algorithm computes data using a systematic method with precise parameters",
327 );
328 assert!(
330 scores.x < 0.5,
331 "x = {} should be < 0.5 for logic text",
332 scores.x
333 );
334 }
335
336 #[test]
337 fn emotion_text_scores_toward_one_x() {
338 let encoder = SemanticEncoder::new();
339 let scores = encoder
340 .encode("I feel love and joy in my heart, a deep passion and empathy for beauty");
341 assert!(
343 scores.x > 0.5,
344 "x = {} should be > 0.5 for emotion text",
345 scores.x
346 );
347 }
348
349 #[test]
350 fn micro_text_scores_toward_zero_y() {
351 let encoder = SemanticEncoder::new();
352 let scores = encoder
353 .encode("Each individual element and tiny detail of the specific component matters");
354 assert!(
356 scores.y < 0.5,
357 "y = {} should be < 0.5 for micro text",
358 scores.y
359 );
360 }
361
362 #[test]
363 fn macro_text_scores_toward_one_y() {
364 let encoder = SemanticEncoder::new();
365 let scores =
366 encoder.encode("The global architecture is a universal framework on a cosmic scale");
367 assert!(
369 scores.y > 0.5,
370 "y = {} should be > 0.5 for macro text",
371 scores.y
372 );
373 }
374
375 #[test]
376 fn time_text_scores_toward_zero_z() {
377 let encoder = SemanticEncoder::new();
378 let scores = encoder.encode(
379 "Before and after that moment, the timeline showed a chronological sequence of events",
380 );
381 assert!(
383 scores.z < 0.5,
384 "z = {} should be < 0.5 for time text",
385 scores.z
386 );
387 }
388
389 #[test]
390 fn space_text_scores_toward_one_z() {
391 let encoder = SemanticEncoder::new();
392 let scores = encoder.encode(
393 "The spatial layout of the region defines the boundary and geometry of the area",
394 );
395 assert!(
397 scores.z > 0.5,
398 "z = {} should be > 0.5 for space text",
399 scores.z
400 );
401 }
402
403 #[test]
404 fn encode_is_deterministic() {
405 let encoder = SemanticEncoder::new();
406 let a = encoder.encode("The algorithm processes data with logic and analysis");
407 let b = encoder.encode("The algorithm processes data with logic and analysis");
408 assert_eq!(a, b);
409 }
410
411 #[test]
412 fn similar_texts_produce_similar_coordinates() {
413 let encoder = SemanticEncoder::new();
414 let a = encoder.encode_coordinate(
415 "The algorithm computes data using a systematic method",
416 0.5,
417 0.5,
418 );
419 let b = encoder.encode_coordinate(
420 "The algorithm processes data using a systematic approach",
421 0.5,
422 0.5,
423 );
424 let c = encoder.encode_coordinate(
425 "I feel love and joy in my heart with deep passion",
426 0.5,
427 0.5,
428 );
429
430 let dist_ab = a.semantic_distance_to(&b);
431 let dist_ac = a.semantic_distance_to(&c);
432
433 assert!(
435 dist_ab < dist_ac,
436 "dist(a,b)={dist_ab:.4} should be < dist(a,c)={dist_ac:.4}"
437 );
438 }
439
440 #[test]
441 fn encode_coordinate_produces_valid_range() {
442 let encoder = SemanticEncoder::new();
443 let coord = encoder.encode_coordinate("test content", 0.7, 0.9);
444 assert!(coord.x >= 0.0 && coord.x <= 1.0);
445 assert!(coord.y >= 0.0 && coord.y <= 1.0);
446 assert!(coord.z >= 0.0 && coord.z <= 1.0);
447 assert!((coord.w - 0.7).abs() < f32::EPSILON);
448 assert!((coord.v - 0.9).abs() < f32::EPSILON);
449 }
450
451 #[test]
452 fn mixed_content_produces_intermediate_scores() {
453 let encoder = SemanticEncoder::new();
454 let scores = encoder
455 .encode("The algorithm processes data with emotional passion and systematic beauty");
456 assert!(
458 (0.3..=0.7).contains(&scores.x),
459 "x = {} should be in [0.3, 0.7] for mixed text",
460 scores.x
461 );
462 }
463
464 #[test]
465 fn case_insensitive_matching() {
466 let encoder = SemanticEncoder::new();
467 let lower = encoder.encode("the algorithm computes data");
468 let upper = encoder.encode("The ALGORITHM COMPUTES DATA");
469 assert_eq!(lower, upper);
470 }
471
472 #[test]
473 fn semantic_scores_neutral() {
474 assert_eq!(
475 SemanticScores::neutral(),
476 SemanticScores {
477 x: 0.5,
478 y: 0.5,
479 z: 0.5
480 }
481 );
482 }
483}