Skip to content

Digital Array Models API

ADC/DAC converters, bandwidth calculations, and timeline scheduling for digital phased arrays.

Overview

from phased_array_systems.models.digital import (
    # Converters
    enob_to_snr,
    snr_to_enob,
    enob_to_sfdr,
    sfdr_to_enob,
    quantization_noise_floor,
    sample_rate_for_bandwidth,
    max_signal_bandwidth,
    adc_dynamic_range,
    dac_output_power,
    # Bandwidth
    beam_bandwidth_product,
    max_simultaneous_beams,
    digital_beamformer_data_rate,
    channelizer_output_rate,
    processing_margin,
    beamformer_operations,
    # Scheduling
    Dwell,
    Timeline,
    Function,
    timeline_utilization,
    max_update_rate,
    search_timeline,
    interleaved_timeline,
)

Converters

Functions for analyzing ADC/DAC performance including ENOB, SNR, SFDR, and quantization noise.

Key Relationships

  • SNR (ideal) = 6.02 * ENOB + 1.76 dB
  • SFDR ~ SNR for ideal converters
  • Nyquist: fs >= 2 * BW (practical: fs >= 2.5 * BW)

enob_to_snr

enob_to_snr(enob: float) -> float

Convert Effective Number of Bits to SNR.

The ideal SNR for a converter with ENOB effective bits is

SNR = 6.02 * ENOB + 1.76 dB

This assumes full-scale sinusoidal input and ideal quantization.

PARAMETER DESCRIPTION
enob

Effective number of bits

TYPE: float

RETURNS DESCRIPTION
float

SNR in dB

Source code in src/phased_array_systems/models/digital/converters.py
def enob_to_snr(enob: float) -> float:
    """Convert Effective Number of Bits to SNR.

    The ideal SNR for a converter with ENOB effective bits is:
        SNR = 6.02 * ENOB + 1.76 dB

    This assumes full-scale sinusoidal input and ideal quantization.

    Args:
        enob: Effective number of bits

    Returns:
        SNR in dB
    """
    return 6.02 * enob + 1.76

snr_to_enob

snr_to_enob(snr_db: float) -> float

Convert SNR to Effective Number of Bits.

Inverse of enob_to_snr

ENOB = (SNR - 1.76) / 6.02

Useful for determining effective resolution from measured SNR.

PARAMETER DESCRIPTION
snr_db

Signal-to-noise ratio in dB

TYPE: float

RETURNS DESCRIPTION
float

Effective number of bits

Source code in src/phased_array_systems/models/digital/converters.py
def snr_to_enob(snr_db: float) -> float:
    """Convert SNR to Effective Number of Bits.

    Inverse of enob_to_snr:
        ENOB = (SNR - 1.76) / 6.02

    Useful for determining effective resolution from measured SNR.

    Args:
        snr_db: Signal-to-noise ratio in dB

    Returns:
        Effective number of bits
    """
    return (snr_db - 1.76) / 6.02

enob_to_sfdr

enob_to_sfdr(enob: float, margin_db: float = 0.0) -> float

Estimate SFDR from ENOB.

For an ideal converter, SFDR is approximately equal to SNR. Real converters may have SFDR limited by harmonic distortion.

PARAMETER DESCRIPTION
enob

Effective number of bits

TYPE: float

margin_db

Derate factor for non-ideal behavior (default 0)

TYPE: float DEFAULT: 0.0

RETURNS DESCRIPTION
float

Estimated SFDR in dB

Source code in src/phased_array_systems/models/digital/converters.py
def enob_to_sfdr(enob: float, margin_db: float = 0.0) -> float:
    """Estimate SFDR from ENOB.

    For an ideal converter, SFDR is approximately equal to SNR.
    Real converters may have SFDR limited by harmonic distortion.

    Args:
        enob: Effective number of bits
        margin_db: Derate factor for non-ideal behavior (default 0)

    Returns:
        Estimated SFDR in dB
    """
    return enob_to_snr(enob) - margin_db

sfdr_to_enob

sfdr_to_enob(sfdr_db: float) -> float

Convert SFDR to equivalent ENOB.

Useful for determining effective dynamic range in bits.

PARAMETER DESCRIPTION
sfdr_db

Spurious-free dynamic range in dB

TYPE: float

RETURNS DESCRIPTION
float

Equivalent ENOB

Source code in src/phased_array_systems/models/digital/converters.py
def sfdr_to_enob(sfdr_db: float) -> float:
    """Convert SFDR to equivalent ENOB.

    Useful for determining effective dynamic range in bits.

    Args:
        sfdr_db: Spurious-free dynamic range in dB

    Returns:
        Equivalent ENOB
    """
    return snr_to_enob(sfdr_db)

quantization_noise_floor

quantization_noise_floor(enob: float, full_scale_dbm: float, bandwidth_hz: float, sample_rate_hz: float) -> float

Calculate quantization noise floor in dBm/Hz.

The quantization noise power is spread across the Nyquist bandwidth. Noise floor density = Full scale - SNR - 10*log10(fs/2)

PARAMETER DESCRIPTION
enob

Effective number of bits

TYPE: float

full_scale_dbm

Full-scale input power in dBm

TYPE: float

bandwidth_hz

Signal bandwidth in Hz

TYPE: float

sample_rate_hz

