Skip to content

Tutorial: Radar Detection Trade Study

Design and optimize a phased array radar for target detection.

Objective

By the end of this tutorial, you will:

  • Configure a radar detection scenario
  • Set radar-specific requirements
  • Explore the power-aperture trade-off
  • Analyze detection performance across designs

Scenario

Design a search radar to detect a 1 m² target at 100 km with:

  • 90% detection probability
  • 10⁻⁶ false alarm probability
  • Budget constraint of $500,000

Step 1: Setup

"""Radar detection trade study tutorial."""

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from pathlib import Path

from phased_array_systems.architecture import Architecture, ArrayConfig, RFChainConfig, CostConfig
from phased_array_systems.scenarios import RadarDetectionScenario
from phased_array_systems.requirements import Requirement, RequirementSet
from phased_array_systems.evaluate import evaluate_case
from phased_array_systems.trades import (
    DesignSpace,
    generate_doe,
    BatchRunner,
    filter_feasible,
    extract_pareto,
    rank_pareto,
)
from phased_array_systems.viz import pareto_plot, scatter_matrix
from phased_array_systems.io import export_results

Step 2: Define the Scenario

scenario = RadarDetectionScenario(
    # Operating frequency
    freq_hz=10e9,              # X-band

    # Target characteristics
    target_rcs_m2=1.0,         # 1 m² RCS
    range_m=100e3,             # 100 km detection range

    # Detection requirements
    required_pd=0.9,           # 90% detection probability
    pfa=1e-6,                  # 10⁻⁶ false alarm rate

    # Waveform parameters
    pulse_width_s=10e-6,       # 10 μs pulse
    prf_hz=1000,               # 1 kHz PRF

    # Integration
    n_pulses=10,               # Integrate 10 pulses
    integration_type="coherent",  # Coherent integration

    # Target model
    swerling_model=1,          # Swerling 1 (slow fluctuation)

    # Scan
    scan_angle_deg=0.0,        # Boresight
)

print(f"Radar Scenario:")
print(f"  Frequency: {scenario.freq_hz/1e9:.1f} GHz")
print(f"  Target RCS: {scenario.target_rcs_m2} m²")
print(f"  Range: {scenario.range_m/1e3:.0f} km")
print(f"  Required Pd: {scenario.required_pd}")
print(f"  Pfa: {scenario.pfa}")
print(f"  Integration: {scenario.n_pulses} pulses ({scenario.integration_type})")

Step 3: Define Requirements

requirements = RequirementSet(
    requirements=[
        Requirement(
            id="DET-001",
            name="Positive SNR Margin",
            metric_key="snr_margin_db",
            op=">=",
            value=0.0,
            units="dB",
            severity="must",
        ),
        Requirement(
            id="DET-002",
            name="5 dB SNR Margin",
            metric_key="snr_margin_db",
            op=">=",
            value=5.0,
            units="dB",
            severity="should",
        ),
        Requirement(
            id="COST-001",
            name="Maximum Cost",
            metric_key="cost_usd",
            op="<=",
            value=500000.0,
            units="USD",
            severity="must",
        ),
        Requirement(
            id="PWR-001",
            name="Maximum Power",
            metric_key="prime_power_w",
            op="<=",
            value=10000.0,
            units="W",
            severity="must",
        ),
    ],
    name="Radar Detection Requirements",
)

print(f"\nRequirements: {len(requirements)} defined")
for req in requirements:
    print(f"  {req.id}: {req.name}")

Step 4: Define the Design Space

