qualia_core_db/modalities/
fuzzy.rs1use crate::NQuin;
2
3#[inline]
11pub fn t_norm_godel(a: f32, b: f32) -> f32 {
12 a.min(b)
13}
14
15#[inline]
17pub fn t_norm_lukasiewicz(a: f32, b: f32) -> f32 {
18 (a + b - 1.0).max(0.0)
19}
20
21#[inline]
23pub fn t_conorm_godel(a: f32, b: f32) -> f32 {
24 a.max(b)
25}
26
27#[inline]
30pub fn degree(quin: &NQuin) -> f32 {
31 crate::frame_layout::truth_degree(quin.metadata).clamp(0.0, 1.0)
32}
33
34pub fn conjunction(quins: &[NQuin]) -> f32 {
37 let mut acc = 1.0f32;
38 for q in quins {
39 acc = t_norm_godel(acc, degree(q));
40 }
41 acc
42}
43
44#[inline]
48pub fn t_conorm_lukasiewicz(a: f32, b: f32) -> f32 {
49 (a + b).min(1.0)
50}
51
52#[inline]
54pub fn t_norm_product(a: f32, b: f32) -> f32 {
55 a * b
56}
57
58#[inline]
60pub fn t_conorm_product(a: f32, b: f32) -> f32 {
61 a + b - a * b
62}
63
64#[inline]
66pub fn t_norm_drastic(a: f32, b: f32) -> f32 {
67 if a >= 1.0 {
68 b
69 } else if b >= 1.0 {
70 a
71 } else {
72 0.0
73 }
74}
75
76#[inline]
78pub fn t_conorm_drastic(a: f32, b: f32) -> f32 {
79 if a <= 0.0 {
80 b
81 } else if b <= 0.0 {
82 a
83 } else {
84 1.0
85 }
86}
87
88#[inline]
90pub fn fuzzy_not(a: f32) -> f32 {
91 (1.0 - a).clamp(0.0, 1.0)
92}
93
94pub fn disjunction(quins: &[NQuin]) -> f32 {
97 let mut acc = 0.0f32;
98 for q in quins {
99 acc = t_conorm_godel(acc, degree(q));
100 }
101 acc
102}
103
104#[inline]
108pub fn hedge_very(mu: f32) -> f32 {
109 mu * mu
110}
111
112#[inline]
114pub fn hedge_extremely(mu: f32) -> f32 {
115 mu * mu * mu
116}
117
118#[inline]
120pub fn hedge_more_or_less(mu: f32) -> f32 {
121 mu.max(0.0).sqrt()
122}
123
124pub fn defuzz_centroid(u: &[f32], mu: &[f32]) -> Option<f32> {
132 if u.len() != mu.len() || u.is_empty() {
133 return None;
134 }
135 let mut num = 0.0f32;
136 let mut den = 0.0f32;
137 for i in 0..u.len() {
138 num += u[i] * mu[i];
139 den += mu[i];
140 }
141 if den.abs() < 1e-9 {
142 None
143 } else {
144 Some(num / den)
145 }
146}
147
148pub fn defuzz_mean_of_max(u: &[f32], mu: &[f32]) -> Option<f32> {
150 if u.len() != mu.len() || u.is_empty() {
151 return None;
152 }
153 let mut max = f32::MIN;
154 for &m in mu {
155 if m > max {
156 max = m;
157 }
158 }
159 let mut sum = 0.0f32;
160 let mut n = 0u32;
161 for i in 0..u.len() {
162 if (mu[i] - max).abs() < 1e-6 {
163 sum += u[i];
164 n += 1;
165 }
166 }
167 if n == 0 {
168 None
169 } else {
170 Some(sum / n as f32)
171 }
172}
173
174pub fn defuzz_smallest_of_max(u: &[f32], mu: &[f32]) -> Option<f32> {
176 if u.len() != mu.len() || u.is_empty() {
177 return None;
178 }
179 let mut max = f32::MIN;
180 for &m in mu {
181 if m > max {
182 max = m;
183 }
184 }
185 for i in 0..u.len() {
186 if (mu[i] - max).abs() < 1e-6 {
187 return Some(u[i]); }
189 }
190 None
191}
192
193pub fn defuzz_bisector(u: &[f32], mu: &[f32]) -> Option<f32> {
195 if u.len() != mu.len() || u.is_empty() {
196 return None;
197 }
198 let total: f32 = mu.iter().sum();
199 if total.abs() < 1e-9 {
200 return None;
201 }
202 let half = total / 2.0;
203 let mut cum = 0.0f32;
204 for i in 0..u.len() {
205 cum += mu[i];
206 if cum >= half {
207 return Some(u[i]);
208 }
209 }
210 Some(u[u.len() - 1])
211}
212
213pub fn firing_strength(antecedent_mu: &[f32]) -> f32 {
223 let mut acc = 1.0f32;
224 for &m in antecedent_mu {
225 acc = t_norm_godel(acc, m);
226 }
227 acc
228}
229
230#[derive(Debug, Clone, Copy)]
233pub struct MamdaniRule<'a> {
234 pub firing: f32,
235 pub consequent_mu: &'a [f32],
236}
237
238pub fn mamdani_infer(universe: &[f32], rules: &[MamdaniRule], scratch: &mut [f32]) -> Option<f32> {
243 if scratch.len() != universe.len() {
244 return None;
245 }
246 for s in scratch.iter_mut() {
247 *s = 0.0;
248 }
249 for r in rules {
250 let n = scratch.len().min(r.consequent_mu.len());
251 for i in 0..n {
252 let clipped = r.firing.min(r.consequent_mu[i]);
254 if clipped > scratch[i] {
255 scratch[i] = clipped;
256 }
257 }
258 }
259 defuzz_centroid(universe, scratch)
260}
261
262#[derive(Debug, Clone, Copy)]
265pub struct SugenoRule {
266 pub firing: f32,
267 pub consequent: f32,
268}
269
270pub fn sugeno_infer(rules: &[SugenoRule]) -> Option<f32> {
273 let mut num = 0.0f32;
274 let mut den = 0.0f32;
275 for r in rules {
276 num += r.firing * r.consequent;
277 den += r.firing;
278 }
279 if den.abs() < 1e-9 {
280 None
281 } else {
282 Some(num / den)
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn t_norms_and_conjunction() {
292 assert!((t_norm_godel(0.7, 0.4) - 0.4).abs() < 1e-6);
293 assert!((t_norm_lukasiewicz(0.7, 0.4) - 0.1).abs() < 1e-6);
294 assert!((t_norm_lukasiewicz(0.3, 0.4) - 0.0).abs() < 1e-6);
295 assert!((t_conorm_godel(0.7, 0.4) - 0.7).abs() < 1e-6);
296
297 let mk = |d: f32| {
298 let mut q = NQuin::default();
299 q.metadata = d.to_bits() as u64;
300 q
301 };
302 assert!((conjunction(&[mk(0.9), mk(0.6), mk(0.8)]) - 0.6).abs() < 1e-6);
304 assert!((conjunction(&[]) - 1.0).abs() < 1e-6);
305 }
306
307 fn close(a: f32, b: f32) -> bool {
308 (a - b).abs() < 1e-6
309 }
310
311 #[test]
312 fn t_norm_conorm_families() {
313 assert!(close(t_norm_product(0.5, 0.4), 0.2));
315 assert!(close(t_conorm_product(0.5, 0.4), 0.7)); assert!(close(t_conorm_lukasiewicz(0.7, 0.4), 1.0)); assert!(close(t_conorm_lukasiewicz(0.3, 0.4), 0.7));
319 assert!(close(t_norm_drastic(1.0, 0.4), 0.4));
321 assert!(close(t_norm_drastic(0.6, 0.4), 0.0));
322 assert!(close(t_conorm_drastic(0.0, 0.4), 0.4));
323 assert!(close(t_conorm_drastic(0.6, 0.4), 1.0));
324 assert!(close(fuzzy_not(0.3), 0.7));
326 let (a, b) = (0.6f32, 0.4f32);
328 assert!(t_norm_drastic(a, b) <= t_norm_product(a, b));
329 assert!(t_norm_product(a, b) <= t_norm_godel(a, b));
330 }
331
332 #[test]
333 fn hedges_concentrate_and_dilate() {
334 assert!(close(hedge_very(0.5), 0.25));
335 assert!(close(hedge_extremely(0.5), 0.125));
336 assert!(close(hedge_more_or_less(0.25), 0.5));
337 assert!(hedge_very(0.6) < 0.6);
339 assert!(hedge_more_or_less(0.6) > 0.6);
340 }
341
342 #[test]
343 fn defuzzification_methods() {
344 let u = [0.0, 1.0, 2.0, 3.0, 4.0];
346 let mu = [0.0, 0.5, 1.0, 0.5, 0.0];
347 assert!(close(defuzz_centroid(&u, &mu).unwrap(), 2.0));
348 assert!(close(defuzz_mean_of_max(&u, &mu).unwrap(), 2.0));
349 assert!(close(defuzz_smallest_of_max(&u, &mu).unwrap(), 2.0));
350 assert!(close(defuzz_bisector(&u, &mu).unwrap(), 2.0));
351
352 let mu2 = [0.0, 1.0, 1.0, 0.2, 0.0];
354 assert!(close(defuzz_mean_of_max(&u, &mu2).unwrap(), 1.5));
355 assert!(close(defuzz_smallest_of_max(&u, &mu2).unwrap(), 1.0));
356
357 assert!(defuzz_centroid(&u, &[0.0; 5]).is_none());
359 assert!(defuzz_centroid(&[1.0, 2.0], &[0.1]).is_none());
360 assert!(defuzz_centroid(&[], &[]).is_none());
361 }
362
363 #[test]
364 fn firing_strength_is_godel_t_norm() {
365 assert!(close(firing_strength(&[0.8, 0.5, 0.9]), 0.5));
366 assert!(close(firing_strength(&[]), 1.0));
367 }
368
369 #[test]
370 fn mamdani_fis_clips_aggregates_and_defuzzifies() {
371 let u = [0.0, 1.0, 2.0, 3.0, 4.0];
373 let low = [1.0, 1.0, 0.5, 0.0, 0.0];
374 let high = [0.0, 0.0, 0.5, 1.0, 1.0];
375 let mut scratch = [0.0f32; 5];
376
377 let r_low_only = [MamdaniRule {
379 firing: 1.0,
380 consequent_mu: &low,
381 }];
382 let y_low = mamdani_infer(&u, &r_low_only, &mut scratch).unwrap();
383 let r_high_only = [MamdaniRule {
385 firing: 1.0,
386 consequent_mu: &high,
387 }];
388 let y_high = mamdani_infer(&u, &r_high_only, &mut scratch).unwrap();
389 assert!(
390 y_low < y_high,
391 "low-only ({y_low}) must sit below high-only ({y_high})"
392 );
393 assert!(y_low < 2.0 && y_high > 2.0);
394
395 let both = [
397 MamdaniRule {
398 firing: 1.0,
399 consequent_mu: &low,
400 },
401 MamdaniRule {
402 firing: 1.0,
403 consequent_mu: &high,
404 },
405 ];
406 let y_both = mamdani_infer(&u, &both, &mut scratch).unwrap();
407 assert!(close(y_both, 2.0));
408
409 let weak_high = [MamdaniRule {
411 firing: 0.2,
412 consequent_mu: &high,
413 }];
414 assert!(mamdani_infer(&u, &weak_high, &mut scratch).unwrap() > 2.0);
415 let mut bad = [0.0f32; 3];
417 assert!(mamdani_infer(&u, &both, &mut bad).is_none());
418 }
419
420 #[test]
421 fn sugeno_fis_is_firing_weighted_average() {
422 let rules = [
424 SugenoRule {
425 firing: 0.25,
426 consequent: 0.0,
427 },
428 SugenoRule {
429 firing: 0.75,
430 consequent: 10.0,
431 },
432 ];
433 assert!(close(sugeno_infer(&rules).unwrap(), 7.5));
434 let eq = [
436 SugenoRule {
437 firing: 0.5,
438 consequent: 2.0,
439 },
440 SugenoRule {
441 firing: 0.5,
442 consequent: 6.0,
443 },
444 ];
445 assert!(close(sugeno_infer(&eq).unwrap(), 4.0));
446 assert!(sugeno_infer(&[SugenoRule {
448 firing: 0.0,
449 consequent: 9.0
450 }])
451 .is_none());
452 }
453}