Sample rate in Hz

TYPE: float

RETURNS DESCRIPTION
float

Noise floor spectral density in dBm/Hz

Source code in src/phased_array_systems/models/digital/converters.py
def quantization_noise_floor(
    enob: float,
    full_scale_dbm: float,
    bandwidth_hz: float,
    sample_rate_hz: float,
) -> float:
    """Calculate quantization noise floor in dBm/Hz.

    The quantization noise power is spread across the Nyquist bandwidth.
    Noise floor density = Full scale - SNR - 10*log10(fs/2)

    Args:
        enob: Effective number of bits
        full_scale_dbm: Full-scale input power in dBm
        bandwidth_hz: Signal bandwidth in Hz
        sample_rate_hz: Sample rate in Hz

    Returns:
        Noise floor spectral density in dBm/Hz
    """
    snr_db = enob_to_snr(enob)
    nyquist_bw = sample_rate_hz / 2
    noise_floor_dbm_hz = full_scale_dbm - snr_db - 10 * math.log10(nyquist_bw)
    return noise_floor_dbm_hz

sample_rate_for_bandwidth

sample_rate_for_bandwidth(signal_bandwidth_hz: float, oversampling_ratio: float = 2.5) -> float

Calculate minimum sample rate for a given signal bandwidth.

Nyquist requires fs >= 2*BW, but practical systems use oversampling to ease anti-aliasing filter requirements.

PARAMETER DESCRIPTION
signal_bandwidth_hz

Signal bandwidth in Hz

TYPE: float

oversampling_ratio

Ratio of sample rate to Nyquist rate - 2.0: Minimum (steep filter required) - 2.5: Typical (recommended) - 4.0: Relaxed filtering

TYPE: float DEFAULT: 2.5

RETURNS DESCRIPTION
float

Required sample rate in Hz

Source code in src/phased_array_systems/models/digital/converters.py
def sample_rate_for_bandwidth(
    signal_bandwidth_hz: float,
    oversampling_ratio: float = 2.5,
) -> float:
    """Calculate minimum sample rate for a given signal bandwidth.

    Nyquist requires fs >= 2*BW, but practical systems use oversampling
    to ease anti-aliasing filter requirements.

    Args:
        signal_bandwidth_hz: Signal bandwidth in Hz
        oversampling_ratio: Ratio of sample rate to Nyquist rate
            - 2.0: Minimum (steep filter required)
            - 2.5: Typical (recommended)
            - 4.0: Relaxed filtering

    Returns:
        Required sample rate in Hz
    """
    return signal_bandwidth_hz * oversampling_ratio

max_signal_bandwidth

max_signal_bandwidth(sample_rate_hz: float, oversampling_ratio: float = 2.5) -> float

Calculate maximum signal bandwidth for a given sample rate.

Inverse of sample_rate_for_bandwidth.

PARAMETER DESCRIPTION
sample_rate_hz

ADC/DAC sample rate in Hz

TYPE: float

oversampling_ratio

Ratio of sample rate to Nyquist rate

TYPE: float DEFAULT: 2.5

RETURNS DESCRIPTION
float

Maximum signal bandwidth in Hz

Source code in src/phased_array_systems/models/digital/converters.py
def max_signal_bandwidth(
    sample_rate_hz: float,
    oversampling_ratio: float = 2.5,
) -> float:
    """Calculate maximum signal bandwidth for a given sample rate.

    Inverse of sample_rate_for_bandwidth.

    Args:
        sample_rate_hz: ADC/DAC sample rate in Hz
        oversampling_ratio: Ratio of sample rate to Nyquist rate

    Returns:
        Maximum signal bandwidth in Hz
    """
    return sample_rate_hz / oversampling_ratio

adc_dynamic_range

adc_dynamic_range(enob: float, noise_figure_db: float = 0.0, input_noise_dbm_hz: float = -174.0, bandwidth_hz: float = 1.0) -> dict[str, float]

Calculate ADC dynamic range metrics.

Computes the usable dynamic range considering both quantization noise and thermal noise contributions.

PARAMETER DESCRIPTION
enob

Effective number of bits

TYPE: float

noise_figure_db

Front-end noise figure in dB

TYPE: float DEFAULT: 0.0

input_noise_dbm_hz

Input noise density (default: thermal at 290K)

TYPE: float DEFAULT: -174.0

bandwidth_hz

Signal bandwidth for integrated noise

TYPE: float DEFAULT: 1.0

RETURNS DESCRIPTION
dict[str, float]

Dictionary with: - snr_db: Quantization-limited SNR - noise_floor_dbm: Integrated noise floor - max_input_dbm: Maximum input before clipping - dynamic_range_db: Usable dynamic range

