Internal boundaries: dependency inventory

This is an architectural assessment of mpylab at commit 9d09c9a (2026-10-18), before any boundary refactoring. It proposes internal ownership within the existing repository. It does not introduce packages, move modules, change import paths, or define new supported APIs.

Scope and method

All 229 Python source files under src/mpylab were inspected through an AST import inventory. Imports inside functions and relative imports were included; 14 files in device/legacy, tools/legacy and the forwarding module device/driver.py were distinguished from the other 215 files. Generated files, including _version.py and limits_ui.py, remain in the inventory. Module counts describe locations, not effort or architectural quality.

An edge means that one module explicitly imports another, including function imports. Repeated imports between the same pair count once in the figures below. Attribute imports were resolved to a source module when possible. Imports do not prove that a branch executes, or that an imported name is a supported public API. Package initialization, re-exports, configuration-driven loading and pickle restoration require additional inspection. In particular, the absence of a static edge does not imply runtime independence.

Tracked Python sources in TEMField, Conducted Emission, PExplorer, Shielding Effectiveness, both active configuration repositories and MpyConfig were also searched. Application/configuration sources, tests and archived/migrated configurations were distinguished. No configuration script or driver was executed. No measurement procedure or formula was assessed.

Current locations and responsibilities

Source inventory

Location

Files

Present responsibility

tools

53

Quantities, configuration, graph, numerics, compatibility exports and CLI tools

device excluding legacy

89

Contracts, transports, drivers, virtual devices, UI helpers and windows

env excluding domain subpackages

19

Measurement lifecycle, EUT monitoring, UI contracts, Qt adapter and runners

env/tem

18

TEM workflows, evaluation, history, reports and interactive review

env/msc

5

MSC workflows, protected acquisition and threshold search

env/univers

3

Amplifier-test workflow and name maps

limits

22

Limit models, profiles, registry and Qt explorer

gui

4

Qt-free identity/availability, branding, macOS helper and launcher

Package root / legacy

2 / 14

Version/package initialization / historical compatibility

Main findings

  1. TEM and MSC already have a useful separation. No explicit imports from either domain into the other, or into env/univers, were found. Both use the shared measurement layer and tools. There are five distinct module-pair edges from TEM to shared env modules and three from MSC. There are 23 from TEM to tools, 13 from MSC and six from the amplifier workflow. No non-domain source module explicitly imports these domain modules. External applications and configuration loading still use them.

  2. Directory names do not describe Qt ownership. Twenty-six non-legacy source modules directly import PySide6, Shiboken or a Matplotlib Qt backend. They span device, env, env/tem, gui and limits. Six of these are concrete drivers with function-local GUI launchers, not drivers requiring Qt for normal imports. Conversely, gui/__init__.py contains Qt-free metadata and availability checks. device/ui_quantity_display.py, ui_driver_lifecycle.py and receiver_scan_worker.py are also Qt-free; the ui_ prefix alone is not a reason to move a module.

  3. TEM has an explicit UI dependency during interactive review. TEMCell.py locally imports emission_sweep_ui._require_display and review_sweep. The latter module imports Qt only inside functions, so a normal TEM import remains Qt-free. A stricter domain boundary would pass review/display services through an adapter, while keeping acquisition, review decisions, autosave and resume state owned by the workflow.

  4. The limit calculation layer is largely separate from its explorer. limit.py uses NumPy and the registry dynamically loads named profiles. Three FCC profile modules use canonical conversions from tools.uconv; four profiles have local example imports from tools.spacing. The Qt explorer additionally imports device.ui_frequency and shared branding. TEM prescreening imports limits.registry.resolve_limit. This is a sensible domain-to-calculation dependency, not a dependency on the explorer.

  5. The explorer has a small explicit cycle. gui.launchers.limit_explorer_main locally imports limits.limits_ui_main.main. That main imports the private gui.launchers._limit_parser. This is the sole multi-module strongly connected component in the explicitly resolved non-legacy import graph. Both edges are deferred: it is not evidence of an import-time crash. A neutral parser module or an argument-parsing boundary can remove it.

  6. Tools mix foundation and orchestration. tools.config_inspect imports env.config_view and env.script_cli; tools.nport_check imports device.nport; tools.visa_probe imports private communication_gpib._resource_manager. These are application/validation tools rather than evidence that all numerical helpers depend on devices. A blanket rule forbidding every tools -> env/device edge would be misleading. Separate responsibilities logically before enforcing an import policy.

  7. Compatibility exports are heavily used. Fifty-nine distinct non-legacy modules import tools.util; 40 import tools.uconv and 31 import tools.configuration. util re-exports existing implementations from focused modules such as logging_utils, text and sequences. Prefer those implementation modules for new internal imports, while retaining the existing facade for consumers. uconv is already the canonical conversion backend and should not be duplicated in domains or limits.

