🗺 Phase Map 1 · Mindset 2 · Questions A1 · Telemetry A2 · Windows A3 · Rate Limiter A4 · State Machine A5 · Packet Loss A6 · OOL Alarms A7 · CMD ACK 4 · Bugs 5 · Scripts 6 · Q&A
🚀 Launch Simulator ← Mission Hub

🚀 Rocket Lab Technical Screen Reference

Candidate: Aidan McDowell  |  Role: Ground Software Engineer II — Long Beach, CA  |  Interviewer: Anh Thai (Ground Software Engineer)  |  Format: 30 min · Microsoft Teams · ~20–25 min coding

📍 CHRONOLOGICAL CALL NAVIGATION & VISUAL TRIGGER MAP

Click any blue button to jump to that section. Every section has a [ ↩ Back ] button to return here.

⏱ Phase 1 · Problem Reveal (Minutes 0–7)
🎯 Trigger: Anh shares screen / pastes prompt and says "Take a look, let me know if you have questions."

Step 1 — Identify the archetype in 5 seconds:

Step 2 — Ask clarifying questions while reading: 2 · Clarifying Questions

"Before I start — can packets arrive as None or with missing keys? And can the input list be completely empty?"
⌨️ Phase 2 · Active Coding (Minutes 8–20)
🎯 Trigger: You start typing. Narrator mode ON.
3 · Code Skeletons 5 · Verbal Scripts
🚨 Phase 3 · Red Error in Terminal
🎯 Trigger: You hit Run Tests and see an error.
4 · Fatal Bugs Table
📈 Phase 4 · Scale Curveball (Minutes 20–24)
🎯 Trigger: Anh says — "That works. What happens at 100,000 packets per second over a 12-minute pass?"
1 · Batch vs Streaming 5 · Scale Script
"Right now this is O(N) memory — storing every packet bloats RAM. To run at constant O(1) memory, I'd maintain running state only: a running sum, packet count, and current max per subsystem — never buffering the raw list."
🙋 Phase 5 · Candidate Q&A (Final 4 Minutes)
🎯 Trigger: Anh says "We have a few minutes left — what questions do you have for me?"
6 · Questions to Ask Anh
"How is the Ground Software team architecting the boundary between real-time binary ingestion from the ground stations and the WebSocket feeds pushing to mission control displays?"

1. The Ground Station Mindset: Plain-English Primer

⬆️ Back to Map ↩️ Back to Phase 4 (Scale)

2. The 3 Clarifying Questions to Ask Before Coding

⬆️ Back to Map ↩️ Back to Phase 1
  1. "Can packets arrive as None, or with missing keys like subsystem or voltage?"
  2. "Can timestamps arrive out of order, or can the input list be completely empty?"
  3. "Is this a discrete in-memory batch, or should I structure it for an unbounded stream?"
These buy you ~60 seconds while Anh answers — use that time to glance at the right archetype below.

3. The 4 Ground Software Code Archetypes

⬆️ Back to Map ↩️ Back to Phase 1 ↩️ Back to Phase 2

Archetype 1 · Telemetry Aggregation & Anomaly Filter Scenario 1 process_bus_telemetry

⬆️ Back to Map ↩️ Back to Phase 1 ↩️ Back to Phase 2

Keywords: voltage, temperature, status, corrupt, missing, average, max, threshold, flag

The 5-Step Mental Model:

  1. Guard empty input → return {}
  2. Loop with .get() on every key
  3. Skip: status != "OK" or voltage is None or not name
  4. Accumulate: running sum, count, max per subsystem
  5. Build output with avg and flagged = avg > 32.0