Source code in src/phased_array_systems/models/digital/converters.py
def adc_dynamic_range(
    enob: float,
    noise_figure_db: float = 0.0,
    input_noise_dbm_hz: float = -174.0,
    bandwidth_hz: float = 1.0,
) -> dict[str, float]:
    """Calculate ADC dynamic range metrics.

    Computes the usable dynamic range considering both quantization
    noise and thermal noise contributions.

    Args:
        enob: Effective number of bits
        noise_figure_db: Front-end noise figure in dB
        input_noise_dbm_hz: Input noise density (default: thermal at 290K)
        bandwidth_hz: Signal bandwidth for integrated noise

    Returns:
        Dictionary with:
            - snr_db: Quantization-limited SNR
            - noise_floor_dbm: Integrated noise floor
            - max_input_dbm: Maximum input before clipping
            - dynamic_range_db: Usable dynamic range
    """
    snr_db = enob_to_snr(enob)

    # Thermal noise floor
    thermal_noise_dbm = input_noise_dbm_hz + noise_figure_db + 10 * math.log10(bandwidth_hz)

    # Full scale (assume 0 dBm reference, adjust as needed)
    full_scale_dbm = 0.0

    # Quantization noise
    quant_noise_dbm = full_scale_dbm - snr_db

    # Total noise (power sum)
    total_noise_linear = 10 ** (thermal_noise_dbm / 10) + 10 ** (quant_noise_dbm / 10)
    total_noise_dbm = 10 * math.log10(total_noise_linear)

    dynamic_range_db = full_scale_dbm - total_noise_dbm

    return {
        "snr_db": snr_db,
        "noise_floor_dbm": total_noise_dbm,
        "max_input_dbm": full_scale_dbm,
        "dynamic_range_db": dynamic_range_db,
        "thermal_noise_dbm": thermal_noise_dbm,
        "quant_noise_dbm": quant_noise_dbm,
    }

dac_output_power

dac_output_power(enob: float, full_scale_dbm: float, backoff_db: float = 6.0) -> dict[str, float]

Calculate DAC output power metrics.

DACs typically operate with backoff from full scale to maintain linearity and avoid clipping on signal peaks.

PARAMETER DESCRIPTION
enob

Effective number of bits

TYPE: float

full_scale_dbm

Full-scale output power in dBm

TYPE: float

backoff_db

Operating backoff from full scale

TYPE: float DEFAULT: 6.0

RETURNS DESCRIPTION
dict[str, float]

Dictionary with: - full_scale_dbm: Maximum output power - operating_power_dbm: Power with backoff - snr_db: Signal-to-quantization-noise ratio - sfdr_db: Estimated spurious-free dynamic range - noise_floor_dbm: Quantization noise floor

Source code in src/phased_array_systems/models/digital/converters.py
def dac_output_power(
    enob: float,
    full_scale_dbm: float,
    backoff_db: float = 6.0,
) -> dict[str, float]:
    """Calculate DAC output power metrics.

    DACs typically operate with backoff from full scale to maintain
    linearity and avoid clipping on signal peaks.

    Args:
        enob: Effective number of bits
        full_scale_dbm: Full-scale output power in dBm
        backoff_db: Operating backoff from full scale

    Returns:
        Dictionary with:
            - full_scale_dbm: Maximum output power
            - operating_power_dbm: Power with backoff
            - snr_db: Signal-to-quantization-noise ratio
            - sfdr_db: Estimated spurious-free dynamic range
            - noise_floor_dbm: Quantization noise floor
    """
    snr_db = enob_to_snr(enob)
    sfdr_db = enob_to_sfdr(enob)
    operating_power_dbm = full_scale_dbm - backoff_db
    noise_floor_dbm = full_scale_dbm - snr_db

    return {
        "full_scale_dbm": full_scale_dbm,
        "operating_power_dbm": operating_power_dbm,
        "backoff_db": backoff_db,
        "snr_db": snr_db,
        "sfdr_db": sfdr_db,
        "noise_floor_dbm": noise_floor_dbm,
    }

Bandwidth

Functions for analyzing digital beamformer bandwidth constraints, beam-bandwidth products, and data rates.

beam_bandwidth_product

beam_bandwidth_product(n_beams: int, bandwidth_per_beam_hz: float) -> float

Calculate total beam-bandwidth product.

The beam-bandwidth product represents the total instantaneous processing bandwidth required for simultaneous beams.

PARAMETER DESCRIPTION
n_beams

Number of simultaneous beams

TYPE: int

bandwidth_per_beam_hz

Bandwidth per beam in Hz

TYPE: float

RETURNS DESCRIPTION
float

Total beam-bandwidth product in Hz

Source code in src/phased_array_systems/models/digital/bandwidth.py
def beam_bandwidth_product(
    n_beams: int,
    bandwidth_per_beam_hz: float,
) -> float:
    """Calculate total beam-bandwidth product.

    The beam-bandwidth product represents the total instantaneous
    processing bandwidth required for simultaneous beams.

    Args:
        n_beams: Number of simultaneous beams
        bandwidth_per_beam_hz: Bandwidth per beam in Hz

    Returns:
        Total beam-bandwidth product in Hz
    """
    return n_beams * bandwidth_per_beam_hz

max_simultaneous_beams

max_simultaneous_beams(processing_bandwidth_hz: float, bandwidth_per_beam_hz: float, overhead_factor: float = 1.1) -> int

Calculate maximum number of simultaneous beams.

Given a fixed processing bandwidth, determine how many beams can be formed simultaneously.

PARAMETER DESCRIPTION
processing_bandwidth_hz

Total available processing bandwidth

TYPE: float

bandwidth_per_beam_hz

Required bandwidth per beam

TYPE: float

overhead_factor

Processing overhead (default 10%)

TYPE: float DEFAULT: 1.1

RETURNS DESCRIPTION
int

Maximum number of simultaneous beams (integer)

