Про интервальную арифметику

Давно хотел попробовать воспользоваться «интервальной арифметикой» для вычислений с учётом паспортных погрешностей датчиков, кварцев и другой аппаратуры.

Интервальная арифметика определена стандартом IEEE 1788-2015 - IEEE Standard for Interval Arithmetic, но там реальные, зубодробительные дебри. Настолько реальные, что через два года вышел «облегчённый» стандарт 1788.1-2017 - IEEE Standard for Interval Arithmetic (Simplified), но и в нём в разы больше всякой фигни, чем нам с Вами нужно.

Если кому-то интересна библиотека, полностью реализующая IEEE 1788-2015, их есть у нас, пожалуйста (под Ардуино сами адаптируйте). Я же для своего эксперимента написал сегодня на коленке микро-реализацию интервальной арифметики (только нужные мне операции, но они правильные - по стандарту) и решил поэкспериментировать с нею, а именно, написать программку измерения расстояния датчиком HC-SR04 с температурной компенсацией (по формуле Лапласа). При этом предлагалось учитывать погрешности кварца, датчика расстояния, датчика температуры и погрешность, вносимую особенностями функции pulseIn.

Вот собсна библиотека
#ifndef	TINY1788_H
#define	TINY1788_H

//	Базовый тип хранения чисел
// TNumber может быть: float, double или long double
typedef double TNumber;

//
// Нигде и никак не првоеряется корректность операций
//	(деление на 0, извлечение корня из отрицатльеного числа и т.п.)
//	после операция можно проверить всё ли нормально методом isValid()
//

class Tiny1788 : public Printable{
public:
	Tiny1788(const TNumber f = 0.0, const TNumber s = 0.0) : m_first(f), m_second(s) {
		normalize();
	}

	Tiny1788 & setValue(const TNumber val, const TNumber precission) {
		m_first = val - precission;
		m_second = val + precission;
		normalize(); // дуракоустойчивость а вдруг precission < 0
		return * this;
	}

	bool isValid(void) const {
		return 
			isfinite(m_first) &&
			isfinite(m_second) &&
			m_first <= m_second;
	}

	TNumber wid(void) const { return m_second - m_first; }	
	TNumber mid(void) { return (m_second + m_first) / 2.0; }
	TNumber rad(void) { return wid() / 2.0; }

	TNumber lower(void) const { return m_first; }
	TNumber upper(void) const { return m_second; }

	Tiny1788 & operator += (const Tiny1788 &a) { return *this = *this + a; }
	Tiny1788 & operator -= (const Tiny1788 &a) { return *this = *this - a; }
	Tiny1788 & operator *= (const Tiny1788 &a) { return *this = *this * a; }
	Tiny1788 & operator /= (const Tiny1788 &a) { return *this = *this / a; }

	Tiny1788 & operator += (const TNumber a) { return *this = *this + a; }
	Tiny1788 & operator -= (const TNumber a) { return *this = *this - a; }
	Tiny1788 & operator *= (const TNumber a) { return *this = *this * a; }
	Tiny1788 & operator /= (const TNumber a) { return *this = *this / a; }

	size_t printTo(Print & p) const { // в качестве p нам передадут, например, Serial
		const double lg = log10(wid());
		int pos = lg < 0 ? 1 - static_cast<int>(floor(lg)): 2; 
		if (pos > 7) pos = 7; 
		size_t res = p.print('[');
		res += p.print(m_first, pos);
		res += p.print(',');
		res += p.print(m_second, pos);
		return res + p.print(']');
	}
	
	
private:
	TNumber m_first, m_second;
	
	void normalize(void) {
		if (m_first > m_second) {
			const TNumber c = m_first;
			m_first = m_second;
			m_second = c;
		}
	}

	static TNumber fmax4(const TNumber a, const TNumber b, const TNumber c, const TNumber d) { return fmax(fmax(a, b) , fmax(c, d)); }
	static TNumber fmin4(const TNumber a, const TNumber b, const TNumber c, const TNumber d) { return fmin(fmin(a, b) , fmin(c, d)); }