def process_bus_telemetry(packets):
    # SAY: "First thing — guard the empty case up front"
    if not packets:
        return {}

    subsystems = {}  # SAY: "Dict to accumulate per-subsystem running stats"

    for packet in packets:
        if not isinstance(packet, dict):    # SAY: "Skip corrupt non-dict entries from the downlink"
            continue

        # SAY: "Using .get() — never bracket access on untrusted data"
        name    = packet.get("subsystem")
        voltage = packet.get("voltage")
        status  = packet.get("status")

        # SAY: "Triple filter — skip ERROR status, None voltages, missing names"
        if status != "OK" or voltage is None or not name:
            continue

        # SAY: "Initialize new subsystem with first voltage as seed max"
        if name not in subsystems:
            subsystems[name] = {"sum": 0, "count": 0, "max": voltage}

        # SAY: "Running accumulation — O(1) per packet"
        subsystems[name]["sum"]   += voltage
        subsystems[name]["count"] += 1
        if voltage > subsystems[name]["max"]:
            subsystems[name]["max"] = voltage

    # SAY: "Build output dict — count > 0 is guaranteed since we only stored valid packets"
    result = {}
    for name, data in subsystems.items():
        avg = data["sum"] / data["count"]
        result[name] = {
            "max"    : data["max"],
            "avg"    : avg,
            "count"  : data["count"],
            "flagged": avg > 32.0  # SAY: "Flag subsystems above 32V threshold"
        }
    return result
Edge CaseGuard Used
Empty listif not packets: return {}
None voltagevoltage is None → continue
Missing keypacket.get("subsystem")
Corrupt non-dictisinstance(packet, dict)
ZeroDivisionErrorcount guaranteed > 0 since only valid packets enter accumulator

Archetype 2 · Ground Station Contact Window Merging Scenario 2 merge_passes

⬆️ Back to Map ↩️ Back to Phase 1 ↩️ Back to Phase 2

Keywords: passes, schedules, contact windows, overlap, intervals, merge, antenna, LOS

The 3-Step Mental Model:

  1. Guard empty → return []
  2. Sort by start time
  3. Walk through: if current_start <= prev_end → merge (extend end); else → append new block
def merge_passes(intervals):
    # Guard: empty input
    if not intervals:
        return []

    # Step 1: Sort chronologically by start time
    intervals.sort(key=lambda x: x[0])

    merged = [intervals[0][:]  ]    # copy of first window

    for start, end in intervals[1:]:
        prev_start, prev_end = merged[-1]

        if start <= prev_end:          # overlapping — extend
            merged[-1][1] = max(prev_end, end)
        else:                           # gap — new block
            merged.append([start, end])

    return merged
InputOutput
[[10,14],[12,18],[20,25]][[10,18],[20,25]]
[][]
[[1,5],[6,10]][[1,5],[6,10]] — not overlapping
[[20,25],[1,3],[2,7],[15,22]] (unsorted)[[1,7],[15,25]]

Key insight: Adjacent intervals like [1,5] and [6,10] are not overlapping — overlap requires start <= prev_end (strictly less than or equal).

Archetype 3 · Uplink Command Rate Limiter (Sliding Window) Scenario 3 CommandLimiter

⬆️ Back to Map ↩️ Back to Phase 1 ↩️ Back to Phase 2

Keywords: throttle, max commands, rate limit, sliding window, transceiver, uplink

The 3-Step Mental Model:

  1. __init__: store max_cmds, window_sec, and a deque of timestamps
  2. allow_command(t): purge timestamps older than t - window_sec
  3. If len(deque) < max_cmds: append t → return True. Else False.
from collections import deque

class CommandLimiter:
    def __init__(self, max_cmds: int, window_sec: int):
        self.max_cmds   = max_cmds
        self.window_sec = window_sec
        self.timestamps = deque()          # O(1) popleft

    def allow_command(self, timestamp: float) -> bool:
        cutoff = timestamp - self.window_sec

        # Step 1: Purge expired timestamps from the front
        while self.timestamps and self.timestamps[0] <= cutoff:
            self.timestamps.popleft()

        # Step 2: Check capacity
        if len(self.timestamps) < self.max_cmds:
            self.timestamps.append(timestamp)
            return True

        return False
