makefile based build

This commit is contained in:
2026-03-28 08:55:31 -04:00
parent 244dc0afa6
commit 52114dae41
5 changed files with 845 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
# Compiler
CXX := g++
# Common settings
STD := -std=c++14
TARGET := floating_demo
SRC := main.cpp
# Debug flags
CXXFLAGS_DEBUG := $(STD) -O0 -Wall -Wextra -pedantic
# Release flags
CXXFLAGS_RELEASE := $(STD) -O2
# Default target
all: debug
# Debug build
debug:
$(CXX) $(CXXFLAGS_DEBUG) $(SRC) -o $(TARGET)
# Release build
release:
$(CXX) $(CXXFLAGS_RELEASE) $(SRC) -o $(TARGET)
# Clean
clean:
rm -f $(TARGET)
# Phony targets
.PHONY: all debug release clean

View File

@@ -0,0 +1,78 @@
90,101c90,101
< 0 24.97779766 false true true
< 1 25.01916948 false true true
< 2 25.02972687 false true true
< 3 25.01870257 false true true
< 4 24.96365349 false true true
< 5 25.02417787 false true true
< 6 25.01521257 false true true
< 7 25.01746188 false true true
< 8 25.01469876 false true true
< 9 24.95849519 false true true
< 10 25.00975313 false true true
< 11 24.96665774 false true true
---
> 0 24.97471804 false true true
> 1 25.0413491 false true true
> 2 24.99852989 false true true
> 3 24.99317494 false true true
> 4 25.00292729 false true true
> 5 25.03122934 false true true
> 6 24.97063462 false true true
> 7 24.9619692 false true true
> 8 24.99279928 false true true
> 9 24.97964776 false true true
> 10 25.04053616 false true true
> 11 25.0353959 false true true
105,116c105,116
< 0 24.99675513 false true true
< 1 24.9971971 false true true
< 2 25.00143947 false true true
< 3 24.99902736 false true true
< 4 24.99946988 false true true
< 5 24.99607726 false true true
< 6 24.99667581 false true true
< 7 25.00177208 false true true
< 8 24.99822762 false true true
< 9 25.00066511 false true true
< 10 24.997119 false true true
< 11 25.0030632 false true true
---
> 0 25.00020134 false true true
> 1 25.00382071 false true true
> 2 25.00383611 false true true
> 3 24.99614752 false true true
> 4 25.00265027 false true true
> 5 25.00074494 false true true
> 6 24.99856782 false true true
> 7 25.00364029 false true true
> 8 25.00150722 false true true
> 9 25.00310794 false true true
> 10 25.00211411 false true true
> 11 25.00197054 false true true
120,131c120,131
< 0 24.99998171 false true true
< 1 25.00043692 false false false
< 2 24.99991995 false true true
< 3 24.99953881 false false false
< 4 24.99997226 false true true
< 5 25.00001312 false true true
< 6 24.99969943 false false false
< 7 25.00025024 false false false
< 8 24.99970481 false false false
< 9 25.0004967 false false false
< 10 24.99993727 false true true
< 11 24.99984134 false false false
---
> 0 25.00034068 false false false
> 1 24.99978583 false false false
> 2 24.99965013 false false false
> 3 25.00030996 false false false
> 4 25.00044598 false false false
> 5 25.00007675 false true true
> 6 25.00043085 false false false
> 7 24.99969316 false false false
> 8 24.99999024 false true true
> 9 24.99991615 false true true
> 10 25.00012491 false false false
> 11 24.99951951 false false false

View File