	friend Tiny1788 operator + (const Tiny1788 &, const Tiny1788 &);
	friend Tiny1788 operator - (const Tiny1788 &, const Tiny1788 &);
	friend Tiny1788 operator * (const Tiny1788 &, const Tiny1788 &);
	friend Tiny1788 operator / (const Tiny1788 &, const Tiny1788 &);
	
	friend Tiny1788 operator + (const Tiny1788 &, const TNumber);
	friend Tiny1788 operator - (const Tiny1788 &, const TNumber);
	friend Tiny1788 operator - (const TNumber, const Tiny1788 &);
	friend Tiny1788 operator * (const Tiny1788 &, const TNumber);
	friend Tiny1788 operator / (const Tiny1788 &, const TNumber);

};

//Сложение: [a,b] + [c,d] = [a + c, b + d]
inline Tiny1788 operator + (const Tiny1788 &a, const Tiny1788 &b) {
	return Tiny1788(a.lower() + b.lower(), a.upper() + b.upper());
}

//Вычитание: [a,b] − [c,d] = [a − d, b − c]
inline Tiny1788 operator - (const Tiny1788 &a, const Tiny1788 &b) {
	return Tiny1788(a.lower() - b.upper(), a.upper() + b.lower());
}

//Умножение: [a,b] × [c,d] = [min (ac, ad, bc, bd), max (ac, ad, bc, bd)]
inline Tiny1788 operator * (const Tiny1788 &a, const Tiny1788 &b) {
	const TNumber ac = a.lower() * b.lower();
	const TNumber ad = a.lower() * b.upper();
	const TNumber bc = a.upper() * b.lower();
	const TNumber bd = a.upper() * b.upper();
	return Tiny1788(Tiny1788::fmin4(ac, ad, bc, bd), Tiny1788::fmax4(ac, ad, bc, bd));
}

// После деления, результат надо проверять на isValid
//	т.к. в делителе могут быть нули и здесь это не проверяется
//Деление: [a,b] / [c,d] = [min (a/c, a/d, b/c, b/d), max (a/c, a/d, b/c, b/d)]
inline Tiny1788 operator / (const Tiny1788 &a, const Tiny1788 &b) {
	const TNumber ac = a.lower() / b.lower();
	const TNumber ad = a.lower() / b.upper();
	const TNumber bc = a.upper() / b.lower();
	const TNumber bd = a.upper() / b.upper();
	return Tiny1788(Tiny1788::fmin4(ac, ad, bc, bd), Tiny1788::fmax4(ac, ad, bc, bd));
}

inline Tiny1788 operator + (const Tiny1788 &a, const TNumber b) { return a + Tiny1788(b, b); }
inline Tiny1788 operator + (const TNumber a, const Tiny1788 &b) { return b + a; }
inline Tiny1788 operator - (const Tiny1788 &a, const TNumber b) { return a - Tiny1788(b, b); }
inline Tiny1788 operator - (const TNumber b, const Tiny1788 &a) { return Tiny1788(b, b) - a; }
inline Tiny1788 operator * (const Tiny1788 &a, const TNumber b) { return a * Tiny1788(b, b); }
inline Tiny1788 operator * (const TNumber a, const Tiny1788 &b) { return b * a; }
inline Tiny1788 operator / (const Tiny1788 &a, const TNumber b) { return a / Tiny1788(b, b); }
inline Tiny1788 operator / (const TNumber b, const Tiny1788 &a) { return Tiny1788(b, b) / a; }

inline Tiny1788 sqrt(const Tiny1788 &a) { return Tiny1788(sqrt(a.lower()), sqrt(a.upper())); }

#endif	//	TINY1788_H

А вот программа, с её использованием. Места, где я учитываю погрешности, я прокомментировал. Все вычисления ведутся над интервалами (тип Tiny1788). Результатом тоже является интервал.

Программа
#define printVar(x) do { Serial.print(#x); Serial.print('='); Serial.println(x); } while (false)

#include <OneWire.h>
#include <DallasTemperature.h>
#include "Tiny1788.h"

constexpr uint8_t triggerPin = 2;
constexpr uint8_t theEchoPin = 3;
constexpr uint8_t oneWireBus = 4;