CallResultWhy
allow(1.0)✅ True1st of 2 allowed
allow(2.0)✅ True2nd of 2 allowed
allow(3.0)❌ False2 commands in [1.0–6.0]
allow(6.5)✅ True1.0 expired (6.5–5=1.5)

Why deque? popleft() is O(1). A list's pop(0) is O(N). At 100k commands/sec that matters.

Archetype 4 · Flight State Machine Sequence Validator Scenario 4 validate_sequence

⬆️ Back to Map ↩️ Back to Phase 1 ↩️ Back to Phase 2

Keywords: flight modes, stage separation, transitions, ABORT, illegal jump, state, sequence

The 4-Step Mental Model:

  1. Guard: empty list → {"valid": False, "error": "No events"}
  2. Guard: first event must be "PRE_LAUNCH"
  3. Dict maps each state to allowed next states
  4. Walk pairs: if next not in allowed → return error with index
VALID_TRANSITIONS = {
    "PRE_LAUNCH"      : ["BOOST"],
    "BOOST"           : ["STAGE_SEP"],
    "STAGE_SEP"       : ["COAST"],
    "COAST"           : ["PAYLOAD_DEPLOY"],
    "PAYLOAD_DEPLOY"  : ["MISSION_COMPLETE"],
    "MISSION_COMPLETE": []
}

def validate_sequence(events):
    if not events:
        return {"valid": False, "error": "No events provided"}

    if events[0] != "PRE_LAUNCH":
        return {"valid": False,
                "error": f"Must start with PRE_LAUNCH, got {events[0]}"}

    for i in range(1, len(events)):
        current = events[i - 1]
        nxt     = events[i]
        allowed = VALID_TRANSITIONS.get(current, [])  # safe: unknown state → []

        if nxt not in allowed:
            return {"valid": False,
                    "error": f"Invalid transition from {current} to {nxt} at index {i}"}

    return {"valid": True, "final_state": events[-1]}
InputOutput
["PRE_LAUNCH","BOOST","STAGE_SEP"]✅ valid, final_state STAGE_SEP
["PRE_LAUNCH","PAYLOAD_DEPLOY"]❌ Invalid at index 1
["BOOST","STAGE_SEP"]❌ Must start with PRE_LAUNCH
[]❌ No events provided
["PRE_LAUNCH","BOOST","WARP_DRIVE"]❌ WARP_DRIVE not in allowed

Archetype 5 · Telemetry Packet Loss & Sequence Continuity Scenario 5 detect_packet_drops

⬆️ Back to Map ↩️ Back to Phase 1 ↩️ Back to Phase 2

Keywords: seq_num, sequence counter, monotonic, dropped frames, packet loss, RF drop, gap, loss_rate_pct, frame_gap

The 4-Step Mental Model:

  1. Guard empty → return {received:0, dropped:0, loss_rate_pct:0.0, missing_seqs:[]}
  2. Extract valid seq_nums with .get() + isinstance check, sort them
  3. Span = (last_seq - first_seq) + 1 — this is expected count, not received
  4. Convert to set(), loop range(first, last+1) — anything missing goes in missing[]
def detect_packet_drops(packets):
    # SAY: "Guard empty input first — can't index an empty list"
    if not packets:
        return {"received": 0, "dropped": 0, "loss_rate_pct": 0.0, "missing_seqs": []}

    # SAY: "Filter with .get() — skip None seq_nums and non-dict packets"
    valid_seqs = sorted([
        p.get("seq_num")
        for p in packets
        if isinstance(p, dict) and p.get("seq_num") is not None
    ])

    if not valid_seqs:
        return {"received": 0, "dropped": 0, "loss_rate_pct": 0.0, "missing_seqs": []}

    first_seq = valid_seqs[0]
    last_seq  = valid_seqs[-1]
    expected_count = (last_seq - first_seq) + 1   # SAY: "Span of expected IDs"
    received_set   = set(valid_seqs)               # SAY: "O(1) membership check"

    missing = []
    for num in range(first_seq, last_seq + 1):
        if num not in received_set:
            missing.append(num)

    dropped_count = len(missing)
    # SAY: "Loss rate over EXPECTED span — not over received"
    loss_pct = round((dropped_count / expected_count) * 100.0, 2)

    return {
        "received"     : len(valid_seqs),
        "dropped"      : dropped_count,
        "loss_rate_pct": loss_pct,
        "missing_seqs" : missing
    }
