Systems Engineer Recipes#

Recipes for systems engineers designing phased array architectures.

Subarray Architecture Design#

Problem: Determine optimal subarray size for cost vs. performance.

import phased_array as pa
import numpy as np

# System requirements
total_elements_x = 64
total_elements_y = 64
max_scan_angle = 60  # degrees
max_beam_squint = 2  # degrees at band edge

# Analyze different subarray sizes
subarray_sizes = [4, 8, 16, 32]

print("Subarray | Phase Shifters | TTD Units | Est. Squint")
print("-" * 55)

for sub_size in subarray_sizes:
    n_subarrays = (total_elements_x // sub_size) ** 2
    n_phase_shifters = total_elements_x * total_elements_y
    n_ttd = n_subarrays

    # Estimate squint (proportional to subarray aperture)
    subarray_aperture = sub_size * 0.5  # wavelengths
    est_squint = 0.1 * subarray_aperture * np.tan(np.deg2rad(max_scan_angle))

    print(f"  {sub_size}x{sub_size}   |     {n_phase_shifters:5d}      |   {n_ttd:4d}    | {est_squint:.1f} deg")

Scan Coverage Analysis#

Problem: Verify pattern performance across the full scan volume.

geom = pa.create_rectangular_array(16, 16, dx=0.5, dy=0.5)
k = pa.wavelength_to_k(1.0)

# Define scan grid
theta_scan = np.arange(0, 61, 10)
phi_scan = np.arange(0, 361, 45)

# Store metrics for each scan position
results = []

for theta in theta_scan:
    for phi in phi_scan:
        weights = pa.steering_vector(k, geom.x, geom.y, theta, phi)
        weights *= pa.taylor_taper_2d(16, 16, sidelobe_dB=-30)

        # Compute metrics
        theta_arr, phi_arr, pattern = pa.compute_full_pattern(
            geom.x, geom.y, weights, k
        )

        # Peak gain (relative to broadside)
        peak_gain = pattern.max()

        # Approximate directivity
        theta_deg, E_plane, H_plane = pa.compute_pattern_cuts(
            geom.x, geom.y, weights, k
        )
        hpbw_e = pa.compute_half_power_beamwidth(theta_deg, E_plane)
        hpbw_h = pa.compute_half_power_beamwidth(theta_deg, H_plane)

        results.append({
            'theta': theta, 'phi': phi,
            'gain_dB': peak_gain,
            'hpbw_e': hpbw_e, 'hpbw_h': hpbw_h
        })

# Analyze scan loss
broadside_gain = [r['gain_dB'] for r in results if r['theta'] == 0][0]
for r in results:
    r['scan_loss'] = broadside_gain - r['gain_dB']

# Report worst case
worst = max(results, key=lambda x: x['scan_loss'])
print(f"Worst scan loss: {worst['scan_loss']:.1f} dB at theta={worst['theta']}, phi={worst['phi']}")

Array Failure Analysis#

Problem: Determine minimum operational element count.

def analyze_failure_threshold(geom, k, weights, min_gain_loss_dB=3.0,
                               max_sll_increase_dB=6.0):
    """Find failure rate where performance drops below threshold."""

    failure_rates = np.arange(0.0, 0.31, 0.02)

    for rate in failure_rates:
        # Average over multiple trials
        gain_losses = []
        sll_increases = []

        for trial in range(10):
            w_failed, _ = pa.simulate_element_failures(
                weights, rate, mode='off', seed=trial
            )

            _, E_nom, _ = pa.compute_pattern_cuts(geom.x, geom.y, weights, k)
            _, E_fail, _ = pa.compute_pattern_cuts(geom.x, geom.y, w_failed, k)

            gain_loss = E_nom.max() - E_fail.max()
            sll_nom = np.max(E_nom[90:])  # Outside main beam
            sll_fail = np.max(E_fail[90:])
            sll_increase = sll_fail - sll_nom

            gain_losses.append(gain_loss)
            sll_increases.append(sll_increase)

        mean_gain_loss = np.mean(gain_losses)
        mean_sll_increase = np.mean(sll_increases)

        if mean_gain_loss > min_gain_loss_dB or mean_sll_increase > max_sll_increase_dB:
            return rate - 0.02, rate

    return failure_rates[-1], None

weights = pa.steering_vector(k, geom.x, geom.y, 0, 0)
weights *= pa.taylor_taper_2d(16, 16, sidelobe_dB=-30)

safe_rate, fail_rate = analyze_failure_threshold(geom, k, weights)
print(f"Safe failure rate: {safe_rate*100:.0f}%")
print(f"Min operational elements: {int(geom.n_elements * (1-safe_rate))}")

Multi-Function Array Scheduling#

Problem: Verify beam switching between functions doesn’t cause conflicts.

def compute_beam_transition_time(geom, k, theta1, phi1, theta2, phi2,
                                  max_phase_rate=360e6):  # deg/sec
    """
    Estimate beam transition time based on phase shifter slew rate.
    """
    weights1 = pa.steering_vector(k, geom.x, geom.y, theta1, phi1)
    weights2 = pa.steering_vector(k, geom.x, geom.y, theta2, phi2)

    # Maximum phase change
    phase_diff = np.abs(np.angle(weights2) - np.angle(weights1))
    phase_diff = np.minimum(phase_diff, 2*np.pi - phase_diff)
    max_phase_change = np.rad2deg(np.max(phase_diff))

    transition_time = max_phase_change / max_phase_rate

    return transition_time * 1e6  # microseconds

# Example: radar search to track transition
t_us = compute_beam_transition_time(geom, k, 30, 0, 45, 90)
print(f"Beam transition time: {t_us:.1f} μs")

Interference Analysis#

Problem: Assess vulnerability to jammers at specific angles.

def analyze_jammer_vulnerability(geom, k, weights, jammer_directions,
                                  jammer_power_dB=40):
    """
    Analyze pattern response to jammers.

    Returns required null depth to achieve 20 dB J/S improvement.
    """
    results = []

    for theta_j, phi_j in jammer_directions:
        # Pattern response at jammer direction
        theta_rad = np.deg2rad(theta_j)
        phi_rad = np.deg2rad(phi_j)
        theta_arr = np.array([[theta_rad]])
        phi_arr = np.array([[phi_rad]])

        AF = pa.array_factor_vectorized(
            theta_arr, phi_arr, geom.x, geom.y, weights, k
        )
        response_dB = 20 * np.log10(np.abs(AF[0, 0]) + 1e-10)

        # Required null depth for 20 dB J/S improvement
        required_null = response_dB - jammer_power_dB - 20

        results.append({
            'direction': (theta_j, phi_j),
            'response_dB': response_dB,
            'required_null_dB': required_null
        })

    return results

jammer_dirs = [(45, 0), (60, 45), (30, 180)]
vuln = analyze_jammer_vulnerability(geom, k, weights, jammer_dirs)

for v in vuln:
    print(f"Jammer at {v['direction']}: Response {v['response_dB']:.1f} dB, "
          f"need {v['required_null_dB']:.1f} dB null")