DOT reference for measurement graphs

A DOT file describes active instruments, passive correction elements, and their connections. It is not a workflow program: frequency loops, operator interaction, and evaluation remain responsibilities of the measurement class. An mpylab.tools.mgraph.MGraph instance connects the graph to device creation, conditions, switching actions, and path corrections.

Production parser and input forms

MGraph uses pydot as its authoritative production parser. It accepts:

  • a file name or os.PathLike object;

  • a readable file-like object;

  • direct DOT text;

  • structured pydot-compatible graph data.

Relative file names are resolved through SearchPaths. A missing file, invalid DOT text, or multiple concatenated graphs raise mpylab.tools.mgraph.GraphInputError. A measurement graph contains exactly one graph or digraph.

from mpylab.tools.mgraph import MGraph

graph = MGraph(
    "measurement.dot",
    SearchPaths=["lab/config", "shared/config"],
)

Pickle persistence and traceability

Pickling an MGraph embeds its complete DOT text. Restoration treats this embedded text as authoritative and does not read the original file again. A measurement history therefore remains reproducible if the local DOT file is missing or has subsequently changed. The original file name remains as provenance in fname_or_data.

Supported DOT subset

The DOT subset used by mpylab includes in particular:

  • graph, digraph, and the optional strict qualifier;

  • node and edge statements;

  • one or more attribute lists in [...];

  • directed -> and undirected -- connections;

  • identifiers, numbers, and quoted strings;

  • #, //, and /* ... */ comments.

Measurement graphs normally use digraph.

Nodes and edges

digraph {
    Sg [ini="sg.ini"]
    Amp [ini="amplifier.ini"
         condition="FREQUENCY <= 1e9"]
    Cable [ini="cable.ini"]

    Sg -> Amp_Input
    Amp_Input -> Amp_Output [dev=Amp what="S21"]
    Amp_Output -> Pm [dev=Cable what="S21"]
}

Important attributes are:

ini

Configuration of an instrument instance. The file name is resolved through SearchPaths.

dev

Instrument node whose channel describes an edge. The node must be declared explicitly in the graph.

what

Data-channel name such as S21. It may be omitted when an edge deliberately references an instrument without a data channel.

condition

Boolean expression that activates a node or edge.

action

Safely restricted method call that may run when an element with a condition becomes active.

context_controller

Marks an instrument that maps logical context values to physical switch state and reads that state back. Its value names the context keys that require reapplication, for example context_controller="mode".

Pure connection or measurement-position nodes need no INI file. The INI and DAT reference documents channel layout, units, and DAT references.

Instrument types and drivers

DESCRIPTION/TYPE in the INI file names the entry class exported by the module in DESCRIPTION/DRIVER. Canonical types correspond to instrument base classes such as SIGNALGENERATOR, POWERMETER, FIELDPROBE, RECEIVER, SPECTRUMANALYZER, NETWORKANALYZER, VLISN, and TLISN.

Resolution ignores case and historical surrounding quotes, but it does not use fuzzy string matching. TYPE = Custom additionally requires DRIVER and CLASS; its module is loaded from SearchPaths.

Missing INI or driver files, unknown types, and missing entry classes raise mpylab.tools.mgraph.DeviceConfigurationError. CreateDevices only creates Python driver instances. Hardware access starts in Init_Devices.

Selecting active instruments

Instrument operations such as Init_Devices, SetFreq_Devices, ConfReceivers, RFOff_Devices, and RunReceiverScans consistently default to IgnoreInactive=True. They therefore address only instrument nodes activated by the current conditions. Passing IgnoreInactive=False deliberately includes inactive but configured instruments, for example for a complete diagnostic or an explicitly graph-wide cleanup operation.

Name mapping

A measurement class uses domain roles such as sg, pm_out, or fieldprobe. The names dictionary from conf.py is passed to MGraph as themap:

names = {
    "sg": "Sg",
    "pm_out": "Pm",
    "fieldprobe": ["Probe1", "Probe2"],
}
graph = MGraph(dotfile, themap=names)

The authoritative direction is logical name -> DOT node. Empty lists are valid for optional roles. When restoring historical pickles, MGraph normalizes unambiguously reversed or bidirectional mappings.

Semantic validation rejects unknown mapping targets and dev references. After CreateDevices, it additionally checks that every referenced device has an actual driver instance.

Field-probe orientation

Field-probe drivers always return their components as [probe_x, probe_y, probe_z]. Mapping into cell coordinates belongs to the measurement configuration and should normally be stored on the field-probe node in the DOT graph. A signed axis permutation can be written as:

Probe [probe_axis_map="cell_x:-probe_y,cell_y:+probe_x,cell_z:+probe_z"]

For arbitrary rotations, provide a proper orthonormal rotation matrix with determinant +1. It maps the probe vector to the cell vector:

Probe [probe_rotation_matrix="0,-1,0;1,0,0;0,0,1"]

The more readable fixed-cell-axis alternative is probe_rotation_angles_deg. Its order is cell z, then cell x, and finally cell y. Orientation metadata may also come from the configuration dictionary or a field-probe INI file. Conflicting definitions require explicit resolution; mpylab does not silently apply an orientation twice.

Conditions and explicit context

New graphs use stable names such as FREQUENCY. condition_map keeps those names independent of variables in application code:

from mpylab.tools.mgraph import FREQUENCY_CONDITION_MAP, MGraph

graph = MGraph(
    dotfile,
    condition_map=FREQUENCY_CONDITION_MAP,
    allow_legacy_condition_context=False,
)
graph.EvaluateConditions(context={"frequency": current_frequency})
AmpLF [condition="80e6 <= FREQUENCY <= 1e9"]
AmpHF [condition="1e9 < FREQUENCY <= 6e9"]