Input (seq_nums)receiveddroppedmissing_seqsloss_rate_pct
[101,102,104,105]41[103]20.0%
[10,11,12]30[]0.0%
[]00[]0.0%
[5, None, 7] (None skipped)21[6]50.0%

Key trap: Loss rate denominator = expected span (last − first + 1), not received count. 4 received out of span 5 → 20% loss, not 25%.

Curveball: "The seq_num wraps at 65535 (16-bit). How does your detector handle [65534, 65535, 0, 1]?" — Answer: naively it would report 65534 false drops. Fix: detect rollover with if last_seq < first_seq: last_seq += 65536.

Archetype 6 · Out-of-Limits (OOL) Telemetry Alarm Monitor Scenario 6 evaluate_telemetry_limits

⬆️ Back to Map ↩️ Back to Phase 1 ↩️ Back to Phase 2

Keywords: OOL, Out-of-Limits, yellow_low/high, red_low/high, WARNING, CRITICAL, alarm_severity, abort threshold, limit band

The 4-Step Mental Model:

  1. Guard: if not telemetry_points or not limit_table: return []
  2. Per point: isinstance check, .get() for sensor/value/ts, skip if unknown sensor or val is None
  3. Red (CRITICAL) check FIRST — then elif Yellow (WARNING). Never check yellow before red.
  4. Append alarm dict only if severity is not None
def evaluate_telemetry_limits(telemetry_points, limit_table):
    if not telemetry_points or not limit_table:
        return []

    alarms = []
    for point in telemetry_points:
        if not isinstance(point, dict):
            continue
        sensor = point.get("sensor")
        val    = point.get("value")
        ts     = point.get("timestamp")

        if sensor not in limit_table or val is None:
            continue

        limits   = limit_table[sensor]
        severity = None

        # SAY: "Red check FIRST — critical violations take precedence"
        if val < limits.get("red_low",  float('-inf')) or \
           val > limits.get("red_high", float('inf')):
            severity = "CRITICAL"
        elif val < limits.get("yellow_low",  float('-inf')) or \
             val > limits.get("yellow_high", float('inf')):
            severity = "WARNING"

        if severity:
            alarms.append({
                "sensor"  : sensor,
                "value"   : val,
                "severity": severity,
                "timestamp": ts
            })
    return alarms
pressure_psi valueYellow [2800–3200]Red [2500–3500]Alarm
3000✅ nominal✅ nominalNone
3300❌ above 3200✅ nominalWARNING
3600❌❌ above 3500CRITICAL
2400❌❌ below 2500CRITICAL
None——Skipped

Key trap: Use float('-inf') / float('inf') as .get() defaults so missing limit keys never raise a TypeError.

Curveball: "Transient spikes — alert only after 3 consecutive OOL readings." — Answer: add a consecutive = {} dict tracking per-sensor streak; only emit alarm when consecutive[sensor] >= 3.

Archetype 7 · Uplink Command ACK & Timeout Tracker Scenario 7 verify_command_acks

⬆️ Back to Map ↩️ Back to Phase 1 ↩️ Back to Phase 2

Keywords: ACK, command acknowledgment, uplink, telemetry_ack, round_trip_latency, command_timeout, CONFIRMED, REJECTED, TIMEOUT

