Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,24 @@ jobs:
- name: Check code formatting
run: cargo fmt -- --check

- name: Run Clippy Linter
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Run Clippy Linter (Standard f64)
run: cargo clippy --all-targets --features std,serde,ffi -- -D warnings

- name: Run Clippy Linter (Single-Precision f32)
run: cargo clippy --lib --features single-precision,ffi -- -D warnings

- name: Run Test Suite (Standard Library)
run: cargo test --all-features --verbose
run: cargo test --features std,serde,ffi --verbose

- name: Verify Pure #![no_std] Core Compilation (Bare-Metal target)
run: cargo check --no-default-features --target thumbv7em-none-eabihf

- name: Verify Bare-Metal Release Build & Hardening (panic=abort)
run: cargo build --release --no-default-features --target thumbv7em-none-eabihf

- name: Verify Cortex-M Single-Precision FPU Target (#![no_std] f32 build)
run: cargo build --release --no-default-features --features single-precision --target thumbv7em-none-eabihf

- name: Run Test Suite (Single-Precision Mode)
run: cargo test --lib --features single-precision --verbose

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ default = ["std", "ffi"]
std = []
serde = ["dep:serde"]
ffi = []
single-precision = []

[profile.dev]
panic = "abort"
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ With optional `serde` support for state persistence and telemetry serialization:
aegis-filter = { version = "0.4.0", features = ["serde"] }
```

For **ARM Cortex-M4 / Cortex-M33** and single-precision hardware FPUs, enable `single-precision` to compile state representations, covariances, and math routines as `f32`:

```toml
[dependencies]
aegis-filter = { version = "0.4.0", default-features = false, features = ["single-precision"] }
```

---

## Example Usage
Expand Down
83 changes: 49 additions & 34 deletions include/aegis_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@
#include <stdint.h>
#include <stdbool.h>

/**
* @brief Configurable floating-point precision type.
*
* Defaults to aegis_real (f64). Define AEGIS_SINGLE_PRECISION or compile with
* the `single-precision` feature to switch to float (f32) for ARM Cortex-M
* single-precision FPUs.
*/
#if defined(AEGIS_SINGLE_PRECISION)
typedef float aegis_real;
#define AEGIS_DEFAULT_GATE_SIGMA 3.0f
#define AEGIS_GATE_DISABLE (-1.0f)
#else
typedef aegis_real aegis_real;
#define AEGIS_DEFAULT_GATE_SIGMA 3.0
#define AEGIS_GATE_DISABLE (-1.0)
#endif

#ifdef __cplusplus
extern "C" {
#endif
Expand Down Expand Up @@ -45,9 +62,7 @@ typedef enum {
AEGIS_FUSION_UNREGISTERED = 5 /**< Unknown sensor ID */
} AegisFusionStatusCode;

/* Sentinel constants for outlier gating */
#define AEGIS_DEFAULT_GATE_SIGMA 3.0
#define AEGIS_GATE_DISABLE (-1.0)
/* Sentinel constants defined above with aegis_real */

/* Forward-declared opaque handles */
typedef struct AegisScalarFilter AegisScalarFilter;
Expand All @@ -70,10 +85,10 @@ typedef struct AegisFusion4 AegisFusion4;
*/
AegisStatusCode aegis_scalar_filter_init(
AegisScalarFilter* filter,
double initial_state,
double initial_variance,
double process_noise_q,
double measurement_noise_r
aegis_real initial_state,
aegis_real initial_variance,
aegis_real process_noise_q,
aegis_real measurement_noise_r
);

/**
Expand All @@ -85,15 +100,15 @@ AegisStatusCode aegis_scalar_filter_init(
*/
AegisStatusCode aegis_scalar_filter_set_default_gate(
AegisScalarFilter* filter,
double gate_threshold_sigma
aegis_real gate_threshold_sigma
);

/**
* @brief Propagates filter state forward in time: x = x + u, P = P + Q.
*/
AegisStatusCode aegis_scalar_filter_predict(
AegisScalarFilter* filter,
double control_input
aegis_real control_input
);

/**
Expand All @@ -108,20 +123,20 @@ AegisStatusCode aegis_scalar_filter_predict(
*/
AegisStatusCode aegis_scalar_filter_update(
AegisScalarFilter* filter,
double measurement,
double gate_threshold_sigma,
double* out_state
aegis_real measurement,
aegis_real gate_threshold_sigma,
aegis_real* out_state
);

/**
* @brief Retrieves current state mean.
*/
double aegis_scalar_filter_get_state(const AegisScalarFilter* filter);
aegis_real aegis_scalar_filter_get_state(const AegisScalarFilter* filter);

/**
* @brief Retrieves current error variance.
*/
double aegis_scalar_filter_get_variance(const AegisScalarFilter* filter);
aegis_real aegis_scalar_filter_get_variance(const AegisScalarFilter* filter);

/* ========================================================================= */
/* 2. H-INFINITY MINIMAX ROBUST FILTER (Bounded Disturbance) */
Expand All @@ -139,39 +154,39 @@ double aegis_scalar_filter_get_variance(const AegisScalarFilter* filter);
*/
AegisStatusCode aegis_h_inf_filter_init(
AegisHInfFilter* filter,
double initial_state,
double initial_variance,
double process_bound_q,
double measurement_bound_r,
double gamma
aegis_real initial_state,
aegis_real initial_variance,
aegis_real process_bound_q,
aegis_real measurement_bound_r,
aegis_real gamma
);

/**
* @brief Propagates H-infinity state forward: x = x + u, P = P + Q.
*/
AegisStatusCode aegis_h_inf_filter_predict(
AegisHInfFilter* filter,
double control_input
aegis_real control_input
);

/**
* @brief Updates state under discrete-time bounded-real Riccati criteria.
*/
AegisStatusCode aegis_h_inf_filter_update(
AegisHInfFilter* filter,
double measurement,
double* out_state
aegis_real measurement,
aegis_real* out_state
);

/**
* @brief Retrieves current H-infinity state estimate.
*/
double aegis_h_inf_filter_get_state(const AegisHInfFilter* filter);
aegis_real aegis_h_inf_filter_get_state(const AegisHInfFilter* filter);

/**
* @brief Retrieves current error bound (P).
*/
double aegis_h_inf_filter_get_bound(const AegisHInfFilter* filter);
aegis_real aegis_h_inf_filter_get_bound(const AegisHInfFilter* filter);

/* ========================================================================= */
/* 3. MULTI-SENSOR FDIR FUSION ENGINE (4 Channels) */
Expand All @@ -182,9 +197,9 @@ double aegis_h_inf_filter_get_bound(const AegisHInfFilter* filter);
*/
AegisStatusCode aegis_fusion4_init(
AegisFusion4* fusion,
double x0,
double p0,
double q
aegis_real x0,
aegis_real p0,
aegis_real q
);

/**
Expand All @@ -200,9 +215,9 @@ AegisStatusCode aegis_fusion4_init(
AegisStatusCode aegis_fusion4_register_sensor(
AegisFusion4* fusion,
uint8_t sensor_id,
double variance,
aegis_real variance,
uint32_t max_consecutive_anomalies,
double gate_threshold_sigma,
aegis_real gate_threshold_sigma,
uint32_t auto_recovery_threshold
);

Expand All @@ -211,7 +226,7 @@ AegisStatusCode aegis_fusion4_register_sensor(
*/
AegisStatusCode aegis_fusion4_predict(
AegisFusion4* fusion,
double control_input
aegis_real control_input
);

/**
Expand All @@ -225,24 +240,24 @@ AegisStatusCode aegis_fusion4_predict(
AegisStatusCode aegis_fusion4_fuse(
AegisFusion4* fusion,
uint8_t sensor_id,
double value,
aegis_real value,
AegisFusionStatusCode* out_status
);

/**
* @brief Retrieves fused state estimate.
*/
double aegis_fusion4_get_state(const AegisFusion4* fusion);
aegis_real aegis_fusion4_get_state(const AegisFusion4* fusion);

/**
* @brief Retrieves fused error covariance.
*/
double aegis_fusion4_get_variance(const AegisFusion4* fusion);
aegis_real aegis_fusion4_get_variance(const AegisFusion4* fusion);

/**
* @brief Retrieves real-time health score of a sensor (1.0 = nominal, 0.0 = quarantined).
*/
double aegis_fusion4_get_sensor_health(const AegisFusion4* fusion, uint8_t sensor_id);
aegis_real aegis_fusion4_get_sensor_health(const AegisFusion4* fusion, uint8_t sensor_id);

#ifdef __cplusplus
}
Expand Down
57 changes: 53 additions & 4 deletions src/common.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
//! Core mathematical types, errors, and `#![no_std]` floating point helpers.

#[cfg(not(feature = "single-precision"))]
/// Type alias for floating-point precision used across aegis-filter (f64 default).
pub type Real = f64;

#[cfg(feature = "single-precision")]
/// Type alias for floating-point precision used across aegis-filter (f32 for single-precision FPUs).
pub type Real = f32;

/// Standard recommended 3-sigma innovation threshold for outlier and anomaly rejection.
pub const DEFAULT_OUTLIER_GATE_SIGMA: f64 = 3.0;
pub const DEFAULT_OUTLIER_GATE_SIGMA: Real = 3.0;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down Expand Up @@ -50,11 +58,11 @@ impl core::fmt::Display for FilterError {
#[cfg(feature = "std")]
impl std::error::Error for FilterError {}

/// Trait providing safe, deterministic `#![no_std]` math operations for f64.
/// Trait providing safe, deterministic `#![no_std]` math operations for floating-point types.
pub trait FloatOps {
fn is_valid_float(&self) -> bool;
fn abs_val(self) -> f64;
fn sqrt_val(self) -> f64;
fn abs_val(self) -> Self;
fn sqrt_val(self) -> Self;
}

impl FloatOps for f64 {
Expand Down Expand Up @@ -97,3 +105,44 @@ impl FloatOps for f64 {
}
}
}

impl FloatOps for f32 {
#[inline(always)]
fn is_valid_float(&self) -> bool {
self.is_finite()
}

#[inline(always)]
fn abs_val(self) -> f32 {
f32::from_bits(self.to_bits() & !(1u32 << 31))
}

#[inline]
fn sqrt_val(self) -> f32 {
#[cfg(feature = "std")]
{
self.sqrt()
}
#[cfg(not(feature = "std"))]
{
if self <= 0.0 {
return 0.0;
}
if !self.is_finite() {
return self;
}
// Fast IEEE-754 single-precision exponent halving initial guess,
// followed by quadratic Newton-Raphson iterations.
let init_bits = (self.to_bits() >> 1) + (127u32 << 22);
let mut curr = f32::from_bits(init_bits);
for _ in 0..6 {
let prev = curr;
curr = 0.5 * (curr + self / curr);
if (curr - prev).abs_val() <= 1e-6 * curr {
break;
}
}
curr
}
}
}
Loading
Loading