constexpr double quarzAccuracy = 20e-6; // погрешность кварца в ppm (с потолка, но разумная для кварцев без термокомпенсации)
const Tiny1788 ds18b20Accuracy(-0.5, 0.5); // погрешность DS18B20 из даташита
const Tiny1788 HCSR04Accuracy(-0.3, 0.3);	// погрешность HC-SR04 из даташита 


OneWire oneWire(oneWireBus);
DallasTemperature sensors(&oneWire);

// Возвращает длительность в микросекундах в виде интервала "от и до"
Tiny1788 getDuration(void) {
	digitalWrite(triggerPin, LOW);
	delayMicroseconds(2);
	digitalWrite(triggerPin, HIGH);
	delayMicroseconds(10);
	digitalWrite(triggerPin, LOW);
	double res = pulseIn(theEchoPin, HIGH);
	//
	// Поскольку pulseIn выдаёт целые микросекунды, считаем,
	// что это даёт погрешность плюс/минус полмикросекунды.
	// также здесь учитываем погрешность кварца
	// В результате возвращаем интервал
	return Tiny1788((1 - quarzAccuracy) * (res - 0.5), (1 + quarzAccuracy) * (res + 0.5));
}

// возвращает температуру в градусах Цельсия в виде интервала "от и до"
Tiny1788 getTemperature(void) {
	sensors.requestTemperatures();
	return ds18b20Accuracy + sensors.getTempCByIndex(0);
}

void setup(void) {
	pinMode(theEchoPin, INPUT);
	pinMode(triggerPin, OUTPUT);
	Serial.begin(9600);
	sensors.begin();
	sensors.setResolution(12);

	Tiny1788 temperature = getTemperature();
	Tiny1788 duration = getDuration();

	const Tiny1788 absTemp = 273.15 + temperature; // абсолютная температура 
	// Здесь считается скорость звука по формуле Лапласа
	//	умножение на 1e-4 - для перевода из м/с в см/мкс
	const Tiny1788 c = 20.05 * sqrt(absTemp) * 1e-4; // скорость звука в сантиметрах в микросекунду
	// Считаем расстояния и прибавляем к нему паспортную погрешность HC-SR04
	const Tiny1788 distance = c * duration / 2 + HCSR04Accuracy; // расстояние
	printVar(distance); // печатаем.
}

void loop(void) {}

Ну, что тщательно выставил расстояние до преграды - 20 см.

Результат

distance=[19.49,20.14]

попробовал с другими расстояниями (до 3 м). Реальное расстояние всегда оказывалось внутри интервала - результата. Мне понравилось.

Пока не осознал, нужно ли мне это. Но, в принципе, если хочется знать поточнее, то правильный путь - такой, а не считать со 100500 знаками после запятой, когда погрешность датчика ±0,5. Здесь получается правильная точность, а не карнавал бессмысленных цифирек после запятой.

2 лайка

вот прямо с ходу задача, счетчик для магнитофона, при вычислении скорости ленты (количество импульсов в секунду преобразуем в скорость (9.53 или 19.06)), тут как раз поможет

Так и делайте.

Tiny1788 someInterval(<минимальное>, <максимальное>);

А дальше можно с этим someInterval как с обычным числом. Результатом будет опять же интервал.

1 лайк

Евгений Петрович, для таких тупых как я (например) минимальным количеством понятных слов - а что такое вообще «интервальная математика»?
Прошу прощения за свою тупость заранее…

Принято говорить не “интервальная математика”, а “интервальная арифметика” (подозреваю, что с алгеброй там еще хуже).
Это арифметика, которая позволяет дать правильный ответ с учетом всех погрешностей.

1 лайк

Интервал - это пара чисел - “нижняя граница” и “верхняя граница”. Например, если ds18b20 выдал 16,7 градуса, то с учётом того, что его точность ±0,5 градуса, мы можем говорить, что температура находится между 16,2 и 17,2 градуса. Записывается это как: t = [16,2, 17,2]

Интервальная арифметика - набор операций над интервалами. Т.е. с интервалам обращаются как с числами - для них (интервалов) определены все арифметические операции и т.п.