@@ -0,0 +1,356 @@
#include <iostream>
#include <iomanip>
#include <cmath>
#include <algorithm>
#include <limits>
#include <cstdlib>
#include <ctime>
#include <string>
template<typename T>
bool equal_naive (T a, T b) {
return a == b;
}
template<typename T>
bool equal_abs (T a, T b, T eps) {
return std::fabs (a - b) <= eps;
}
template<typename T>
bool equal_rel (T a, T b, T eps) {
const T scale = std::max (std::fabs (a), std::fabs (b));
return std::fabs (a - b) <= eps * scale;
}
template<typename T>
bool equal_combined (T a, T b, T abs_eps, T rel_eps) {
const T scale = std::max (std::fabs (a), std::fabs (b));
return std::fabs (a - b) <= std::max (abs_eps, rel_eps * scale);
}
template<typename T>
void print_bool_result (const std::string &label, bool value) {
std::cout << " " << std::left << std::setw (28) << label
<< ": " << (value ? "true" : "false") << "\n";
}
template<typename T>
void print_value_line (const std::string &label, T value) {
std::cout << " " << std::left << std::setw (28) << label
<< ": " << std::setprecision (std::numeric_limits<T>::digits10 + 2)
<< value << "\n";
}
void print_section (const std::string &title) {
std::cout << "\n============================================================\n";
std::cout << title << "\n";
std::cout << "============================================================\n";
}
void example_basic_01_plus_02() {
print_section ("1. Basic classic: 0.1 + 0.2 != 0.3");
const double a = 0.1 + 0.2;
const double b = 0.3;
print_value_line ("a", a);
print_value_line ("b", b);
print_value_line ("abs(a - b)", std::fabs (a - b));
print_bool_result<double> ("a == b", equal_naive (a, b));
print_bool_result<double> ("abs eps = 1e-12", equal_abs (a, b, 1e-12));
print_bool_result<double> ("abs eps = 1e-9", equal_abs (a, b, 1e-9));
print_bool_result<double> ("combined", equal_combined (a, b, 1e-12, 1e-12));
}
void example_large_numbers() {
print_section ("2. Large numbers: fixed absolute epsilon becomes useless");
const double a = 1e9;
const double b = a + 0.1;
print_value_line ("a", a);
print_value_line ("b", b);
print_value_line ("abs(a - b)", std::fabs (a - b));
print_bool_result<double> ("a == b", equal_naive (a, b));
print_bool_result<double> ("abs eps = 1e-12", equal_abs (a, b, 1e-12));
print_bool_result<double> ("abs eps = 1e-9", equal_abs (a, b, 1e-9));
print_bool_result<double> ("abs eps = 1e-3", equal_abs (a, b, 1e-3));
print_bool_result<double> ("rel eps = 1e-12", equal_rel (a, b, 1e-12));
print_bool_result<double> ("rel eps = 1e-9", equal_rel (a, b, 1e-9));
print_bool_result<double> ("combined", equal_combined (a, b, 1e-9, 1e-9));
}
void example_small_numbers() {
print_section ("3. Very small numbers: fixed absolute epsilon can be too large");
const double a = 1e-12;
const double b = 2e-12;
print_value_line ("a", a);
print_value_line ("b", b);
print_value_line ("abs(a - b)", std::fabs (a - b));
print_bool_result<double> ("a == b", equal_naive (a, b));
print_bool_result<double> ("abs eps = 1e-9", equal_abs (a, b, 1e-9));
print_bool_result<double> ("abs eps = 1e-12", equal_abs (a, b, 1e-12));
print_bool_result<double> ("rel eps = 1e-9", equal_rel (a, b, 1e-9));
print_bool_result<double> ("rel eps = 0.5", equal_rel (a, b, 0.5));
print_bool_result<double> ("combined", equal_combined (a, b, 1e-15, 1e-6));
}
void example_near_zero_relative_problem() {
print_section ("4. Near zero: relative comparison alone is weak");
const double a = 0.0;
const double b = 1e-15;
print_value_line ("a", a);
print_value_line ("b", b);
print_value_line ("abs(a - b)", std::fabs (a - b));
print_bool_result<double> ("a == b", equal_naive (a, b));
print_bool_result<double> ("rel eps = 1e-6", equal_rel (a, b, 1e-6));
print_bool_result<double> ("abs eps = 1e-12", equal_abs (a, b, 1e-12));
print_bool_result<double> ("combined", equal_combined (a, b, 1e-12, 1e-6));
}
void example_associativity() {
print_section ("5. Same math on paper, different result in floating point");
const double a = 1e16;
const double b = -1e16;
const double c = 1.0;
const double x = (a + b) + c;
const double y = a + (b + c);
print_value_line ("x = (a + b) + c", x);
print_value_line ("y = a + (b + c)", y);
print_value_line ("abs(x - y)", std::fabs (x - y));
print_bool_result<double> ("x == y", equal_naive (x, y));
print_bool_result<double> ("combined", equal_combined (x, y, 1e-12, 1e-12));
}
void example_accumulation() {
print_section ("6. Accumulation of error: repeated addition");
double x = 0.0;
for (int i = 0; i < 1000000; ++i)
x += 0.1;
const double y = 100000.0;
print_value_line ("x", x);
print_value_line ("y", y);
print_value_line ("abs(x - y)", std::fabs (x - y));
print_bool_result<double> ("x == y", equal_naive (x, y));
print_bool_result<double> ("abs eps = 1e-9", equal_abs (x, y, 1e-9));
print_bool_result<double> ("abs eps = 1e-6", equal_abs (x, y, 1e-6));
print_bool_result<double> ("combined", equal_combined (x, y, 1e-6, 1e-12));
}
void example_float_vs_double() {
print_section ("7. float vs double: same idea, different precision");
float af = 0.1f + 0.2f;
float bf = 0.3f;
double ad = 0.1 + 0.2;
double bd = 0.3;
print_value_line ("float a", af);
print_value_line ("float b", bf);
print_value_line ("float abs diff", std::fabs (af - bf));
print_bool_result<float> ("float a == b", equal_naive (af, bf));
std::cout << "\n";
print_value_line ("double a", ad);
print_value_line ("double b", bd);
print_value_line ("double abs diff", std::fabs (ad - bd));
print_bool_result<double> ("double a == b", equal_naive (ad, bd));
}
double random_noise (double amplitude) {
const double unit = static_cast<double> (std::rand()) / static_cast<double> (RAND_MAX);
return (unit * 2.0 - 1.0) * amplitude;
}
void run_sensor_simulation (double noise_amplitude, double abs_eps) {
const double target = 25.0;
const double rel_eps = 1e-6;
std::cout << "\nNoise amplitude: +/-" << noise_amplitude
<< ", abs_eps: " << abs_eps << "\n";
std::cout << " " << std::left
<< std::setw (10) << "sample"
<< std::setw (18) << "value"
<< std::setw (10) << "=="
<< std::setw (10) << "abs"
<< std::setw (10) << "comb"
<< "\n";
for (int i = 0; i < 12; ++i) {
const double value = target + random_noise (noise_amplitude);
const bool naive = equal_naive (value, target);
const bool abs_ok = equal_abs (value, target, abs_eps);
const bool comb_ok = equal_combined (value, target, abs_eps, rel_eps);
std::cout << " " << std::left
<< std::setw (10) << i
<< std::setw (18) << std::setprecision (10) << value
<< std::setw (10) << (naive ? "true" : "false")
<< std::setw (10) << (abs_ok ? "true" : "false")
<< std::setw (10) << (comb_ok ? "true" : "false")
<< "\n";
}
}
void example_sensor_noise() {
print_section ("8. Sensor-like values: 'equal' is often the wrong question");
run_sensor_simulation (0.05, 0.1);
run_sensor_simulation (0.005, 0.01);
run_sensor_simulation (0.0005, 0.0001);
}
void example_scaled_integer_vs_double() {
print_section ("9. Scaled integer values: often better to compare raw domain");
const int raw_value = 1234; // imagine 123.4 in deci-units
const int raw_target = 1234;
const double scaled_value = raw_value * 0.1;
const double scaled_target = 123.4;
print_value_line ("raw_value", raw_value);
print_value_line ("raw_target", raw_target);
print_bool_result<int> ("raw_value == raw_target", raw_value == raw_target);
std::cout << "\n";
print_value_line ("scaled_value", scaled_value);
print_value_line ("scaled_target", scaled_target);
print_value_line ("abs diff", std::fabs (scaled_value - scaled_target));
print_bool_result<double> ("scaled_value == scaled_target",
equal_naive (scaled_value, scaled_target));
print_bool_result<double> ("combined",
equal_combined (scaled_value, scaled_target, 1e-12, 1e-12));
std::cout << "\n Note: if your system is naturally discrete, comparing raw units can be simpler\n";
std::cout << " and more honest than converting everything to floating point too early.\n";
}
void example_rounding_is_not_magic() {
print_section ("10. Rounding is not a universal fix");
const double a = 1.0049;
const double b = 1.0051;
const double rounded_a_2 = std::round (a * 100.0) / 100.0;
const double rounded_b_2 = std::round (b * 100.0) / 100.0;
const double rounded_a_3 = std::round (a * 1000.0) / 1000.0;
const double rounded_b_3 = std::round (b * 1000.0) / 1000.0;
print_value_line ("a", a);
print_value_line ("b", b);
std::cout << "\n";
print_value_line ("round(a, 2)", rounded_a_2);
print_value_line ("round(b, 2)", rounded_b_2);
print_bool_result<double> ("equal after round(2)", rounded_a_2 == rounded_b_2);
std::cout << "\n";
print_value_line ("round(a, 3)", rounded_a_3);
print_value_line ("round(b, 3)", rounded_b_3);
print_bool_result<double> ("equal after round(3)", rounded_a_3 == rounded_b_3);
std::cout << "\n Rounding may hide differences or invent equality depending on chosen precision.\n";
}
void example_hysteresis() {
print_section ("11. Hysteresis: in real systems you often want state logic, not equality");
const double target = 25.0;
const double on_threshold = target - 0.1;
const double off_threshold = target + 0.1;
const double signal[] = {
24.92, 24.97, 25.01, 25.05, 24.99, 25.08, 25.11, 25.06,
24.98, 24.91, 24.89, 24.95, 25.02, 25.12, 25.07, 24.93
};
bool heater_simple = true;
bool heater_hysteresis = true;
std::cout << " " << std::left
<< std::setw (8) << "step"
<< std::setw (12) << "value"
<< std::setw (16) << "simple_ctrl"
<< std::setw (16) << "hyst_ctrl"
<< "\n";
for (size_t i = 0; i < (sizeof (signal) / sizeof (signal[0])); ++i) {
const double value = signal[i];
/*
* Simple control:
* heater ON below target
* heater OFF at or above target
* This tends to chatter near the threshold.
*/
heater_simple = value < target;
/*
* Hysteresis control:
* heater turns ON only below lower threshold
* heater turns OFF only above upper threshold
* This reduces chatter around the target.
*/
if (heater_hysteresis && value > off_threshold)
heater_hysteresis = false;
else if (!heater_hysteresis && value < on_threshold)
heater_hysteresis = true;
std::cout << " " << std::left
<< std::setw (8) << i
<< std::setw (12) << std::setprecision (6) << value
<< std::setw (16) << (heater_simple ? "HEATER ON" : "HEATER OFF")
<< std::setw (16) << (heater_hysteresis ? "HEATER ON" : "HEATER OFF")
<< "\n";
}
std::cout << "\n This is the key engineering point:\n";
std::cout << " sometimes the answer is not a better equality test,\n";
std::cout << " but a better control model.\n";
}
int main() {
std::srand (static_cast<unsigned int> (std::time (NULL)));
std::cout << std::boolalpha;
example_basic_01_plus_02();
example_large_numbers();
example_small_numbers();
example_near_zero_relative_problem();
example_associativity();
example_accumulation();
example_float_vs_double();
example_sensor_noise();
example_scaled_integer_vs_double();
example_rounding_is_not_magic();
example_hysteresis();
std::cout << "\nDone.\n";
return 0;
}

