diff --git a/.github/workflows/ci_meson.yml b/.github/workflows/ci_meson.yml index a58a3ce..d1e454b 100644 --- a/.github/workflows/ci_meson.yml +++ b/.github/workflows/ci_meson.yml @@ -3,45 +3,24 @@ on: push: branches: - main - paths: - - '**.c' - - '**.h' - - 'meson.build' - - 'meson_options.txt' - pull_request: - branches: - - main - paths: - - '**.c' - - '**.h' - - 'meson.build' - - 'meson_options.txt' - jobs: build: - name: Build and Test on ${{ matrix.os }} with Meson v${{ matrix.meson_version }} + name: Build and Test on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - # meson_version: ["1.2.0", "1.3.0", "1.4.0"] meson_version: ["1.4.0"] steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.x' - - name: Install dependencies - run: python -m pip install meson==${{ matrix.meson_version }} ninja - - name: Configure Project - run: meson setup builddir/ - - name: Run Tests - run: meson test -C builddir/ -v - - name: Upload Test Log - uses: actions/upload-artifact@v4 + - run: python -m pip install meson==${{ matrix.meson_version }} ninja + - run: meson setup builddir/ + - run: meson test -C builddir/ -v + - uses: actions/upload-artifact@v4 if: failure() with: name: ${{ matrix.os }}_Meson_Testlog diff --git a/README.md b/README.md index 8ccebe2..6796f5e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Epsilon -Epsilon is a library with small functions for machine learning and statistics -written in plain C. The functions are decoupled and well tested. + +Epsilon is a C library of small, decoupled, and well-tested functions for machine learning and statistics. It is designed for microcontrollers and resource-constrained environments. [![CI Meson](https://github.com/breuderink/epsilon/actions/workflows/ci_meson.yml/badge.svg)](https://github.com/breuderink/epsilon/actions/workflows/ci_meson.yml) @@ -31,40 +31,50 @@ To allow machine learning to run on microcontrollers, the implementations: - work with fixed-point math when realistic, and - are easy to tune. -# Building -Epsilon uses Meson for building. Install Meson and Ninja, create a build directory, and configure the project. In the repository root, configure and build the project, then run the unit tests and examples: +# Folder Structure + +``` +/ # Project root +├── LICENSE +├── README.md +├── meson.build +├── docs/ # Documentation and references +├── src/ # Source code (.c/.h) +├── tests/ # Unit tests +├── examples/ # Example programs +├── subprojects/ # External dependencies (e.g., Unity) +``` + +# Building & Testing +Epsilon uses Meson for building. In the repository root: + ```bash -$ meson setup builddir -$ meson test -C builddir +meson setup builddir +meson compile -C builddir +meson test -C builddir ``` + # Algorithms + ## Pseudo-random number generation -- [Xorshift](docs/marsaglia2003xrn.pdf) is a fast and simple -pseudo-random number generator by George Marsaglia that has good statistical -properties. See the [xorshift example](examples/example_rng.c). +- [Xorshift](docs/marsaglia2003xrn.pdf): Fast, simple PRNG with good statistical properties. See `examples/example_rng.c`. ## Hashing -- The [FNV hash](https://tools.ietf.org/html/draft-eastlake-fnv-17) is a fast -hash function that maps variable length input to a fixed output -([example](examples/example_hash.c)). It can be used for [feature -hashing](https://en.wikipedia.org/wiki/Feature_hashing). +- [FNV hash](https://tools.ietf.org/html/draft-eastlake-fnv-17): Fast hash for feature hashing. See `examples/example_hash.c`. ## Statistics -- Welford's method computes mean and variance in a single pass. See the -[example of Welford's method](examples/example_stats.c). +- Welford's method: Online mean and variance in one pass. See `examples/example_stats.c`. ## Transformations -- Fast Walsh-Hadamard transform (FWHT) implements the Walsh-Hadamard -transform in O(n log n) time. FWHT is similar to the fast Fourier transform -and the Haar transform. See the [FWHT example](examples/example_transform.c). +- Fast Walsh-Hadamard Transform (FWHT): O(n log n) transform, similar to FFT. See `examples/example_transform.c`. ## Passive-aggressive learning -- [Online passive-aggressive (PA)](docs/crammer2006opa.pdf) regression solves a -regression problem by only updating the model on prediction mistakes. +- [Online passive-aggressive (PA)](docs/crammer2006opa.pdf): Regression with updates only on mistakes. + # Other solutions for Tiny ML or Edge AI - [TensorFlow Lite](https://www.tensorflow.org/lite/) diff --git a/epsilon/hash.h b/epsilon/hash.h deleted file mode 100644 index 7da4afa..0000000 --- a/epsilon/hash.h +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include - -// Fowler-Noll-Vo 1a hash [1]. -uint32_t FNV1a32_update(uint32_t hash, uint8_t data); -uint32_t FNV1a32_hash(const void *data, size_t n); - -/* -# References - -[1] Fowler, Glenn, et al. “The FNV Non-Cryptographic Hash Algorithm.” IETF - Tools, Network Working Group, tools.ietf.org/html/draft-eastlake-fnv-03. -*/ diff --git a/epsilon/pa.h b/epsilon/pa.h deleted file mode 100644 index aad1a99..0000000 --- a/epsilon/pa.h +++ /dev/null @@ -1,14 +0,0 @@ - -// Passive-aggressive parameters, see [1]. -typedef struct { - float C; // Perform aggressive updates with high C. - float eps; // Size of insensitive band for regression. -} PA_t; - -/* -# References - - -[1] Crammer, K. et al. “Online Passive-Aggressive Algorithms.” J. Mach. Learn. - Res. (2003). -*/ \ No newline at end of file diff --git a/epsilon/rng.h b/epsilon/rng.h deleted file mode 100644 index 58c1821..0000000 --- a/epsilon/rng.h +++ /dev/null @@ -1,12 +0,0 @@ -#include - -// Marsaglia's xorshift random number generator [1]; -uint32_t xorshift32(uint32_t y); - -/* -# References - -[1] Marsaglia, George. "Xorshift RNGs." Journal of Statistical Software 8.14 - (2003): 1-6. - -*/ diff --git a/epsilon/stats.h b/epsilon/stats.h deleted file mode 100644 index eec54a8..0000000 --- a/epsilon/stats.h +++ /dev/null @@ -1,29 +0,0 @@ -#include - -// Structure for computing the variance in one pass using Welford's method [1]. -typedef struct { - float mean, squared_diff; -} Welfords_method_t; - -typedef struct { - size_t n; - Welfords_method_t Welford; -} online_stats_t; - -// Update online statistics with observation. -void observe(online_stats_t *s, float x); - -// Current mean from online statistics. -float mean(const online_stats_t *s); - -// Current (biased) population variance. -float pvariance(const online_stats_t *s); - -// Current sample variance. -float variance(const online_stats_t *s); - -/* -# References -[1] Welford, B. P. "Note on a method for calculating corrected sums of - squares and products." Technometrics 4.3 (1962): 419-420. -*/ diff --git a/epsilon/transform.h b/epsilon/transform.h deleted file mode 100644 index 98b41d8..0000000 --- a/epsilon/transform.h +++ /dev/null @@ -1,4 +0,0 @@ -#include - -// Perform in-place Fast Walsh-Hadamard transform. -void FWHT(float *x, uint8_t nbits); diff --git a/examples/example_hash.c b/examples/example_hash.c index ec05019..2755cae 100644 --- a/examples/example_hash.c +++ b/examples/example_hash.c @@ -1,4 +1,4 @@ -#include "epsilon/hash.h" +#include "hash.h" #include #include #include diff --git a/examples/example_rng.c b/examples/example_rng.c index 7254368..f1228a9 100644 --- a/examples/example_rng.c +++ b/examples/example_rng.c @@ -1,4 +1,4 @@ -#include "epsilon/rng.h" +#include "rng.h" #include #include diff --git a/examples/example_stats.c b/examples/example_stats.c index c63fd9b..819ed94 100644 --- a/examples/example_stats.c +++ b/examples/example_stats.c @@ -1,4 +1,4 @@ -#include "epsilon/stats.h" +#include "stats.h" #include int main(void) { diff --git a/examples/example_transform.c b/examples/example_transform.c index 8fe77b5..08afeb7 100644 --- a/examples/example_transform.c +++ b/examples/example_transform.c @@ -1,4 +1,4 @@ -#include "epsilon/transform.h" +#include "transform.h" #include #define LOG2_DIMS 3 diff --git a/meson.build b/meson.build index 938c80b..5406d93 100644 --- a/meson.build +++ b/meson.build @@ -3,19 +3,30 @@ project('epsilon', 'c', default_options: ['c_std=c99', 'warning_level=3', 'werror=true'] ) +# Dependencies cc = meson.get_compiler('c') math_dep = cc.find_library('m', required: false) unity_dep = dependency('unity', required: true) -# Library sources -epsilon_sources = [ - 'epsilon/rng.c', - 'epsilon/stats.c', - 'epsilon/hash.c', - 'epsilon/transform.c' -] +# Library configuration -# Build the library +epsilon_sources = files( + 'src/rng.c', + 'src/stats.c', + 'src/hash.c', + 'src/transform.c' +) + + +epsilon_headers = files( + 'src/rng.h', + 'src/stats.h', + 'src/hash.h', + 'src/transform.h', + 'src/pa.h' +) + +# Build library epsilon_lib = library('epsilon', sources: epsilon_sources, dependencies: math_dep, @@ -23,51 +34,56 @@ epsilon_lib = library('epsilon', version: meson.project_version() ) -# Dependency object for downstream use +# Declare dependency for downstream use + epsilon_dep = declare_dependency( link_with: epsilon_lib, + include_directories: include_directories('src'), dependencies: math_dep ) # Install headers -epsilon_headers = [ - 'epsilon/rng.h', - 'epsilon/stats.h', - 'epsilon/hash.h', - 'epsilon/transform.h', - 'epsilon/pa.h' -] + install_headers(epsilon_headers, subdir: 'epsilon') -# Examples -example_exes = [ - ['example_hash', 'examples/example_hash.c'], - ['example_transform', 'examples/example_transform.c'], - ['example_stats', 'examples/example_stats.c'], - ['example_rng', 'examples/example_rng.c'] -] - -foreach pair : example_exes - exe = executable(pair[0], pair[1], dependencies: epsilon_dep, install: false) - test(pair[0], exe) +# Examples and tests configuration +examples = { + 'example_hash': 'examples/example_hash.c', + 'example_transform': 'examples/example_transform.c', + 'example_stats': 'examples/example_stats.c', + 'example_rng': 'examples/example_rng.c' +} + +tests = { + 'hash_test': 'tests/hash_test.c', + 'rng_test': 'tests/rng_test.c', + 'stats_test': 'tests/stats_test.c', + 'transform_test': 'tests/transform_test.c', +} + +# Build and register examples +foreach name, source : examples + exe = executable(name, source, + dependencies: epsilon_dep, + install: false + ) + test(name, exe) endforeach -# Tests -test_exes = [ - ['hash_test', 'tests/hash_test.c'], - ['rng_test', 'tests/rng_test.c'], - ['stats_test', 'tests/stats_test.c'], - ['transform_test', 'tests/transform_test.c'] -] - -foreach pair : test_exes - exe = executable(pair[0], pair[1], dependencies: [epsilon_dep, unity_dep], install: false) - test(pair[0], exe) +# Build and register tests +foreach name, source : tests + exe = executable(name, source, + dependencies: [epsilon_dep, unity_dep], + install: false + ) + test(name, exe) endforeach -# Documentation -install_data('README.md', install_dir: 'share/doc/epsilon') -install_data('LICENSE', install_dir: 'share/licenses/epsilon') +# Documentation installation +install_data( + files('README.md', 'LICENSE'), + install_dir: 'share/doc/epsilon' +) -# Optional metadata +# Distribution metadata meson.add_dist_script('echo', 'Package contact: boris@cortext.nl') diff --git a/epsilon/hash.c b/src/hash.c similarity index 91% rename from epsilon/hash.c rename to src/hash.c index e4c7923..03542ab 100644 --- a/epsilon/hash.c +++ b/src/hash.c @@ -1,3 +1,5 @@ + +// FNV-1a 32-bit hash implementation. #include #include diff --git a/src/hash.h b/src/hash.h new file mode 100644 index 0000000..e2af8c8 --- /dev/null +++ b/src/hash.h @@ -0,0 +1,12 @@ + +#ifndef EPSILON_HASH_H +#define EPSILON_HASH_H + +// FNV-1a 32-bit hash interface +#include +#include + +uint32_t FNV1a32_update(uint32_t hash, uint8_t data); +uint32_t FNV1a32_hash(const void *data, size_t n); + +#endif // EPSILON_HASH_H diff --git a/src/pa.h b/src/pa.h new file mode 100644 index 0000000..9d06bed --- /dev/null +++ b/src/pa.h @@ -0,0 +1,6 @@ + +// Passive-aggressive regression parameters. +typedef struct { + float C; + float eps; +} PA_t; diff --git a/epsilon/rng.c b/src/rng.c similarity index 50% rename from epsilon/rng.c rename to src/rng.c index 9a8419f..98745d4 100644 --- a/epsilon/rng.c +++ b/src/rng.c @@ -1,9 +1,7 @@ + +// Simple xorshift32 random number generator. #include -// Algorithm `xor` on page 4 of [1]. -// -// [1] Marsaglia, George. "Xorshift RNGs." Journal of Statistical Software 8.14 -// (2003): 1-6. uint32_t xorshift32(uint32_t y) { if (y == 0) y = 1; diff --git a/src/rng.h b/src/rng.h new file mode 100644 index 0000000..7227b37 --- /dev/null +++ b/src/rng.h @@ -0,0 +1,10 @@ + +#ifndef EPSILON_RNG_H +#define EPSILON_RNG_H + +// xorshift32 random number generator interface +#include + +uint32_t xorshift32(uint32_t y); + +#endif // EPSILON_RNG_H diff --git a/epsilon/stats.c b/src/stats.c similarity index 76% rename from epsilon/stats.c rename to src/stats.c index 0274d3f..fef6bb4 100644 --- a/epsilon/stats.c +++ b/src/stats.c @@ -1,4 +1,6 @@ -#include "epsilon/stats.h" + +// Online mean and variance (Welford's method). +#include "stats.h" #include #include #include @@ -12,10 +14,6 @@ void Welford_observe(Welfords_method_t *w, size_t n, float x) { float delta_after = (x - w->mean); w->squared_diff += delta_before * delta_after; assert(isfinite(w->squared_diff)); - - // Note that we can track the population variance instead of the squared - // difference. That seems more symmetrical, and would prevent squared_diff - // from getting to big for a fixed-point implementation. } void observe(online_stats_t *s, float x) { @@ -23,7 +21,6 @@ void observe(online_stats_t *s, float x) { if (s->n < SIZE_MAX) { s->n++; } - Welford_observe(&s->Welford, s->n, x); } diff --git a/src/stats.h b/src/stats.h new file mode 100644 index 0000000..4f922e4 --- /dev/null +++ b/src/stats.h @@ -0,0 +1,23 @@ + +#ifndef EPSILON_STATS_H +#define EPSILON_STATS_H + +#include + +// Online mean and variance (Welford's method) interface +typedef struct { + float mean, squared_diff; +} Welfords_method_t; + +typedef struct { + size_t n; + Welfords_method_t Welford; +} online_stats_t; + +void observe(online_stats_t *s, float x); +float mean(const online_stats_t *s); +float pvariance(const online_stats_t *s); +float variance(const online_stats_t *s); + +#endif // EPSILON_STATS_H + diff --git a/epsilon/transform.c b/src/transform.c similarity index 74% rename from epsilon/transform.c rename to src/transform.c index 1d9040d..e5a6580 100644 --- a/epsilon/transform.c +++ b/src/transform.c @@ -1,4 +1,6 @@ -#include "epsilon/transform.h" + +// In-place Fast Walsh-Hadamard transform. +#include "transform.h" #include #include #include @@ -10,15 +12,11 @@ inline static void WHT_butterfly(float *const s, float *const d) { *d = temp - *d; } -// Perform in-place Fast Walsh-Hadamard transform. void FWHT(float *const x, const uint8_t nbits) { const size_t n = 1 << nbits; for (size_t width = n; width > 1; width >>= 1) { - // width halves each iteration. for (size_t block = 0; block < n; block += width) { - // block shifts by with. for (size_t i = 0; i < (width >> 1); ++i) { - // i loops to half a block. WHT_butterfly(x + block + i, x + (width >> 1) + block + i); } } diff --git a/src/transform.h b/src/transform.h new file mode 100644 index 0000000..c581eff --- /dev/null +++ b/src/transform.h @@ -0,0 +1,10 @@ + +#ifndef EPSILON_TRANSFORM_H +#define EPSILON_TRANSFORM_H + +// In-place Fast Walsh-Hadamard transform interface +#include + +void FWHT(float *x, uint8_t nbits); + +#endif // EPSILON_TRANSFORM_H diff --git a/tests/hash_test.c b/tests/hash_test.c index fc4adde..8349ff3 100644 --- a/tests/hash_test.c +++ b/tests/hash_test.c @@ -1,4 +1,4 @@ -#include "epsilon/hash.h" +#include "hash.h" #include #include diff --git a/tests/rng_test.c b/tests/rng_test.c index 5606455..1970195 100644 --- a/tests/rng_test.c +++ b/tests/rng_test.c @@ -1,4 +1,4 @@ -#include "epsilon/rng.h" +#include "rng.h" #include #include #include diff --git a/tests/stats_test.c b/tests/stats_test.c index ae98596..e2447dd 100644 --- a/tests/stats_test.c +++ b/tests/stats_test.c @@ -1,4 +1,4 @@ -#include "epsilon/stats.h" +#include "stats.h" #include #include diff --git a/tests/transform_test.c b/tests/transform_test.c index c076c46..0ea8db8 100644 --- a/tests/transform_test.c +++ b/tests/transform_test.c @@ -1,4 +1,4 @@ -#include "epsilon/transform.h" +#include "transform.h" #include #include #include