Source code in src/phased_array_systems/models/digital/bandwidth.py
def max_simultaneous_beams(
    processing_bandwidth_hz: float,
    bandwidth_per_beam_hz: float,
    overhead_factor: float = 1.1,
) -> int:
    """Calculate maximum number of simultaneous beams.

    Given a fixed processing bandwidth, determine how many beams
    can be formed simultaneously.

    Args:
        processing_bandwidth_hz: Total available processing bandwidth
        bandwidth_per_beam_hz: Required bandwidth per beam
        overhead_factor: Processing overhead (default 10%)

    Returns:
        Maximum number of simultaneous beams (integer)
    """
    effective_bw = processing_bandwidth_hz / overhead_factor
    return int(effective_bw / bandwidth_per_beam_hz)

digital_beamformer_data_rate

digital_beamformer_data_rate(n_elements: int, sample_rate_hz: float, bits_per_sample: int, n_channels: int = 2, overhead_factor: float = 1.25) -> dict[str, float]

Calculate digital beamformer input data rate.

Computes the raw data rate from ADCs into the digital beamformer.

PARAMETER DESCRIPTION
n_elements

Number of array elements (each with ADC)

TYPE: int

sample_rate_hz

ADC sample rate in Hz

TYPE: float

bits_per_sample

Bits per sample (typically 12-16)

TYPE: int

n_channels

Number of channels per element (2 for I/Q)

TYPE: int DEFAULT: 2

overhead_factor

Protocol overhead (framing, sync, etc.)

TYPE: float DEFAULT: 1.25

RETURNS DESCRIPTION
dict[str, float]

Dictionary with: - raw_rate_bps: Raw data rate in bits/second - raw_rate_gbps: Raw data rate in Gbps - with_overhead_gbps: Rate including overhead - per_element_gbps: Rate per element

Source code in src/phased_array_systems/models/digital/bandwidth.py
def digital_beamformer_data_rate(
    n_elements: int,
    sample_rate_hz: float,
    bits_per_sample: int,
    n_channels: int = 2,  # I and Q
    overhead_factor: float = 1.25,
) -> dict[str, float]:
    """Calculate digital beamformer input data rate.

    Computes the raw data rate from ADCs into the digital beamformer.

    Args:
        n_elements: Number of array elements (each with ADC)
        sample_rate_hz: ADC sample rate in Hz
        bits_per_sample: Bits per sample (typically 12-16)
        n_channels: Number of channels per element (2 for I/Q)
        overhead_factor: Protocol overhead (framing, sync, etc.)

    Returns:
        Dictionary with:
            - raw_rate_bps: Raw data rate in bits/second
            - raw_rate_gbps: Raw data rate in Gbps
            - with_overhead_gbps: Rate including overhead
            - per_element_gbps: Rate per element
    """
    raw_rate_bps = n_elements * sample_rate_hz * bits_per_sample * n_channels
    raw_rate_gbps = raw_rate_bps / 1e9
    with_overhead_gbps = raw_rate_gbps * overhead_factor
    per_element_gbps = with_overhead_gbps / n_elements

    return {
        "raw_rate_bps": raw_rate_bps,
        "raw_rate_gbps": raw_rate_gbps,
        "with_overhead_gbps": with_overhead_gbps,
        "per_element_gbps": per_element_gbps,
        "n_elements": n_elements,
        "sample_rate_hz": sample_rate_hz,
        "bits_per_sample": bits_per_sample,
    }

channelizer_output_rate

channelizer_output_rate(input_bandwidth_hz: float, n_channels: int, overlap_factor: float = 1.0, bits_per_output: int = 32) -> dict[str, float]

Calculate polyphase channelizer output data rate.

A channelizer divides a wideband input into narrowband channels. Output rate depends on channel count and overlap.

PARAMETER DESCRIPTION
input_bandwidth_hz

Total input bandwidth

TYPE: float

n_channels

Number of output channels

TYPE: int

overlap_factor

Channel overlap (1.0 = no overlap, 2.0 = 50% overlap)

TYPE: float DEFAULT: 1.0

bits_per_output

Bits per output sample (32 for complex float)

TYPE: int DEFAULT: 32

RETURNS DESCRIPTION
dict[str, float]

Dictionary with: - channel_bandwidth_hz: Bandwidth per channel - channel_sample_rate_hz: Sample rate per channel - output_rate_gbps: Total output data rate - samples_per_channel_per_sec: Output samples per channel

Source code in src/phased_array_systems/models/digital/bandwidth.py
def channelizer_output_rate(
    input_bandwidth_hz: float,
    n_channels: int,
    overlap_factor: float = 1.0,
    bits_per_output: int = 32,  # Complex float
) -> dict[str, float]:
    """Calculate polyphase channelizer output data rate.

    A channelizer divides a wideband input into narrowband channels.
    Output rate depends on channel count and overlap.

    Args:
        input_bandwidth_hz: Total input bandwidth
        n_channels: Number of output channels
        overlap_factor: Channel overlap (1.0 = no overlap, 2.0 = 50% overlap)
        bits_per_output: Bits per output sample (32 for complex float)

    Returns:
        Dictionary with:
            - channel_bandwidth_hz: Bandwidth per channel
            - channel_sample_rate_hz: Sample rate per channel
            - output_rate_gbps: Total output data rate
            - samples_per_channel_per_sec: Output samples per channel
    """
    channel_bandwidth_hz = input_bandwidth_hz / n_channels
    channel_sample_rate_hz = channel_bandwidth_hz * overlap_factor

    total_output_samples = n_channels * channel_sample_rate_hz
    output_rate_bps = total_output_samples * bits_per_output * 2  # Complex
    output_rate_gbps = output_rate_bps / 1e9

    return {
        "channel_bandwidth_hz": channel_bandwidth_hz,
        "channel_sample_rate_hz": channel_sample_rate_hz,
        "output_rate_gbps": output_rate_gbps,
        "samples_per_channel_per_sec": channel_sample_rate_hz,
        "n_channels": n_channels,
        "input_bandwidth_hz": input_bandwidth_hz,
    }