During migration, FREQUENCY_CONDITION_MAP accepts both f and FREQUENCY and maps them to frequency. Maintained graphs use FREQUENCY. Missing names, invalid expressions, and non-Boolean results raise mpylab.tools.mgraph.ConditionContextError.

For compatibility, EvaluateConditions can still inspect the caller frame when no explicit context is supplied. This deprecated path emits mpylab.tools.mgraph.LegacyConditionContextWarning once per graph instance. New applications disable it with allow_legacy_condition_context=False. The context mapping and this setting are preserved in pickle files.

Physical context controllers

When a measurement graph must switch physical hardware in addition to selecting active paths, a context controller encapsulates that mapping. The application continues to supply logical values only:

from mpylab.tools.mgraph import MGraph, TEM_CONDITION_MAP

graph = MGraph(
    dotfile,
    condition_map=TEM_CONDITION_MAP,
    allow_legacy_condition_context=False,
)
graph.CreateDevices()
graph.Init_Devices()
result = graph.ApplyContext({
    "frequency": 150e6,
    "mode": "GTEM",
})
sw [ini="sw_gtem.ini" context_controller="mode"]
source -> gtem [condition="MODE == 'GTEM'"]
source -> term [condition="MODE == 'TERM'"]

The instrument driver implements PrepareGraphContext(context), ApplyGraphContext(context), and GetGraphState(). It may additionally implement GraphStateMatches(expected, actual) to define domain-specific equality. PrepareGraphContext must return the complete expected state, not only the relay most recently changed. GetGraphState must query the physical state; an incomplete or unparsable response is not a success.

ApplyContext switches RF off before a relevant context or frequency change, applies required controller changes, evaluates conditions, sets the frequency, and then verifies the read-back values. Keys named in the attribute determine when ApplyGraphContext is called again. With context_controller="mode", a frequency-only change therefore does not reapply the mode; the complete current switch state is still planned and verified.

A state mismatch raises mpylab.tools.mgraph.GraphStateMismatchError and switches RF off. CheckContextState verifies immediately, while PollContextState can rate-limit queries. RFOn_Devices, Read, and NBRead perform this check at their safe program boundaries. Only an explicit operator decision should subsequently call ReapplyContext; general measurement applications must not know the concrete switch type.

Safe actions

An active element with a condition may also contain an action:

SwitchLF [condition="FREQUENCY <= 1e9"
          action="switch.switch_to(0)"]
graph.EvaluateConditions(context={
    "frequency": frequency,
    "switch": switch_instance,
})

Exactly one direct method call on an object from the context with literal arguments is permitted. Arbitrary Python code, attribute chains as arguments, and multiple statements are rejected. doAction=False only evaluates graph state and does not switch hardware. This is the appropriate mode for preflight and path validation.

Path validation

mpylab.tools.mgraph.MGraph.check_paths() evaluates conditions over a parameter range without executing actions or changing graph state:

report = graph.check_paths(
    "Sg",
    "TxAnt",
    "frequency",
    start_value=80e6,
    stop_value=6e9,
    context={"measurement_mode": "immunity"},
)
if not report.ok:
    print(report.format_text())

The default exactly_one_path mode requires exactly one complete active path. mode="warn_parallel" downgrades multiple complete paths to a warning; a missing path remains an error. Active dangling branches are also reported.

Simple comparisons are checked with boundary awareness: the boundary and the immediately lower and higher floating-point values are tested. This detects </<= gaps and overlaps. Actual measurement points may be added using values=frequencies. Complex expressions cannot always be analyzed completely; report.boundary_analysis_complete is then False.

For a single context, active_paths returns active paths without changing state. See path corrections for correction calculation and application.

Migrating local DOT files

mpylab-dot-migrate replaces the historical condition token f with FREQUENCY. Its default is a dry run with syntax validation and a unified diff:

mpylab-dot-migrate local-config.dot
mpylab-dot-migrate --write local-config.dot
mpylab-dot-migrate --check --recursive path/to/configurations

Only the exact name token in condition is changed. Comments, labels, filenames, string literals, and action remain untouched. --write writes atomically; directories are traversed only with --recursive. Symbolic links are not followed.

For CI, --check returns status 1 when migration is required and status 2 for invalid input. Path checks may be added directly:

mpylab-dot-migrate \
    --path Sg:TxAnt \
    --frequency-range 80e6 6e9 \
    --path-mode exactly_one_path \
    --context "MODE='immunity'" \
    local-config.dot

All options are listed in the CLI reference.

Validating configuration repositories

A collection of production graphs should be inventoried in a version-controlled manifest. Named TOML tables can record expected device counts, permitted fallbacks, and domain path checks:

[configurations.large_gtem_immunity]
dot = "LargeGTEM/dot/gtem-immunity.dot"
expected_device_count = 15
allowed_fallback_modules = []

[[configurations.large_gtem_immunity.path_checks]]
start = "sg"
end = "gtem"
parameter = "frequency"
start_value = 1.0e6
stop_value = 4.2e9

The manifest belongs to the configuration repository, not to the MGraph file format. Generic tests can use it to verify complete inventory, resolved INI references, device creation, and path checks. Store unfinished graphs in an explicitly marked area. See Validate and migrate configurations for further checks.

Independent parser

mpylab.tools.dotparser implements the required DOT subset using only the Python standard library. MGraph does not select it automatically and continues to use pydot in production. The independent parser serves as a comparison implementation and possible basis for future development.

Tests parse the version-controlled graphs with both parsers and compare nodes, edges, and attributes. The earlier Yapps parser generated from dot.g was never connected to the production MGraph path and was removed together with its runtime dependency.