Transforms Reference
Overview
Transforms process signal values in data flow chains. Each transform implements the ITransform interface:
virtual double apply(double input, double dt) = 0; // Process one sample
virtual void reset() = 0; // Reset internal state
virtual std::unique_ptr<ITransform> clone() const = 0; // Deep copy
All transforms are deterministic - same inputs/dt always produce same output.
1. Linear Transform
Type: linear
Function: Affine transformation with optional clamping
Formula:
y = scale * x + offset
if clamp_min set: y = max(y, clamp_min)
if clamp_max set: y = min(y, clamp_max)
Parameters:
scale(double, default=1.0) - Multiplicative factoroffset(double, default=0.0) - Additive offsetclamp_min(optional double) - Minimum output valueclamp_max(optional double) - Maximum output value
State: Stateless (dt-independent)
Use Cases:
- Sensor calibration
- Range remapping
- Conditioning within a single unit contract
Example:
EdgeSpec edge;
edge.source_path = "sensor.raw";
edge.target_path = "sensor.calibrated";
edge.transform.type = "linear";
edge.transform.params["scale"] = 2.5;
edge.transform.params["offset"] = -10.0;
edge.transform.params["clamp_min"] = 0.0;
edge.transform.params["clamp_max"] = 100.0;
1.5 Unit Convert Transform
Type: unit_convert
Function: Explicit cross-unit conversion using registry-derived coefficients.
Parameters:
to_unit(string, required) - Target unit symbolfrom_unit(string, optional) - Source unit assertion
State: Stateless (dt-independent)
Use Cases:
degC<->Kconversions- Canonical unit normalization between subsystems
- Strict-mode replacement for implicit unit-boundary
linearusage
2. First-Order Lag
Type: first_order_lag
Function: Low-pass filter (exponential smoothing)
Formula:
tau * dy/dt = x - y
Discrete: y_{n+1} = y_n + (dt/tau) * (x_n - y_n)
Parameters:
tau_s(double, required) - Time constant in seconds- 63% response in tau seconds
- 95% response in 3*tau seconds
- 99% response in 5*tau seconds
State: Current filtered value y
Behavior:
- Larger tau = more smoothing, slower response
- tau=0 -> passthrough (no filtering)
- Output initializes to first input
Use Cases:
- Sensor noise filtering
- Smooth transitions/setpoints
- Simulate physical inertia
Example:
EdgeSpec edge;
edge.source_path = "sensor.noisy";
edge.target_path = "sensor.filtered";
edge.transform.type = "first_order_lag";
edge.transform.params["tau_s"] = 2.0; // 2-second time constant
Frequency Response:
- 3dB cutoff frequency: f*c = 1 / (2 * pi _ tau)
- For tau=1.0: f_c ~= 0.16 Hz
Analytical Validation: Step response matches exp(-t/tau) to within 0.001 error (see analytical tests).
3. Delay Transform
Type: delay
Function: Time-shift signal by fixed duration
Formula:
y(t) = x(t - delay_sec)
Parameters:
delay_sec(double, required) - Delay duration in seconds
State: Circular buffer of past samples
Behavior:
- Output is input from delay_sec ago
- Buffer sized for delay_sec / dt samples
- Filled with 0.0 initially
Use Cases:
- Model transport delays (pipes, cables)
- Simulate processing latency
- Audio effects
Example:
EdgeSpec edge;
edge.source_path = "source.signal";
edge.target_path = "delayed.signal";
edge.transform.type = "delay";
edge.transform.params["delay_sec"] = 0.5; // 500ms delay
Memory: Approx delay_sec/dt * 8 bytes (one double per buffered sample)
Analytical Validation: Time shift exact to 1e-6 error for step, ramp, and sine inputs (see analytical tests).
4. Noise Transform
Type: noise
Function: Add Gaussian white noise
Formula:
y = x + N(0, amplitude)
where N is normal distribution
Parameters:
amplitude(double, required) - Standard deviation of noiseseed(optional uint32_t) - Random seed for repeatability
State: Random number generator state
Behavior:
- Deterministic if seed specified
- Different seeds give different noise sequences
- amplitude=0.0 -> passthrough
Use Cases:
- Simulate sensor noise
- Test robustness to disturbances
- Add realism to simulations
Example:
EdgeSpec edge;
edge.source_path = "sensor.ideal";
edge.target_path = "sensor.noisy";
edge.transform.type = "noise";
edge.transform.params["amplitude"] = 0.5; // +/- ~1.0 range (2*sigma)
edge.transform.params["seed"] = 42; // Reproducible noise
Statistical Properties:
- Mean = input (unbiased)
- Std dev = amplitude
- 68% of samples within +/- amplitude
- 95% within +/- 2*amplitude
5. Saturation Transform
Type: saturation
Function: Clamp signal to min/max bounds
Formula:
y = clamp(x, min_value, max_value)
= max(min_value, min(max_value, x))
Parameters:
min_value(double, required) - Minimum outputmax_value(double, required) - Maximum output
State: Stateless
Behavior:
- If x < min_value: y = min_value
- If x > max_value: y = max_value
- Otherwise: y = x
Use Cases:
- Enforce physical limits (0-100% valve)
- Prevent overdrive in controllers
- Model actuator saturation
Example:
EdgeSpec edge;
edge.source_path = "controller.output";
edge.target_path = "actuator.command";
edge.transform.type = "saturation";
edge.transform.params["min_value"] = 0.0;
edge.transform.params["max_value"] = 100.0;
Analytical Validation: Exact clamping (EXPECT_DOUBLE_EQ) with no overshoot (see analytical tests).
6. Deadband Transform
Type: deadband
Function: Zero output below threshold (noise gate)
Formula:
y = (|x| < threshold) ? 0.0 : x
Parameters:
threshold(double, required) - Sensitivity threshold
State: Stateless
Behavior:
- Small signals suppressed
- Large signals pass through unchanged
- Applied to absolute value (symmetric)
Use Cases:
- Filter noise near zero
- Implement hysteresis
- Deadzone in joystick input
Example:
EdgeSpec edge;
edge.source_path = "joystick.raw";
edge.target_path = "joystick.gated";
edge.transform.type = "deadband";
edge.transform.params["threshold"] = 0.05; // Ignore small movements
Analytical Validation: Exact threshold enforcement (EXPECT_DOUBLE_EQ) at boundary (see analytical tests).
7. Rate Limiter
Type: rate_limiter
Function: Limit how fast signal can change
Formula:
max_change = max_rate * dt
y_{n+1} = y_n + clamp(x_n - y_n, -max_change, max_change)
Parameters:
max_rate(double, required) - Maximum rate of change (units/sec)
State: Current output value y
Behavior:
- Output changes by at most max_rate * dt per tick
- Gradual approach to target
- Prevents sudden jumps
Use Cases:
- Smooth actuator commands (prevent jerking)
- Ramp setpoints gradually
- Model slew rate limits
Example:
EdgeSpec edge;
edge.source_path = "setpoint.target";
edge.target_path = "setpoint.ramped";
edge.transform.type = "rate_limiter";
edge.transform.params["max_rate"] = 5.0; // Max 5 units/sec change
Time to settle (approx):
t = |x_final - y_initial| / max_rate
Analytical Validation: Slope constraint |dy/dt| <= max_rate verified (see analytical tests).
8. Moving Average
Type: moving_average
Function: Sliding window average (FIR filter)
Formula:
y_n = (1/N) * sum(x_{n-i}) for i=0 to N-1
where N = window_size
Parameters:
window_size(int, required) - Number of samples to average
State: Circular buffer of past samples
Behavior:
- Averages last window_size samples
- Initial behavior: averages available samples (< window_size)
- window_size=1 -> passthrough
Use Cases:
- Reduce high-frequency noise
- Smooth jitter
- Cheap low-pass filter
Example:
EdgeSpec edge;
edge.source_path = "sensor.jittery";
edge.target_path = "sensor.smoothed";
edge.transform.type = "moving_average";
edge.transform.params["window_size"] = 10; // Average last 10 samples
Memory: window_size * 8 bytes (double buffer)
Frequency Response:
- Acts as low-pass FIR filter
- Cutoff frequency: ~1/(window_size * dt)
Latency: (window_size - 1) / 2 * dt (group delay)
Analytical Validation: Constant input gives exact average (EXPECT_DOUBLE_EQ), step response correct (see analytical tests).
Transform Comparison
| Transform | Stateful? | Memory | Latency | Use Case |
|---|---|---|---|---|
| Linear | No | 0 | 0 | Scaling, calibration |
| FirstOrderLag | Yes | O(1) | ~3*tau | Smooth filtering |
| Delay | Yes | O(delay/dt) | delay_sec | Transport lag |
| Noise | Yes | O(1) | 0 | Add disturbance |
| Saturation | No | 0 | 0 | Enforce limits |
| Deadband | No | 0 | 0 | Noise gate |
| RateLimiter | Yes | O(1) | Varies | Slew rate limit |
| MovingAverage | Yes | O(window) | window/2*dt | Smooth jitter |
Chaining Transforms
Transforms can be chained by connecting target of one to source of another:
// sensor.raw -> noise -> filter -> saturation -> output
EdgeSpec edge1;
edge1.source_path = "sensor.raw";
edge1.target_path = "sensor.noisy";
edge1.transform.type = "noise";
edge1.transform.params["amplitude"] = 1.0;
EdgeSpec edge2;
edge2.source_path = "sensor.noisy";
edge2.target_path = "sensor.filtered";
edge2.transform.type = "first_order_lag";
edge2.transform.params["tau_s"] = 0.5;
EdgeSpec edge3;
edge3.source_path = "sensor.filtered";
edge3.target_path = "sensor.output";
edge3.transform.type = "saturation";
edge3.transform.params["min_value"] = 0.0;
edge3.transform.params["max_value"] = 100.0;
Execution order: Graph compiler determines correct order via topological sort.
Custom Transform Example
Implement ITransform for custom behavior:
class HysteresisTransform : public fluxgraph::ITransform {
private:
double m_on_threshold;
double m_off_threshold;
bool m_state = false;
public:
HysteresisTransform(double on, double off)
: m_on_threshold(on), m_off_threshold(off) {}
double apply(double input, double dt) override {
if (input > m_on_threshold) m_state = true;
if (input < m_off_threshold) m_state = false;
return m_state ? 1.0 : 0.0;
}
void reset() override {
m_state = false;
}
std::unique_ptr<ITransform> clone() const override {
return std::make_unique<HysteresisTransform>(*this);
}
};
See EMBEDDING.md for registration details.
Transform Best Practices
1. Choose dt Appropriately
For FirstOrderLag:
dt << tau (at least 10x smaller)
Example: tau=1.0s -> dt <= 0.1s
For Delay:
dt should divide delay_sec evenly
Example: delay=0.5s -> dt=0.1s (5 samples) or 0.05s (10 samples)
2. Order Matters in Chains
// Good: filter then limit
noise -> first_order_lag -> saturation
// Bad: limit then filter (spikes can still pass through limiter)
noise -> saturation -> first_order_lag
3. Reset State After Re-runs
engine.reset(); // Resets all transform state
// Now ready for fresh simulation run
4. Avoid Excessive Buffering
// Avoid: 10-second delay at 0.01s dt = 1000 samples = 8KB per edge
edge.transform.params["delay_sec"] = 10.0; // High memory cost
// Better: Use FirstOrderLag if you just need smoothing
edge.transform.params["tau_s"] = 2.0; // Minimal state, similar effect
5. Combine Transforms Efficiently
// Inefficient: Two linear transforms
edge1: y = 2*x
edge2: z = y + 10
// Better: Single linear transform
edge: z = 2*x + 10
Scientific Validation
All transforms validated against analytical solutions (see tests/analytical/):
- FirstOrderLag: Matches exp(-t/tau) to 0.001 error
- ThermalMass: Matches heat equation to 0.1 degC
- Delay: Exact time shift to 1e-6 error
- Linear: Exact (EXPECT_DOUBLE_EQ)
- Saturation: Exact clamping
- Deadband: Exact threshold
- RateLimiter: Slope constraint verified
- MovingAverage: Exact average for constant input
Conclusion: Numerically sound for real-world use.
Performance Notes
Relative costs (Debug build):
- Linear: ~1ns
- Saturation/Deadband: ~2ns
- FirstOrderLag: ~5ns
- RateLimiter: ~5ns
- MovingAverage: ~10ns * window_size
- Delay: ~3ns (circular buffer)
- Noise: ~20ns (RNG call)
Release builds (-O3): 2-5x faster
Bottleneck: Typically models (physics) dominate, not transforms.
Future Enhancements
Additional Transforms Under Consideration:
- PID Controller - Proportional-Integral-Derivative control
- LUT (Lookup Table) - Piecewise linear interpolation
- Integrator - Accumulate signal over time
- Differentiator - Compute derivative (noisy, use carefully)
- HighPassFilter - Complement to FirstOrderLag
- NotchFilter - Reject specific frequency
- Quantizer - Round to discrete levels
- Hysteresis - Prevent chatter (see custom example above)
To request: Submit issue on GitHub with use case.
Summary
FluxGraph provides 8 scientifically-validated transforms covering:
- Scaling (Linear)
- Filtering (FirstOrderLag, MovingAverage)
- Delays (Delay)
- Noise (Noise)
- Limiting (Saturation, Deadband, RateLimiter)
All transforms:
- Deterministic
- Validated analytically
- Efficient (sub-microsecond per call in Release)
Next: See API.md for usage details, EMBEDDING.md for integration.