Python Examples
The Python examples demonstrate a couple practical applications of the Python bindings for solving real-world VLBI data processing problems. These examples show how to use the Python API for custom calibration, data correction, and analysis tasks. Python plugins can be used to correct known instrumental problems and preprocess data before fringe fitting. They can also support automated generation and refinement of calibration corrections, assessment of phase stability across channels, and development of other custom calibration algorithms for specialized or multi-pass processing workflows.
Minimal Plugin (example1.py)
File |
example1.py |
Category |
Python Examples |
Function |
dummy(fringe_data_interface) |
Primary Functionality |
Minimal “hello world” plugin |
Key Features |
Parameter store lookup by path
Reference/remote station identification
|
The simplest possible plugin. It only demonstrates how to access the parameters store by looking up the reference and remote station Mark4 IDs and prints them. You can use this as a basic starting point when writing a new plugin.
Implementation Details:
def dummy(fringe_data_interface):
param_interface_obj = fringe_data_interface.get_parameter_store()
ref_station_id = param_interface_obj.get_by_path("/ref_station/mk4id")
rem_station_id = param_interface_obj.get_by_path("/rem_station/mk4id")
print("reference station", ref_station_id, ", remote station: ", rem_station_id)
NOEMA Phase Jump Correction (example2.py)
File |
example2.py |
Category |
Python Examples |
Function |
fix_noema_jumps(fringe_data_interface) |
Primary Functionality |
Corrects known phase jumps in NOEMA telescope data |
Key Features |
Channel-specific phase corrections
Polarization-dependent adjustments
Frequency-based jump detection
Direct manipulation of visibility arrays
|
The NOEMA phase jump correction example demonstrates how to use the Python API to correct known instrumental phase jumps in visibility data.
Implementation Details:
def fix_noema_jumps(fringe_data_interface):
cstore_interface_obj = fringe_data_interface.get_container_store()
param_interface_obj = fringe_data_interface.get_parameter_store()
# grab the UUID of the visibility object
vis_uuid = param_interface_obj.get_by_path("/uuid/visibilities")
visib_obj = cstore_interface_obj.get_object(vis_uuid)
if visib_obj is None:
return # bail out
# figure out if NOEMA is reference or remote station
stidx = 0
ref_id = param_interface_obj.get_by_path("/ref_station/site_id")
rem_id = param_interface_obj.get_by_path("/rem_station/site_id")
if ref_id == "Nn":
stidx = 0
if rem_id == "Nn":
stidx = 1
# grab the underlying visibility 4-d array and the axis information we care about
vis_arr = visib_obj.get_numpy_array()
axis0 = visib_obj.get_axis(0) # polprod axis
axis1 = visib_obj.get_axis(1) # channel axis
axis3 = visib_obj.get_axis(3) # spectral point axis (sub-channel)
chan_meta_data = visib_obj.get_axis_metadata(1) # channel axis meta data object
channel_info = chan_meta_data["index_labels"] # channel bin label dict
#Note: these dictionaries are truncated for this example, see the source code for entirety
jumps_l = {
"a": [25.0, -1.0],
}
jumps_r = {
"a": [25.0, -1.0],
}
# stash both pols in one object
jumps = dict()
jumps["R"] = jumps_r
jumps["L"] = jumps_l
# apply the corrections, looping over pol-product, channel, and spectral point
nspectral = visib_obj.get_dimension(3)
for pp in range(0, len(axis0)):
polprod = axis0[pp]
pol = polprod[stidx:stidx + 1]
for ch in range(0, len(axis1)):
chan_key = str(ch) # meta data keys must be strings, not integers
fourfit_chan_label = channel_info[chan_key]["channel_label"]
if pol in jumps and fourfit_chan_label in jumps[pol]:
jump_freq, jump_phasor = jumps[pol][fourfit_chan_label]
for sp in range(0, nspectral):
freq = axis3[sp]
if freq > jump_freq:
vis_arr[pp, ch, :, sp] *= jump_phasor
Phase Calibration Generation (example3.py)
File |
example3.py |
Category |
Python Examples |
Function |
generate_pcphases(fringe_data_interface) |
Primary Functionality |
Generates phase calibration corrections from fringe fit results |
Key Features |
Plot data analysis for phase residuals
Channel-based phase residual computation
Circular mean phase calculation
Fourfit-compatible output format generation
|
The phase calibration generation example shows how to derive manual phase calibration corrections from fringe fitting results and generate output compatible with the fourfit control file format.
Implementation Details:
import numpy as np
import scipy.stats
from vpal import utility
def generate_pcphases(fringe_data_interface):
# keep in mind that 'plot_data' is only available in the 'finalize' step
# so this function must be called as a 'python_finalize' operator
plot_data = fringe_data_interface.get_plot_data()
param_interface_obj = fringe_data_interface.get_parameter_store()
# get the remote station mk4id (this function always generates pc_phases
# as if they were for the remote_station)
rem_station_id = param_interface_obj.get_by_path("/rem_station/mk4id")
# get channel label and phase
ch_labels = plot_data["PLOT_INFO"]["#Ch"]
ch_phase = plot_data["PLOT_INFO"]["Phase"]
nchan = len(ch_labels) - 1
phase_residuals = dict()
for i in list(range(0, nchan)):
chan_id = ch_labels[i]
phase_residuals[chan_id] = ch_phase[i] # in degrees
# now compute the phase corrections
phase_corrections = phase_residuals.copy()
phase_list_proxy = []
channel_list = []
for ch, ch_phase in list(phase_corrections.items()):
channel_list.append(ch)
phase_list_proxy.append(-1.0 * ch_phase) # invert to get corrections from residuals
# compute the circular mean phase, then subtract it off and limit to [-180, 180)
mean_phase = scipy.stats.circmean(np.asarray(phase_list_proxy), high=180.0, low=-180.0)
phase_list_proxy = [utility.limit_periodic_quantity_to_range((x - mean_phase), -180.0, 180.0)
for x in phase_list_proxy]
# assign the corrections
for i in list(range(0, len(phase_list_proxy))):
phase_corrections[channel_list[i]] = phase_list_proxy[i]
chan_names = ""
phase_list_str = ""
for elem in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789":
if elem in channel_list:
chan_names += elem
phase_list_str += str(round(phase_corrections[elem], 2)) + " "
print("if station", rem_station_id, " pc_phases ", chan_names, phase_list_str)
This example depends on scipy.stats.circmean for the circular mean phase
calculation and on vpal.utility.limit_periodic_quantity_to_range (from the
vpal module shipped with HOPS) to wrap phases into [-180, 180).
Output Format:
The generated output is a single if station ... pc_phases ... line,
matching the control-file conditional syntax, and can be pasted directly into
a fourfit control file, e.g.:
if station G pc_phases abcd 1.2 -3.4 5.6 -7.8
Operator Toolbox Reconfiguration (example4.py)
File |
example4.py |
Category |
Python Examples |
Function |
set_pc_phase_offset_y(fringe_data_interface) |
Primary Functionality |
Retrieves and reconfigures a calibration operator from the toolbox |
Key Features |
Operator toolbox inspection
Lookup of operators by name and by station
Runtime reconfiguration of an operator’s parameters
Re-initialization after reconfiguration
|
This example demonstrates how to retrieve an already-constructed calibration
operator from the operator toolbox and override its configuration at runtime.
It retrieves the pc_phase_offset_y operator (an MHO_ManualPolPhaseCorrection
instance created by the builder when the control file contains a
pc_phase_offset_y statement), finds the copy that matches a specific
station, and overrides its phase offset.
Implementation Details:
import pyMHO_Containers
import pyMHO_Operators
import pyMHO_Calibration # must import so pybind11 can downcast MHO_Operator* to the correct type
def set_pc_phase_offset_y(fringe_data_interface):
toolbox = fringe_data_interface.get_operator_toolbox()
if toolbox is None:
print("example4: operator toolbox not available")
return
print("example4: operators in toolbox:")
for name in toolbox.get_operator_names():
print(f" {name}")
# Because pyMHO_Calibration is imported, pybind11 downcasts the returned
# MHO_Operator* to MHO_ManualPolPhaseCorrection. Multiple copies may have
# been created by the control file machinery, so inspect each one for the
# station we're interested in.
station_mk4_id = "E" # westford
station_id = "Wf" # westford
ops = toolbox.get_all_operators_by_name("pc_phase_offset_y")
if len(ops) == 0:
print("example4: 'pc_phase_offset_y' operator not found in toolbox "
"(check that the control file contains a pc_phase_offset_y statement)")
return
ph_off = 140.0
for op in ops:
stid = op.get_station_identifier()
if stid == station_mk4_id or stid == station_id:
print(f"example4: found operator '{op.get_name()}', for station: '{stid}', setting phase offset to '{ph_off}'")
op.set_pc_phase_offset(ph_off)
# Re-initialize so the new offset is picked up before Execute() is called.
ok = op.initialize()
if not ok:
print("example4: warning - initialize() returned False after reconfiguration")
Note that after changing an operator’s configuration, initialize() must be
called so the operator re-caches any pre-computed values before Execute()
runs.
Python API Usage Patterns
The previous examples demonstrate several important usage patterns for the Python API, these are:
Data Access Pattern:
# Access the parameter store and container store
parameter_store = fringe_data_interface.get_parameter_store()
container_store = fringe_data_interface.get_container_store()
# Look up an object's UUID via the parameter store, then fetch it
# from the container store
vis_uuid = parameter_store.get_by_path("/uuid/visibilities")
visibility_data = container_store.get_object(vis_uuid)
# 'plot_data' is only populated during the finalize step, so functions
# that use it must be registered as a 'python_finalize' operator
plot_data = fringe_data_interface.get_plot_data()
Array Manipulation Pattern:
# Get the NumPy array view of the underlying C++ data
vis_array = visibility_data.get_numpy_array()
# Modify data in-place, indexed by [polprod, channel, time, spectral_point]
vis_array[pp, ch, :, sp] *= correction_factor
# Changes are automatically reflected in the C++ data, but DO NOT resize/reshape the numpy arrays!
Metadata Access Pattern:
# Access axis values and axis metadata (JSON tags) for a container
channel_axis = visibility_data.get_axis(1)
channel_meta_data = visibility_data.get_axis_metadata(1)
channel_labels = channel_meta_data["index_labels"]
Error Handling Pattern:
try:
# Python processing code
result = process_data(fringe_data_interface)
except Exception as e:
# Handle errors gracefully
print(f"Processing error: {e}")
return False