HOPS Python Plugin User Guide
This document describes how to add Python-based plugins to the HOPS fringe fitter pipeline. There are three categories of Python plugins:
Python Data Operators – inject Python functions into the calibration pipeline
Custom Plot Functions – replace the default fringe plot with your own
Python Control Files – write the entire control config as a Python script
Each category is covered below with the exact API, file placement, and control file syntax required for each extension.
1. Where to Install Your Python Scripts
The HOPS embedded Python interpreter searches for python plugins in two directories:
Default plugins directory:
${CMAKE_INSTALL_PREFIX}/plugin_scripts/This path is baked into the fourfit binary at compile time via the CMake variable
PLUGINS_INSTALL_DIR. It is not an environment variable you can override at runtime. The actual path is printed to the fourfit log at startup (tagged[main]), which is the easiest way to confirm it.User plugins directory (optional):
$HOPS_USER_PLUGINS_DIR/(set via theHOPS_USER_PLUGINS_DIRenvironment variable)
Both directories are appended to Python’s sys.path at startup.
To use a custom user directory, export it before running fourfit:
export HOPS_USER_PLUGINS_DIR=/home/user/my_hops_plugins
The Python scripts you write must be importable as Python modules. This means:
A file
my_plugin.pyis importable asimport my_pluginA file
my_pkg/module.pyis importable asimport my_pkg.moduleIf using packages, include
__init__.pyin the directory
The scripts do NOT need to be compiled. They are loaded by the embedded
interpreter at runtime. Any third-party dependencies your script requires
(e.g. pandas) must also be importable in the same Python environment
that fourfit was built against.
2. Python Data Operators
A Python data operator injects a Python function into the fringe fitter’s
operator pipeline. The function receives an MHO_PyFringeDataInterface object
giving it read/write access to visibilities, weights, parameters, and scan data.
While the Python operator has full read/write access to these data containers
(exposed as numpy arrays), the C++ backend owns the memory. Any attempt to
resize, reshape, or reallocate these arrays from Python will fail.
2.1 Required API
Your Python module must define a function with the following signature. The name can be anything, but it must be a free function accepting a fringe data interface object:
def my_operator(fringe_data_interface):
...
The function takes a single argument: a MHO_PyFringeDataInterface.
It returns nothing (None). Any Python exception raised will be caught by the
C++ side, logged under the python_bindings tag, and execution will attempt
to continue with the next pipeline stage.
Note
sys.exit() / SystemExit is an exception to this rule: if your plugin
calls sys.exit(), the embedded interpreter propagates it and fourfit exits
immediately rather than logging and continuing.
2.2 Available Categories and Priorities
Python operators can be placed in any of these pipeline categories:
Category |
Control keyword |
Default priority |
|---|---|---|
labeling |
python_labeling |
0.2 (runs first) |
flagging |
python_flagging |
2.9 |
calibration |
python_calibration |
3.9 |
prefit |
python_prefit |
7.9 |
postfit |
python_postfit |
8.9 |
finalize |
python_finalize |
9.9 (runs after fitting, plot data available) |
2.3 Control File Syntax
Add one line per python operator in your control file, example:
python_calibration my_module my_function [optional_priority]
Where:
python_calibrationis the category keyword (python_labeling,python_flagging,python_calibration,python_prefit,python_postfit,python_finalize)my_moduleis the Python module path (dot-separated, e.g.my_pkg.sub.module)my_functionis the function name inside the module[optional_priority]is an optional float to override the default execution order
Example control file lines:
python_calibration fix_noema_jumps fix_noema_jumps
python_finalize generate_pcphases generate_pcphases 9.95
2.4 Complete Working Example
File: $HOPS_USER_PLUGINS_DIR/my_calibration.py
import numpy as np
def zero_dc_point(fringe_data_interface):
"""Zero out DC point in each channel of visibility data."""
# Get parameter store and container store
pstore = fringe_data_interface.get_parameter_store()
cstore = fringe_data_interface.get_container_store()
# Read a parameter
ref_station = pstore.get_by_path("/ref_station/mk4id")
print(f"Reference station: {ref_station}")
# Get the visibility object by UUID
vis_uuid = pstore.get_by_path("/uuid/visibilities")
vis_obj = cstore.get_object(vis_uuid)
if vis_obj is None:
return
# Get the data as a zero-copy numpy array (modifications affect C++ side)
vis_arr = vis_obj.get_numpy_array()
# Get axis information
polprod_axis = vis_obj.get_axis(0) # polarization products
channel_axis = vis_obj.get_axis(1) # channels
time_axis = vis_obj.get_axis(2) # time (AP)
freq_axis = vis_obj.get_axis(3) # spectral points
# Apply a simple correction: zero out the first spectral point
for pp in range(len(polprod_axis)):
for ch in range(len(channel_axis)):
for ti in range(len(time_axis)):
vis_arr[pp, ch, ti, 0] = 0.0 + 0.0j
# You can also set parameters from python:
pstore.set_by_path("/control/config/my_correction_applied", True)
3. Python Operator Toolbox Access
From any Python operator (any category), you can retrieve and reconfigure existing C++ calibration operators that were built by the control file machinery. This is useful for dynamically adjusting operator parameters based on runtime data.
3.1 Imports
pyMHO_Containers, pyMHO_Operators, and pyMHO_Calibration are all
imported automatically by the embedded interpreter before any user plugin is
called (see 7. Pre-imported Modules). You do not need to import
them in your plugin scripts for the toolbox and type downcasting to work.
If you want explicit imports for IDE autocompletion or clarity:
import pyMHO_Operators
import pyMHO_Calibration
3.2 API
toolbox = fringe_data_interface.get_operator_toolbox()
if toolbox is None:
return # toolbox not available in this context
# Get all operator names (one entry per operator, in priority order)
names = toolbox.get_operator_names()
# Get all operators with a specific name (returns list sorted by priority)
ops = toolbox.get_all_operators_by_name("operator_name")
# Get operators in a specific category (sorted by priority)
cal_ops = toolbox.get_operators_by_category("calibration")
# Get total operator count
n = toolbox.get_n_operators()
All list-returning methods return operators sorted by ascending priority. The returned operator objects are references into C++-owned memory; do not store them past the end of your plugin function.
3.3 Complete Working Example
File: $HOPS_USER_PLUGINS_DIR/reconfigure_op.py
import pyMHO_Operators
import pyMHO_Calibration
def adjust_pc_phase_offset(fringe_data_interface):
"""Find a pc_phase_offset_y operator and change its phase offset."""
toolbox = fringe_data_interface.get_operator_toolbox()
if toolbox is None:
print("operator toolbox not available")
return
# Retrieve all operators named "pc_phase_offset_y"
ops = toolbox.get_all_operators_by_name("pc_phase_offset_y")
if not ops:
print("operator not found in toolbox")
return
# Iterate and find the one for a specific station
target_station = "E"
for op in ops:
stid = op.get_station_identifier()
if stid == target_station:
op.set_pc_phase_offset(140.0)
# Re-initialize after reconfiguration
op.initialize()
print(f"Adjusted phase offset for station {stid}")
4. Custom Plot Functions
Replace the default fringe plot with your own Python matplotlib function.
4.1 Required API
Your function must have this signature:
def my_plot_function(fringe_data_interface):
plot_data = fringe_data_interface.get_plot_data()
import matplotlib.pyplot as plt
plt.figure()
plt.plot(...)
plt.savefig("my_custom_plot.png", dpi=300)
plt.close()
The plot_data dictionary contains the following top-level keys (a subset
may be absent if the corresponding data was not computed):
PLOT_INFO– per-channel table (dict of lists; see below)DLYRATE,DLYRATE_XAXIS– delay-rate search amplitude and axis valuesMBD_AMP,MBD_AMP_XAXIS– multiband delay search amplitude and axisSBD_AMP,SBD_AMP_XAXIS– singleband delay amplitude and axisXPSPEC-ABS,XPSPEC-ARG,XPSPEC_XAXIS– cross-power spectrum (amplitude, phase, freq axis)SEG_AMP,SEG_PHS– segment amplitudes and phasesSEG_FRAC_USB,SEG_FRAC_LSB– USB/LSB validity fractions per segmentNSeg,NPlots– number of time segments and channel plotsChannelsPlotted– list of channel labels included in the plot (optional)Quality,SNR,Amp,ResPhase,PFD,IntgTime– fringe quality metricsResidSbd(us),ResidMbd(us)– residual single- and multiband delaysFringeRate(Hz),IonTEC(TEC)– fringe rate and ionospheric TECRefFreq(MHz),AP(sec)– reference frequency and accumulation periodExperName,ExperNum,YearDOY,Start,Stop– observation metadataFRT,CorrTime,FFTime,BuildTime– processing timestampsRA,Dec– source coordinatesRootScanBaseline,CorrVers,PolStr– header identification fieldsextra– dict of optional supplementary fields (see below)
Selected extra sub-keys:
pol_product– polarization product string (e.g."RR","I"for pseudo-Stokes)ref_station,rem_station– dicts with az, el, pa, u, v per stationu,v– UV baseline coordinatessb_win,mb_win,dr_win,ion_win– search window limits[min, max]ref_mtpc_phase_segs,rem_mtpc_phase_segs– multitone PCAL phase segments per channeldtec_array,dtec_amp_array– ionospheric dispersion data (when ionosphere fitting is enabled)
Note: you are not limited to the data in plot_data. The full fringe data
interface gives you access to visibilities, weights, and the parameter store,
so you can compute any derived quantities in Python. Those computations will
generally be slower than equivalent C++ implementations.
4.2 Control File Syntax
python_custom_plot my_plot_module my_plot_function
Where:
my_plot_moduleis the Python module path (dot-separated)my_plot_functionis the function to call
Example:
python_custom_plot custom_fourfit_plot make_fourfit_plot_wrapper
4.3 Plot Backend Selection
The plot backend is controlled by the plot_backend parameter in the
control file:
plot_backend matplotlib
When plot_backend is "matplotlib", the MHO_DefaultPythonPlotVisitor
is used to call a Python matplotlib-based plot function.
If python_custom_plot is also set, it overrides the module and function
that the visitor calls. If python_custom_plot is not set, the defaults are:
module:
hops_visualization.fourfit_plotfunction:
make_fourfit_plot_wrapper
If plot_backend is not set in the control file and gnuplot support was
compiled in, gnuplot is used instead. When gnuplot support is not compiled
in, matplotlib is the fallback.
Note
python_custom_plot only takes effect when plot_backend matplotlib
is also set (or when matplotlib is selected as the fallback). Setting
python_custom_plot alone has no effect if gnuplot is active.
4.4 Complete Working Example
File: $HOPS_USER_PLUGINS_DIR/my_simple_plot.py
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
def simple_delay_rate_plot(fringe_data_interface):
"""Plot delay rate vs time and save to file."""
plot_data = fringe_data_interface.get_plot_data()
pstore = fringe_data_interface.get_parameter_store()
dlyrate = plot_data.get("DLYRATE", [])
plt.figure(figsize=(10, 6))
plt.plot(dlyrate, "b-", linewidth=1.0)
plt.xlabel("Time integration")
plt.ylabel("Delay rate (ns/s)")
plt.title("Delay Rate Search")
plt.grid(True)
# Determine output file
if pstore.is_present("/cmdline/disk_file"):
out_file = pstore.get_by_path("/cmdline/disk_file")
else:
out_file = "simple_delay_rate.png"
plt.savefig(out_file, dpi=300)
plt.close()
5. Python Control Files
Instead of writing a fourfit domain-specific language (DSL) control file, you
can write a Python script (.py) that defines the same configuration
programmatically. This approach supports conditional logic but has some
structural constraints described below.
5.1 Required API
Your Python control file must define:
from hops_control import PassInfo, Config
def configure(p: PassInfo, cfg: Config):
...
5.2 PassInfo (read-only)
PassInfo is an immutable description of the fringe pass (baseline, source, etc.)
constructed by the C++ runtime before configure() is called.
Read-only properties:
p.baseline # full baseline string, e.g. "EG" or "Gs-Wf"
p.ref_station # single-char Mk4 ID of reference station, e.g. "E"
p.rem_station # single-char Mk4 ID of remote station, e.g. "G"
p.source # source name, e.g. "3C279"
p.fgroup # frequency group character, e.g. "X"
p.scan_name # scan name/time string (used for ordering comparisons)
p.polprod # polarization product, e.g. "RR"
Condition helper methods (use these inside cfg.IF() chains, or for plain
Python conditionals – both are valid):
p.station("E") # True if "E" is ref or remote Mk4 ID;
# pass canonical name (e.g. "Gs") to match by long code;
# "?" always returns True (wildcard)
p.baseline_match("GE") # True if baseline matches; supports "?" wildcard per station
p.source_match("3C279") # True if source matches; "?" is wildcard
p.fgroup_match("X") # True if fgroup matches; "?" is wildcard
p.scan_before("100-1200") # True if current scan name < argument (lexicographic)
p.scan_after("100-1200") # True if current scan name > argument (lexicographic)
p.scan_between("a", "b") # True if a <= scan_name <= b (inclusive, lexicographic)
5.3 Config (writer)
Config accumulates control statements and serializes them into the
intermediate representation consumed by the operator builders. Every control
keyword defined in the JSON format specifiers is available as a method on
this class. To see all available keywords at runtime:
print(cfg.available_keywords())
Example usage:
cfg.ref_freq(215000.0)
cfg.pc_mode("multitone")
cfg.ion_smooth(True)
cfg.weak_channel(0.1)
cfg.samplers(["abcdefgh", "ijklmnop", "qrstuvwx", "yzABCDEF"])
Compound parameters (multiple arguments):
cfg.pc_phases_x("abcdefghijklmnop", [1.0, -2.0, 3.0, ...])
cfg.pc_tonemask("cdejnprwBC", [2, 16, 16, 1, 16, 16, 16, 2, 16, 16])
Conditional blocks (mirrors DSL “if station X”):
with cfg.IF().station("E"):
cfg.sampler_delay_x([-20, -20, -20, -20])
with cfg.IF().baseline("GE"):
cfg.ion_npts(11)
with cfg.IF().source("3C279").AND().fgroup("X"):
cfg.ref_freq(86000.0)
with cfg.IF().station("E").OR().station("G"):
cfg.weak_channel(0.05)
Convenience shortcuts for single-predicate conditionals:
with cfg.if_station("E"):
...
with cfg.if_baseline("GE"):
...
with cfg.if_source("3C279"):
...
with cfg.if_fgroup("X"):
...
Important
Nested conditional blocks are not allowed. Attempting to nest with
blocks raises a RuntimeError at runtime. Combine all predicates in a
single chain instead:
# OK: combine with .AND() / .OR() in one chain
with cfg.IF().station("G").AND().source("3C279"):
...
# NOT OK: raises RuntimeError
with cfg.IF().station("G"):
with cfg.IF().source("3C279"): # <-- error
...
The with-block construct is required because the conditional
information must be passed to an intermediate control-statement
representation before the operator builders consume it. Standard Python
if statements (e.g. if p.station("G"):) are fine for non-conditional
global statements but cannot produce the station/baseline/source-conditional
blocks that the operator pipeline needs.
5.4 Complete Working Example
File: my_config.py (used as control file directly (MUST have .py extension))
from hops_control import PassInfo, Config
def configure(p: PassInfo, cfg: Config):
# Global settings (always applied)
cfg.ref_freq(215000.0)
cfg.pc_mode("multitone")
cfg.pc_period(1)
cfg.ion_smooth(True)
cfg.weak_channel(0.1)
# Per-station settings
with cfg.IF().station("E"):
cfg.sampler_delay_x([-20, -20, -20, -20])
cfg.sampler_delay_y([-20, -20, -20, -20])
cfg.pc_delay_y(0.734)
cfg.pc_phase_offset_y(138.2)
with cfg.IF().station("G"):
cfg.sampler_delay_x([-140, 180, 180, 180])
cfg.sampler_delay_y([-140, 180, 180, 180])
# Baseline-specific settings
with cfg.IF().baseline("GE"):
cfg.ion_npts(11)
cfg.ion_win([-20.0, 20.0])
# Source-specific frequency override
with cfg.IF().source("3C279").AND().fgroup("X"):
cfg.ref_freq(86000.0)
To use this as a control file, pass it to fourfit4 just like an ordinary control file (note the .py extension):
fourfit4 -c my_config.py ...
6. Python API Reference
6.1 MHO_PyFringeDataInterface (main entry point)
Methods:
get_parameter_store()->MHO_PyParameterStoreInterfaceGet the current parameter store (returns a reference; not a copy).
get_container_store()->MHO_PyContainerStoreInterfaceGet the data container store (visibilities, weights, etc.; returns a reference).
get_scan_store()->MHO_PyScanStoreInterfaceGet the scan data store (returns a reference).
get_vex()->dictGet VEX/root metadata as a Python dictionary (returns a copy).
get_plot_data()->dictGet the plot data dictionary (returns a copy). Only populated after the fringe fit completes; in all pipeline categories before
finalize, this dict will be empty.get_operator_toolbox()->MHO_OperatorToolboxorNoneGet the operator toolbox to query/reconfigure C++ operators. Returns
Nonewhen the toolbox has not been wired up (e.g. in stand-alone plotting contexts); always check forNonebefore use.
6.2 MHO_PyParameterStoreInterface
is_present(path: str)->boolReturn
Trueif a parameter exists at the given path.get_by_path(path: str)->anyGet the value at the given path. Returns Python native types (int, float, str, list, or dict); JSON objects are auto-converted. If the path does not exist, prints an error to the console and returns an empty/
Noneobject. Useis_present()first if the path may be absent.set_by_path(path: str, value: any)Set a parameter at the given path. Value must be JSON-serialisable.
get_contents()->dictGet the entire parameter store as a nested dictionary.
Parameter paths follow the convention:
/config/<name>– global config parameters/control/config/<name>– control file config parameters/control/station/<CODE>/<name>– per-station parameters/control/fit/<name>– fit-time parameters/ref_station/<field>– reference station info (mk4id, site_id, etc.)/rem_station/<field>– remote station info/uuid/<object_name>– UUID of named data objects/cmdline/<option>– command-line options/status/<flag>– runtime status flags (e.g./status/skipped)
6.3 MHO_PyContainerStoreInterface
is_valid()->boolReturn
Trueif the store is valid.get_nobjects()->intReturn the number of objects in the store.
is_object_present(uuid: str)->boolReturn
Trueif an object with the given UUID exists.get_object_id_list()->list[dict]Return a list of dicts, each with keys
"type_uuid","object_uuid", and"shortname".get_object(uuid: str)->MHO_PyTableContainerordictorNoneRetrieve the data object by UUID. The return type depends on the underlying data type:
MHO_PyTableContainerfor array types:visibility_type,weight_type,phasor_type,station_coord_type,multitone_pcal_type,visibility_store_type,weight_store_typedictforMHO_ObjectTags(tag/metadata objects)Noneif the UUID is not found
Useful UUID paths for runtime object lookup:
/uuid/visibilities– visibility data/uuid/weights– weight data/uuid/phasors– fringe-fit phasor averages
6.4 MHO_PyTableContainer (for visibility/weight/phasor objects)
get_rank()->intReturn the number of dimensions (4 for
visibility_type).get_dimension(index: int)->intReturn the size of dimension
index.get_classname()->strReturn the C++ type name of the underlying container (e.g.
"visibility_type").get_numpy_array()->numpy.ndarrayReturn the underlying data as a zero-copy numpy array backed by C++ memory. Modifications in Python are immediately visible on the C++ side. The array cannot be resized, reshaped, or reallocated.
get_axis(index: int)->listReturn the coordinate labels for the axis at dimension
indexas a new Python list (a copy, not a view into C++ memory).get_axis_metadata(index: int)->dictReturn metadata for the axis at dimension
indexas a dict. The dict includes an"index_labels"sub-dict for per-coordinate metadata (e.g. per-channel information for axis 1 of a visibility array).set_axis_metadata(index: int, metadata: dict)Replace the entire axis metadata dict. Derive the new value from
get_axis_metadata()first to avoid discarding existing fields.set_axis_label(dim_index: int, coord_index: int, label_value: any)Modify a single coordinate label on an axis. The type of
label_valuemust match the axis element type or a cast exception will be raised.get_metadata()->dictReturn the table-level metadata dictionary.
set_metadata(metadata: dict)Replace the table-level metadata dictionary.
6.5 MHO_OperatorToolbox
get_operator_names()->list[str]Return the name of every operator in the toolbox, in priority order (one entry per operator, including duplicates with the same name).
get_n_operators()->intReturn the total operator count.
get_all_operators_by_name(name: str)->list[MHO_Operator]Return all operators whose name equals
name, sorted by ascending priority. Returns an empty list if none are found.get_operators_by_category(category: str)->list[MHO_Operator]Return all operators in the given category (e.g.
"calibration","flagging"), sorted by ascending priority.
All returned operator objects are references into C++-owned memory.
Do not store them beyond the scope of your plugin function.
pyMHO_Calibration is pre-imported at startup, so pybind11 can always
downcast the base MHO_Operator* pointers to their concrete derived types
without any explicit import in your script.
6.6 Common Data Types and Array Shapes
visibility_type– 4D complex array [polprod, channel, time, freq]weight_type– 4D real array [polprod, channel, time, freq]phasor_type– time/freq averaged complex phasorssbd_type– single-band delay datastation_coord_type– station coordinates
For visibility_type, the 4 axes are:
Axis 0: polarization products (e.g. “RR”, “LL”, “RL”, “LR”, “XX”, “XY”)
Axis 1: channel index (0, 1, 2, …)
Axis 2: time integration index (0, 1, 2, …)
Axis 3: spectral point within channel (frequency coordinate)
7. Pre-imported Modules
The following pybind11 extension modules are imported automatically by the embedded interpreter before any user plugin is called:
pyMHO_Containers– Container store, parameter store, table containerspyMHO_Operators– Operator base class, operator toolbox, and theget_operator_toolbox()method onMHO_PyFringeDataInterfacepyMHO_Calibration– Concrete calibration operator classes (required for pybind11 to downcastMHO_Operator*pointers to derived types)
Because all three are pre-loaded, you do not need to import them in
your plugin scripts for downcasting or toolbox access to work. Adding
explicit import statements in your scripts is harmless and can be
useful for IDE autocompletion.
The hops_control and hops_visualization Python packages are
installed alongside HOPS and can be imported normally:
from hops_control import PassInfo, Config # for Python control files
import hops_visualization # for plotting utilities
8. Debugging Tips
Import errors: Make sure your script is in
$HOPS_USER_PLUGINS_DIRor the default plugins directory. The default directory path is baked into the binary at compile time (not an environment variable); fourfit prints it to the log at startup tagged[main]. You can also confirm your user directory:echo $HOPS_USER_PLUGINS_DIR
Module not found: Verify the module path in the control file uses dot syntax (e.g.
my_pkg.my_module), matching the directory structure under the plugins directory.Function not found: The function name in the control file must match exactly the
defname in the Python file (case-sensitive).pybind11 cast errors: If you see
"Unable to convert call argument"or"terminate called after throwing an instance of 'pybind11::cast_error'", make surepyMHO_Calibrationhas been imported. It is pre-imported automatically, so this error usually indicates the embedded interpreter was not initialised before your script ran (which should not happen under normal fourfit operation).Python exceptions in operator: Exceptions are caught by the C++ side, logged, and execution continues. Check the fourfit log for messages tagged
python_bindings. Note thatsys.exit()/SystemExitis not caught – it terminates fourfit immediately.stdout buffering: The embedded interpreter reconfigures
sys.stdoutto line-buffered mode, soprint()output should appear promptly. If you still see delays, write directly to a file:with open("/tmp/my_debug.log", "a") as f: f.write(f"debug: {value}\n")
numpy version: The zero-copy numpy array relies on numpy being available at both build time and runtime. The C++ build links against the numpy present at compile time; the runtime numpy should be compatible (same major version).
Plot data availability:
get_plot_data()only returns populated data in thefinalizepipeline category. In earlier categories (calibration,prefit,postfit), the plot data dict will be empty.
9. Quick Reference: Checklist
To add a new Python data operator:
[ ] Write a Python function:
def my_func(fringe_data_interface):[ ] Place the
.pyfile in$HOPS_USER_PLUGINS_DIRor default plugin dir[ ] Add to control file:
python_calibration my_module my_func [priority][ ] Verify the module is importable:
python3 -c "import my_module"
To add a custom plot function:
[ ] Write a Python function:
def my_plot(fringe_data_interface):[ ] Use
fringe_data_interface.get_plot_data()to get plot dictionary[ ] Place the
.pyfile in the plugin directory[ ] Add to control file:
python_custom_plot my_plot_module my_plot[ ] Set
plot_backendto"matplotlib"in the control file
To write a Python control file:
[ ] Create a
.pyfile with:from hops_control import PassInfo, Config[ ] Define:
def configure(p: PassInfo, cfg: Config):[ ] Use
cfg.<keyword>(value)for global settings[ ] Use
with cfg.IF().station("X"):for conditional settings[ ] Pass the
.pyfile to fourfit as the-cargument