Overview
In the MSc course Fault Diagnostics and Security for Control Systems (4DM110, Q4 2025–2026) at TU Eindhoven, I designed and evaluated a stack of diagnostic and cyber-defence methods for a three-axis spacecraft attitude determination system. A spacecraft closes its control loop over a radio link that is shared, open to eavesdropping, and exposed to two very different threats: faults (a gyro that develops a bias, a reaction wheel that delivers the wrong torque) and deliberate cyber-attacks (an adversary corrupting the data crossing the network — possibly in a way a classical detector never sees).
The project works through three increasingly adversarial layers on the same plant — the published spacecraft model of Pirmoradi et al.:
- Fault diagnostics (T1–T2) — an unknown-input observer (UIO) that detects and reconstructs sensor faults from the output residual.
- Secure state estimation (T3) — a multi-observer estimator that reconstructs the true state when an unknown sensor is under attack, with a sharp structural finding about when it works and when it cannot.
- Resilient control (T4) — the nonlinear-encoding defence of Joo, Qu and Namerikawa (2021), which uses two synchronised chaotic circuits to make a perfectly stealthy integrity attack visible at zero performance cost.
System and model
From nonlinear rigid body to decoupled double integrators
The spacecraft has three rate gyroscopes measuring the body rates and three vector sensors measuring the attitude angles , actuated by three reaction-wheel torques. Euler's rigid-body equations are nonlinear because of the gyroscopic coupling between axes, but around the stationary operating point , the gyroscopic cross-products vanish and the small-angle kinematics give . The six states decouple into three identical double integrators:
with state , input the reaction-wheel torques, and full-state measurement (, three gyros + three vector sensors). The moments of inertia are , , .
Output-feedback PD controller
The open-loop plant has six poles at the origin — in space there is no natural damping, so any disturbance makes the attitude drift. A decentralised PD law stabilises each axis. Matching the per-axis characteristic polynomial to the canonical gives
Scaling each gain by its axis inertia cancels the in , so a single choice of and gives identical closed-loop behaviour on all three axes regardless of inertia — about overshoot and a settling time near s.

Closed-loop step response per axis for . Inertia-scaled gains make all three axes track the same second-order response.
Discretisation — exploiting a nilpotent
The estimators run on a digital computer, so the model is discretised with a zero-order hold at s. The key shortcut is that is nilpotent (), so the matrix exponential and the ZOH integral both terminate after two terms — the discretisation is exact, and ZOH and Tustin happen to coincide:
The closed-loop has spectral radius , so the discrete loop is Schur stable — a fact the fault diagnostics exploit directly.
Tasks 1–2 — Fault diagnostics with an unknown-input observer
Fault model
A sensor fault is an unknown input that corrupts the measurement without touching the physical motion. So the fault never enters the dynamics () and appears only at the output, where each affected channel contributes a unit column to :
Because each broken sensor adds an independent column to , the stacked fault matrix always keeps full column rank — so any combination of simultaneous sensor faults is, in principle, separable.
The UIO that collapses to a physics simulator
A general unknown-input observer reconstructs the state from a window of outputs and recovers the fault from the residual,
For this spacecraft three simplifications collapse the full design to almost nothing. Sensor faults appear instantly in the measurement, so no look-ahead window is needed (). There is no actuator-path disturbance to decouple, so the decoupling matrix . And because the PD loop has already made Schur, no observer feedback is needed, so . Together:
The "observer" becomes a pure physics simulation of the healthy closed-loop model, deliberately blind to the sensors. Any divergence in the residual is then read directly as a fault on the corresponding channel.

Scenario 1 — pitch-gyro fault ( at s). The reconstructed fault locks onto the true bias within a sample or two; steady-state RMSE of sits exactly at the sensor noise floor, so the estimator adds no bias of its own.
Across all scenarios — single gyro fault, single angle-sensor fault, and two simultaneous faults (yaw gyro + pitch sensor) — the reconstructed followed each injected bias within one to two samples, with steady-state RMSE at the noise floor on every channel.
Robustness to inertia uncertainty
Since the diagnostic runs on a model built from the inertias , a TA question prompted a robustness study: what if the true inertia drifts, ? Because the gains scale as while , the product is nominally inertia-independent, so the closed loop barely moves. Sweeping from to , the worst-case fault-free residual grows with but never crosses the detection threshold — inertia uncertainty alone does not raise a false alarm.

