The Thruster That Spun It Faster — Python Bug Hunt

Modelled on the loss of JAXA's Hitomi (ASTRO-H) X-ray observatory in March 2016.

  • Language: Python
  • Layer: Backend
  • Difficulty: Hard
  • Concepts: Config, State, Validation
  • Modelled on: JAXA Hitomi · 2016
  • Visible tests: safe-hold damps an x-axis spin; safe-hold damps a z-axis spin
  • Reward: 50 XP for a complete fix

Briefing

Modelled on the loss of JAXA's Hitomi (ASTRO-H) X-ray observatory in March 2016. JAXA's investigation found that the attitude system wrongly believed the craft was rotating and tried to correct it, which made it actually rotate; when it then entered safe mode, the thrusters fired using incorrect parameters that had been uploaded after a software change and not properly checked. The firing increased the spin instead of stopping it, and parts of the spacecraft, including the solar array paddles, broke off.

This project is a reconstruction. safehold.py damps body rates by firing the thruster that produces the opposing torque, looked up in a hand-entered command table. The thruster geometry in geometry.py (mounting point r, thrust direction f, torque = r × f) is the ground truth; sim.py applies the resulting torque.

Fix the command table so every entry matches the geometry and safe-hold removes spin on every axis.

Bug report

BUG-ASTRO-H · Priority: Critical (loss of vehicle) · Reported by: attitude control team

COMMAND_TABLE maps a demanded torque ("+x", "-x", "+y", "-y", "+z", "-z") to the thruster that produces it: geometry.torque(COMMAND_TABLE[key]) must be a unit vector along exactly that axis, with that sign.

select(rate) returns, in x, y, z order, the thrusters to fire for one pulse:

  • axes with abs(rate) <= DEADBAND (0.05 rad/s) fire nothing
  • otherwise fire the thruster demanded by the OPPOSITE sign of that axis' rate (rate > 0 demands "-axis", rate < 0 demands "+axis")

Running sim.run(rate, select, pulses) long enough must bring every axis inside the deadband.

Observed: a z-axis spin gets faster with every safe-hold pulse.

Logs

[aocs] SAFE_HOLD entered rate=(0.00, 0.00, 0.50) rad/s
[aocs] pulse 5 rate_z=1.00
[aocs] pulse 10 rate_z=1.50 — structural limit exceeded

The code as shipped

src/aocs/safehold.py (editable)

# Thruster selection for safe-hold rate damping.
# Key: the torque the controller demands.
COMMAND_TABLE = {
    "+x": "RCS-2",
    "-x": "RCS-1",
    "+y": "RCS-3",
    "-y": "RCS-4",
    "+z": "RCS-6",
    "-z": "RCS-5",
}

DEADBAND = 0.05
AXES = ["x", "y", "z"]


def select(rate):
    """Thrusters to fire this pulse to oppose the measured body rate."""
    fired = []
    for i in range(3):
        if abs(rate[i]) <= DEADBAND:
            continue
        demand = ("-" if rate[i] > 0 else "+") + AXES[i]
        fired.append(COMMAND_TABLE[demand])
    return fired

Read-only context: src/aocs/geometry.py, src/aocs/sim.py.

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More Python bug hunts.