processing_margin

processing_margin(available_throughput_gops: float, required_throughput_gops: float) -> dict[str, float]

Calculate processing margin for digital beamformer.

Compares available FPGA/GPU throughput against requirements.

PARAMETER DESCRIPTION
available_throughput_gops

Available processing (Giga-ops/sec)

TYPE: float

required_throughput_gops

Required processing (Giga-ops/sec)

TYPE: float

RETURNS DESCRIPTION
dict[str, float]

Dictionary with: - margin_ratio: Available / Required (>1 is good) - margin_db: Margin in dB - utilization_percent: Percentage of capacity used - headroom_percent: Remaining capacity

Source code in src/phased_array_systems/models/digital/bandwidth.py
def processing_margin(
    available_throughput_gops: float,
    required_throughput_gops: float,
) -> dict[str, float]:
    """Calculate processing margin for digital beamformer.

    Compares available FPGA/GPU throughput against requirements.

    Args:
        available_throughput_gops: Available processing (Giga-ops/sec)
        required_throughput_gops: Required processing (Giga-ops/sec)

    Returns:
        Dictionary with:
            - margin_ratio: Available / Required (>1 is good)
            - margin_db: Margin in dB
            - utilization_percent: Percentage of capacity used
            - headroom_percent: Remaining capacity
    """
    margin_ratio = available_throughput_gops / required_throughput_gops
    margin_db = 10 * math.log10(margin_ratio) if margin_ratio > 0 else float("-inf")
    utilization_percent = (required_throughput_gops / available_throughput_gops) * 100
    headroom_percent = 100 - utilization_percent

    return {
        "margin_ratio": margin_ratio,
        "margin_db": margin_db,
        "utilization_percent": utilization_percent,
        "headroom_percent": headroom_percent,
        "available_gops": available_throughput_gops,
        "required_gops": required_throughput_gops,
    }

beamformer_operations

beamformer_operations(n_elements: int, n_beams: int, sample_rate_hz: float, fft_size: int = 0) -> dict[str, float]

Estimate digital beamformer computational requirements.

Calculates operations per second for time-domain or frequency-domain beamforming.

PARAMETER DESCRIPTION
n_elements

Number of array elements

TYPE: int

n_beams

Number of simultaneous beams

TYPE: int

sample_rate_hz

Sample rate in Hz

TYPE: float

fft_size

FFT size (0 for time-domain beamforming)

TYPE: int DEFAULT: 0

RETURNS DESCRIPTION
dict[str, float]

Dictionary with: - complex_mults_per_sec: Complex multiplications/sec - complex_adds_per_sec: Complex additions/sec - total_gops: Total Giga-operations/sec - method: 'time_domain' or 'frequency_domain'

Source code in src/phased_array_systems/models/digital/bandwidth.py
def beamformer_operations(
    n_elements: int,
    n_beams: int,
    sample_rate_hz: float,
    fft_size: int = 0,
) -> dict[str, float]:
    """Estimate digital beamformer computational requirements.

    Calculates operations per second for time-domain or frequency-domain
    beamforming.

    Args:
        n_elements: Number of array elements
        n_beams: Number of simultaneous beams
        sample_rate_hz: Sample rate in Hz
        fft_size: FFT size (0 for time-domain beamforming)

    Returns:
        Dictionary with:
            - complex_mults_per_sec: Complex multiplications/sec
            - complex_adds_per_sec: Complex additions/sec
            - total_gops: Total Giga-operations/sec
            - method: 'time_domain' or 'frequency_domain'
    """
    if fft_size > 0:
        # Frequency-domain: FFT + multiply + IFFT
        # FFT ops ≈ 5 * N * log2(N) per transform
        fft_ops = 5 * fft_size * math.log2(fft_size)
        transforms_per_sec = sample_rate_hz / fft_size

        # Per beam: FFT(input) + N_elem multiplies + IFFT(output)
        fft_total_ops = transforms_per_sec * fft_ops * n_elements  # Input FFTs
        mult_ops = transforms_per_sec * fft_size * n_elements * n_beams
        ifft_ops = transforms_per_sec * fft_ops * n_beams  # Output IFFTs

        total_ops = fft_total_ops + mult_ops + ifft_ops
        method = "frequency_domain"
    else:
        # Time-domain: weight and sum per sample
        # Each beam: N_elements complex multiplies + (N_elements-1) adds
        mults_per_sample = n_elements * n_beams
        adds_per_sample = (n_elements - 1) * n_beams

        complex_mults_per_sec = mults_per_sample * sample_rate_hz
        complex_adds_per_sec = adds_per_sample * sample_rate_hz

        # Complex mult ≈ 6 real ops, complex add ≈ 2 real ops
        total_ops = complex_mults_per_sec * 6 + complex_adds_per_sec * 2
        method = "time_domain"

    total_gops = total_ops / 1e9

    return {
        "total_gops": total_gops,
        "total_ops_per_sec": total_ops,
        "method": method,
        "n_elements": n_elements,
        "n_beams": n_beams,
        "sample_rate_hz": sample_rate_hz,
    }

