Mercurial > ~darius > hgwebdir.cgi > modulator
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; +} +