Observed consumer contracts

These are used import paths, not a proposal to make every imported name public. All paths below are relative to the corresponding repository.

Representative production consumers

Consumer

Evidence

Boundary implication

TEMField 7f12792

src/temfield_mpylab/TestSusceptibility.py imports dBm2W from env.univers.AmplifierTest

A conversion should come from canonical tools.uconv, without importing an amplifier workflow.

Shielding Effectiveness 8b000c3

report.py and report_logo.py import helpers/errors from env.tem.report_profile

Shared report metadata currently lives under TEM; a neutral home needs compatible defaults and old-path exports.

Conducted Emission 7f739e5

ce.py and limit_dialog.py use limit models/profiles/registry; the measurement uses MGraph and quantity helpers

Preserve calculation APIs independently of the explorer. Audit re-exports such as units from aunits separately.

PExplorer 3cb5af1

src/pexplorer/pickle_loading.py uses env.Measure.load_pickle_compat

Pickle viewing depends on restoring concrete classes, even when there is no direct TEM/MSC import.

GTEM configuration b473175

sw_gtem.py uses the device.driver.DRIVER facade; tests exercise TEMCell and MGraph

The legacy driver facade is still an active external contract.

RC configuration 6e92692

virtual/build.py uses env.msc.acquisition.MSC_CONDITION_MAP and MGraph; custom sensors subclass virtual drivers

Preserve condition-map semantics, custom-driver loading and virtual driver contracts.

MpyConfig 0be1460 and the migrated trees contain historical callers. They were examined separately and are compatibility evidence, not additional active applications to modernize in this step. TEMField also implements its own measurement workflows using Measure; it is not simply a TEMCell GUI.

Dependencies beyond ordinary imports

  • tools.mgraph.MGraph.CreateDevices builds mpylab.device.<driver> names from INI configuration and also loads custom driver files. This is a real graph-to-device dependency absent from the static module-pair counts.

  • env.script_cli and env.qt_runner load user configuration modules. Device windows select driver modules dynamically. Limit discovery/registry also uses dynamic imports. These extension mechanisms need explicit ownership.

  • env.Measure.MpyLabCompatUnpickler maps historical mpy paths, then resolves stored classes. Moving a class or removing an old module can break saved measurements and PExplorer. Preserve old import paths and test real restoration before moving persisted types. No measurement files were opened for this analysis.

  • device.pm_rxatt contains a demonstration that loads an external switch script by absolute path. This is demonstration/configuration coupling, not an automatic dependency of every power-meter import. Do not execute it as an import-graph probe.

Proposed internal ownership

Keep one distribution and the current public import paths. Define logical ownership first; existing directory-wide rules would be too coarse.

digraph proposed_boundaries {
    rankdir=LR;
    node [shape=box];
    "Qt adapters / applications" -> "TEM / MSC / amplifier workflows";
    "Qt adapters / applications" -> "Limits calculation";
    "TEM / MSC / amplifier workflows" -> "Measurement lifecycle / EUT / UI contracts";
    "TEM / MSC / amplifier workflows" -> "Graph / configuration";
    "TEM / MSC / amplifier workflows" -> "Limits calculation" [label="where needed"];
    "Graph / configuration" -> "Device contracts / drivers / transports";
    "Measurement lifecycle / EUT / UI contracts" -> "Graph / configuration";
    "Device contracts / drivers / transports" -> "Quantities / conversion / numerics";
    "Limits calculation" -> "Quantities / conversion / numerics";
}

The diagram is a target dependency direction, not a complete current call graph. Qt-free application identity and shared report metadata are supporting services. Measurement UI contracts remain Qt-free. Amplifier protection, RF-off and input limits stay shared safety responsibilities; an amplifier-test workflow does not own all amplifier safety. TEM-specific calculations in tools may retain their paths until a separately reviewed ownership decision.

First increment: explorer argument boundary

