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 :class:`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 :class:`mpylab.tools.mgraph.GraphInputError`. A measurement graph contains exactly one ``graph`` or ``digraph``. .. code-block:: python 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 --------------- .. code-block:: dot 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 :doc:`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 :class:`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``: .. code-block:: python 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. .. _en-dot-fieldprobe-orientation: 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: .. code-block:: dot 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: .. code-block:: dot 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: .. code-block:: python 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}) .. code-block:: dot 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 :class:`mpylab.tools.mgraph.ConditionContextError`. For compatibility, ``EvaluateConditions`` can still inspect the caller frame when no explicit context is supplied. This deprecated path emits :class:`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: .. code-block:: python 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", }) .. code-block:: dot 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 :class:`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``: .. code-block:: dot SwitchLF [condition="FREQUENCY <= 1e9" action="switch.switch_to(0)"] .. code-block:: python 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 --------------- :meth:`mpylab.tools.mgraph.MGraph.check_paths` evaluates conditions over a parameter range without executing actions or changing graph state: .. code-block:: python 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 :doc:`path corrections <../framework/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: .. code-block:: console 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: .. code-block:: console 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 :doc:`CLI reference <../../api/commands>`. 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: .. code-block:: toml [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 :doc:`validation` 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. Related pages ------------- * :doc:`instrument-data` for INI channels and DAT correction data; * :doc:`validation` for configuration and CI checks; * :doc:`../framework/path-corrections` for calculating and applying path corrections; * :doc:`../../api/graph` for the curated ``MGraph`` API.