Scheduling

Classes and functions for timeline and scheduling in multi-function arrays.

Classes

Function

Bases: str, Enum

Array function types for multi-function scheduling.

Dwell dataclass

Dwell(function: Function, duration_us: float, azimuth_deg: float = 0.0, elevation_deg: float = 0.0, bandwidth_hz: float = 0.0, priority: int = 1, metadata: dict = dict())

A single dwell (beam position) in the timeline.

ATTRIBUTE DESCRIPTION
function

Type of function being performed

TYPE: Function

duration_us

Dwell duration in microseconds

TYPE: float

azimuth_deg

Beam azimuth angle in degrees

TYPE: float

elevation_deg

Beam elevation angle in degrees

TYPE: float

bandwidth_hz

Instantaneous bandwidth for this dwell

TYPE: float

priority

Scheduling priority (higher = more important)

TYPE: int

metadata

Additional function-specific parameters

TYPE: dict

duration_ms property

duration_ms: float

Duration in milliseconds.

duration_s property

duration_s: float

Duration in seconds.

Timeline dataclass

Timeline(dwells: list[Dwell], frame_time_ms: float, name: str = '')

A complete timeline of dwells over a frame period.

ATTRIBUTE DESCRIPTION
dwells

List of dwells in chronological order

TYPE: list[Dwell]

frame_time_ms

Total frame duration in milliseconds

TYPE: float

name

Optional timeline identifier

TYPE: str

total_dwell_time_ms property

total_dwell_time_ms: float

Sum of all dwell durations.

n_dwells property

n_dwells: int

Number of dwells in timeline.

dwells_by_function

dwells_by_function(function: Function) -> list[Dwell]

Get all dwells for a specific function.

Source code in src/phased_array_systems/models/digital/scheduling.py
def dwells_by_function(self, function: Function) -> list[Dwell]:
    """Get all dwells for a specific function."""
    return [d for d in self.dwells if d.function == function]

time_for_function

time_for_function(function: Function) -> float

Total time allocated to a function in ms.

Source code in src/phased_array_systems/models/digital/scheduling.py
def time_for_function(self, function: Function) -> float:
    """Total time allocated to a function in ms."""
    return sum(d.duration_ms for d in self.dwells_by_function(function))

Functions

timeline_utilization

timeline_utilization(timeline: Timeline) -> dict[str, float]

Calculate timeline utilization metrics.

Analyzes how efficiently the timeline uses available time and breaks down allocation by function.

PARAMETER DESCRIPTION
timeline

Timeline object to analyze

TYPE: Timeline

RETURNS DESCRIPTION
dict[str, float]

Dictionary with: - total_utilization: Fraction of frame time used (0-1) - idle_time_ms: Unused time in milliseconds - by_function: Dict of function -> time allocation - by_function_percent: Dict of function -> percentage

Source code in src/phased_array_systems/models/digital/scheduling.py
def timeline_utilization(timeline: Timeline) -> dict[str, float]:
    """Calculate timeline utilization metrics.

    Analyzes how efficiently the timeline uses available time
    and breaks down allocation by function.

    Args:
        timeline: Timeline object to analyze

    Returns:
        Dictionary with:
            - total_utilization: Fraction of frame time used (0-1)
            - idle_time_ms: Unused time in milliseconds
            - by_function: Dict of function -> time allocation
            - by_function_percent: Dict of function -> percentage
    """
    total_dwell_ms = timeline.total_dwell_time_ms
    idle_time_ms = max(0, timeline.frame_time_ms - total_dwell_ms)
    utilization = total_dwell_ms / timeline.frame_time_ms if timeline.frame_time_ms > 0 else 0

    # Breakdown by function
    by_function = {}
    by_function_percent = {}

    for func in Function:
        time_ms = timeline.time_for_function(func)
        by_function[func.value] = time_ms
        by_function_percent[func.value] = (
            (time_ms / timeline.frame_time_ms * 100) if timeline.frame_time_ms > 0 else 0
        )

    return {
        "total_utilization": utilization,
        "total_dwell_time_ms": total_dwell_ms,
        "idle_time_ms": idle_time_ms,
        "frame_time_ms": timeline.frame_time_ms,
        "n_dwells": timeline.n_dwells,
        "by_function": by_function,
        "by_function_percent": by_function_percent,
    }

max_update_rate

max_update_rate(scan_volume_sr: float, beam_solid_angle_sr: float, dwell_time_us: float, overhead_us: float = 10.0) -> dict[str, float]

Calculate maximum volume update rate for search.

Determines how quickly a phased array can search a given volume.

PARAMETER DESCRIPTION
scan_volume_sr

Search volume in steradians

TYPE: float