Robustness to inertia error . Left: the loop stays stable and always settles. Right: the worst-case fault-free residual rises with but never crosses the alarm threshold .
Task 3 — Secure state estimation under sensor attacks
Task 3 raises the adversary: now an unknown sensor is being actively attacked, and the estimator must reconstruct the true state without knowing which one. The model adds a per-sensor attack signal,
and we defend against a single attacked sensor (). The reason for is itself a finding — for no two-sensor subset of this six-state plant is observable, so the algorithm is infeasible.
Observer bank and decoder
The secure estimator (Chong–Wakaiki–Hespanha, 2015) runs a bank of Luenberger observers over sensor subsets and trusts the one that is most self-consistent. For every of sensors it runs a level-1 observer, and for every of sensors a level-2 cross-check. The decoder scores each level-1 subset by its worst disagreement with its own sub-observers, then selects the minimum:
A clean subset excludes the attacked sensor, so its observer converges and scores ; an attacked subset disagrees with its sub-observers and scores high. The least-disagreeing subset is the trustworthy estimate.
The headline finding — rate decodes, angle doesn't
The catch is observability. Theorem 1 requires every -sensor subset to be observable. Checking all subsets in MATLAB, only three are observable. The reason is structural: each axis is a pair, and means you can recover a rate from an angle, but not an angle from a rate alone (you'd need the integration constant). So a subset is observable if and only if it keeps all three angle sensors :
This splits the defence cleanly in two:
- Attack a rate gyro (sensors 1–3) → it can be dropped while keeping all angle sensors → an observable hypothesis exists → the decoder isolates it correctly. ✅
- Attack an angle sensor (sensors 4–6) → every observable subset is forced to keep all three angle sensors → no subset can exclude the corrupted one → the decoder is fooled. ❌
A deterministic bias sweep confirmed this is structural, not noise- or magnitude-dependent: rate-sensor attacks decode correctly from to , while a roll-angle attack is decoded wrong from to .

Failure mode — roll-angle-sensor attack (sensor 4). The true subset would have to exclude sensor 4 (red dashed), but that hypothesis is unobservable and never enters the candidate pool, so the decoder picks a wrong subset and the secure estimate (green) follows the attack instead of the true state (black).
Restoring one-fault observability
The blind spot is fixable: it exists because dropping an angle sensor unobserves its axis. Adding redundant cross-axis "summation" sensors that measure linear combinations of two angles each,
closes a linearly independent cycle over the attitude angles, so a missing angle can always be reconstructed from the others. With , every one of the subsets becomes observable — the system is now globally one-fault observable. (I also worked through a compact variant that hits the same rank condition without adding any sensors — at the cost of coupling rate and angle into the same channel.)

The same roll-angle attack with the augmented summation-sensor architecture: the decoder now rejects the corrupted sensor and the secure estimate stays clean — the angle-sensor blind spot is resolved.
Try it yourself. The demo below runs the full multi-observer estimator live in your browser — the exact algorithm, observability test, and decoder described above. Attack a rate gyro (sensors 1–3) and the decoder isolates it; attack an angle sensor (4–6) and watch the secure estimate get dragged off the truth — then tick Add redundant summation sensors and watch the same attack get rejected. Nothing about the rate/angle split is hard-coded; it emerges from which sensor subsets are observable.
Angle-sensor attacks (red) sit in the observability blind spot — unless you add redundant sensors below.
Live simulation. Top: the level-1 disagreement score $\pi_S$ for every observable subset (log scale) — a clean subset drops toward zero. Bottom: the true state, the secure estimate, and the noisy (possibly attacked) measurement on the targeted channel. Attack an angle sensor and watch the estimate get dragged off the truth — then tick the redundant-sensor box and watch it recover.
Task 4 — Resilient control against stealthy integrity attacks
Task 3's angle-sensor attack was effectively a stealthy attack — it lived in an observability blind spot and a passive estimator never saw it. Task 4 implements an active defence from Joo, Qu and Namerikawa (2021): the goal is to make a perfectly stealthy attack visible, at zero performance cost when there is no attack. As in the paper, this layer is studied in continuous time.
The perfectly stealthy attack
An attacker who knows the dynamics can match the closed-loop model (, ) and inject a sensor signal that exactly cancels the physical effect of its own actuator tampering in the residual:
Against a classical residual-based detector this is invisible — the residual stays flat while the spacecraft is driven off course.
Nonlinear encoding and decoding
The defence multiplies the control signal by a fast, chaotic modulation factor before it crosses the network, and divides it back out at the plant:
where is the plant's chaotic signal and the controller's synchronised copy. The bound keeps the denominator in so it never divides by zero; the encoding depth is . When the two circuits are synchronised () and there is no attack, the factors cancel exactly and the plant receives precisely — zero performance cost. But an attacker cannot reproduce , so an injected is scaled by an uncancellable chaotic factor and forced into the residual. This gives the clean detection guarantee
Chaos from Chua's circuit
The secret signal comes from a Chua circuit running the canonical double-scroll attractor, with its piecewise-linear negative-resistor diode
A master circuit on the plant generates ; a slave on the controller side sees only the scalar and locks onto it through a Pecora–Carroll coupling term (gain ). Joo's Lemma 2 guarantees the slave's tracking error decays exponentially once exceeds a threshold ( here), so — verified in simulation below.

Pecora–Carroll synchronisation of the two Chua circuits. The slave locks onto the master within a fraction of a second (error below by s), so and the encoding cancels cleanly under no attack.
Catching the attack T3 missed
With the encoding switched on, the same stealthy attack that the classical detector never sees jumps the residual decisively above the alarm threshold at the instant of injection — while the no-attack response is identical with encoding on or off, confirming there is no performance penalty. Crucially, the encoded detector catches a roll-angle attack — the exact attack class that lived in Task 3's observability blind spot. The two deliverables are complementary: passive secure estimation and active nonlinear encoding cover each other's gaps.

Encoding off vs on. The classical residual (blue) never moves; the encoded residual (green) leaps above the threshold at injection ( s). With no attack the two responses are identical — detection costs nothing. Note the middle panel: detection is not prevention — the attack is revealed, but still drives the spacecraft off course.
Try it yourself
The simulation below runs the same continuous-time implementation in your browser — the full 3-axis attitude loop, the twin Chua circuits, and Joo's Lemma-1 attacker, integrated live with RK4. Launch the attack and watch the encoded residual leap eleven decades at the injection instant while the classical detector never moves. Two honest details to notice: the attacker's internal model self-stabilises (), so the encoded residual decays back after firing — the alarm therefore latches at the first crossing, exactly like a real intrusion alert. And detection is not prevention: the bottom panel shows the true pitch being dragged toward while the deceived estimate stays put. Untick the attack to watch the controller recover once the sensor mask disappears.
Live simulation — a faithful port of the course MATLAB (spacecraft_encoding_continuous.m): the full 3-axis attitude loop, Joo et al.'s Lemma-1 self-stabilising attacker, and the twin Chua circuits with the paper's coupling (Lemma 2, lc = 8). Left: the master circuit tracing the double-scroll attractor that generates the shared secret ξ. Top right: the detector residual with encoding off (classical, blue) vs on (encoded, green) — under the same perfectly-stealthy attack the classical residual sits at the numerical floor while the encoded one leaps 11 decades at onset. Because the attacker's model self-settles (ua → 0), the encoded residual decays back after firing — so the alarm latches at the first crossing. Bottom right: detection is not prevention — the true pitch (orange) is dragged away toward ra/Kp ≈ 2865° while the deceived estimate (teal) never moves. Untick the attack to watch the controller recover.
A sweep over the encoding depth confirmed the design trade-off: deeper encoding raises the peak residual and shrinks the detection delay (down to s), with sitting comfortably in the fast-detection region while respecting the safety bound. The closed loop also remained input-to-state stable — the peak attitude deviation grows linearly with the attack magnitude , giving an operator a predictable window to respond.
Results & Reflections
This was the most adversarially-minded controls project I have done, and the through-line was honest negative results. The secure state estimator does not simply "work" — it works for rate-sensor attacks and provably fails for angle-sensor attacks, and chasing down exactly why (the open-loop observability structure, not noise or tuning) was more instructive than a clean success would have been. Reporting that failure sharply, then designing the redundant-sensor architecture that fixes it, turned a limitation into the most interesting part of the work.
The three layers also taught me how differently faults and cyber-attacks have to be treated. A fault shows up in the residual by construction, so a collapsed UIO — literally a healthy-model simulation — suffices. A stealthy attack is engineered to be invisible to exactly that residual, so the only defence is to inject a secret the attacker cannot reproduce. Watching two chaotic circuits synchronise and then quietly cancel — leaving the loop untouched until the moment an attacker tries to hide inside it — was the most elegant idea in the course. And the recurring caveat, detection is not prevention, is a good reminder that the estimator's job is to tell the operator the truth, not to save the spacecraft on its own.
Technologies Used
MATLAB (place, obsv, expm, c2d, ode45, exportgraphics),
unknown-input observer design, Luenberger multi-observer banks, secure state
estimation (Chong–Wakaiki–Hespanha), observability analysis over sensor
subsets, nonlinear chaotic encoding (Joo et al. 2021), Chua-circuit chaos and
Pecora–Carroll synchronisation, Lyapunov / ISS stability arguments, LaTeX