View File

@@ -0,0 +1,190 @@
============================================================
1. Basic classic: 0.1 + 0.2 != 0.3
============================================================
a : 0.30000000000000004
b : 0.29999999999999999
abs(a - b) : 5.5511151231257827e-17
a == b : false
abs eps = 1e-12 : true
abs eps = 1e-9 : true
combined : true
============================================================
2. Large numbers: fixed absolute epsilon becomes useless
============================================================
a : 1000000000
b : 1000000000.1
abs(a - b) : 0.10000002384185791
a == b : false
abs eps = 1e-12 : false
abs eps = 1e-9 : false
abs eps = 1e-3 : false
rel eps = 1e-12 : false
rel eps = 1e-9 : true
combined : true
============================================================
3. Very small numbers: fixed absolute epsilon can be too large
============================================================
a : 9.9999999999999998e-13
b : 2e-12
abs(a - b) : 9.9999999999999998e-13
a == b : false
abs eps = 1e-9 : true
abs eps = 1e-12 : true
rel eps = 1e-9 : false
rel eps = 0.5 : true
combined : false
============================================================
4. Near zero: relative comparison alone is weak
============================================================
a : 0
b : 1.0000000000000001e-15
abs(a - b) : 1.0000000000000001e-15
a == b : false
rel eps = 1e-6 : false
abs eps = 1e-12 : true
combined : true
============================================================
5. Same math on paper, different result in floating point
============================================================
x = (a + b) + c : 1
y = a + (b + c) : 0
abs(x - y) : 1
x == y : false
combined : false
============================================================
6. Accumulation of error: repeated addition
============================================================
x : 100000.00000133288
y : 100000
abs(x - y) : 1.3328826753422618e-06
x == y : false
abs eps = 1e-9 : false
abs eps = 1e-6 : false
combined : false
============================================================
7. float vs double: same idea, different precision
============================================================
float a : 0.30000001
float b : 0.30000001
float abs diff : 0
float a == b : true
double a : 0.30000000000000004
double b : 0.29999999999999999
double abs diff : 5.5511151231257827e-17
double a == b : false
============================================================
8. Sensor-like values: 'equal' is often the wrong question
============================================================
Noise amplitude: +/-0.050000000000000003, abs_eps: 0.10000000000000001
sample value == abs comb
0 24.97779766 false true true
1 25.01916948 false true true
2 25.02972687 false true true
3 25.01870257 false true true
4 24.96365349 false true true
5 25.02417787 false true true
6 25.01521257 false true true
7 25.01746188 false true true
8 25.01469876 false true true
9 24.95849519 false true true
10 25.00975313 false true true
11 24.96665774 false true true
Noise amplitude: +/-0.005, abs_eps: 0.01
sample value == abs comb
0 24.99675513 false true true
1 24.9971971 false true true
2 25.00143947 false true true
3 24.99902736 false true true
4 24.99946988 false true true
5 24.99607726 false true true
6 24.99667581 false true true
7 25.00177208 false true true
8 24.99822762 false true true
9 25.00066511 false true true
10 24.997119 false true true
11 25.0030632 false true true
Noise amplitude: +/-0.0005, abs_eps: 0.0001
sample value == abs comb
0 24.99998171 false true true
1 25.00043692 false false false
2 24.99991995 false true true
3 24.99953881 false false false
4 24.99997226 false true true
5 25.00001312 false true true
6 24.99969943 false false false
7 25.00025024 false false false
8 24.99970481 false false false
9 25.0004967 false false false
10 24.99993727 false true true
11 24.99984134 false false false
============================================================
9. Scaled integer values: often better to compare raw domain
============================================================
raw_value : 1234
raw_target : 1234
raw_value == raw_target : true
scaled_value : 123.40000000000001
scaled_target : 123.40000000000001
abs diff : 0
scaled_value == scaled_target: true
combined : true
Note: if your system is naturally discrete, comparing raw units can be simpler
and more honest than converting everything to floating point too early.
============================================================
10. Rounding is not a universal fix
============================================================
a : 1.0048999999999999
b : 1.0051000000000001
round(a, 2) : 1
round(b, 2) : 1.01
equal after round(2) : false
round(a, 3) : 1.0049999999999999
round(b, 3) : 1.0049999999999999
equal after round(3) : true
Rounding may hide differences or invent equality depending on chosen precision.
============================================================
11. Hysteresis: in real systems you often want state logic, not equality
============================================================
step value simple_ctrl hyst_ctrl
0 24.92 HEATER ON HEATER ON
1 24.97 HEATER ON HEATER ON
2 25.01 HEATER OFF HEATER ON
3 25.05 HEATER OFF HEATER ON
4 24.99 HEATER ON HEATER ON
5 25.08 HEATER OFF HEATER ON
6 25.11 HEATER OFF HEATER OFF
7 25.06 HEATER OFF HEATER OFF
8 24.98 HEATER ON HEATER OFF
9 24.91 HEATER ON HEATER OFF
10 24.89 HEATER ON HEATER ON
11 24.95 HEATER ON HEATER ON
12 25.02 HEATER OFF HEATER ON
13 25.12 HEATER OFF HEATER OFF
14 25.07 HEATER OFF HEATER OFF
15 24.93 HEATER ON HEATER OFF
This is the key engineering point:
sometimes the answer is not a better equality test,
but a better control model.
Done.

