#!/usr/bin/env python3 """ Apollo PRN Ranging Demo -- generate, delay, correlate, measure. Demonstrates the Apollo ranging system by: 1. Verifying component code properties (lengths, periodicity, balance) 2. Generating the PRN ranging code 3. NRZ encoding to a bipolar waveform 4. Applying a known propagation delay (simulating spacecraft distance) 5. Optionally adding AWGN noise 6. Cross-correlating to recover the delay 7. Comparing measured range to true range The demo works at chip rate (1 sample per chip) for simplicity and speed. No GNU Radio runtime is required. Usage: uv run python examples/ranging_demo.py uv run python examples/ranging_demo.py --range-km 100 uv run python examples/ranging_demo.py --range-km 384400 # Moon distance uv run python examples/ranging_demo.py --snr 20 uv run python examples/ranging_demo.py --chips 200000 """ import argparse import time import numpy as np from apollo.ranging import ( RANGING_A_LENGTH, RANGING_B_LENGTH, RANGING_C_LENGTH, RANGING_CHIP_RATE_HZ, RANGING_CL_LENGTH, RANGING_X_LENGTH, SPEED_OF_LIGHT_M_S, RangingCodeGenerator, RangingCorrelator, range_m_to_chips, verify_code_properties, ) def main(): parser = argparse.ArgumentParser(description="Apollo PRN ranging demo") parser.add_argument( "--range-km", type=float, default=100.0, help="Target range in km (default: 100)", ) parser.add_argument( "--snr", type=float, default=None, help="SNR in dB (default: no noise)", ) parser.add_argument( "--chips", type=int, default=50_000, help="Number of chips for correlation (default: 50000)", ) args = parser.parse_args() print("=" * 60) print("Apollo PRN Ranging Demo") print("=" * 60) # ------------------------------------------------------------------ # Step 1: Verify code properties # ------------------------------------------------------------------ print() print("1. Component code verification") print("-" * 40) props = verify_code_properties() for name in ("cl", "x", "a", "b", "c"): p = props[name] status = "OK" if (p["length_correct"] and p["periodic"]) else "FAIL" balance = "" if "balance_correct" in p: balance = f", balance={'OK' if p['balance_correct'] else 'FAIL'}" print( f" {name.upper():>2}: length={p['length']:>3} " f"(1s={p['ones_count']}, 0s={p['zeros_count']}), " f"periodic={p['periodic']}{balance} [{status}]" ) lp = props["length_product"] print( f" Code length: {lp['expected']:,} " f"= {RANGING_CL_LENGTH}*{RANGING_X_LENGTH}*{RANGING_A_LENGTH}" f"*{RANGING_B_LENGTH}*{RANGING_C_LENGTH} " f"[{'OK' if lp['matches_constant'] else 'FAIL'}]" ) cs = props["composite_sample"] print(f" Composite sample ({cs['length']:,} chips): balance={cs['balance']:.3f} (ideal ~0.5)") # ------------------------------------------------------------------ # Step 2: Generate PRN code # ------------------------------------------------------------------ print() print(f"2. Generating {args.chips:,} PRN chips...") t0 = time.time() gen = RangingCodeGenerator() code = gen.generate_sequence(n_chips=args.chips) t_gen = time.time() - t0 print(f" Generated in {t_gen * 1000:.1f} ms") print( f" Ones: {int(np.sum(code)):,} / {args.chips:,} ({100 * np.sum(code) / args.chips:.1f}%)" ) # ------------------------------------------------------------------ # Step 3: NRZ encode # ------------------------------------------------------------------ nrz = code.astype(np.float32) * 2.0 - 1.0 # ------------------------------------------------------------------ # Step 4: Apply delay # ------------------------------------------------------------------ true_range_m = args.range_km * 1000.0 delay_chips = range_m_to_chips(true_range_m, two_way=True) delay_samples = int(round(delay_chips)) # At chip rate, 1 sample = 1 chip # Wrap delay within sequence length delay_samples = delay_samples % args.chips print() print(f"3. Simulating range: {args.range_km:.1f} km") print(f" Round-trip distance: {true_range_m * 2 / 1000:.1f} km") print(f" Round-trip time: {2 * true_range_m / SPEED_OF_LIGHT_M_S * 1000:.4f} ms") print(f" Delay: {delay_chips:.2f} chips ({delay_samples} samples)") received = np.roll(nrz, delay_samples) # ------------------------------------------------------------------ # Step 5: Add noise # ------------------------------------------------------------------ if args.snr is not None: noise_power = 1.0 / (10.0 ** (args.snr / 10.0)) noise = np.random.default_rng(42).standard_normal(len(received)).astype(np.float32) noise *= np.sqrt(noise_power) received = received + noise print(f" Added AWGN noise at {args.snr:.0f} dB SNR (noise power = {noise_power:.4f})") # ------------------------------------------------------------------ # Step 6: Correlate # ------------------------------------------------------------------ print() print("4. Cross-correlating...") correlator = RangingCorrelator( chip_rate=RANGING_CHIP_RATE_HZ, sample_rate=RANGING_CHIP_RATE_HZ, # 1 sample per chip two_way=True, ) t0 = time.time() result = correlator.correlate(received) t_corr = time.time() - t0 measured_range_m = result["range_m"] error_m = abs(measured_range_m - true_range_m) # The quantization error at chip rate: # one chip = c / (2 * chip_rate) meters (two-way) quant_m = SPEED_OF_LIGHT_M_S / (2.0 * RANGING_CHIP_RATE_HZ) print(f" Correlation time: {t_corr * 1000:.1f} ms") print(f" Peak-to-average ratio: {result['peak_to_avg_ratio']:.1f}") # ------------------------------------------------------------------ # Step 7: Results # ------------------------------------------------------------------ print() print("5. Range measurement results") print("-" * 40) print(f" True range: {args.range_km:>12.1f} km") print(f" Measured range: {measured_range_m / 1000:>12.1f} km") print(f" Error: {error_m:>12.1f} m") print(f" Quantization step: {quant_m:>12.1f} m (1 chip, two-way)") print(f" Delay (chips): {result['delay_chips']:>12.2f}") print(f" Delay (samples): {result['delay_samples']:>12d}") print(f" Correlation peak: {result['correlation_peak']:>12.0f}") if error_m <= quant_m: print() print(" Error is within one quantization step -- measurement is correct.") elif error_m <= quant_m * 2: print() print(" Error is within two quantization steps -- acceptable.") else: print() print(" Error exceeds quantization limit. Try more --chips or higher --snr.") print() print("=" * 60) if __name__ == "__main__": main()