Conditions, switching matrix, and path corrections

Path corrections connect transmission paths represented by a DOT graph to the quantities that were actually measured. A frequency-dependent setup must coordinate three steps:

  1. select active graph branches;

  2. set real instruments and the switching matrix to the frequency;

  3. read and apply the correction of the active path.

The DOT reference fully documents nodes, edges, conditions, and the dev/what attributes.

Correction values are commonly supplied through an INI channel and a DAT file. Syntax, units, complex values, and uncertainty are documented in the INI and DAT reference.

Evaluating conditions

EvaluateConditions activates nodes and edges using the explicit context:

mg.EvaluateConditions(context={"frequency": frequency})

With doAction=False, it computes graph state only. This is important for preflight, scan planning, and path validation because those operations must not switch hardware.

An active element may additionally provide an action attribute. Only a direct method call on an object from the context with literal arguments is executed. Arbitrary Python code is not permitted. Actions are suitable for small explicitly configured state changes; prefer the driver for instrument-specific switching logic.

A small matrix may alternatively be switched directly through actions:

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

This approach and a frequency-dependent SetFreq implementation in the switch driver are alternatives. Do not set the same relay state through both paths at once.

Switching matrix through SetFreq

A switching matrix is a regular MGraph instrument. Its driver implements SetFreq(frequency) and selects the required relay state from its INI configuration. SetFreq_Devices invokes this method together with the other frequency-dependent instruments:

mg.EvaluateConditions(context={"frequency": frequency})
mg.SetFreq_Devices(frequency)

Conditions and the driver’s switching threshold must use the same range boundaries. For example, the graph selects the LF branch up to and including 1 GHz while the switch driver chooses the same relay state for f <= 1e9.

Before switching, reset a signal generator to a safe initial level or disable RF. This is particularly important when changing frequency activates a different amplifier with a different input limit.

More complex TEM example

digraph {
    Switch [ini="sw_gtem.ini"]
    Sg     [ini="sg.ini"]
    AmpLF  [ini="amp-lf.ini"
            condition="FREQUENCY <= 1e9"]
    AmpHF  [ini="amp-hf.ini"
            condition="FREQUENCY > 1e9"]

    PathSgLF   [ini="sg-lf.ini"]
    PathSgHF   [ini="sg-hf.ini"]
    PathLFCell [ini="lf-cell.ini"]
    PathHFCell [ini="hf-cell.ini"]

    Sg -> SgLF [condition="FREQUENCY <= 1e9"]
    Sg -> SgHF [condition="FREQUENCY > 1e9"]

    SgLF -> AmpInLF [dev=PathSgLF what="S21"]
    SgHF -> AmpInHF [dev=PathSgHF what="S21"]
    AmpInLF -> AmpOutLF [dev=AmpLF what="S21"]
    AmpInHF -> AmpOutHF [dev=AmpHF what="S21"]
    AmpOutLF -> GTEM [dev=PathLFCell what="S21"]
    AmpOutHF -> GTEM [dev=PathHFCell what="S21"]
}
Frequency-dependent LF and HF paths of a TEM verification setup with switching matrix

The condition context selects the red or blue correction path. In parallel, SetFreq_Devices sets the switching matrix and all active instruments to the same frequency.

This simplified example shows the forward path only. A real TEM verification adds branches to forward and reverse power meters, including their coupler and cable corrections.

Reading the total correction

get_path_correction returns the total correction between two measurement positions:

from mpylab.tools.aunits import POWERRATIO

correction = mg.get_path_correction(
    names["sg"],
    names["port"],
    POWERRATIO,
)

Without unit, it returns AMPLITUDERATIO. Converting a complex amplitude ratio to a power ratio uses abs(S21) ** 2. Units and uncertainty are preserved.

get_path_corrections additionally returns the individual correction elements. Its "total" key contains the combined result:

details = mg.get_path_corrections("Sg", "GTEM")
total = details["total"]

This detailed form is useful for diagnostics and reports. Application logic should normally use get_path_correction for the total correction.

Coherent series and parallel paths

All active paths are combined as complex amplitude ratios, regardless of the requested output unit:

\[H_p = \prod_{k\in p} S_{21,k},\qquad H = \sum_p H_p,\qquad R_P = |H|^2.\]

POWERRATIO is formed only after this sum (and any reference-path division). For two unit-amplitude contributions, phase differences of 0, 90 and 180 degrees therefore give power ratios of 4, 2 and 0. A series path multiplies complex amplitudes; phases add. Shared segments retain the same uncertain inputs.

An S-parameter magnitude d in dB corresponds to amplitude 10**(d/20). Polar DAT values retain their phase. The historical DAT representation POWERRATIO encodes |S21|**2 * exp(1j*phase); it is converted back to amplitude before composition. It must not be interpreted as a physical complex power. A scalar magnitude-only input implicitly assumes zero phase and cannot establish the interference of an actual parallel setup.

The model assumes coherent contributions of one source with consistent wave normalization and negligible or already included reflection effects. Splitting and combining coefficients must be included in the configured transfer paths. The factor of four above is relative to one unit contribution at the summation point; it does not imply passive power gain from splitting one source. Full S-matrix networks, mismatch, and incoherent power addition are outside this model. For the wave conventions, see F. Caspers, RF engineering basic concepts: S-parameters.

get_path_correction(end, start) returns the inverse correction, not S12. A zero denominator raises ZeroDivisionError; missing connectivity raises ValueError. get_path_corrections retains zero for an absent forward path and unity for an empty path from a node to itself. Its device entries remain the original quantities; only "total" follows the requested unit. Each device/channel is read once per query, including common-parent reference ratios. A subsequent query reads it again, so frequency changes cannot reuse stale values.

Uncertainty at cancellation

The result retains SCUQ expression dependencies instead of replacing each path with an independent uncertainty input. Mixed real and complex expressions keep shared scalar inputs, too. Complex power uses the Cartesian squared magnitude with Jacobian (2*Re(H), 2*Im(H)); this avoids differentiating abs(H) at zero.

SCUQ still propagates uncertainty to first order. At nominal cancellation that Jacobian is zero even when independent amplitude errors produce nonzero power in realizations. Thus a zero first-order power uncertainty is not evidence of exact cancellation. Retain the amplitude covariance and use a suitable higher-order or Monte Carlo evaluation for such cases. The regression tests include analytic interference, shared-input cancellation, and reproducible Monte Carlo comparisons illustrating this limit.

Compatibility

The INI/DAT format and diagnostic dictionary keys are unchanged. Previously, requesting POWERRATIO added path powers or retained an encoded transfer phase; corrected results can therefore differ for parallel or phased paths. Stored measurement values are not rewritten automatically. Re-evaluation uses the corrected calculation. Returned corrections retain SCUQ expressions; new pickles containing these expressions require an mpylab version providing the transfer-expression helpers.

Applying a correction

Select the direction explicitly according to the physical calculation:

corrected = mg.apply_path_correction(
    measured,
    correction,
    operation="divide",
    magnitude=True,
    output_unit=VOLT,
)

operation is "multiply" or "divide". magnitude=False preserves phase; magnitude=True produces a real magnitude quantity with uncertainty.

Fast repeated application

When processing many values with the same correction, prepare it once:

prepared = mg.prepare_path_correction(
    correction,
    operation="divide",
    magnitude=True,
    output_unit=VOLT,
)

corrected = [
    mg.apply_path_correction(value, prepared)
    for value in measured_values
]

An exactly known constant correction can then use a fast scaling path. Uncertain and unsupported cases automatically fall back to the complete SCUQ calculation.

Reuse the prepared object only while frequency, active graph path, and underlying calibration data remain unchanged. Read and, where appropriate, prepare the correction again after every EvaluateConditions call or frequency change.

Validation rules

  • Evaluate conditions before reading a correction.

  • Configure switching matrix and graph with identical range boundaries.

  • Test boundary frequencies for missing or duplicate paths.

  • Keep corrections as SCUQ quantities.

  • State multiplication or division and phase removal explicitly.

  • Do not reuse prepared corrections across a graph-state change.