beam_solid_angle_sr

Beam solid angle in steradians (≈ θ_az * θ_el)

TYPE: float

dwell_time_us

Time per beam position in microseconds

TYPE: float

overhead_us

Beam switching overhead in microseconds

TYPE: float DEFAULT: 10.0

RETURNS DESCRIPTION
dict[str, float]

Dictionary with: - n_beam_positions: Number of beams to cover volume - frame_time_ms: Time to complete one scan - update_rate_hz: Volume scans per second - scan_time_s: Time for one complete scan

Source code in src/phased_array_systems/models/digital/scheduling.py
def max_update_rate(
    scan_volume_sr: float,
    beam_solid_angle_sr: float,
    dwell_time_us: float,
    overhead_us: float = 10.0,
) -> dict[str, float]:
    """Calculate maximum volume update rate for search.

    Determines how quickly a phased array can search a given volume.

    Args:
        scan_volume_sr: Search volume in steradians
        beam_solid_angle_sr: Beam solid angle in steradians (≈ θ_az * θ_el)
        dwell_time_us: Time per beam position in microseconds
        overhead_us: Beam switching overhead in microseconds

    Returns:
        Dictionary with:
            - n_beam_positions: Number of beams to cover volume
            - frame_time_ms: Time to complete one scan
            - update_rate_hz: Volume scans per second
            - scan_time_s: Time for one complete scan
    """
    n_beam_positions = math.ceil(scan_volume_sr / beam_solid_angle_sr)
    time_per_position_us = dwell_time_us + overhead_us
    frame_time_us = n_beam_positions * time_per_position_us
    frame_time_ms = frame_time_us / 1000
    scan_time_s = frame_time_us / 1e6
    update_rate_hz = 1 / scan_time_s if scan_time_s > 0 else float("inf")

    return {
        "n_beam_positions": n_beam_positions,
        "frame_time_ms": frame_time_ms,
        "scan_time_s": scan_time_s,
        "update_rate_hz": update_rate_hz,
        "time_per_position_us": time_per_position_us,
    }

search_timeline

search_timeline(azimuth_range_deg: tuple[float, float], elevation_range_deg: tuple[float, float], azimuth_step_deg: float, elevation_step_deg: float, dwell_time_us: float, function: Function = RADAR_SEARCH) -> Timeline

Generate a raster search timeline.

Creates a timeline of dwells covering a rectangular search volume using a raster scan pattern.

PARAMETER DESCRIPTION
azimuth_range_deg

(min, max) azimuth in degrees

TYPE: tuple[float, float]

elevation_range_deg

(min, max) elevation in degrees

TYPE: tuple[float, float]

azimuth_step_deg

Azimuth step between beams

TYPE: float

elevation_step_deg

Elevation step between beams

TYPE: float

dwell_time_us

Dwell time per position

TYPE: float

function

Function type for dwells

TYPE: Function DEFAULT: RADAR_SEARCH

RETURNS DESCRIPTION
Timeline

Timeline with search dwells

Source code in src/phased_array_systems/models/digital/scheduling.py
def search_timeline(
    azimuth_range_deg: tuple[float, float],
    elevation_range_deg: tuple[float, float],
    azimuth_step_deg: float,
    elevation_step_deg: float,
    dwell_time_us: float,
    function: Function = Function.RADAR_SEARCH,
) -> Timeline:
    """Generate a raster search timeline.

    Creates a timeline of dwells covering a rectangular search volume
    using a raster scan pattern.

    Args:
        azimuth_range_deg: (min, max) azimuth in degrees
        elevation_range_deg: (min, max) elevation in degrees
        azimuth_step_deg: Azimuth step between beams
        elevation_step_deg: Elevation step between beams
        dwell_time_us: Dwell time per position
        function: Function type for dwells

    Returns:
        Timeline with search dwells
    """
    dwells = []

    az_min, az_max = azimuth_range_deg
    el_min, el_max = elevation_range_deg

    el = el_min
    row = 0
    while el <= el_max:
        # Alternate scan direction for efficiency
        if row % 2 == 0:
            az_range = _frange(az_min, az_max, azimuth_step_deg)
        else:
            az_range = _frange(az_max, az_min, -azimuth_step_deg)

        for az in az_range:
            dwell = Dwell(
                function=function,
                duration_us=dwell_time_us,
                azimuth_deg=az,
                elevation_deg=el,
            )
            dwells.append(dwell)

        el += elevation_step_deg
        row += 1

    total_time_ms = sum(d.duration_ms for d in dwells)

    return Timeline(
        dwells=dwells,
        frame_time_ms=total_time_ms,
        name=f"Search {az_min:.0f}:{az_max:.0f} az, {el_min:.0f}:{el_max:.0f} el",
    )

interleaved_timeline

interleaved_timeline(functions: list[dict], frame_time_ms: float) -> Timeline

Generate an interleaved multi-function timeline.

Creates a timeline that allocates time to multiple functions based on specified priorities and time allocations.

PARAMETER DESCRIPTION
functions

List of dicts with: - function: Function enum value - time_percent: Percentage of frame time - dwell_time_us: Duration of each dwell - dwells_per_burst: Number of consecutive dwells

TYPE: list[dict]

frame_time_ms

Total frame duration

TYPE: float

RETURNS DESCRIPTION
Timeline