Вот, смотрите на мою программу. к строке №55 мы имеем интервал absTemp - он содержит нижнюю и верхнюю границу абсолютной температуры. В строке №55 мы вычисляем скорость звука (опять же интервал). Это значит, что скорость звука лежит в пределах “от и до”. К 56-ой строке мы имеем два интервала: с (скорость звука) и duration (время ответа ультразвукового датчика). Это именно интервалы - они показывают не одно значение, а “от и до”. В строке 56 мы проводим ряд арифметических операций с этими интервалами и получаем интервал distance - расстояние, опять же “от и до”. Попробуйте взять мою программу и вставьте печати переменных после из вычисления (интервалы можно печатать просто Serial.print - в библиотеке это сделано).

Ну, а дальше, если у Вас есть интервал температуры и, как в моём примере, интервал длительности прохождения звука, считаете интервал расстояния. Просто, вместо чисел, везде интервалы - это позволяет получить более разумный результат, чем голимое число с непонятно какой точностью.

2 лайка

Интересна реализация булевых операций с этим типом, КАК? К примеру получили число 50, если оно входит в интервал, то true иначе false

Ну, проверка на вхождение в интервал элементарна, у него же есть верхняя и нижняя граница, берите, да проверяйте. А так, там много всяких хитрых операций, например, сравнение интервалов на больше меньше - гороху объешься.

помнится, мы когда-то это все в институте изучали. Называлось как-то вроде “Статистическая обработка экспериментальных данных”. Все что помню - название “правило трех сигм”.

что-то полезное однозначно вынес для себя, так можно оказывается, не знал не знал…

хм… а зачем тут do {} while (false) ?
Я бы написал просто

#define printVar(x) Serial.print(#x);Serial.print('=');Serial.println(x)

в лоб написать и я бы смог, а вот используя цикл, главное жеж здесь - идея!

Фига се! На старом форуме этот макрос приводился раза три по разным поводам. Думал его уже все знают.

макросы они как женщина, должно быть желание для исследования, не было )))

Когда Вы пишете макрос в виде функции, он должен позволять использовать его как функцию без ограничений.

Если написать так, как Вы предлагаете, его нельзя будет использовать, например так:

if (a > 1) printVar(kaka);

И, кстати, обратите внимание, что у меня в конце нет точки запятой. Если её туда добавить, то нельзя будет использовать вот так:

if (a > 1) printVar(kaka); else printVar(mumu);

А так, как у меня написано, можно использовать везде, как вызов функции и не париться.

P.S.
Кстати, чуть не забыл. Второй из приведённых примеров не сработает и при таком определении макроса:

#define printVar(x) { Serial.print(#x);Serial.print('=');Serial.println(x); }

Кстати, тоже тема для вики - “Макросы это очень просто”

Не думаю.

С появлением inline и constexpr осталось крайне мало мест, где макросы уместны. Вот в этом примере - да, согласен, к месту. Но в большинстве случаев использования макросов, лучше их не пользовать, а использовать современные вещи.

Макросы-то ведь тоже … там всё очень не просто. Не зря ведь было сказано, о перспективных «творческих применениях оператора #define».

Я вот тут как-то на соседнем форуме выкладывал кусочек кода. Попробуйте разобраться, что (а, главное, как) он делает. Можете запустить, он вполне рабочий, хоть в протеусе, хоть на макетке.

Дак, а всё не просто. Но, зачастую, их опасность сильно преувеличена. Как и многого другого. Тех же глобальных переменных, например, goto, и пр. Считаю, если аккуратненько, то можно всё. Лишь бы было заради чего).

это чем тебя глобальные переменные не устраивают, если память позволяет?

Так уже неоднократно озвучивалось: неиндицируемым конфликтом имен, что ведет к труднообнаруживаемым ошибкам.

Программ без ошибок не бывает. (это даже если не вспоминать “человеку свойственно ошибаться”)
Следовательно программы надо писать так, чтобы ошибки мог обнаружить компилятор, а не приходилось самому ползать по коду в несколько тысяч строк в тщетных попытках выяснить, куда же закралась эта чертова ошибка.