design_space = (
    DesignSpace(name="Radar Array Trade Study")
    # Array size (power-of-2 for sub-array constraints)
    .add_variable("array.nx", type="categorical", values=[8, 16, 32])
    .add_variable("array.ny", type="categorical", values=[8, 16, 32])
    # TX power per element (significant range for radar)
    .add_variable("rf.tx_power_w_per_elem", type="float", low=5.0, high=30.0)
    # PA efficiency
    .add_variable("rf.pa_efficiency", type="float", low=0.15, high=0.35)
    # Cost per element (radar modules more expensive)
    .add_variable("cost.cost_per_elem_usd", type="float", low=200.0, high=500.0)
    # Fixed parameters
    .add_variable("array.geometry", type="categorical", values=["rectangular"])
    .add_variable("array.dx_lambda", type="float", low=0.5, high=0.5)
    .add_variable("array.dy_lambda", type="float", low=0.5, high=0.5)
    .add_variable("array.enforce_subarray_constraint", type="categorical", values=[True])
    .add_variable("rf.noise_figure_db", type="float", low=4.0, high=4.0)
    .add_variable("rf.n_tx_beams", type="int", low=1, high=1)
    .add_variable("rf.feed_loss_db", type="float", low=1.5, high=1.5)
    .add_variable("cost.nre_usd", type="float", low=50000.0, high=50000.0)
    .add_variable("cost.integration_cost_usd", type="float", low=25000.0, high=25000.0)
)

print(f"\nDesign Space: {design_space.n_dims} dimensions")

Step 5: Quick Baseline Check

# Evaluate a baseline design first
baseline = Architecture(
    array=ArrayConfig(nx=16, ny=16, dx_lambda=0.5, dy_lambda=0.5),
    rf=RFChainConfig(tx_power_w_per_elem=10.0, pa_efficiency=0.25, noise_figure_db=4.0),
    cost=CostConfig(cost_per_elem_usd=300.0, nre_usd=50000.0, integration_cost_usd=25000.0),
    name="Baseline 16x16",
)

baseline_metrics = evaluate_case(baseline, scenario)

print(f"\nBaseline Design: 16×16 array, 10 W/elem")
print(f"  Single-Pulse SNR: {baseline_metrics['snr_single_pulse_db']:.1f} dB")
print(f"  Integrated SNR: {baseline_metrics['snr_integrated_db']:.1f} dB")
print(f"  Required SNR: {baseline_metrics['snr_required_db']:.1f} dB")
print(f"  SNR Margin: {baseline_metrics['snr_margin_db']:.1f} dB")
print(f"  Cost: ${baseline_metrics['cost_usd']:,.0f}")
print(f"  Prime Power: {baseline_metrics['prime_power_w']:.0f} W")

# Check requirements
report = requirements.verify(baseline_metrics)
print(f"  Requirements: {'PASS' if report.passes else 'FAIL'}")

Step 6: Run DOE

n_samples = 100
seed = 42

doe = generate_doe(design_space, method="lhs", n_samples=n_samples, seed=seed)
print(f"\nGenerated {len(doe)} DOE cases")

runner = BatchRunner(scenario, requirements)

def progress(completed, total):
    if completed % 25 == 0 or completed == total:
        print(f"  Progress: {completed}/{total}")

print("\nRunning batch evaluation...")
results = runner.run(doe, n_workers=1, progress_callback=progress)
print(f"Completed: {len(results)} cases")

Step 7: Analyze Results

feasible_mask = results["verification.passes"] == 1.0
feasible = filter_feasible(results, requirements)

print(f"\nResults Summary:")
print(f"  Total cases: {len(results)}")
print(f"  Feasible: {len(feasible)} ({len(feasible)/len(results)*100:.1f}%)")

print(f"\nSNR Margin Range:")
print(f"  All: {results['snr_margin_db'].min():.1f} to {results['snr_margin_db'].max():.1f} dB")
if len(feasible) > 0:
    print(f"  Feasible: {feasible['snr_margin_db'].min():.1f} to {feasible['snr_margin_db'].max():.1f} dB")

print(f"\nCost Range:")
print(f"  All: ${results['cost_usd'].min():,.0f} to ${results['cost_usd'].max():,.0f}")
if len(feasible) > 0:
    print(f"  Feasible: ${feasible['cost_usd'].min():,.0f} to ${feasible['cost_usd'].max():,.0f}")

Step 8: Pareto Analysis