View File

@@ -0,0 +1,190 @@
============================================================
1. Basic classic: 0.1 + 0.2 != 0.3
============================================================
a : 0.30000000000000004
b : 0.29999999999999999
abs(a - b) : 5.5511151231257827e-17
a == b : false
abs eps = 1e-12 : true
abs eps = 1e-9 : true
combined : true
============================================================
2. Large numbers: fixed absolute epsilon becomes useless
============================================================
a : 1000000000
b : 1000000000.1
abs(a - b) : 0.10000002384185791
a == b : false
abs eps = 1e-12 : false
abs eps = 1e-9 : false
abs eps = 1e-3 : false
rel eps = 1e-12 : false
rel eps = 1e-9 : true
combined : true
============================================================
3. Very small numbers: fixed absolute epsilon can be too large
============================================================
a : 9.9999999999999998e-13
b : 2e-12
abs(a - b) : 9.9999999999999998e-13
a == b : false
abs eps = 1e-9 : true
abs eps = 1e-12 : true
rel eps = 1e-9 : false
rel eps = 0.5 : true
combined : false
============================================================
4. Near zero: relative comparison alone is weak
============================================================
a : 0
b : 1.0000000000000001e-15
abs(a - b) : 1.0000000000000001e-15
a == b : false
rel eps = 1e-6 : false
abs eps = 1e-12 : true
combined : true
============================================================
5. Same math on paper, different result in floating point
============================================================
x = (a + b) + c : 1
y = a + (b + c) : 0
abs(x - y) : 1
x == y : false
combined : false
============================================================
6. Accumulation of error: repeated addition
============================================================
x : 100000.00000133288
y : 100000
abs(x - y) : 1.3328826753422618e-06
x == y : false
abs eps = 1e-9 : false
abs eps = 1e-6 : false
combined : false
============================================================
7. float vs double: same idea, different precision
============================================================
float a : 0.30000001
float b : 0.30000001
float abs diff : 0
float a == b : true
double a : 0.30000000000000004
double b : 0.29999999999999999
double abs diff : 5.5511151231257827e-17
double a == b : false
============================================================
8. Sensor-like values: 'equal' is often the wrong question
============================================================
Noise amplitude: +/-0.050000000000000003, abs_eps: 0.10000000000000001
sample value == abs comb
0 24.97471804 false true true
1 25.0413491 false true true
2 24.99852989 false true true
3 24.99317494 false true true
4 25.00292729 false true true
5 25.03122934 false true true
6 24.97063462 false true true
7 24.9619692 false true true
8 24.99279928 false true true
9 24.97964776 false true true
10 25.04053616 false true true
11 25.0353959 false true true
Noise amplitude: +/-0.005, abs_eps: 0.01
sample value == abs comb
0 25.00020134 false true true
1 25.00382071 false true true
2 25.00383611 false true true
3 24.99614752 false true true
4 25.00265027 false true true
5 25.00074494 false true true
6 24.99856782 false true true
7 25.00364029 false true true
8 25.00150722 false true true
9 25.00310794 false true true
10 25.00211411 false true true
11 25.00197054 false true true
Noise amplitude: +/-0.0005, abs_eps: 0.0001
sample value == abs comb
0 25.00034068 false false false
1 24.99978583 false false false
2 24.99965013 false false false
3 25.00030996 false false false
4 25.00044598 false false false
5 25.00007675 false true true
6 25.00043085 false false false
7 24.99969316 false false false
8 24.99999024 false true true
9 24.99991615 false true true
10 25.00012491 false false false
11 24.99951951 false false false
============================================================
9. Scaled integer values: often better to compare raw domain
============================================================
raw_value : 1234
raw_target : 1234
raw_value == raw_target : true
scaled_value : 123.40000000000001
scaled_target : 123.40000000000001
abs diff : 0
scaled_value == scaled_target: true
combined : true
Note: if your system is naturally discrete, comparing raw units can be simpler
and more honest than converting everything to floating point too early.
============================================================
10. Rounding is not a universal fix
============================================================
a : 1.0048999999999999
b : 1.0051000000000001
round(a, 2) : 1
round(b, 2) : 1.01
equal after round(2) : false
round(a, 3) : 1.0049999999999999
round(b, 3) : 1.0049999999999999
equal after round(3) : true
Rounding may hide differences or invent equality depending on chosen precision.
============================================================
11. Hysteresis: in real systems you often want state logic, not equality
============================================================
step value simple_ctrl hyst_ctrl
0 24.92 HEATER ON HEATER ON
1 24.97 HEATER ON HEATER ON
2 25.01 HEATER OFF HEATER ON
3 25.05 HEATER OFF HEATER ON
4 24.99 HEATER ON HEATER ON
5 25.08 HEATER OFF HEATER ON
6 25.11 HEATER OFF HEATER OFF
7 25.06 HEATER OFF HEATER OFF
8 24.98 HEATER ON HEATER OFF
9 24.91 HEATER ON HEATER OFF
10 24.89 HEATER ON HEATER ON
11 24.95 HEATER ON HEATER ON
12 25.02 HEATER OFF HEATER ON
13 25.12 HEATER OFF HEATER OFF
14 25.07 HEATER OFF HEATER OFF
15 24.93 HEATER ON HEATER OFF
This is the key engineering point:
sometimes the answer is not a better equality test,
but a better control model.
Done.