1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//! Fuzzy logic temperature controller example.
//!
//! Demonstrates a Mamdani fuzzy inference engine controlling a simulated
//! thermostat. Linguistic variables for temperature error and error rate
//! drive a heating output through a set of IF-THEN rules.
//!
//! Run with:
//! cargo run --example fuzzy_temperature --features "fuzzy"
use oxictl::fuzzy::{
centroid_of_gravity, Antecedent, Consequent, FuzzyRule, MamdaniEngine, MembershipFn, TNorm,
Trapezoidal, Triangular,
};
fn main() {
println!("=== Fuzzy Logic Thermostat Controller ===\n");
// ── Universe of discourse ──────────────────────────────────────────────────
// Temperature error: [-10, 10] °C (NB, NS, ZE, PS, PB)
// Error rate: [-5, 5] °C/s (NB, NS, ZE, PS, PB)
// Heating output: [0, 100] % power (VL, L, ME, H, VH)
// ── Error membership functions (universe: [-10, 10]) ─────────────────────
// NB = Negative Big, NS = Negative Small, ZE = Zero,
// PS = Positive Small, PB = Positive Big
let err_nb = Trapezoidal::new(-10.0_f64, -10.0, -6.0, -3.0).expect("NB error MF construction");
let err_ns = Triangular::new(-6.0_f64, -3.0, 0.0).expect("NS error MF construction");
let err_ze = Triangular::new(-3.0_f64, 0.0, 3.0).expect("ZE error MF construction");
let err_ps = Triangular::new(0.0_f64, 3.0, 6.0).expect("PS error MF construction");
let err_pb = Trapezoidal::new(3.0_f64, 6.0, 10.0, 10.0).expect("PB error MF construction");
// ── Error rate membership functions (universe: [-5, 5]) ──────────────────
let rate_nb = Trapezoidal::new(-5.0_f64, -5.0, -3.0, -1.0).expect("Rate NB MF construction");
let rate_ns = Triangular::new(-3.0_f64, -1.5, 0.0).expect("Rate NS MF construction");
let rate_ze = Triangular::new(-1.5_f64, 0.0, 1.5).expect("Rate ZE MF construction");
let rate_ps = Triangular::new(0.0_f64, 1.5, 3.0).expect("Rate PS MF construction");
let rate_pb = Trapezoidal::new(1.0_f64, 3.0, 5.0, 5.0).expect("Rate PB MF construction");
// ── Output (heating) membership functions (universe: [0, 100]) ───────────
// VL = Very Low, L = Low, ME = Medium, H = High, VH = Very High
let heat_vl = Trapezoidal::new(0.0_f64, 0.0, 10.0, 25.0).expect("VL heating MF construction");
let heat_lo = Triangular::new(10.0_f64, 25.0, 45.0).expect("L heating MF construction");
let heat_me = Triangular::new(30.0_f64, 50.0, 70.0).expect("ME heating MF construction");
let heat_hi = Triangular::new(55.0_f64, 75.0, 90.0).expect("H heating MF construction");
let heat_vh =
Trapezoidal::new(75.0_f64, 90.0, 100.0, 100.0).expect("VH heating MF construction");
// ── MF slices for inference ───────────────────────────────────────────────
// var 0 = error (indices: 0=NB, 1=NS, 2=ZE, 3=PS, 4=PB)
// var 1 = rate (indices: 0=NB, 1=NS, 2=ZE, 3=PS, 4=PB)
let err_mfs: &[&dyn MembershipFn<f64>] = &[&err_nb, &err_ns, &err_ze, &err_ps, &err_pb];
let rate_mfs: &[&dyn MembershipFn<f64>] = &[&rate_nb, &rate_ns, &rate_ze, &rate_ps, &rate_pb];
let input_mfs: &[&[&dyn MembershipFn<f64>]] = &[err_mfs, rate_mfs];
// output MF indices: 0=VL, 1=L, 2=ME, 3=H, 4=VH
let output_mfs: &[&dyn MembershipFn<f64>] = &[&heat_vl, &heat_lo, &heat_me, &heat_hi, &heat_vh];
// ── Mamdani rule base (9 representative rules) ────────────────────────────
// Rule format: IF error=X AND rate=Y THEN heating=Z
// Indices: error terms [NB=0,NS=1,ZE=2,PS=3,PB=4]
// rate terms [NB=0,NS=1,ZE=2,PS=3,PB=4]
// heat terms [VL=0,L=1,ME=2,H=3,VH=4]
let rules: &[(usize, usize, usize)] = &[
// (error_set_idx, rate_set_idx, heat_set_idx)
(4, 3, 4), // IF error=PB AND rate=PS THEN heating=VH
(4, 2, 4), // IF error=PB AND rate=ZE THEN heating=VH
(4, 1, 3), // IF error=PB AND rate=NS THEN heating=H
(3, 2, 3), // IF error=PS AND rate=ZE THEN heating=H
(3, 1, 2), // IF error=PS AND rate=NS THEN heating=ME
(2, 2, 2), // IF error=ZE AND rate=ZE THEN heating=ME
(2, 3, 3), // IF error=ZE AND rate=PS THEN heating=H
(1, 2, 1), // IF error=NS AND rate=ZE THEN heating=L
(0, 2, 0), // IF error=NB AND rate=ZE THEN heating=VL
];
// Build the Mamdani engine with capacity for 16 rules and 8 MFs per var
let mut engine: MamdaniEngine<f64, 16, 8> = MamdaniEngine::new(TNorm::Min);
for &(e_idx, r_idx, h_idx) in rules {
let mut ant = Antecedent::new();
ant.add(0, e_idx).expect("Add error condition");
ant.add(1, r_idx).expect("Add rate condition");
let con = Consequent::unit(0, h_idx);
engine
.add_rule(FuzzyRule::new(ant, con))
.expect("Add Mamdani rule");
}
// ── Thermostat simulation ──────────────────────────────────────────────────
// Simple first-order thermal plant: T[k+1] = T[k] + dt*(P*heat/100 - k_loss*(T-T_env))
// Parameters: P=2.5 °C/s per 100% power, k_loss=0.05/s, T_env=20 °C
let setpoint = 60.0_f64; // Target temperature
let t_env = 20.0_f64; // Ambient temperature
let p_heat = 2.5_f64; // Heating power coefficient (°C/s at 100%)
let k_loss = 0.05_f64; // Heat loss rate (1/s)
let dt = 0.5_f64; // Time step (s)
let n_steps = 100_usize;
let mut temp = t_env; // Start at ambient
let mut prev_error = setpoint - temp;
println!(
"{:>6} {:>8} {:>10} {:>10} {:>10}",
"Step", "Time(s)", "Temp(°C)", "Error(°C)", "Heat(%)"
);
println!("{}", "-".repeat(52));
for k in 0..n_steps {
let t_sim = k as f64 * dt;
let error = setpoint - temp;
let error_rate = (error - prev_error) / dt;
// Clamp inputs to universe of discourse
let error_clamped = error.clamp(-10.0, 10.0);
let rate_clamped = error_rate.clamp(-5.0, 5.0);
let crisp_inputs = [error_clamped, rate_clamped];
// Fuzzy inference → crisp heating percentage
let heat_pct = match engine.infer(&crisp_inputs, input_mfs, output_mfs, 0.0_f64, 100.0_f64)
{
Ok(samples) => {
// Defuzzify with centroid of gravity
centroid_of_gravity(&samples).unwrap_or(50.0)
}
Err(_) => 50.0, // fallback to 50% if inference fails
};
// Print every 10 steps
if k % 10 == 0 {
println!(
"{:>6} {:>8.1} {:>10.3} {:>10.3} {:>10.2}",
k, t_sim, temp, error, heat_pct
);
}
// Update thermal plant
let t_dot = p_heat * (heat_pct / 100.0) - k_loss * (temp - t_env);
temp += dt * t_dot;
prev_error = error;
}
// ── Final report ──────────────────────────────────────────────────────────
let final_error = (temp - setpoint).abs();
println!("\n=== Final State ===");
println!("Temperature: {:.3} °C", temp);
println!("Setpoint: {:.3} °C", setpoint);
println!("Final error: {:.3} °C", final_error);
if final_error < 5.0 {
println!("Status: CONVERGED (error < 5°C)");
} else {
println!("Status: NOT YET CONVERGED (more steps needed)");
}
println!("\nFuzzy rule base: {} active rules", rules.len());
println!("Inference: Mamdani with Min T-norm, CoG defuzzification");
}