Date: 2026-03-03
Version: 1.3.0
Phase Engine is a high-performance middleware daemon that transforms an array of independent ka9q-radio SDR receivers into a coherent, steerable phased array.
It implements a transparent Proxy Architecture: to any downstream client (like hf-timestd or SWL-ka9q), Phase Engine looks and behaves exactly like a physical radiod receiver. However, when a client requests a frequency, Phase Engine intercepts the command, fetches the raw IQ streams from all physical antennas simultaneously, performs complex spatial beamforming (MVDR, MRC, etc.), and seamlessly pushes a single, high-SNR output stream back to the client.
Phase Engine operates as a "middlebox" transparently closing the loop between the client application and the physical SDRs.
sequenceDiagram
participant C as Client (e.g. hf-timestd)
participant PE as Phase Engine Proxy
participant R as radiod Cluster (North, East, Center)
Note over C,PE: 1. The Request (Control Plane)
C->>PE: UDP TLV: Create Channel (10 MHz, target="WWV", method="mvdr")
PE-->>C: UDP TLV ACK: OK, Assigned SSRC 1000
Note over PE,R: 2. The Fan-Out
PE->>R: UDP TLV: Create Channel (10 MHz) on all antennas
Note over R,PE: 3. The Ingress Data Plane
R-->>PE: Multicast RTP IQ (Antenna North)
R-->>PE: Multicast RTP IQ (Antenna East)
R-->>PE: Multicast RTP IQ (Antenna Center)
Note over PE,PE: 4. DSP Beamforming
PE->>PE: Align time-stamps & apply MVDR steering weights
Note over PE,C: 5. The Egress Data Plane
PE-->>C: Unicast RTP IQ (SSRC 1000) - Clean, beamformed float32 stream
- Status Multicaster: Broadcasts binary TLV status packets on a configurable multicast group (default
239.99.1.1) soka9q-pythonclients candiscover_channels()and learn the assigned output stream address. - TLV Server: Listens for standard
ka9q-radioCMD packets on UDP port 5006. When a client sends{SSRC, FREQ, PRESET, SAMPLE_RATE}, the engine assigns a deterministic output multicast address (derived from SSRC), lazily opens physical channels on all radiod sources, and echoes the assigned address back in the STATUS ACK. Clients never supply a destination address — phase-engine always assigns it.
- Ingress: Pulls raw
complex64RTP streams from the physical radios. - DSP Combiner: Uses the geographic location of the transmitter and the local array geometry to calculate steering vectors and combine the streams.
- Egress: Packages the mathematically combined
numpyarray back into standard RTP UDP packets and pushes them to the client.
This daemon operates in a continuous, high-throughput environment alongside physical GPSDO-locked hardware. To ensure deterministic reliability, the codebase adheres to a Python-adapted version of the "Power of 10" rules:
- Strict Type Hinting: Full type annotation coverage enforced via
mypyto prevent subtle casting errors in DSP math. - No Catch-All Exceptions: Bare
except:blocks are forbidden. All exceptions (e.g.,OSError,ValueError) are caught explicitly to prevent background threads from silently swallowing critical errors or OS signals. - Immutable Data Structures: Control messages and configurations use
@dataclass(frozen=True)to prevent accidental state mutation across multi-threaded data planes. - Dependency Isolation: Strict enforcement of
venvto prevent "floating" dependency versions (especially for NumPy/SciPy) from altering matrix solver behaviors.
- Transparent Integration: Clients using
ka9q-pythonrequire zero modification to use basic spatial combining. - Extended Spatial API: Clients that are aware of Phase Engine can append geographic extensions to their requests:
# Standard ka9q-python channel creation channel = control.create_channel( frequency_hz=10e6, preset="iq", # Phase Engine spatial extensions: reception_mode="focus", # Beam toward target target="WWV", # Station name or Lat/Lon null_targets=["BPM"], # Interference to null combining_method="mvdr", # Use Capon beamforming )
- Supported DSP Methods:
mrc(Maximum Ratio Combining): Optimal for white-noise dominant environments.egc(Equal Gain Combining): Aligns phases but ignores amplitude.selection: Simply selects the antenna with the highest raw SNR.focus/beamform: Standard delay-and-sum spatial matched filter.mvdr/adaptive: Minimum Variance Distortionless Response. Forces a deep null on interference while maintaining unity gain on the target.
IMPORTANT: Development Requirements
- You MUST use a Python virtual environment (
venv) for all development, testing, and execution. - Do NOT use global installations of
pipdependencies. This project requires strict isolation to prevent conflicts with system packages or other projects.
Note:
status_addressinconfig/phase-engine.tomlmust be a multicast IP (e.g.239.99.1.1), not a hostname. Hostnames are rejected by the internal socket layer.
- Create and activate a virtual environment:
python3 -m venv venv
source venv/bin/activate- Install dependencies:
pip install -e .[dev,plot]- Define your array geometry in
phase-engine.toml. Antenna positions are specified in meters relative to the phase center (ENU coordinate system). - Specify your QTH (latitude/longitude) so the engine can accurately calculate true geographic bearings to shortwave transmitters.
- Start the daemon:
phase-engine daemon --config config/phase-engine.tomlFor long-running environments, it is recommended to install phase-engine as a systemd service so it starts automatically alongside your physical radiod instances.
An installation script is provided to set up a dedicated phase-engine user, create the virtual environment in /opt/phase-engine, and install the systemd unit file.
- Run the install script as root:
sudo ./install.sh- Configure your array geometry:
sudo nano /etc/phase-engine/config.toml- Start and enable the service:
sudo systemctl enable phase-engine
sudo systemctl start phase-engine- Monitor the logs:
sudo journalctl -u phase-engine -fPhase Engine includes a built-in CLI tool to generate high-resolution polar plots of your theoretical array radiation pattern based on your physical layout and DSP configuration.
# Plot the MVDR beam pattern targeting WWV while nulling BPM
phase-engine plot --config config/phase-engine.toml \
--freq 10e6 \
--method mvdr \
--target "WWV,40.6780,-105.0470" \
--null "BPM,34.9500,109.5330" \
--out pattern_wwv.pngFor detailed mathematical documentation on the Geographic Bearing and Steering Vector logic, see docs/GEOMETRY_MATH.md.