The launcher/parser cycle identified in the baseline above has now been removed. gui.limit_explorer_cli owns the program name and argument parsing and imports neither the launcher, the explorer nor Qt. The console launcher validates arguments once, checks Qt availability, then calls the internal window runner. The existing limits.limits_ui_main.main(argv=None) validates arguments through the same CLI module and calls that runner. There is no import back to the launcher.

The console command, direct module entry point, help/version text and Qt exit code are retained. Console help/version and invalid-argument handling still work without Qt; an actual GUI request explains the mpylab[gui] extra when absent. This does not extend Qt-free behavior to the old direct GUI module import. Window creation, branding and event-loop ownership are unchanged. No domain, driver, limit calculation or persisted type was moved.

Second increment: shared report profiles

reporting.profile now owns profile validation, merging, origin tracking and metadata tables. It has no domain/device/Qt imports. The TEM history writer and the Shielding Effectiveness feature branch use this neutral service. The latter requires mpylab 1.0.33 and must wait for that release before merging/publishing. env.tem.report_profile remains an explicit compatibility facade with the same exported objects. Profile/error pickle globals retain their historical module path; attributes, defaults, errors, logo policy and TSV output are unchanged. The generic implementation retains the historical fallback title; Shielding continues to supply its own title and PDF-logo opt-in.

Third increment: interchangeable sweep review

TEMCell and the emission script now use the Qt-free sweep-review adapter contract. Adapters return decisions and evaluation proposals from detached data; the workflow validates and commits metadata/selections. Existing Qt dialogs remain behind an adapter. The terminal adapter saves the same curves as PNGs and explicitly asks for repeat/continue/finish/save-exit and evaluation sets. Auto mode falls back to terminal when Qt/display is absent; unavailable input fails before device creation. EOF/Ctrl-C retains pending data for resume. Adapters remain invocation-local and are not added to measurement pickles. The plot helper is Qt-free; historical private plotting imports remain aliases. See TEM emission data for configuration and operator steps.

Enforced direct-import rules

The quality job now runs the stdlib-only checker:

python tools/check_import_boundaries.py

Rules are defined explicitly in tools/check_import_boundaries.py. They cover 49 modules at introduction; the count changes as modules are added. No runtime module is imported or hardware accessed by the checker.

Enforced scope

Rule

Selected sources

Forbidden direct dependencies

report-profile

reporting.profile

env, device, gui, Qt bindings/backends

sweep-contract

env.ui.sweep_review

TEM/MSC/amplifier domains, devices, GUI and concrete review adapters

tem-workflow-ui

env.tem.TEMCell

Qt bindings/backends and concrete Qt frontends, including emission_sweep_ui

limit-calculation

limits initializer, limit, registry, fcc_ism and both profile packages

Qt bindings/backends, gui, device, env and the three explorer modules

limit-cli

gui.limit_explorer_cli

Launcher, limits/explorer, devices, env and concrete Qt frontends

tem-domain / msc-domain / amplifier-domain

Respective env.tem / env.msc / env.univers package trees

Either of the other two domain trees

Qt targets include PySide2/6, PyQt5/6, Shiboken2/6 and the explicitly listed Matplotlib Qt adapters. Exact source-module selectors do not implicitly include children; source-package selectors include future child modules. Forbidden prefixes match complete name components, so temporal does not match tem. Imports inside functions, classes, conditional/try blocks and TYPE_CHECKING are checked. Relative imports and from parent import child are resolved syntactically, without executing Python or looking up installed packages.

Violations report file, line, target, rule ID and reason. Exit status is 0 for success, 1 for a boundary violation and 2 for unreadable/invalid selected source or a missing source root. A rule matching no source module also fails with 2, preventing an empty scan from passing silently. --source-root accepts a package directory named mpylab for isolated checks.

This is not a full dependency or cycle analysis. It does not follow re-exports, parent-package side effects, aliases assigned at runtime, dynamic import calls, or runtime Matplotlib backend selection. Wildcards only expose their named base, not unknown re-exported children. MGraph/custom-driver/configuration/limit loading and pickle globals remain runtime contracts. Compatibility facades and historical module-path strings remain valid; there is no blanket ban on imports from tools to devices or measurement code and no legacy exception baseline. Rule changes need an explicit scope/reason and corresponding tests/documentation; do not add a suppression simply to hide a new dependency. The installed-core job without Qt and the existing configuration/pickle tests remain required.