Timeline with interleaved function dwells

Source code in src/phased_array_systems/models/digital/scheduling.py
def interleaved_timeline(
    functions: list[dict],
    frame_time_ms: float,
) -> Timeline:
    """Generate an interleaved multi-function timeline.

    Creates a timeline that allocates time to multiple functions
    based on specified priorities and time allocations.

    Args:
        functions: List of dicts with:
            - function: Function enum value
            - time_percent: Percentage of frame time
            - dwell_time_us: Duration of each dwell
            - dwells_per_burst: Number of consecutive dwells
        frame_time_ms: Total frame duration

    Returns:
        Timeline with interleaved function dwells
    """
    dwells = []

    # Calculate time budget for each function
    for func_spec in functions:
        func = func_spec["function"]
        time_budget_ms = frame_time_ms * func_spec["time_percent"] / 100
        dwell_time_us = func_spec["dwell_time_us"]

        # How many dwells fit in budget?
        dwell_time_ms = dwell_time_us / 1000
        n_dwells = int(time_budget_ms / dwell_time_ms)

        # Create dwells (simple placeholder positions)
        for _ in range(n_dwells):
            dwell = Dwell(
                function=func,
                duration_us=dwell_time_us,
                azimuth_deg=0.0,  # Would be populated by scheduler
                elevation_deg=0.0,
                priority=func_spec.get("priority", 1),
            )
            dwells.append(dwell)

    # Sort by priority (higher priority dwells interleaved more frequently)
    # This is a simplified scheduling - real systems use more sophisticated algorithms
    dwells.sort(key=lambda d: -d.priority)

    return Timeline(
        dwells=dwells,
        frame_time_ms=frame_time_ms,
        name="Interleaved multi-function",
    )

Usage Examples

ADC Performance Analysis

from phased_array_systems.models.digital import (
    enob_to_snr,
    adc_dynamic_range,
    sample_rate_for_bandwidth,
)

# 14-bit ADC analysis
enob = 14
snr = enob_to_snr(enob)
print(f"Ideal SNR: {snr:.1f} dB")  # 86.0 dB

# Dynamic range with front-end noise
result = adc_dynamic_range(
    enob=14,
    noise_figure_db=3,
    bandwidth_hz=100e6
)
print(f"Dynamic Range: {result['dynamic_range_db']:.1f} dB")

# Sample rate for 100 MHz signal
fs = sample_rate_for_bandwidth(100e6)
print(f"Required Sample Rate: {fs/1e6:.0f} MHz")  # 250 MHz

Digital Beamformer Data Rate

from phased_array_systems.models.digital import (
    digital_beamformer_data_rate,
    beam_bandwidth_product,
    max_simultaneous_beams,
)

# 256-element array with 1 GSPS ADCs
result = digital_beamformer_data_rate(
    n_elements=256,
    sample_rate_hz=1e9,
    bits_per_sample=14,
)
print(f"Total Data Rate: {result['with_overhead_gbps']:.1f} Gbps")

# How many beams with 10 GHz processing bandwidth?
n_beams = max_simultaneous_beams(
    processing_bandwidth_hz=10e9,
    bandwidth_per_beam_hz=100e6,
)
print(f"Max Simultaneous Beams: {n_beams}")

Radar Search Timeline

from phased_array_systems.models.digital import (
    search_timeline,
    timeline_utilization,
    max_update_rate,
)

# Generate raster search pattern
tl = search_timeline(
    azimuth_range_deg=(-60, 60),
    elevation_range_deg=(0, 30),
    azimuth_step_deg=3.0,
    elevation_step_deg=3.0,
    dwell_time_us=100,
)
print(f"Search requires {tl.n_dwells} beam positions")

# Analyze utilization
util = timeline_utilization(tl)
print(f"Frame time: {util['frame_time_ms']:.1f} ms")
print(f"Utilization: {util['total_utilization']*100:.1f}%")

Multi-Function Interleaved Timeline

from phased_array_systems.models.digital import (
    Function,
    interleaved_timeline,
    timeline_utilization,
)

# Create interleaved search/track timeline
tl = interleaved_timeline(
    functions=[
        {"function": Function.RADAR_SEARCH, "time_percent": 60,
         "dwell_time_us": 100, "priority": 1},
        {"function": Function.RADAR_TRACK, "time_percent": 30,
         "dwell_time_us": 50, "priority": 2},
        {"function": Function.ESM, "time_percent": 10,
         "dwell_time_us": 200, "priority": 1},
    ],
    frame_time_ms=100,
)

util = timeline_utilization(tl)
print(f"Search allocation: {util['by_function_percent']['radar_search']:.1f}%")
print(f"Track allocation: {util['by_function_percent']['radar_track']:.1f}%")

Key Equations

ENOB-SNR Relationship

\[ SNR = 6.02 \times ENOB + 1.76 \text{ dB} \]

Quantization Noise Floor

\[ N_{floor} = P_{fs} - SNR - 10\log_{10}\left(\frac{f_s}{2}\right) \text{ dBm/Hz} \]

Beam-Bandwidth Product

\[ BBP = N_{beams} \times BW_{per\_beam} \]

Digital Beamformer Data Rate

\[ R_{data} = N_{elements} \times f_s \times bits \times N_{channels} \times overhead \]

See Also