diff q/t.c @ 14:388074ff9474

Add fixed point code
author Daniel O'Connor <darius@dons.net.au>
date Tue, 25 Feb 2025 13:28:29 +1030
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/q/t.c	Tue Feb 25 13:28:29 2025 +1030
@@ -0,0 +1,581 @@
+/**@file t.c
+ * @brief Q-Number (Q16.16, signed) library test bench
+ * @copyright Richard James Howe (2018)
+ * @license MIT
+ * @email howe.r.j.89@gmail.com
+ * @site <https://github.com/howerj/q>
+ *
+ * This file contains a wrapper for testing the Q Number library,
+ * it includes a command processor and some built in tests. View
+ * the help string later on for more information */
+#include "q.h"
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define N    (16)
+#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))
+
+static int qprint(FILE *out, const q_t p) {
+	assert(out);
+	char buf[64 + 1] = { 0, };
+	const int r = qsprint(p, buf, sizeof buf);
+	return r < 0 ? r : fprintf(out, "%s", buf);
+}
+
+static int printq(FILE *out, q_t q, const char *msg) {
+	assert(out);
+	assert(msg);
+	if (fprintf(out, "%s = ", msg) < 0)
+		return -1;
+	if (qprint(out, q) < 0)
+		return -1;
+	if (fputc('\n', out) != '\n')
+		return -1;
+	return 0;
+}
+
+static int print_sincos(FILE *out, const q_t theta) {
+	assert(out);
+	q_t sine  = qinfo.zero, cosine = qinfo.zero;
+	qsincos(theta, &sine, &cosine);
+	if (qprint(out, theta) < 0)
+		return -1;
+	if (fputc(',', out) != ',')
+		return -1;
+	if (qprint(out, sine) < 0)
+		return -1;
+	if (fputc(',', out) != ',')
+		return -1;
+	if (qprint(out, cosine) < 0)
+		return -1;
+	if (fputc('\n', out) != '\n')
+		return -1;
+	return 0;
+}
+
+static int print_sincos_table(FILE *out) {
+	assert(out);
+	const q_t tpi   = qdiv(qinfo.pi, qint(20));
+	const q_t end   = qmul(qinfo.pi, qint(2));
+	const q_t start = qnegate(end);
+	if (fprintf(out, "theta,sine,cosine\n") < 0)
+		return -1;
+	for (q_t i = start; qless(i, end); i = qadd(i, tpi))
+		if (print_sincos(out, i) < 0)
+			return -1;
+	return 0;
+}
+
+static int qinfo_print(FILE *out, const qinfo_t *qi) {
+	assert(out);
+	assert(qi);
+	if (fprintf(out, "Q%u.%u Info\n", (unsigned)qi->whole, (unsigned)qi->fractional) < 0) return -1;
+	if (printq(out, qi->bit,   "bit") < 0) return -1;
+	if (printq(out, qi->one,   "one") < 0) return -1;
+	if (printq(out, qi->zero,  "zero") < 0) return -1;
+	if (printq(out, qi->pi,    "pi") < 0) return -1;
+	if (printq(out, qi->e,     "e") < 0) return -1;
+	if (printq(out, qi->sqrt2, "sqrt2") < 0) return -1;
+	if (printq(out, qi->sqrt3, "sqrt3") < 0) return -1;
+	if (printq(out, qi->ln2,   "ln2") < 0) return -1;
+	if (printq(out, qi->ln10,  "ln10") < 0) return -1;
+	if (printq(out, qi->min,   "min") < 0) return -1;
+	if (printq(out, qi->max,   "max") < 0) return -1;
+	if (printq(out, qcordic_circular_gain(-1),   "circular-gain") < 0) return -1;
+	if (printq(out, qcordic_hyperbolic_gain(-1), "hyperbolic-gain") < 0) return -1;
+	return 0;
+}
+
+static int qconf_print(FILE *out, const qconf_t *qc) {
+	assert(out);
+	assert(qc);
+	if (fprintf(out, "Q Configuration\n") < 0) return -1;
+	const char *bounds = "unknown";
+	qbounds_t bound = qc->bound;
+	if (bound == qbound_saturate)
+		bounds = "saturate";
+	if (bound == qbound_wrap)
+		bounds = "wrap";
+	if (fprintf(out, "overflow handler: %s\n", bounds) < 0) return -1;
+	if (fprintf(out, "input/output radix: %u (0 = special case)\n", qc->base) < 0) return -1;
+	if (fprintf(out, "decimal places: %d\n", qc->dp) < 0) return -1;
+	return 0;
+}
+
+static FILE *fopen_or_die(const char *file, const char *mode) {
+	assert(file && mode);
+	FILE *h = NULL;
+	errno = 0;
+	if (!(h = fopen(file, mode))) {
+		(void)fprintf(stderr, "file open \"%s\" (mode %s) failed: %s\n", file, mode, strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+	return h;
+}
+
+typedef enum {
+	EVAL_OK_E,
+	EVAL_COMMENT_E,
+	EVAL_ERROR_SCAN_E,
+	EVAL_ERROR_TYPE_E,
+	EVAL_ERROR_CONVERT_E,
+	EVAL_ERROR_OPERATION_E,
+	EVAL_ERROR_ARG_COUNT_E,
+	EVAL_ERROR_UNEXPECTED_RESULT_E,
+	EVAL_ERROR_LIMIT_MODE_E,
+
+	EVAL_ERROR_MAX_ERRORS_E, /**< not an error, but a count of errors */
+} eval_errors_e;
+
+static const char *eval_error(int e) {
+	if (e < 0 || e >= EVAL_ERROR_MAX_ERRORS_E)
+		return "unknown";
+	const char *msgs[EVAL_ERROR_MAX_ERRORS_E] = {
+		[EVAL_OK_E]                      = "ok",
+		[EVAL_COMMENT_E]                 = "(comment)",
+		[EVAL_ERROR_SCAN_E]              = "invalid input line",
+		[EVAL_ERROR_TYPE_E]              = "unknown function type",
+		[EVAL_ERROR_CONVERT_E]           = "numeric conversion failed",
+		[EVAL_ERROR_OPERATION_E]         = "unknown operation",
+		[EVAL_ERROR_ARG_COUNT_E]         = "incorrect argument count",
+		[EVAL_ERROR_LIMIT_MODE_E]        = "unknown limit mode ('|' or '%' allowed)",
+		[EVAL_ERROR_UNEXPECTED_RESULT_E] = "unexpected result",
+	};
+	return msgs[e] ? msgs[e] : "unknown";
+}
+
+static int eval_unary_arith(q_t (*m)(q_t), q_t expected, q_t bound, q_t arg1, q_t *result) {
+	assert(m);
+	assert(result);
+	const q_t r = m(arg1);
+	*result = r;
+	if (qwithin_interval(r, expected, bound))
+		return 0;
+	return -1;
+}
+
+static int eval_binary_arith(q_t (*f)(q_t, q_t), q_t expected, q_t bound, q_t arg1, q_t arg2, q_t *result) {
+	assert(f);
+	assert(result);
+	const q_t r = f(arg1, arg2);
+	*result = r;
+	if (qwithin_interval(r, expected, bound))
+		return 0;
+	return -1;
+}
+
+static int comment(char *line) {
+	assert(line);
+	const size_t length = strlen(line);
+	for (size_t i = 0; i < length; i++) {
+		if (line[i] == '#')
+			return 1;
+		if (!isspace(line[i]))
+			break;
+	}
+	for (size_t i = 0; i < length; i++) {
+		if (line[i] == '#') {
+			line[i] = 0;
+			return 0;
+		}
+	}
+	return 0;
+}
+
+static void bounds_set(char method) {
+	assert(method == '|' || method == '%');
+	if (method == '|')
+		qconf.bound = qbound_saturate;
+	qconf.bound = qbound_wrap;
+}
+
+static int eval(char *line, q_t *result) {
+	assert(line);
+	assert(result);
+	*result = qinfo.zero;
+	if (comment(line))
+		return EVAL_COMMENT_E;
+	char operation[N] = { 0 }, expected[N] = { 0 }, bounds[N], arg1[N] = { 0 }, arg2[N] = { 0 };
+	char limit = '|';
+	const int count = sscanf(line, "%15s %15s +- %15s %c %15s %15s", operation, expected, bounds, &limit, arg1, arg2);
+	if (limit != '|' && limit != '%')
+		return -EVAL_ERROR_LIMIT_MODE_E;
+	bounds_set(limit);
+	if (count < 5)
+		return -EVAL_ERROR_SCAN_E;
+	const qoperations_t *func = qop(operation);
+	if (!func)
+		return -EVAL_ERROR_OPERATION_E;
+	q_t e = qinfo.zero, b = qinfo.zero, a1 = qinfo.zero, a2 = qinfo.zero;
+	const int argc = count - 4;
+	if (func->arity != argc)
+		return -EVAL_ERROR_ARG_COUNT_E;
+	if (qconv(&e, expected) < 0)
+		return -EVAL_ERROR_CONVERT_E;
+	if (qconv(&b, bounds) < 0)
+		return -EVAL_ERROR_CONVERT_E;
+	if (qconv(&a1, arg1) < 0)
+		return -EVAL_ERROR_CONVERT_E;
+	switch (func->arity) {
+	case 1: if (eval_unary_arith(func->eval.unary, e, b, a1, result) < 0)
+			return -EVAL_ERROR_UNEXPECTED_RESULT_E;
+		break;
+	case 2: if (qconv(&a2, arg2) < 0)
+			return -EVAL_ERROR_CONVERT_E;
+		if (eval_binary_arith(func->eval.binary, e, b, a1, a2, result) < 0)
+			return -EVAL_ERROR_UNEXPECTED_RESULT_E;
+		break;
+	default:
+		return -EVAL_ERROR_TYPE_E;
+	}
+	return EVAL_OK_E;
+}
+
+static void trim(char *s) {
+	assert(s);
+	const int size = strlen(s);
+	for (int i = size - 1; i >= 0; i--) {
+		if (!isspace(s[i]))
+			break;
+		s[i] = 0;
+	}
+}
+
+static int eval_file(FILE *input, FILE *output) {
+	assert(input);
+	assert(output);
+	char line[256] = { 0 };
+	int rv = 0;
+	while (fgets(line, sizeof(line) - 1, input)) {
+		q_t result = 0;
+		const int r = eval(line, &result);
+		if (r == EVAL_COMMENT_E)
+			continue;
+		if (r < 0) {
+			const char *msg = eval_error(-r);
+			trim(line);
+			if (fprintf(output, "error: %s = ", line) < 0) return -1;
+			if (qprint(output, result) < 0) return -1;
+			if (fprintf(output, " : %s/%d\n", msg, r) < 0) return -1;
+			rv = -1;
+			continue;
+		}
+		trim(line);
+		char rstring[64 + 1] = { 0, };
+		if (qsprint(result, rstring, sizeof(rstring) - 1) < 0) return -1;
+		if (fprintf(output, "ok: %s | (%s)\n", line, rstring) < 0) return -1;
+		memset(line, 0, sizeof line);
+	}
+	return rv;
+}
+
+/* --- Unit Test Framework --- */
+
+typedef struct {
+	unsigned passed, 
+		 run;
+} unit_test_t;
+
+static inline unit_test_t _unit_test_start(const char *file, const char *func, unsigned line) {
+	assert(file);
+	assert(func);
+	unit_test_t t = { .run = 0, .passed = 0 };
+	fprintf(stdout, "Start tests: %s in %s:%u\n\n", func, file, line);
+	return t;
+}
+
+static inline void _unit_test_statement(const char *expr_str) {
+	assert(expr_str);
+	if (fprintf(stdout, "   STATE: %s\n", expr_str) < 0)
+		abort();
+}
+
+static inline void _unit_test(unit_test_t *t, int failed, const char *expr_str, const char *file, const char *func, unsigned line, int die) {
+	assert(t);
+	assert(expr_str);
+	assert(file);
+	assert(func);
+	if (failed) {
+		fprintf(stdout, "  FAILED: %s (%s:%s:%u)\n", expr_str, file, func, line);
+		if (die) {
+			fputs("VERIFY FAILED - EXITING\n", stdout);
+			exit(EXIT_FAILURE);
+		}
+	} else {
+		fprintf(stdout, "      OK: %s\n", expr_str);
+		t->passed++;
+	}
+	t->run++;
+}
+
+static inline int unit_test_finish(unit_test_t *t) {
+	assert(t);
+	fprintf(stdout, "Tests passed/total: %u/%u\n", t->passed, t->run);
+	if (t->run != t->passed) {
+		(void)fputs("[FAILED]\n", stdout);
+		return -1;
+	}
+	if (fputs("[SUCCESS]\n", stdout) < 0)
+		return -1;
+	return 0;
+}
+
+#define unit_test_statement(T, EXPR) do { (void)(T); EXPR; _unit_test_statement(( #EXPR)); } while (0)
+#define unit_test_start()         _unit_test_start(__FILE__, __func__, __LINE__)
+#define unit_test(T, EXPR)        _unit_test((T), 0 == (EXPR), (# EXPR), __FILE__, __func__, __LINE__, 0)
+#define unit_test_verify(T, EXPR) _unit_test((T), 0 == (EXPR), (# EXPR), __FILE__, __func__, __LINE__, 1)
+
+static int test_sanity(void) {
+	unit_test_t t = unit_test_start();
+	q_t t1 = 0, t2 = 0;
+	unit_test_statement(&t, t1 = qint(1));
+	unit_test_statement(&t, t2 = qint(2));
+	unit_test(&t, qtoi(qadd(t1, t2)) == 3);
+	return unit_test_finish(&t);
+}
+
+static int test_pack(void) {
+	unit_test_t t = unit_test_start();
+	q_t p1 = 0, p2 = 0;
+	char buf[sizeof(p1)] = { 0 };
+	unit_test_statement(&t, p1 = qnegate(qinfo.pi));
+	unit_test_statement(&t, p2 = 0);
+	unit_test(&t, qunequal(p1, p2));
+	unit_test(&t,  qpack(&p1, buf, sizeof p1 - 1) < 0);
+	unit_test(&t,  qpack(&p1, buf, sizeof buf) == sizeof(p1));
+	unit_test(&t, qunpack(&p2, buf, sizeof buf) == sizeof(p2));
+	unit_test(&t, qequal(p1, p2));
+	return unit_test_finish(&t);
+}
+
+static int test_fma(void) {
+	unit_test_t t = unit_test_start();
+
+	q_t a, b, c, r;
+	const q_t one_and_a_half = qdiv(qint(3), qint(2));
+
+	/* incorrect, but expected, result due to saturation */
+	unit_test_statement(&t, a = qinfo.max);
+	unit_test_statement(&t, b = one_and_a_half);
+	unit_test_statement(&t, c = qinfo.min);
+	unit_test_statement(&t, r = qadd(qmul(a, b), c));
+	unit_test(&t, qwithin_interval(r, qint(0), qint(1)));
+
+	/* correct result using Fused Multiply Add */
+	unit_test_statement(&t, a = qinfo.max);
+	unit_test_statement(&t, b = one_and_a_half);
+	unit_test_statement(&t, c = qinfo.min);
+	unit_test_statement(&t, r = qfma(a, b, c));
+	unit_test(&t, qwithin_interval(r, qdiv(qinfo.max, qint(2)), qint(1)));
+
+	return unit_test_finish(&t);
+}
+
+static inline int test_filter(void) {
+	unit_test_t t = unit_test_start();
+	qfilter_t lpf = { .raw = 0 }, hpf = { .raw = 0 };
+	const q_t beta = qdiv(qint(1), qint(3));
+	qfilter_init(&lpf, qint(0), beta, qint(0));
+	qfilter_init(&hpf, qint(0), beta, qint(0));
+	for (int i = 0; i < 100; i++) {
+		char low[64 + 1] = { 0, }, high[64 + 1] = { 0, };
+		const q_t step = qdiv(qint(i), qint(100));
+		const q_t input = qint(1);
+		qfilter_low_pass(&lpf, step, input);
+		qfilter_high_pass(&hpf, step, input);
+		if (qsprint(qfilter_value(&lpf),  low, sizeof(low)  - 1ull) < 0) return -1;
+		if (qsprint(qfilter_value(&hpf), high, sizeof(high) - 1ull) < 0) return -1;
+		if (fprintf(stdout, "%2d: %s\t%s\n", i, low, high) < 0) return -1;
+	}
+	return unit_test_finish(&t);
+}
+
+static int qmatrix_print(FILE *out, const q_t *m) {
+	assert(out);
+	assert(m);
+	const size_t alloc = qmatrix_string_length(m);
+	char *ms = malloc(alloc + 1);
+	if (!ms)
+		return -1;
+	int r = qmatrix_sprintb(m, ms, alloc + 1, 10);
+	if (r >= 0) 
+		r = fprintf(out, "%s\n", ms);
+	free(ms);
+	return r;
+}
+
+#define QMATRIX(ROW, COLUMN, ...) { 0, ROW * COLUMN, ROW, COLUMN, __VA_ARGS__  }
+#define QMATRIXZ(ROW, COLUMN)     QMATRIX((ROW), (COLUMN), 0)
+#define QMATRIXSZ(ROW, COLUMN)    ((((ROW)*(COLUMN)) + 4)*sizeof(q_t))
+
+static int test_matrix(void) {
+	unit_test_t t = unit_test_start();
+	FILE *out = stdout;
+	q_t a[] = QMATRIX(2, 3, 
+		QINT(1), QINT(2), QINT(3), 
+		QINT(4), QINT(5), QINT(6),
+	);
+	q_t b[] = QMATRIX(3, 2, 
+		QINT(2), QINT(3), 
+		QINT(4), QINT(5), 
+		QINT(6), QINT(7),
+	);
+	const q_t abr[] = QMATRIX(2, 2,
+		QINT(28), QINT(34),
+		QINT(64), QINT(79),
+	);
+	const q_t abrp[] = QMATRIX(2, 2,
+		QINT(28), QINT(64),
+		QINT(34), QINT(79),
+	);
+	q_t ab[QMATRIXSZ(2, 2)]   = QMATRIXZ(2, 2);
+	q_t abp[QMATRIXSZ(2, 2)]  = QMATRIXZ(2, 2);
+	unit_test_verify(&t, 0 == qmatrix_mul(ab, a, b));
+	unit_test_verify(&t, 0 == qmatrix_transpose(abp, ab));
+	unit_test(&t, qmatrix_equal(ab, abr));
+	unit_test(&t, qmatrix_equal(ab, abrp));
+	qmatrix_print(out, a);
+	qmatrix_print(out, b);
+	qmatrix_print(out, ab);
+	qmatrix_print(out, abp);
+	return unit_test_finish(&t);
+}
+
+static int test_matrix_trace(void) {
+	unit_test_t t = unit_test_start();
+	q_t a[] = QMATRIX(2, 2,
+		QINT(1), QINT(2), 
+		QINT(4), QINT(5), 
+	);
+	q_t b[] = QMATRIX(2, 2,
+		QINT(2), QINT(3), 
+		QINT(4), QINT(5), 
+	);
+	q_t ta[sizeof(a) / sizeof(a[0])] = QMATRIX(2, 2, 0);
+	q_t tb[sizeof(b) / sizeof(b[0])] = QMATRIX(2, 2, 0);
+	q_t apb[sizeof(a) / sizeof(a[0])] = QMATRIX(2, 2, 0);
+	BUILD_BUG_ON(sizeof a != sizeof ta);
+	BUILD_BUG_ON(sizeof a != sizeof tb);
+	BUILD_BUG_ON(sizeof a != sizeof b);
+	BUILD_BUG_ON(sizeof a != sizeof apb);
+	unit_test_verify(&t, 0 == qmatrix_transpose(ta, a));
+	unit_test_verify(&t, 0 == qmatrix_transpose(tb, b));
+	unit_test_verify(&t, 0 == qmatrix_add(apb, a, b));
+	unit_test(&t, qequal(qmatrix_trace(a), QINT(6)));
+	unit_test(&t, qequal(qmatrix_trace(b), QINT(7)));
+	unit_test(&t, qequal(qmatrix_trace(a), qmatrix_trace(ta)));
+	unit_test(&t, qequal(qmatrix_trace(apb), qadd(qmatrix_trace(a), qmatrix_trace(b))));
+	printq(stdout, qmatrix_determinant(a), "det(a)");
+	return unit_test_finish(&t);
+}
+
+static q_t qid(q_t x) { return x; }
+static q_t qsq(q_t x) { return qmul(x, x); }
+
+static int test_simpson(void) {
+	unit_test_t t = unit_test_start();
+	unit_test(&t, qwithin_interval(qsimpson(qid, QINT(0), QINT(10), 100), QINT(50), QINT(1))); // (x^2)/2 + C
+	unit_test(&t, qwithin_interval(qsimpson(qsq, qnegate(QINT(2)), QINT(5), 100), QINT(44), QINT(1))); // (x^3)/3 + C
+	return unit_test_finish(&t);
+}
+
+static int internal_tests(void) {
+	typedef int (*unit_test_t)(void);
+	unit_test_t tests[] = {
+		test_sanity,
+		test_pack,
+		test_fma,
+		// test_filter,
+		test_matrix,
+		test_matrix_trace,
+		test_simpson,
+		NULL
+	};
+	for (size_t i = 0; tests[i]; i++)
+		if (tests[i]() < 0)
+			return -1;
+	return 0;
+}
+
+static int help(FILE *out, const char *arg0) {
+	assert(out);
+	assert(arg0);
+	const char *h = "\n\
+Program: Q-Number (Q16.16, signed) library testbench\n\
+Author:  Richard James Howe (2018)\n\
+License: MIT\n\
+E-mail:  howe.r.j.89@gmail.com\n\
+Site:    https://github.com/howerj/q\n\n\
+Options:\n\
+\t-s\tprint a sine-cosine table\n\
+\t-h\tprint this help message and exit\n\
+\t-i\tprint library information\n\
+\t-t\trun internal unit tests\n\
+\t-v\tprint version information and exit\n\
+\tfile\texecute commands in file\n\n\
+This program wraps up a Q-Number library and allows it to be tested by\n\
+giving it commands, either from stdin, or from a file. The format is\n\
+strict and the parser primitive, but it is only meant to be used to\n\
+test that the library is doing the correct thing and not as a\n\
+calculator.\n\n\
+Commands evaluated consist of an operator with the require arguments\n\
+(either one or two arguments) and compare the result with an expected\n\
+value, which can within a specified bounds for the test to pass. If\n\
+the test fails the program exits, indicating failure. The format is:\n\
+\n\
+\toperator expected +- allowance | arg1 arg2\n\n\
+operators include '+', '-', '/', 'rem', '<', which require two\n\
+arguments, and unary operators like 'sin', and 'negate', which require\n\
+only one. 'expected', 'allowance', 'arg1' and 'arg2' are all fixed\n\
+numbers of the form '-12.45'. 'expected' is the expected result,\n\
+'allowance' the +/- amount the result is allowed to deviated by, and\n\
+'arg1' and 'arg2' the operator arguments.\n\
+\n\n";
+	if (fprintf(out, "usage: %s -h -s -i -v -t -c file\n", arg0) < 0) return -1;
+	if (fputs(h, out) < 0) return -1;
+	return 0;
+}
+
+int main(int argc, char **argv) {
+	bool ran = false;
+	for (int i = 1; i < argc; i++) {
+		if (!strcmp("-h", argv[i])) {
+			if (help(stdout, argv[0]) < 0)
+				return 1;
+			return 0;
+		} else if (!strcmp("-s", argv[i])) {
+			print_sincos_table(stdout);
+			ran = true;
+		} else if (!strcmp("-v", argv[i])) {
+			fprintf(stdout, "version 1.0\n");
+			return 0;
+		} else if (!strcmp("-t", argv[i])) {
+			if (internal_tests() < 0)
+				return 1;
+			ran = true;
+		} else if (!strcmp("-i", argv[i])) {
+			if (qinfo_print(stdout, &qinfo) < 0)
+				return 1;
+			if (qconf_print(stdout, &qconf) < 0)
+				return 1;
+			ran = true;
+		} else {
+			FILE *input = fopen_or_die(argv[i], "rb");
+			FILE *output = stdout;
+			const int r = eval_file(input, output);
+			ran = true;
+			fclose(input);
+			if (r < 0)
+				return 1;
+		}
+	}
+	if (!ran)
+		return eval_file(stdin, stdout);
+	return 0;
+}
+