if len(feasible) > 0:
    objectives = [
        ("cost_usd", "minimize"),
        ("snr_margin_db", "maximize"),
    ]

    pareto = extract_pareto(feasible, objectives)
    ranked = rank_pareto(pareto, objectives, weights=[0.5, 0.5])

    print(f"\nPareto-optimal designs: {len(pareto)}")
    print("\nTop 5 Designs:")
    print("-" * 70)
    for i, (_, row) in enumerate(ranked.head(5).iterrows()):
        print(f"  #{i+1}: {row['case_id']}")
        print(f"      Array: {int(row['array.nx'])}×{int(row['array.ny'])} ({int(row['n_elements'])} elements)")
        print(f"      TX Power: {row['rf.tx_power_w_per_elem']:.1f} W/elem")
        print(f"      SNR Margin: {row['snr_margin_db']:.1f} dB")
        print(f"      Cost: ${row['cost_usd']:,.0f}")
        print(f"      Prime Power: {row['prime_power_w']:.0f} W")
else:
    print("\nNo feasible designs found. Consider relaxing requirements.")
    pareto = None
    ranked = None

Step 9: Visualize

output_dir = Path("./radar_tutorial_results")
output_dir.mkdir(exist_ok=True)

# Cost vs SNR Margin
fig1 = pareto_plot(
    results,
    x="cost_usd",
    y="snr_margin_db",
    pareto_front=pareto,
    feasible_mask=feasible_mask,
    color_by="n_elements",
    title="Cost vs SNR Margin Trade Space",
    x_label="Total Cost (USD)",
    y_label="SNR Margin (dB)",
)
fig1.savefig(output_dir / "pareto_cost_snr.png", dpi=150, bbox_inches="tight")

# Power vs Performance
fig2 = pareto_plot(
    results,
    x="prime_power_w",
    y="snr_margin_db",
    feasible_mask=feasible_mask,
    color_by="cost_usd",
    title="Power vs SNR Margin",
    x_label="Prime Power (W)",
    y_label="SNR Margin (dB)",
)
fig2.savefig(output_dir / "pareto_power_snr.png", dpi=150, bbox_inches="tight")

# Scatter matrix
if len(feasible) > 5:
    fig3 = scatter_matrix(
        feasible,
        columns=["cost_usd", "snr_margin_db", "prime_power_w", "n_elements"],
        color_by="snr_margin_db",
        title="Trade Space Correlations",
    )
    fig3.savefig(output_dir / "scatter_matrix.png", dpi=150, bbox_inches="tight")

plt.close("all")
print(f"\nFigures saved to: {output_dir}")

Step 10: Power-Aperture Trade-off

# Analyze power-aperture product
if len(feasible) > 0:
    feasible["power_aperture"] = feasible["prime_power_w"] * feasible["n_elements"]

    print("\nPower-Aperture Analysis:")
    print(f"  PA Product Range: {feasible['power_aperture'].min():.0f} to {feasible['power_aperture'].max():.0f}")

    # Group by array size
    for nx in [8, 16, 32]:
        subset = feasible[feasible["array.nx"] == nx]
        if len(subset) > 0:
            print(f"\n  {nx}×{nx} arrays ({nx*nx} elements):")
            print(f"    Count: {len(subset)}")
            print(f"    Avg SNR Margin: {subset['snr_margin_db'].mean():.1f} dB")
            print(f"    Avg Cost: ${subset['cost_usd'].mean():,.0f}")

Step 11: Export Results

export_results(results, output_dir / "all_results.parquet")
export_results(feasible, output_dir / "feasible_results.parquet")
if ranked is not None:
    export_results(ranked, output_dir / "pareto_ranked.csv", format="csv")

print(f"\nResults exported to: {output_dir}")

Key Insights

Power-Aperture Trade-off

The radar equation shows that SNR scales with:

\[ SNR \propto P_t \cdot A^2 \propto P_t \cdot N^2 \]

Where \(N\) is element count. This means: - Doubling power → 3 dB improvement - Doubling elements → 6 dB improvement (but higher cost)

Array Size Selection

Size Elements Typical Use
8×8 64 Short range, low cost
16×16 256 Medium range, balanced
32×32 1024 Long range, high performance

Swerling Model Impact

  • Swerling 0 (steady): Requires lowest SNR
  • Swerling 1 (slow): Requires ~2-3 dB more
  • Higher models: Additional SNR needed

Next Steps