The 3-Step Mental Model:

  1. Guard: if not commands: return {}. Build ACK index: ack_map = {a["cmd_id"]: a for a in (acks or [])}
  2. Loop commands with .get() — skip missing cmd_id or sent_time
  3. If cmd_id not in ack_map → "TIMEOUT". Else check status == "REJECTED" first, then timing: ack_time - sent_time <= timeout_sec
def verify_command_acks(commands, acks):
    if not commands:
        return {}

    # SAY: "Index ACKs by cmd_id for O(1) lookup — never loop inside a loop"
    ack_map = {}
    for a in (acks or []):    # (acks or []) handles None acks safely
        if isinstance(a, dict) and "cmd_id" in a:
            ack_map[a["cmd_id"]] = a

    results = {}
    for cmd in commands:
        if not isinstance(cmd, dict):
            continue
        cid     = cmd.get("cmd_id")
        sent    = cmd.get("sent_time")
        timeout = cmd.get("timeout_sec", 5.0)

        if not cid or sent is None:
            continue

        if cid not in ack_map:
            results[cid] = "TIMEOUT"       # No ACK received at all
            continue

        ack        = ack_map[cid]
        ack_time   = ack.get("ack_time", float('inf'))
        ack_status = ack.get("status")

        # SAY: "REJECTED check before timing — status takes priority"
        if ack_status == "REJECTED":
            results[cid] = "REJECTED"
        elif ack_time - sent <= timeout:
            results[cid] = "CONFIRMED"
        else:
            results[cid] = "TIMEOUT"       # ACK arrived, but too late

    return results
cmd_idsent_timetimeout_secack_timeack_statusResult
CMD_001100.05.0103.0OK✅ CONFIRMED
CMD_002200.03.0210.0OK⏱ TIMEOUT (10s > 3s)
CMD_003300.05.0302.0REJECTED❌ REJECTED
CMD_004400.05.0——⏱ TIMEOUT (no ACK)

Key trap: Use (acks or []) — if the caller passes acks=None, iterating over None raises a TypeError. The or [] idiom handles it silently.

Curveball: "Spacecraft sends two ACKs per command: Accepted + Executed. A REJECTED on Executed means it accepted but failed mid-execution." — Answer: store a list of ACKs per cmd_id; take the last ACK as the definitive status.


4. The 4 Fatal Bugs & Exact Fixes

⬆️ Back to Map ↩️ Back to Phase 3 (Error Screen)
ErrorCauseFix
KeyError Using packet["key"] when key is absent packet.get("key")
TypeError: '>' not supported on None Comparing or adding None if val is not None: before math
ZeroDivisionError Dividing by count when no valid packets if count > 0: avg = total / count
AttributeError Packet is a string or None, not a dict if not isinstance(packet, dict): continue

5. Verbal Scripts — What to Say Out Loud While Coding

⬆️ Back to Map ↩️ Back to Phase 2

Scale / O(1) Memory Script

↩️ Back to Phase 4 (Scale Curveball)
"Right now this is O(N) memory — storing every packet in a list means RAM scales with pass duration. To handle 100,000 packets per second at O(1) memory, I'd keep only running state per subsystem: a running sum, a count, and a current max. Three variables total, updated in-place on every packet. Memory stays flat regardless of pass length."

6. High-Impact Questions to Ask Anh (Pick 2)

⬆️ Back to Map ↩️ Back to Phase 5 (Q&A)
  1. "How is the Ground Software team currently architecting the boundary between real-time binary ingestion from the ground stations and the WebSocket feeds pushing to mission control displays?"
  2. "What does the handoff and telemetry schema synchronization look like between the Flight Software team on the vehicle and your Ground Software team?"
  3. "How does the team handle packet ordering and replay when the ground station loses contact mid-pass and then reconnects?"
Close with: "That makes a lot of sense, especially with pass durations being so tight. Thanks for walking through the problem with me, Anh!"