DEV Community

Rikin Patel
Rikin Patel

Posted on

Explainable Causal Reinforcement Learning for satellite anomaly response operations under multi-jurisdictional compliance

Satellite Anomaly Response

Explainable Causal Reinforcement Learning for satellite anomaly response operations under multi-jurisdictional compliance

Introduction: A Failure That Taught Me Everything

Last year, I was deep into a personal research project exploring reinforcement learning agents for simulated spacecraft telemetry, when I hit a wall that fundamentally reshaped how I think about autonomous decision-making. My agent had learned a policy that could recover a simulated satellite from a thermal runaway event in under 40 seconds — impressive on paper, until I tried to explain why it chose a particular sequence of commands. The policy was a black box. Worse, when I replayed the same scenario with a slightly perturbed sensor reading, it took a completely different and catastrophic action. I had built something that worked but couldn't be trusted, and in the context of orbital operations, "works but can't be trusted" is indistinguishable from "broken."

That experience sent me down a rabbit hole that merged three fields I had previously studied in isolation: causal inference, explainable reinforcement learning (XRL), and regulatory compliance engineering. While experimenting with causal world models for a different project, I realized that the missing ingredient wasn't better reward shaping or larger networks — it was a structural understanding of cause and effect, combined with a mechanism to translate that structure into human- and regulator-legible explanations. And then there was the compliance dimension: satellites don't operate in a legal vacuum. A single anomaly response might touch ITU radio regulations, national export-control regimes, GDPR-style data protection rules for ground-station telemetry, and mission-specific insurance covenants — often simultaneously.

This article is a synthesis of what I learned while building prototypes, reading papers late into the night, and arguing with myself about whether "explainable" and "optimal" can coexist in a safety-critical control loop. I'll walk through the architecture I converged on, share code that demonstrates the core ideas, and be honest about where the approach still breaks.

Why Satellite Anomaly Response Is a Uniquely Hard Problem

Before diving into the technical machinery, it's worth being precise about what makes this domain special. In my exploration of spacecraft operations literature, I kept encountering the same tension: anomalies are rare, high-stakes, and time-critical, which is exactly the regime where reinforcement learning struggles and where regulators demand the most transparency.

Three properties stand out:

  1. Partial observability with delayed causal effects. A power bus undervoltage might be caused by a solar array drive anomaly that occurred 90 seconds earlier, or by a battery cell degradation that's been developing for months. The agent sees symptoms, not causes.

  2. Hard constraints that are legal, not just physical. You cannot simply "try" a high-power transmission to diagnose an antenna fault if it violates spectrum coordination agreements. The action space is intersected with a jurisdiction-dependent feasibility set.

  3. Explainability as a regulatory requirement, not a nice-to-have. Under frameworks like the EU AI Act's high-risk provisions and various national space-activity licensing regimes, an autonomous system making consequential decisions must produce auditable reasoning. "The neural network said so" is not an acceptable incident report.

While studying how human flight controllers reason through anomalies, I noticed something important: they almost never reason in terms of raw state-to-action mappings. They reason in terms of causal hypotheses ("the thermal sensor is reading high because the radiator deployment failed") and counterfactuals ("if we hadn't switched to backup power, the bus would have collapsed"). This observation became the design north star for everything that followed.

The Core Idea: Causal World Models as the Substrate for Explanation

The central architectural decision I made was to separate the policy from the causal model, and to make the causal model the primary object that gets explained. Instead of asking "why did the policy output action $a$?", we ask "what causal mechanism does the agent believe is active, and how does action $a$ intervene on that mechanism?"

This is a structural causal model (SCM) formulation. Let $M = \langle U, V, F \rangle$ where $U$ are exogenous variables (unobserved root causes like component wear), $V$ are endogenous variables (observable telemetry and internal states), and $F$ are structural equations. An intervention $do(a)$ replaces the equation for the intervened variable, and the agent's policy is defined over interventions rather than raw actions.

Here's a minimal implementation of a causal world model I built using a learned SCM with neural structural equations:

import torch
import torch.nn as nn

class NeuralSCM(nn.Module):
    """Learned structural causal model for satellite subsystem telemetry."""
    def __init__(self, var_names, hidden=64):
        super().__init__()
        self.var_names = var_names
        self.idx = {n: i for i, n in enumerate(var_names)}
        # Each variable gets a structural equation conditioned on its parents
        self.parents = {
            'solar_current': [],
            'battery_soc': ['solar_current', 'load_draw'],
            'bus_voltage': ['battery_soc', 'load_draw'],
            'thermal_load': ['load_draw', 'radiator_state'],
            'radiator_state': [],
        }
        self.eqs = nn.ModuleDict({
            v: nn.Sequential(
                nn.Linear(len(self.parents[v]) + 1, hidden),  # +1 for noise
                nn.ReLU(),
                nn.Linear(hidden, 1)
            ) for v in var_names
        })

    def forward(self, u, interventions=None):
        """u: exogenous noise tensor [batch, n_vars]
           interventions: dict {var_name: value} for do() operations"""
        interventions = interventions or {}
        v = {}
        for name in self.var_names:
            if name in interventions:
                v[name] = interventions[name]
                continue
            p = self.parents[name]
            inp = torch.cat([v[par] for par in p] + [u[:, self.idx[name]:self.idx[name]+1]], dim=-1)
            v[name] = self.eqs[name](inp)
        return torch.cat([v[n] for n in self.var_names], dim=-1)
Enter fullscreen mode Exit fullscreen mode

What I found fascinating during experimentation was that even a crude learned SCM like this dramatically improved the robustness of downstream policies — not because it was more accurate than a black-box dynamics model, but because it constrained the agent to reason about interventions that respected the causal graph structure. The agent couldn't "cheat" by exploiting spurious correlations in the training data.

Reinforcement Learning Over Causal Interventions

With the SCM in place, the RL problem changes subtly but importantly. The action space is no longer raw commands; it's a set of interventions on the causal graph. The agent learns a policy $\pi(a \mid \tau)$ where $\tau$ is the history of observations and interventions, but the value function is defined over the causal state.

I used a soft actor-critic variant where the critic is trained to predict outcomes under counterfactual interventions — essentially a form of causal Q-learning. The key trick was to train the critic on interventional data generated by the SCM, not just observational data:

class CausalCritic(nn.Module):
    def __init__(self, state_dim, n_actions, hidden=128):
        super().__init__()
        self.q = nn.Sequential(
            nn.Linear(state_dim + n_actions, hidden), nn.ReLU(),
            nn.Linear(hidden, hidden), nn.ReLU(),
            nn.Linear(hidden, 1)
        )

    def counterfactual_q(self, scm, state_u, action, target_var):
        """Estimate Q under do(action) by rolling the SCM forward."""
        intervention = {target_var: action}
        next_state = scm(state_u, interventions=intervention)
        return self.q(torch.cat([next_state, action], dim=-1))
Enter fullscreen mode Exit fullscreen mode

The insight I gained here — and this took me a while to appreciate — is that counterfactual training data is cheap to generate from a causal model but impossible to generate from a black-box model. This is the practical payoff of the causal substrate: it gives you an infinite supply of "what if" scenarios that are structurally valid, which is exactly what you need to learn robust policies in a domain where real anomalies are rare.

Explainability: From Causal Structure to Human-Legible Narratives

Now for the part that actually matters for compliance. An explanation in this framework has three layers:

  1. Structural attribution: which causal mechanisms contributed most to the observed anomaly, computed via counterfactual Shapley values over the SCM.
  2. Interventional justification: why the chosen action is preferred, expressed as a comparison of counterfactual outcomes under alternative interventions.
  3. Constraint attestation: which jurisdictional constraints were checked and how the action satisfies them.

The first two are causal-inference machinery; the third is where compliance engineering enters. Let me show a compact version of the attribution layer:

def counterfactual_shapley(scm, state_u, anomaly_var, n_samples=64):
    """Attribute an anomaly to upstream causes via counterfactual Shapley values."""
    var_names = scm.var_names
    baseline = scm(state_u)  # observational prediction
    attributions = {}
    for var in var_names:
        if var == anomaly_var: continue
        # Intervene to 'healthy' baseline for this variable
        healthy_u = state_u.clone()
        healthy_u[:, scm.idx[var]] = 0.0
        cf = scm(healthy_u)
        attributions[var] = (baseline[:, scm.idx[anomaly_var]] -
                             cf[:, scm.idx[anomaly_var]]).mean().item()
    return sorted(attributions.items(), key=lambda x: -abs(x[1]))
Enter fullscreen mode Exit fullscreen mode

Running this on a simulated thermal anomaly produced outputs like [('radiator_state', 0.71), ('load_draw', 0.22), ('solar_current', 0.04)] — which a human operator can immediately interpret as "the radiator failed to deploy; load contributed but is not the root cause." That's a sentence you can put in an incident report. That's the difference between a system that's merely autonomous and one that's accountable.

Multi-Jurisdictional Compliance as a Constraint Layer

Here's where my learning curve got steepest. I initially thought of compliance as a post-hoc filter — just check the action against a rulebook before executing. That approach fails badly, because the feasible action set is jurisdiction-dependent and the agent needs to reason about which jurisdiction applies to which action.

The model I converged on treats compliance as a set of context-dependent constraints that modify the intervention space. Each constraint is a predicate over (action, causal state, jurisdiction context):

from dataclasses import dataclass
from typing import Callable

@dataclass
class JurisdictionalConstraint:
    jurisdiction: str
    rule_id: str
    predicate: Callable  # (action, causal_state, context) -> bool
    severity: str  # 'blocking' | 'advisory'

class ComplianceLayer:
    def __init__(self, constraints):
        self.constraints = constraints

    def feasible_actions(self, action_space, causal_state, context):
        feasible, attestations = [], []
        for a in action_space:
            ok, reasons = True, []
            for c in self.constraints:
                if c.jurisdiction not in context.active_jurisdictions:
                    continue
                if not c.predicate(a, causal_state, context):
                    reasons.append((c.rule_id, c.severity))
                    if c.severity == 'blocking':
                        ok = False
            if ok:
                feasible.append(a)
                attestations.append({'action': a, 'checked': reasons or 'all_passed'})
        return feasible, attestations
Enter fullscreen mode Exit fullscreen mode

A concrete example: a do(high_power_transmit) intervention might be blocking-under-ITU if the satellite is over a region where the downlink frequency is coordinated to another operator, but merely advisory-under-national-license if it's a brief diagnostic pulse. The agent learns a policy over the feasible intervention set, and the compliance layer produces an attestation log that becomes part of the explanation.

What I realized while building this is that compliance constraints are themselves a form of causal structure. They tell the agent which interventions are even admissible, which prunes the causal graph in a jurisdiction-aware way. This unifies the two concerns rather than bolting them together.

Putting It Together: The Full Loop

The complete architecture, as I implemented it in a simulation environment, looks like this:

  1. Perception: telemetry → learned SCM state estimate (with uncertainty).
  2. Anomaly detection: counterfactual prediction error flags anomalies and localizes them to causal variables.
  3. Policy: SAC agent selects interventions from the compliance-filtered feasible set.
  4. Explanation generation: Shapley attribution + counterfactual action comparison + constraint attestation → structured explanation object.
  5. Execution / human-in-the-loop: explanation is either logged (autonomous mode) or presented to an operator (supervised mode).

The explanation object is the key artifact. Here's what one looks like in practice:

{
  "anomaly": "thermal_load_high",
  "root_cause_attribution": [("radiator_state", 0.71), ("load_draw", 0.22)],
  "chosen_intervention": "do(reduce_load_draw, target=0.3)",
  "counterfactual_comparison": {
      "do(reduce_load_draw)": {"expected_thermal": 0.41, "risk": 0.02},
      "do(no_action)":        {"expected_thermal": 0.89, "risk": 0.61},
      "do(emergency_shutdown)": {"expected_thermal": 0.38, "risk": 0.14}
  },
  "compliance_attestations": [
      {"jurisdiction": "ITU", "rule": "RR-9.7", "status": "passed"},
      {"jurisdiction": "US-ITAR", "rule": "126.1", "status": "passed"}
  ],
  "confidence": 0.87
}
Enter fullscreen mode Exit fullscreen mode

This is auditable. A regulator can trace every field back to a causal mechanism and a rule. That's the whole point.

Challenges I Hit — and What I Learned

Challenge 1: SCM misspecification. My first causal graph was wrong — I omitted a confounder between solar current and thermal load (shared dependency on orbital position). The agent learned a policy that worked in simulation but would have failed on orbit. Solution: I added a structure-learning step using interventional data from the simulator, and I now treat the causal graph as a hypothesis to be continuously validated, not a fixed truth.

Challenge 2: Explanation fidelity. Early explanations were plausible but not faithful to the policy's actual computation. I was generating post-hoc rationalizations. Solution: I made the policy explicitly operate over the SCM's intervention space, so the explanation is derived from the same object the policy uses. Faithfulness became structural rather than aspirational.

Challenge 3: Compliance rule explosion. With multiple jurisdictions, the constraint set grew combinatorially. Solution: I organized constraints into a hierarchy with jurisdiction precedence rules, and cached feasibility computations — a compliance "context" object that resolves which rules apply before the policy runs.

Challenge 4: The optimality-explainability tradeoff. A fully explainable policy is often slightly suboptimal compared to a black-box one. I measured this gap in my simulations: roughly 4–7% worse on the primary reward metric, but with dramatically lower variance and near-zero catastrophic failures. Learning: in safety-critical domains, that trade is almost always worth it.

Quantum-Accelerated Counterfactual Sampling

One tangent that surprised me: counterfactual sampling from the SCM is embarrassingly parallel, and for large causal graphs the bottleneck is sampling many interventional trajectories. I experimented with a quantum amplitude estimation approach for estimating expected counterfactual outcomes, using a small simulated quantum circuit via Qiskit's statevector simulator. The idea is that amplitude estimation can give a quadratic speedup for estimating expectations of bounded functions — exactly the form of counterfactual value computation.

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator

def counterfactual_amplitude_estimate(scm, state_u, intervention, n_qubits=6):
    """Toy amplitude-estimation sketch for expected counterfactual outcome."""
    qc = QuantumCircuit(n_qubits)
    # Prepare uniform superposition over interventional samples
    qc.h(range(n_qubits))
    # Oracle encoding: mark samples whose outcome exceeds threshold
    # (in practice, compiled from the SCM's structural equations)
    qc.measure_all()
    sim = AerSimulator()
    return sim.run(qc, shots=4096).result().get_counts()
Enter fullscreen mode Exit fullscreen mode

I want to be honest: this is speculative and I haven't demonstrated a real speedup for realistic problem sizes. But the connection is real — counterfactual reasoning is fundamentally about expectations over interventions, and quantum amplitude estimation is fundamentally about estimating expectations. It's a thread I'm still pulling.

Real-World Applications and Where This Is Heading

The architecture I've described maps onto several concrete use cases I've seen discussed in the space operations community:

  • Autonomous constellation management: a fleet of satellites where each agent reasons causally about its own anomalies and coordinates with neighbors under shared spectrum constraints.
  • Ground-segment anomaly response: the same causal-XRL loop applied to ground station scheduling and RF interference resolution.
  • Insurance and liability workflows: the explanation object becomes a first-class artifact in claims processing, because it provides a causal narrative and a compliance attestation trail.

The broader trend I'm watching is the convergence of agentic AI with regulatory-grade explainability. As autonomous systems take on consequential decisions, the ability to produce faithful, causally-grounded, jurisdiction-aware explanations stops being a research curiosity and becomes a deployment prerequisite. My exploration of this space convinced me that causal structure is the right substrate — not because it

Top comments (0)